use crate::cli::theme::{ERR, OK, SECONDARY};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::widgets::Widget;
use scope::metrics::{Threshold, Unit};
const BARS: [&str; 8] = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
const STEPS_PER_ROW: u32 = 8;
const THRESHOLD_GLYPH: &str = "─";
const PASS_GLYPH: &str = "✓";
const FAIL_GLYPH: &str = "✗";
pub struct Sparkline<'a> {
values: &'a [Option<f64>],
threshold: Option<Threshold>,
style: Style,
track: bool,
min_span: Option<f64>,
}
impl<'a> Sparkline<'a> {
pub fn new(values: &'a [Option<f64>], style: Style) -> Self {
Self {
values,
threshold: None,
style,
track: false,
min_span: None,
}
}
pub fn min_span(mut self, span: f64) -> Self {
self.min_span = Some(span).filter(|s| s.is_finite() && *s > 0.0);
self
}
pub fn track(mut self) -> Self {
self.track = true;
self
}
pub fn threshold(mut self, threshold: Option<Threshold>) -> Self {
self.threshold = threshold;
self
}
}
pub(crate) fn min_span(unit: Unit, values: &[Option<f64>]) -> f64 {
let latest = values
.iter()
.rev()
.flatten()
.next()
.copied()
.unwrap_or(0.0)
.abs();
match unit {
Unit::Price | Unit::Native => (latest * 0.001).max(1e-9),
Unit::Percent => 0.05,
Unit::Quote | Unit::Usd => (latest * 0.05).max(1.0),
Unit::Count => 1.0,
Unit::Score => 0.5,
Unit::Flag => 1.0,
}
}
pub(crate) fn enlarge_hint(cols: u16, rows: u16, what: &str) -> String {
let n = |k: u16, one: &str, many: &str| format!("{k} {}", if k == 1 { one } else { many });
let by = match (cols, rows) {
(0, r) => format!("{} taller", n(r, "row", "rows")),
(c, 0) => format!("{} wider", n(c, "column", "columns")),
(c, r) => format!(
"{} wider and {} taller",
n(c, "column", "columns"),
n(r, "row", "rows")
),
};
format!("! Window too small: enlarge it {by} to see {what}")
}
fn threshold_value(t: Threshold) -> f64 {
match t {
Threshold::Max(v) | Threshold::Min(v) | Threshold::Target(v) => v,
}
}
fn newest<T>(items: &[T], width: usize) -> &[T] {
&items[items.len().saturating_sub(width)..]
}
impl Widget for Sparkline<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.is_empty() {
return;
}
let shown = newest(self.values, area.width as usize);
let line = self
.threshold
.map(threshold_value)
.filter(|v| v.is_finite());
let known = shown.iter().flatten().copied().filter(|v| v.is_finite());
let (mut lo, mut hi) = known.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), v| {
(lo.min(v), hi.max(v))
});
if let Some(t) = line.filter(|_| !self.track) {
lo = lo.min(t);
hi = hi.max(t);
}
if !lo.is_finite() {
return; }
if let Some(span) = self.min_span
&& hi - lo < span
{
let mid = (lo + hi) / 2.0;
lo = mid - span / 2.0;
hi = mid + span / 2.0;
}
let steps = u32::from(area.height) * STEPS_PER_ROW;
let range = hi - lo;
let height_of = |v: f64| -> u32 {
if range <= 0.0 {
return steps / 2;
}
let h = ((v - lo) / range * f64::from(steps - 1)).round() as u32 + 1;
h.min(steps)
};
let x0 = if self.track {
area.left()
} else {
area.right() - shown.len() as u16
};
if self.track {
let row = area.bottom() - 1;
for x in x0 + shown.len() as u16..area.right() {
buf[(x, row)]
.set_symbol(THRESHOLD_GLYPH)
.set_style(SECONDARY);
}
}
for (i, value) in shown.iter().enumerate() {
let x = x0 + i as u16;
let Some(v) = value.filter(|v| v.is_finite()) else {
continue; };
let style = match (self.track, self.threshold) {
(true, Some(Threshold::Max(t))) if v > t => ERR,
(true, Some(Threshold::Min(t))) if v < t => ERR,
_ => self.style,
};
let mut left = height_of(v);
for row in (area.top()..area.bottom()).rev() {
if left == 0 {
break;
}
let eighths = left.min(STEPS_PER_ROW);
buf[(x, row)]
.set_symbol(BARS[eighths as usize - 1])
.set_style(style);
left -= eighths;
}
}
if let Some(t) = line.filter(|_| !self.track) {
let rows_up = height_of(t).saturating_sub(1) / STEPS_PER_ROW;
let row = area.bottom() - 1 - rows_up as u16;
for x in area.left()..area.right() {
let cell = &mut buf[(x, row)];
if cell.symbol() == " " {
cell.set_symbol(THRESHOLD_GLYPH).set_style(SECONDARY);
}
}
}
}
}
pub struct HealthStrip<'a> {
runs: &'a [bool],
track: bool,
}
impl<'a> HealthStrip<'a> {
pub fn new(runs: &'a [bool]) -> Self {
Self { runs, track: false }
}
pub fn track(mut self) -> Self {
self.track = true;
self
}
}
impl Widget for HealthStrip<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.is_empty() {
return;
}
let shown = newest(self.runs, area.width as usize);
let x0 = if self.track {
for x in area.left() + shown.len() as u16..area.right() {
buf[(x, area.top())]
.set_symbol(THRESHOLD_GLYPH)
.set_style(SECONDARY);
}
area.left()
} else {
area.right() - shown.len() as u16
};
for (i, healthy) in shown.iter().enumerate() {
let (glyph, style) = if *healthy {
(PASS_GLYPH, OK)
} else {
(FAIL_GLYPH, ERR)
};
buf[(x0 + i as u16, area.top())]
.set_symbol(glyph)
.set_style(style);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn draw(widget: impl Widget, width: u16, height: u16) -> Vec<String> {
let area = Rect::new(0, 0, width, height);
let mut buf = Buffer::empty(area);
widget.render(area, &mut buf);
(0..height)
.map(|y| {
(0..width)
.map(|x| buf[(x, y)].symbol().to_string())
.collect()
})
.collect()
}
#[test]
fn test_track_fills_from_the_left_then_scrolls() {
let values = [Some(1.0), Some(2.0)];
let rows = draw(Sparkline::new(&values, Style::new()).track(), 6, 1);
assert_eq!(rows[0], "▁█────", "{rows:?}");
let values: Vec<Option<f64>> = (1..=8).map(|v| Some(f64::from(v))).collect();
let rows = draw(Sparkline::new(&values, Style::new()).track(), 4, 1);
assert!(!rows[0].contains('─'), "a full track scrolls: {rows:?}");
assert!(
rows[0].ends_with('█'),
"the newest at the right edge: {rows:?}"
);
}
#[test]
fn test_track_colors_a_bar_outside_its_limit_and_draws_no_threshold_line() {
let values = [Some(1.0), Some(5.0)];
let area = Rect::new(0, 0, 4, 2);
let mut buf = Buffer::empty(area);
Sparkline::new(&values, OK)
.threshold(Some(Threshold::Max(3.0)))
.track()
.render(area, &mut buf);
assert_eq!(buf[(0, 1)].style().fg, OK.fg, "within the limit");
assert_eq!(buf[(1, 1)].style().fg, ERR.fg, "above the max");
assert_eq!(buf[(2, 0)].symbol(), " ");
assert_eq!(buf[(2, 1)].symbol(), "─");
}
#[test]
fn test_noise_under_the_min_span_reads_flat() {
let values = [Some(1000.0), Some(1001.0), Some(1000.5)];
let rows = draw(Sparkline::new(&values, Style::new()).min_span(100.0), 3, 1);
let heights: Vec<usize> = rows[0]
.chars()
.map(|c| BARS.iter().position(|b| b.starts_with(c)).unwrap())
.collect();
assert!(
heights.iter().all(|h| (3..=4).contains(h)),
"near the middle: {rows:?}"
);
let values = [Some(1000.0), Some(1500.0)];
let rows = draw(Sparkline::new(&values, Style::new()).min_span(100.0), 2, 1);
assert_eq!(rows[0], "▁█", "{rows:?}");
}
#[test]
fn test_health_strip_track_fills_from_the_left() {
let rows = draw(HealthStrip::new(&[true, false]).track(), 5, 1);
assert_eq!(rows[0], "✓✗───");
}
#[test]
fn test_a_missing_value_is_a_gap_not_a_zero() {
let values = [Some(1.0), None, Some(3.0)];
let rows = draw(Sparkline::new(&values, Style::new()), 3, 1);
let cells: Vec<char> = rows[0].chars().collect();
assert_eq!(cells[1], ' ', "{rows:?}");
assert_ne!(cells[0], ' ');
assert_eq!(cells[2], '█', "the top value fills its cell");
}
#[test]
fn test_only_the_newest_values_that_fit_are_drawn() {
let values = [Some(100.0), Some(1.0), Some(2.0)];
let rows = draw(Sparkline::new(&values, Style::new()), 2, 1);
assert_eq!(rows[0], "▁█", "{rows:?}");
}
#[test]
fn test_the_threshold_line_fills_the_empty_cells_of_its_row() {
let values = [Some(0.0), Some(10.0), Some(0.0)];
let chart = Sparkline::new(&values, Style::new()).threshold(Some(Threshold::Max(8.0)));
let rows = draw(chart, 3, 2);
assert_eq!(rows[0], "─█─", "{rows:?}");
assert!(!rows[1].contains('─'), "{rows:?}");
}
#[test]
fn test_the_scale_includes_a_threshold_beyond_the_values() {
let values = [Some(10.0), Some(12.0)];
let chart = Sparkline::new(&values, Style::new()).threshold(Some(Threshold::Min(100.0)));
let rows = draw(chart, 2, 2);
assert_eq!(rows[0], "──", "{rows:?}");
assert!(rows[1].chars().all(|c| c != '█'), "{rows:?}");
}
#[test]
fn test_no_values_draw_nothing() {
let values = [None, None];
let rows = draw(Sparkline::new(&values, Style::new()), 2, 1);
assert_eq!(rows[0], " ");
}
#[test]
fn test_the_health_strip_marks_each_run_newest_last() {
let runs = [true, false, true, true];
let rows = draw(HealthStrip::new(&runs), 3, 1);
assert_eq!(rows[0], "✗✓✓");
}
}