pub mod scale;
pub mod utils;
use super::drawille::Canvas as BrailleCanvas;
use super::drawille::PixelColor;
use scale::Scale;
use std::cmp;
use std::default::Default;
use std::f32;
#[derive(PartialEq)]
enum ChartRangeMethod {
AutoRange,
FixedRange,
}
pub struct Chart<'a> {
width: u32,
height: u32,
xmin: f32,
xmax: f32,
ymin: f32,
ymax: f32,
y_ranging: ChartRangeMethod,
shapes: Vec<(&'a Shape<'a>, Option<PixelColor>)>,
canvas: BrailleCanvas,
}
pub enum Shape<'a> {
Continuous(Box<dyn Fn(f32) -> f32 + 'a>),
Points(&'a [(f32, f32)]),
Lines(&'a [(f32, f32)]),
Steps(&'a [(f32, f32)]),
Bars(&'a [(f32, f32)]),
}
pub trait Plot<'a> {
fn lineplot(&'a mut self, shape: &'a Shape) -> &'a mut Chart;
}
pub trait ColorPlot<'a> {
fn linecolorplot(&'a mut self, shape: &'a Shape, color: PixelColor) -> &'a mut Chart;
}
impl<'a> Default for Chart<'a> {
fn default() -> Self {
Self::new(120, 60, -10.0, 10.0)
}
}
impl<'a> Chart<'a> {
pub fn new(width: u32, height: u32, xmin: f32, xmax: f32) -> Self {
if width < 32 {
panic!("width should be more then 32, {} is provided", width);
}
if height < 32 {
panic!("height should be more then 32, {} is provided", height);
}
Self {
xmin,
xmax,
ymin: f32::INFINITY,
ymax: f32::NEG_INFINITY,
y_ranging: ChartRangeMethod::AutoRange,
width,
height,
shapes: Vec::new(),
canvas: BrailleCanvas::new(width, height),
}
}
pub fn new_with_y_range(
width: u32,
height: u32,
xmin: f32,
xmax: f32,
ymin: f32,
ymax: f32,
) -> Self {
if width < 32 {
panic!("width should be more then 32, {} is provided", width);
}
if height < 32 {
panic!("height should be more then 32, {} is provided", height);
}
Self {
xmin,
xmax,
ymin,
ymax,
y_ranging: ChartRangeMethod::FixedRange,
width,
height,
shapes: Vec::new(),
canvas: BrailleCanvas::new(width, height),
}
}
fn borders(&mut self) {
let w = self.width;
let h = self.height;
self.vline(0);
self.vline(w);
self.hline(0);
self.hline(h);
}
fn vline(&mut self, i: u32) {
if i <= self.width {
for j in 0..=self.height {
if j % 3 == 0 {
self.canvas.set(i, j);
}
}
}
}
fn hline(&mut self, j: u32) {
if j <= self.height {
for i in 0..=self.width {
if i % 3 == 0 {
self.canvas.set(i, self.height - j);
}
}
}
}
#[allow(clippy::inherent_to_string)]
pub fn to_string(&mut self) -> String {
self.figures();
self.axis();
let mut frame = self.canvas.frame();
if let Some(idx) = frame.find('\n') {
frame.insert_str(idx, &format!(" {0:.1}", self.ymax));
frame.push_str(&format!(
" {0:.1}\n{1: <width$.1}{2:.1}\n",
self.ymin,
self.xmin,
self.xmax,
width = (self.width as usize) / 2 - 3
));
}
frame
}
pub fn display(&mut self) {
println!("{}", self.to_string());
}
pub fn nice(&mut self) {
self.borders();
self.display();
}
pub fn axis(&mut self) {
let x_scale = Scale::new(self.xmin..self.xmax, 0.0..self.width as f32);
let y_scale = Scale::new(self.ymin..self.ymax, 0.0..self.height as f32);
if self.xmin <= 0.0 && self.xmax >= 0.0 {
self.vline(x_scale.linear(0.0) as u32);
}
if self.ymin <= 0.0 && self.ymax >= 0.0 {
self.hline(y_scale.linear(0.0) as u32);
}
}
pub fn figures(&mut self) {
for (shape, color) in &self.shapes {
let x_scale = Scale::new(self.xmin..self.xmax, 0.0..self.width as f32);
let y_scale = Scale::new(self.ymin..self.ymax, 0.0..self.height as f32);
let points: Vec<_> = match shape {
Shape::Continuous(f) => (0..self.width)
.filter_map(|i| {
let x = x_scale.inv_linear(i as f32);
let y = f(x);
if y.is_normal() {
let j = y_scale.linear(y).round();
Some((i, self.height - j as u32))
} else {
None
}
})
.collect(),
Shape::Points(dt) | Shape::Lines(dt) | Shape::Steps(dt) | Shape::Bars(dt) => dt
.iter()
.filter_map(|(x, y)| {
let i = x_scale.linear(*x).round() as u32;
let j = y_scale.linear(*y).round() as u32;
if i <= self.width && j <= self.height {
Some((i, self.height - j))
} else {
None
}
})
.collect(),
};
match shape {
Shape::Continuous(_) | Shape::Lines(_) => {
for pair in points.windows(2) {
let (x1, y1) = pair[0];
let (x2, y2) = pair[1];
if let Some(color) = color {
self.canvas.line_colored(x1, y1, x2, y2, *color);
} else {
self.canvas.line(x1, y1, x2, y2);
}
}
}
Shape::Points(_) => {
for (x, y) in points {
self.canvas.set(x, y);
}
}
Shape::Steps(_) => {
for pair in points.windows(2) {
let (x1, y1) = pair[0];
let (x2, y2) = pair[1];
if let Some(color) = color {
self.canvas.line_colored(x1, y2, x2, y2, *color);
self.canvas.line_colored(x1, y1, x1, y2, *color);
} else {
self.canvas.line(x1, y2, x2, y2);
self.canvas.line(x1, y1, x1, y2);
}
}
}
Shape::Bars(_) => {
for pair in points.windows(2) {
let (x1, y1) = pair[0];
let (x2, y2) = pair[1];
if let Some(color) = color {
self.canvas.line_colored(x1, y2, x2, y2, *color);
self.canvas.line_colored(x1, y1, x1, y2, *color);
self.canvas.line_colored(x1, self.height, x1, y1, *color);
self.canvas.line_colored(x2, self.height, x2, y2, *color);
} else {
self.canvas.line(x1, y2, x2, y2);
self.canvas.line(x1, y1, x1, y2);
self.canvas.line(x1, self.height, x1, y1);
self.canvas.line(x2, self.height, x2, y2);
}
}
}
}
}
}
pub fn frame(&self) -> String {
self.canvas.frame()
}
fn rescale(&mut self, shape: &Shape) {
let x_scale = Scale::new(self.xmin..self.xmax, 0.0..self.width as f32);
let ys: Vec<_> = match shape {
Shape::Continuous(f) => (0..self.width)
.filter_map(|i| {
let x = x_scale.inv_linear(i as f32);
let y = f(x);
if y.is_normal() {
Some(y)
} else {
None
}
})
.collect(),
Shape::Points(dt) | Shape::Lines(dt) | Shape::Steps(dt) | Shape::Bars(dt) => dt
.iter()
.filter_map(|(x, y)| {
if *x >= self.xmin && *x <= self.xmax {
Some(*y)
} else {
None
}
})
.collect(),
};
let ymax = *ys
.iter()
.max_by(|x, y| x.partial_cmp(y).unwrap_or(cmp::Ordering::Equal))
.unwrap_or(&0.0);
let ymin = *ys
.iter()
.min_by(|x, y| x.partial_cmp(y).unwrap_or(cmp::Ordering::Equal))
.unwrap_or(&0.0);
self.ymin = f32::min(self.ymin, ymin);
self.ymax = f32::max(self.ymax, ymax);
}
}
impl<'a> ColorPlot<'a> for Chart<'a> {
fn linecolorplot(&'a mut self, shape: &'a Shape, color: PixelColor) -> &'a mut Chart {
self.shapes.push((shape, Some(color)));
if self.y_ranging == ChartRangeMethod::AutoRange {
self.rescale(shape);
}
self
}
}
impl<'a> Plot<'a> for Chart<'a> {
fn lineplot(&'a mut self, shape: &'a Shape) -> &'a mut Chart {
self.shapes.push((shape, None));
if self.y_ranging == ChartRangeMethod::AutoRange {
self.rescale(shape);
}
self
}
}