use crate::base::{Point, Rgba};
use crate::canvas::{fill_v, DotCanvas};
use crate::layout::{Dimension, Style as LayoutStyle};
use crate::theme::TokenSet;
use crate::ui::Element;
#[path = "chart_time.rs"]
mod time;
pub use time::{TimeSeries, TimeSeriesState};
#[cfg(test)]
use crate::canvas::braille_bit;
fn finite_range(series: &[Vec<f32>]) -> Option<(f32, f32)> {
let mut range: Option<(f32, f32)> = None;
for s in series {
for v in s.iter().copied().filter(|v| v.is_finite()) {
range = Some(match range {
None => (v, v),
Some((lo, hi)) => (lo.min(v), hi.max(v)),
});
}
}
range
}
fn value_to_row(v: f32, lo: f32, hi: f32, rows: i32) -> i32 {
let norm = if hi > lo { (v - lo) / (hi - lo) } else { 0.5 };
let r = ((1.0 - norm.clamp(0.0, 1.0)) * (rows - 1) as f32).round() as i32;
r.clamp(0, rows - 1)
}
pub struct Sparkline {
data: Vec<f32>,
slot: usize,
range: Option<(f32, f32)>,
time_axis: Option<std::time::Duration>,
layout: Option<LayoutStyle>,
}
impl Sparkline {
pub fn new(data: Vec<f32>) -> Sparkline {
Sparkline {
data,
slot: 0,
range: None,
time_axis: None,
layout: None,
}
}
pub fn slot(mut self, slot: usize) -> Sparkline {
self.slot = slot;
self
}
pub fn range(mut self, lo: f32, hi: f32) -> Sparkline {
self.range = Some((lo, hi));
self
}
pub fn time_axis(mut self, span: std::time::Duration) -> Sparkline {
self.time_axis = Some(span);
self
}
pub fn layout(mut self, layout: LayoutStyle) -> Sparkline {
self.layout = Some(layout);
self
}
pub fn view(self, cx: crate::reactive::Scope) -> crate::ui::View {
let t = crate::widgets::theme_tokens(cx);
self.element(&t).build()
}
pub fn element(self, t: &TokenSet) -> Element {
let color = t.chart(self.slot);
let label_fg = t.text_faint;
let data = self.data;
let fixed = self.range;
let span = self.time_axis;
let default_h = if span.is_some() { 2 } else { 1 };
let layout = self.layout.unwrap_or_else(|| {
LayoutStyle::default()
.height(Dimension::Cells(default_h))
.grow(1.0)
});
Element::new().style(layout).draw(move |canvas, rect| {
if rect.w <= 0 || rect.h <= 0 || data.is_empty() {
return;
}
let series = [data.clone()];
let (lo, hi) = match fixed.or_else(|| finite_range(&series)) {
Some(r) => r,
None => return,
};
let mut grid = DotCanvas::braille(rect.w, 1);
let cols = grid.dots_w();
let n = data.len() as i32;
let mut prev: Option<(i32, i32)> = None;
for c in 0..cols {
let i = (c as i64 * n as i64 / cols as i64) as usize;
let v = data[i.min(data.len() - 1)];
if !v.is_finite() {
prev = None; continue;
}
let y = value_to_row(v, lo, hi, 4);
match prev {
Some(p) => grid.line(p, (c, y)),
None => grid.set(c, y),
}
prev = Some((c, y));
}
grid.blit(canvas, Point::new(rect.x, rect.y), color);
if let Some(span) = span {
if rect.h >= 2 {
time::draw_time_labels(
canvas,
crate::base::Rect::new(rect.x, rect.y + 1, rect.w, 1),
span,
label_fg,
);
}
}
})
}
}
pub struct LineChart {
series: Vec<Vec<f32>>,
range: Option<(f32, f32)>,
axes: bool,
time_axis: Option<std::time::Duration>,
layout: Option<LayoutStyle>,
}
impl LineChart {
pub fn new(series: Vec<Vec<f32>>) -> LineChart {
LineChart {
series,
range: None,
axes: true,
time_axis: None,
layout: None,
}
}
pub fn range(mut self, lo: f32, hi: f32) -> LineChart {
self.range = Some((lo, hi));
self
}
pub fn axes(mut self, on: bool) -> LineChart {
self.axes = on;
self
}
pub fn time_axis(mut self, span: std::time::Duration) -> LineChart {
self.time_axis = Some(span);
self
}
pub fn layout(mut self, layout: LayoutStyle) -> LineChart {
self.layout = Some(layout);
self
}
pub fn view(self, cx: crate::reactive::Scope) -> crate::ui::View {
let t = crate::widgets::theme_tokens(cx);
self.element(&t).build()
}
pub fn element(self, t: &TokenSet) -> Element {
let colors: Vec<Rgba> = (0..self.series.len().max(1)).map(|i| t.chart(i)).collect();
let axis = t.border;
let label_fg = t.text_faint;
let series = self.series;
let fixed = self.range;
let axes = self.axes;
let time_span = self.time_axis;
let layout = self
.layout
.unwrap_or_else(|| LayoutStyle::default().grow(1.0));
Element::new().style(layout).draw(move |canvas, rect| {
if rect.w <= 0 || rect.h <= 0 {
return;
}
let (lo, hi) = match fixed.or_else(|| finite_range(&series)) {
Some(r) => r,
None => return,
};
let label_w = if axes { max_label_width(lo, hi) + 1 } else { 0 };
let plot = if axes && rect.w > label_w + 2 && rect.h >= 2 {
crate::base::Rect::new(
rect.x + label_w + 1,
rect.y,
rect.w - label_w - 1,
rect.h - 1,
)
} else {
rect
};
let drew_axes = plot != rect;
if drew_axes {
for y in rect.y..plot.bottom() {
canvas.put(Point::new(plot.x - 1, y), '│', axis, Rgba::TRANSPARENT);
}
for x in plot.x - 1..rect.right() {
canvas.put(Point::new(x, plot.bottom()), '─', axis, Rgba::TRANSPARENT);
}
canvas.put(
Point::new(plot.x - 1, plot.bottom()),
'└',
axis,
Rgba::TRANSPARENT,
);
canvas.print(
Point::new(rect.x, rect.y),
&fmt_label(hi),
label_fg,
Rgba::TRANSPARENT,
);
canvas.print(
Point::new(rect.x, plot.bottom() - 1),
&fmt_label(lo),
label_fg,
Rgba::TRANSPARENT,
);
if let Some(span) = time_span {
time::draw_time_labels(
canvas,
crate::base::Rect::new(plot.x, plot.bottom(), plot.w, 1),
span,
label_fg,
);
}
}
for (si, data) in series.iter().enumerate() {
if data.is_empty() {
continue;
}
let mut grid = DotCanvas::braille(plot.w, plot.h);
let cols = grid.dots_w();
let rows = grid.dots_h();
let n = data.len() as i32;
let mut prev: Option<(i32, i32)> = None;
for c in 0..cols {
let i = (c as i64 * n as i64 / cols as i64) as usize;
let v = data[i.min(data.len() - 1)];
if !v.is_finite() {
prev = None;
continue;
}
let y = value_to_row(v, lo, hi, rows);
match prev {
Some(p) => grid.line(p, (c, y)),
None => grid.set(c, y),
}
prev = Some((c, y));
}
let color = colors[si.min(colors.len() - 1)];
grid.blit(canvas, Point::new(plot.x, plot.y), color);
}
})
}
}
fn fmt_label(v: f32) -> String {
if v.abs() >= 100.0 || v.fract() == 0.0 {
format!("{v:.0}")
} else {
format!("{v:.1}")
}
}
fn max_label_width(lo: f32, hi: f32) -> i32 {
fmt_label(lo)
.chars()
.count()
.max(fmt_label(hi).chars().count()) as i32
}
pub struct BarChart {
values: Vec<f32>,
range: Option<(f32, f32)>,
slot: Option<usize>,
bar_w: i32,
gap: i32,
layout: Option<LayoutStyle>,
}
impl BarChart {
pub fn new(values: Vec<f32>) -> BarChart {
BarChart {
values,
range: None,
slot: None,
bar_w: 2,
gap: 1,
layout: None,
}
}
pub fn slot(mut self, slot: usize) -> BarChart {
self.slot = Some(slot);
self
}
pub fn range(mut self, lo: f32, hi: f32) -> BarChart {
self.range = Some((lo, hi));
self
}
pub fn bar(mut self, width: i32, gap: i32) -> BarChart {
self.bar_w = width.max(1);
self.gap = gap.max(0);
self
}
pub fn layout(mut self, layout: LayoutStyle) -> BarChart {
self.layout = Some(layout);
self
}
pub fn view(self, cx: crate::reactive::Scope) -> crate::ui::View {
let t = crate::widgets::theme_tokens(cx);
self.element(&t).build()
}
pub fn element(self, t: &TokenSet) -> Element {
let colors: Vec<Rgba> = match self.slot {
Some(s) => vec![t.chart(s)],
None => (0..8).map(|i| t.chart(i)).collect(),
};
let values = self.values;
let fixed = self.range;
let (bar_w, gap) = (self.bar_w, self.gap);
let layout = self
.layout
.unwrap_or_else(|| LayoutStyle::default().grow(1.0));
Element::new().style(layout).draw(move |canvas, rect| {
if rect.w <= 0 || rect.h <= 0 || values.is_empty() {
return;
}
let hi = match fixed {
Some((_, hi)) => hi,
None => {
let m = values
.iter()
.copied()
.filter(|v| v.is_finite())
.fold(f32::MIN, f32::max);
if m == f32::MIN {
return;
}
m.max(f32::EPSILON)
}
};
let lo = fixed.map(|(lo, _)| lo).unwrap_or(0.0);
let span = (hi - lo).max(f32::EPSILON);
let mut x = rect.x;
for (i, v) in values.iter().enumerate() {
if x >= rect.right() {
break;
}
if !v.is_finite() {
x += bar_w + gap;
continue;
}
let norm = ((v - lo) / span).clamp(0.0, 1.0);
let color = colors[i % colors.len()];
fill_v(
canvas,
crate::base::Rect::new(x, rect.y, bar_w.min(rect.right() - x), rect.h),
norm,
color,
Rgba::TRANSPARENT,
);
x += bar_w + gap;
}
})
}
}
#[cfg(test)]
#[path = "chart_tests.rs"]
mod tests;