use std::fmt;
use std::ops;
pub mod plot;
mod ticks;
pub trait DrawView {
fn draw(&self, view: &View, canvas: &mut ViewCanvas);
}
pub struct Size {
w: usize,
h: usize,
}
impl Size {
pub fn new(w: usize, h: usize) -> Self {
Self { w, h }
}
}
impl Default for Size {
fn default() -> Self {
Self { w: 100, h: 100 }
}
}
pub struct Plot {
title: String,
x_label: String,
y_label: String,
view: View,
with_decoration: bool,
}
impl Default for Plot {
fn default() -> Self {
Self {
title: String::new(),
x_label: String::new(),
y_label: String::new(),
view: View::default(),
with_decoration: true,
}
}
}
impl Plot {
pub fn add_plot(&mut self, plot: Box<dyn DrawView>) -> &mut Self {
self.view.plots.push(plot);
self
}
pub fn set_domain(&mut self, domain: Domain) -> &mut Self {
self.view.domain = domain;
self
}
pub fn set_codomain(&mut self, codomain: Domain) -> &mut Self {
self.view.codomain = codomain;
self
}
pub fn set_title(&mut self, title: &str) -> &mut Self {
self.title = String::from(title);
self
}
pub fn set_x_label(&mut self, label: &str) -> &mut Self {
self.x_label = String::from(label);
self
}
pub fn set_y_label(&mut self, label: &str) -> &mut Self {
self.y_label = String::from(label);
self
}
pub fn set_size(&mut self, size: Size) -> &mut Self {
self.view.size = size;
self
}
}
impl fmt::Display for Plot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let rows = self.view.drawing(self.with_decoration);
if !self.with_decoration {
return write!(f, "{}", rows.join("\n"));
}
let width = rows[0].chars().count();
writeln!(f, "â•{:─^width$}â•®", self.title)?;
for row in rows.iter() {
writeln!(f, "│{row}│")?;
}
writeln!(f, "╰{:─<width$}╯", "")?;
writeln!(f, " {: ^width$} ", self.x_label)?;
writeln!(f, " {: ^width$} ", self.y_label)
}
}
#[derive(Default)]
pub struct View {
pub domain: Domain,
pub codomain: Domain,
pub size: Size,
plots: Vec<Box<dyn DrawView>>,
}
impl View {
fn draw_axis(&self, canvas: &mut ViewCanvas) {
canvas.line(self.domain.min(), 0.0, self.domain.max(), 0.0);
canvas.line(0.0, self.codomain.min(), 0.0, self.codomain.max());
}
fn draw_plots(&self, canvas: &mut ViewCanvas) {
for plot in self.plots.iter() {
plot.draw(&self, canvas);
}
}
pub(crate) fn drawing(&self, with_decoration: bool) -> Vec<String> {
let mut canvas = ViewCanvas::new(&self);
self.draw_axis(&mut canvas);
self.draw_plots(&mut canvas);
let rows = canvas.rows();
if !with_decoration {
return rows;
}
let width = rows[0].chars().count();
let mut out = Vec::new();
let y_ticks = ticks::YTicks::new(&self.codomain, rows.len(), 2);
let offset = y_ticks.display_width();
let x_ticks = ticks::XTicks::new(&self.domain, width, 2);
for (index, row) in rows.iter().enumerate() {
out.push(format!("{: >offset$}{row}", y_ticks.get(index)));
}
out.push(format!("{: >offset$}{x_ticks}", ""));
out
}
}
pub struct Domain(pub std::ops::Range<f64>);
impl Default for Domain {
fn default() -> Self {
Self(-10.0..10.0)
}
}
impl Domain {
pub fn min(&self) -> f64 {
self.0.start
}
pub fn max(&self) -> f64 {
self.0.end
}
pub fn range(&self) -> f64 {
(self.0.end - self.0.start).abs()
}
pub fn iter(&self, steps: usize) -> DomainIterator {
DomainIterator::new(self.0.clone(), self.range() / steps as f64)
}
}
pub struct DomainIterator {
current: f64,
domain: ops::Range<f64>,
step_by: f64,
}
impl DomainIterator {
pub fn new(domain: ops::Range<f64>, step_by: f64) -> Self {
Self {
current: domain.start,
domain,
step_by,
}
}
}
impl Iterator for DomainIterator {
type Item = f64;
fn next(&mut self) -> Option<Self::Item> {
if self.current >= self.domain.end {
return None;
}
let result = self.current;
self.current += self.step_by;
Some(result)
}
}
pub struct ViewCanvas<'view> {
canvas: drawille::Canvas,
view: &'view View,
}
impl<'view> ViewCanvas<'view> {
pub(crate) fn new(view: &'view View) -> Self {
Self {
canvas: drawille::Canvas::new(view.size.w as u32, view.size.h as u32),
view,
}
}
pub(crate) fn rows(&self) -> Vec<String> {
let rows = self.canvas.rows();
rows
}
fn project_on_canvas(&self, x: f64, y: f64) -> (u32, u32) {
let height = self.view.size.h as f64;
let y_tmp = (y - self.view.codomain.min()) / self.view.codomain.range();
let y = (height - y_tmp * height).round().clamp(0.0, height - 1.0);
let width = self.view.size.w as f64;
let x_tmp = (x - self.view.domain.min()) / self.view.domain.range();
let x = (x_tmp * width).round().clamp(0.0, width - 1.0);
(x as u32, y as u32)
}
pub fn line(&mut self, x0: f64, y0: f64, x1: f64, y1: f64) {
let (x0, y0) = self.project_on_canvas(x0, y0);
let (x1, y1) = self.project_on_canvas(x1, y1);
self.canvas.line(x0, y0, x1, y1);
}
pub fn point(&mut self, x: f64, y: f64) {
let (x, y) = self.project_on_canvas(x, y);
self.canvas.set(x, y);
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::Rng;
#[test]
fn simple() {
let mut plot = Plot::default();
plot.set_domain(Domain(-10.0..10.0))
.set_codomain(Domain(-0.3..1.2))
.set_title("Graph title")
.set_x_label("X axis")
.set_y_label("Y axis")
.set_size(Size::new(100, 25))
.add_plot(Box::new(plot::Graph::new(|x| x.sin() / x)));
println!("{plot}");
}
#[test]
fn histogram() {
let mut rng = rand::thread_rng();
let values: Vec<f64> = (0..100).map(|_| rng.gen_range(0.0f64..10.0f64)).collect();
let mut plot = Plot::default();
plot.set_domain(Domain(0.0..11.0))
.set_codomain(Domain(0.0..45.0))
.set_title("Graph title")
.set_x_label("X axis")
.set_y_label("Y axis")
.set_size(Size::new(100, 25))
.add_plot(Box::new(plot::Histogram::new(
values,
vec![0.0..2.0, 2.0..4.0, 4.0..6.0, 6.0..8.0, 8.0..10.0],
)));
println!("{plot}");
}
#[test]
fn composition() {
let mut rng = rand::thread_rng();
let values: Vec<f64> = (0..100).map(|_| rng.gen_range(0.0f64..10.0f64)).collect();
let mut plot = Plot::default();
plot.set_domain(Domain(0.0..11.0))
.set_codomain(Domain(0.0..45.0))
.set_title("Graph title")
.set_x_label("X axis")
.set_y_label("Y axis")
.set_size(Size::new(100, 25))
.add_plot(Box::new(plot::Histogram::new(
values,
vec![0.0..2.0, 2.0..4.0, 4.0..6.0, 6.0..8.0, 8.0..10.0],
)))
.add_plot(Box::new(plot::Graph::new(|x| {
-2.0 * (x - 5.0).powf(2.0) + 40.0
})));
println!("{plot}");
}
}