pub mod scale;
pub mod utils;
use drawille::Canvas as BrailleCanvas;
use drawille::PixelColor;
use rgb::RGB8;
use scale::Scale;
use std::cmp;
use std::default::Default;
use std::f32;
use std::fmt::{Display, Formatter, Result};
#[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<RGB8>)>,
canvas: BrailleCanvas,
x_style: LineStyle,
y_style: LineStyle,
x_label_format: LabelFormat,
y_label_format: LabelFormat,
y_tick_display: TickDisplay,
}
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: RGB8) -> &'a mut Chart;
}
pub trait AxisBuilder<'a> {
fn x_axis_style(&'a mut self, style: LineStyle) -> &'a mut Chart<'a>;
fn y_axis_style(&'a mut self, style: LineStyle) -> &'a mut Chart<'a>;
}
pub trait LabelBuilder<'a> {
fn x_label_format(&'a mut self, format: LabelFormat) -> &'a mut Chart<'a>;
fn y_label_format(&'a mut self, format: LabelFormat) -> &'a mut Chart<'a>;
}
pub trait TickDisplayBuilder<'a> {
fn y_tick_display(&'a mut self, density: TickDisplay) -> &'a mut Chart<'a>;
}
impl<'a> Default for Chart<'a> {
fn default() -> Self {
Self::new(120, 60, -10.0, 10.0)
}
}
#[derive(Clone, Copy)]
pub enum LineStyle {
None,
Solid,
Dotted,
Dashed,
}
pub enum LabelFormat {
None,
Value,
Custom(Box<dyn Fn(f32) -> String>),
}
pub enum TickDisplay {
None,
Sparse,
Dense,
}
impl TickDisplay {
fn get_row_spacing(&self) -> u32 {
match self {
TickDisplay::None => u32::MAX, TickDisplay::Sparse => 4,
TickDisplay::Dense => 2,
}
}
}
impl<'a> Display for Chart<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let mut frame = self.canvas.frame().replace(' ', "\u{2800}");
if let Some(idx) = frame.find('\n') {
let xmin = self.format_x_axis_tick(self.xmin);
let xmax = self.format_x_axis_tick(self.xmax);
frame.insert_str(idx, &format!(" {0}", self.format_y_axis_tick(self.ymax)));
match self.y_tick_display {
TickDisplay::None => {}
TickDisplay::Sparse | TickDisplay::Dense => {
let row_spacing: u32 = self.y_tick_display.get_row_spacing(); let num_steps: u32 = (self.height / 4) / row_spacing; let step_size = (self.ymax - self.ymin) / (num_steps) as f32;
for i in 1..(num_steps) {
if let Some(index) = frame
.match_indices('\n')
.collect::<Vec<(usize, &str)>>()
.get((i * row_spacing) as usize)
{
frame.insert_str(
index.0,
&format!(
" {0}",
self.format_y_axis_tick(self.ymax - (step_size * i as f32))
),
);
}
}
}
}
frame.push_str(&format!(
" {0}\n{1: <width$}{2}\n",
self.format_y_axis_tick(self.ymin),
xmin,
xmax,
width = (self.width as usize) / 2 - xmax.len()
));
}
write!(f, "{}", frame)
}
}
impl<'a> Chart<'a> {
pub fn new(width: u32, height: u32, xmin: f32, xmax: f32) -> Self {
if width < 32 {
panic!("width should be at least 32");
}
if height < 3 {
panic!("height should be at least 3");
}
Self {
xmin,
xmax,
ymin: f32::INFINITY,
ymax: f32::NEG_INFINITY,
y_ranging: ChartRangeMethod::AutoRange,
width,
height,
shapes: Vec::new(),
canvas: BrailleCanvas::new(width, height),
x_style: LineStyle::Dotted,
y_style: LineStyle::Dotted,
x_label_format: LabelFormat::Value,
y_label_format: LabelFormat::Value,
y_tick_display: TickDisplay::None,
}
}
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 at least 32");
}
if height < 3 {
panic!("height should be at least 3");
}
Self {
xmin,
xmax,
ymin,
ymax,
y_ranging: ChartRangeMethod::FixedRange,
width,
height,
shapes: Vec::new(),
canvas: BrailleCanvas::new(width, height),
x_style: LineStyle::Dotted,
y_style: LineStyle::Dotted,
x_label_format: LabelFormat::Value,
y_label_format: LabelFormat::Value,
y_tick_display: TickDisplay::None,
}
}
pub fn borders(&mut self) {
let w = self.width;
let h = self.height;
self.vline(0, LineStyle::Dotted);
self.vline(w, LineStyle::Dotted);
self.hline(0, LineStyle::Dotted);
self.hline(h, LineStyle::Dotted);
}
fn vline(&mut self, i: u32, mode: LineStyle) {
match mode {
LineStyle::None => {}
LineStyle::Solid => {
if i <= self.width {
for j in 0..=self.height {
self.canvas.set(i, j);
}
}
}
LineStyle::Dotted => {
if i <= self.width {
for j in 0..=self.height {
if j % 3 == 0 {
self.canvas.set(i, j);
}
}
}
}
LineStyle::Dashed => {
if i <= self.width {
for j in 0..=self.height {
if j % 4 == 0 {
self.canvas.set(i, j);
self.canvas.set(i, j + 1);
}
}
}
}
}
}
fn hline(&mut self, j: u32, mode: LineStyle) {
match mode {
LineStyle::None => {}
LineStyle::Solid => {
if j <= self.height {
for i in 0..=self.width {
self.canvas.set(i, self.height - j);
}
}
}
LineStyle::Dotted => {
if j <= self.height {
for i in 0..=self.width {
if i % 3 == 0 {
self.canvas.set(i, self.height - j);
}
}
}
}
LineStyle::Dashed => {
if j <= self.height {
for i in 0..=self.width {
if i % 4 == 0 {
self.canvas.set(i, self.height - j);
self.canvas.set(i + 1, self.height - j);
}
}
}
}
}
}
pub fn display(&mut self) {
self.axis();
self.figures();
println!("{}", self);
}
pub fn nice(&mut self) {
self.borders();
self.display();
}
pub fn axis(&mut self) {
self.x_axis();
self.y_axis();
}
pub fn x_axis(&mut self) {
let y_scale = Scale::new(self.ymin..self.ymax, 0.0..self.height as f32);
if self.ymin <= 0.0 && self.ymax >= 0.0 {
self.hline(y_scale.linear(0.0) as u32, self.x_style);
}
}
pub fn y_axis(&mut self) {
let x_scale = Scale::new(self.xmin..self.xmax, 0.0..self.width as f32);
if self.xmin <= 0.0 && self.xmax >= 0.0 {
self.vline(x_scale.linear(0.0) as u32, self.y_style);
}
}
fn format_x_axis_tick(&self, value: f32) -> String {
match &self.x_label_format {
LabelFormat::None => "".to_owned(),
LabelFormat::Value => format!("{:.1}", value),
LabelFormat::Custom(f) => f(value),
}
}
fn format_y_axis_tick(&self, value: f32) -> String {
match &self.y_label_format {
LabelFormat::None => "".to_owned(),
LabelFormat::Value => format!("{:.1}", value),
LabelFormat::Custom(f) => f(value),
}
}
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 {
let color = rgb_to_pixelcolor(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 {
if let Some(color) = color {
let color = rgb_to_pixelcolor(color);
self.canvas.set_colored(x, y, color);
} else {
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 {
let color = rgb_to_pixelcolor(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 {
let color = rgb_to_pixelcolor(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: RGB8) -> &'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
}
}
fn rgb_to_pixelcolor(rgb: &RGB8) -> PixelColor {
PixelColor::TrueColor {
r: rgb.r,
g: rgb.g,
b: rgb.b,
}
}
impl<'a> AxisBuilder<'a> for Chart<'a> {
fn x_axis_style(&'a mut self, style: LineStyle) -> &'a mut Chart {
self.x_style = style;
self
}
fn y_axis_style(&'a mut self, style: LineStyle) -> &'a mut Chart {
self.y_style = style;
self
}
}
impl<'a> LabelBuilder<'a> for Chart<'a> {
fn x_label_format(&mut self, format: LabelFormat) -> &mut Self {
self.x_label_format = format;
self
}
fn y_label_format(&mut self, format: LabelFormat) -> &mut Self {
self.y_label_format = format;
self
}
}
impl<'a> TickDisplayBuilder<'a> for Chart<'a> {
fn y_tick_display(&mut self, density: TickDisplay) -> &mut Self {
match density {
TickDisplay::None => {}
TickDisplay::Sparse => {
self.height = if self.height < 16 {
16
} else {
((self.height + 8) / 16) * 16
}
}
TickDisplay::Dense => {
self.height = if self.height < 8 {
8
} else {
((self.height + 4) / 8) * 8
}
}
}
self.y_tick_display = density;
self
}
}