use crate::style::{Color, Style};
mod axis;
mod bar;
mod braille;
mod grid;
mod render;
pub(crate) use bar::build_histogram_config;
pub(crate) use render::render_chart;
use axis::{build_tui_ticks, format_number, resolve_bounds, TickSpec};
use bar::draw_bar_dataset;
use braille::draw_braille_dataset;
use grid::{
apply_grid, build_legend_items, build_x_tick_col_map, build_y_tick_row_map, center_text,
map_value_to_cell, marker_char, overlay_legend_on_plot, sturges_bin_count, GridSpec,
};
const BRAILLE_BASE: u32 = 0x2800;
pub(crate) const BRAILLE_LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
pub(crate) const BRAILLE_RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
const PALETTE: [Color; 8] = [
Color::Cyan,
Color::Yellow,
Color::Green,
Color::Magenta,
Color::Red,
Color::Blue,
Color::White,
Color::Indexed(208),
];
const BLOCK_FRACTIONS: [char; 9] = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
pub type ColorSpan = (usize, usize, Color);
pub type RenderedLine = (String, Vec<ColorSpan>);
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Marker {
Braille,
Dot,
Block,
HalfBlock,
Cross,
Circle,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GraphType {
Line,
Area,
Scatter,
Bar,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LegendPosition {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
None,
}
#[derive(Debug, Clone)]
pub struct Axis {
pub title: Option<String>,
pub bounds: Option<(f64, f64)>,
pub labels: Option<Vec<String>>,
pub ticks: Option<Vec<f64>>,
pub title_style: Option<Style>,
pub style: Style,
}
impl Default for Axis {
fn default() -> Self {
Self {
title: None,
bounds: None,
labels: None,
ticks: None,
title_style: None,
style: Style::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct Dataset {
pub name: String,
pub data: Vec<(f64, f64)>,
pub color: Color,
pub marker: Marker,
pub graph_type: GraphType,
pub up_color: Option<Color>,
pub down_color: Option<Color>,
}
#[derive(Debug, Clone, Copy)]
pub struct Candle {
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
}
#[derive(Debug, Clone)]
pub struct ChartConfig {
pub title: Option<String>,
pub title_style: Option<Style>,
pub x_axis: Axis,
pub y_axis: Axis,
pub datasets: Vec<Dataset>,
pub legend: LegendPosition,
pub grid: bool,
pub grid_style: Option<Style>,
pub hlines: Vec<(f64, Style)>,
pub vlines: Vec<(f64, Style)>,
pub frame_visible: bool,
pub x_axis_visible: bool,
pub y_axis_visible: bool,
pub width: u32,
pub height: u32,
}
#[derive(Debug, Clone)]
pub(crate) struct ChartRow {
pub segments: Vec<(String, Style)>,
}
#[derive(Debug, Clone)]
#[must_use = "configure histogram before rendering"]
pub struct HistogramBuilder {
pub bins: Option<usize>,
pub color: Color,
pub x_title: Option<String>,
pub y_title: Option<String>,
}
impl Default for HistogramBuilder {
fn default() -> Self {
Self {
bins: None,
color: Color::Cyan,
x_title: None,
y_title: None,
}
}
}
impl HistogramBuilder {
pub fn bins(&mut self, bins: usize) -> &mut Self {
self.bins = Some(bins.max(1));
self
}
pub fn color(&mut self, color: Color) -> &mut Self {
self.color = color;
self
}
pub fn xlabel(&mut self, title: &str) -> &mut Self {
self.x_title = Some(title.to_string());
self
}
pub fn ylabel(&mut self, title: &str) -> &mut Self {
self.y_title = Some(title.to_string());
self
}
}
#[derive(Debug, Clone)]
pub struct DatasetEntry {
dataset: Dataset,
color_overridden: bool,
}
impl DatasetEntry {
pub fn label(&mut self, name: &str) -> &mut Self {
self.dataset.name = name.to_string();
self
}
pub fn color(&mut self, color: Color) -> &mut Self {
self.dataset.color = color;
self.color_overridden = true;
self
}
pub fn marker(&mut self, marker: Marker) -> &mut Self {
self.dataset.marker = marker;
self
}
pub fn color_by_direction(&mut self, up: Color, down: Color) -> &mut Self {
self.dataset.up_color = Some(up);
self.dataset.down_color = Some(down);
self
}
}
#[derive(Debug, Clone)]
#[must_use = "configure chart before rendering"]
pub struct ChartBuilder {
config: ChartConfig,
entries: Vec<DatasetEntry>,
}
impl ChartBuilder {
pub fn new(width: u32, height: u32, x_style: Style, y_style: Style) -> Self {
Self {
config: ChartConfig {
title: None,
title_style: None,
x_axis: Axis {
style: x_style,
..Axis::default()
},
y_axis: Axis {
style: y_style,
..Axis::default()
},
datasets: Vec::new(),
legend: LegendPosition::TopRight,
grid: true,
grid_style: None,
hlines: Vec::new(),
vlines: Vec::new(),
frame_visible: false,
x_axis_visible: true,
y_axis_visible: true,
width,
height,
},
entries: Vec::new(),
}
}
pub fn title(&mut self, title: &str) -> &mut Self {
self.config.title = Some(title.to_string());
self
}
pub fn xlabel(&mut self, label: &str) -> &mut Self {
self.config.x_axis.title = Some(label.to_string());
self
}
pub fn ylabel(&mut self, label: &str) -> &mut Self {
self.config.y_axis.title = Some(label.to_string());
self
}
pub fn xlim(&mut self, min: f64, max: f64) -> &mut Self {
self.config.x_axis.bounds = Some((min, max));
self
}
pub fn ylim(&mut self, min: f64, max: f64) -> &mut Self {
self.config.y_axis.bounds = Some((min, max));
self
}
pub fn xticks(&mut self, values: &[f64]) -> &mut Self {
self.config.x_axis.ticks = Some(values.to_vec());
self
}
pub fn yticks(&mut self, values: &[f64]) -> &mut Self {
self.config.y_axis.ticks = Some(values.to_vec());
self
}
pub fn xtick_labels(&mut self, values: &[f64], labels: &[&str]) -> &mut Self {
self.config.x_axis.ticks = Some(values.to_vec());
self.config.x_axis.labels = Some(labels.iter().map(|label| (*label).to_string()).collect());
self
}
pub fn ytick_labels(&mut self, values: &[f64], labels: &[&str]) -> &mut Self {
self.config.y_axis.ticks = Some(values.to_vec());
self.config.y_axis.labels = Some(labels.iter().map(|label| (*label).to_string()).collect());
self
}
pub fn title_style(&mut self, style: Style) -> &mut Self {
self.config.title_style = Some(style);
self
}
pub fn grid_style(&mut self, style: Style) -> &mut Self {
self.config.grid_style = Some(style);
self
}
pub fn x_axis_style(&mut self, style: Style) -> &mut Self {
self.config.x_axis.style = style;
self
}
pub fn y_axis_style(&mut self, style: Style) -> &mut Self {
self.config.y_axis.style = style;
self
}
pub fn axhline(&mut self, y: f64, style: Style) -> &mut Self {
self.config.hlines.push((y, style));
self
}
pub fn axvline(&mut self, x: f64, style: Style) -> &mut Self {
self.config.vlines.push((x, style));
self
}
pub fn grid(&mut self, on: bool) -> &mut Self {
self.config.grid = on;
self
}
pub fn frame(&mut self, on: bool) -> &mut Self {
self.config.frame_visible = on;
self
}
pub fn x_axis_visible(&mut self, on: bool) -> &mut Self {
self.config.x_axis_visible = on;
self
}
pub fn y_axis_visible(&mut self, on: bool) -> &mut Self {
self.config.y_axis_visible = on;
self
}
pub fn legend(&mut self, position: LegendPosition) -> &mut Self {
self.config.legend = position;
self
}
pub fn line(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
self.push_dataset(data, GraphType::Line, Marker::Braille)
}
pub fn area(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
self.push_dataset(data, GraphType::Area, Marker::Braille)
}
pub fn scatter(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
self.push_dataset(data, GraphType::Scatter, Marker::Braille)
}
pub fn bar(&mut self, data: &[(f64, f64)]) -> &mut DatasetEntry {
self.push_dataset(data, GraphType::Bar, Marker::Block)
}
pub fn build(mut self) -> ChartConfig {
for (index, mut entry) in self.entries.drain(..).enumerate() {
if !entry.color_overridden {
entry.dataset.color = PALETTE[index % PALETTE.len()];
}
self.config.datasets.push(entry.dataset);
}
self.config
}
fn push_dataset(
&mut self,
data: &[(f64, f64)],
graph_type: GraphType,
marker: Marker,
) -> &mut DatasetEntry {
let series_name = format!("Series {}", self.entries.len() + 1);
self.entries.push(DatasetEntry {
dataset: Dataset {
name: series_name,
data: data.to_vec(),
color: Color::Reset,
marker,
graph_type,
up_color: None,
down_color: None,
},
color_overridden: false,
});
let last_index = self.entries.len().saturating_sub(1);
&mut self.entries[last_index]
}
}
#[derive(Debug, Clone)]
pub struct ChartRenderer {
config: ChartConfig,
}
impl ChartRenderer {
pub fn new(config: ChartConfig) -> Self {
Self { config }
}
pub fn render(&self) -> Vec<RenderedLine> {
let rows = render_chart(&self.config);
rows.into_iter()
.map(|row| {
let mut line = String::new();
let mut spans: Vec<(usize, usize, Color)> = Vec::new();
let mut cursor = 0usize;
for (segment, style) in row.segments {
let width = unicode_width::UnicodeWidthStr::width(segment.as_str());
line.push_str(&segment);
if let Some(color) = style.fg {
spans.push((cursor, cursor + width, color));
}
cursor += width;
}
(line, spans)
})
.collect()
}
}