ravenrs_sheet/chart/
sparkline.rs1use crate::model::SparklineKind;
4
5use crate::chart::render::{DOWN, finalize};
6use crate::chart::resolve::MAX_POINTS;
7use crate::chart::{Prim, Rgba};
8
9pub fn render_sparkline(kind: SparklineKind, values: &[Option<f64>], color: Rgba, markers: bool, w: f32, h: f32) -> Vec<Prim> {
12 if !(w.is_finite() && h.is_finite()) || w < 1.0 || h < 1.0 || w > 1e6 || h > 1e6 {
13 return vec![];
14 }
15 let vals: Vec<Option<f64>> = values.iter().take(MAX_POINTS).map(|v| v.filter(|x| x.is_finite()).map(|x| x.clamp(-1e300, 1e300))).collect();
16 let n = vals.len();
17 if n == 0 {
18 return vec![];
19 }
20 let pad = (h * 0.1).clamp(0.5, 2.0);
21 let (x0, y0, iw, ih) = (pad, pad, (w - 2.0 * pad).max(0.5), (h - 2.0 * pad).max(0.5));
22 let mut out = Vec::new();
23 let (lo, hi) = vals.iter().flatten().fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), v| (a.min(*v), b.max(*v)));
24 if !lo.is_finite() {
25 return out;
26 }
27 match kind {
28 SparklineKind::Line => {
29 let span = hi - lo;
30 let fy = |v: f64| {
31 let f = if span > 0.0 { ((v - lo) / span) as f32 } else { 0.5 };
32 y0 + ih - f * ih
33 };
34 let fx = |j: usize| if n > 1 { x0 + j as f32 / (n - 1) as f32 * iw } else { x0 + iw / 2.0 };
35 let mut run: Vec<[f32; 2]> = Vec::new();
36 let flush = |run: &mut Vec<[f32; 2]>, out: &mut Vec<Prim>| {
37 if run.len() >= 2 {
38 let pts = if run.len() > 4000 { run.iter().step_by(run.len() / 2000 + 1).copied().collect() } else { std::mem::take(run) };
39 out.push(Prim::Line { pts, color, width: 1.25, dash: false });
40 }
41 run.clear();
42 };
43 for (j, v) in vals.iter().enumerate() {
44 match v {
45 Some(v) => run.push([fx(j), fy(*v)]),
46 None => flush(&mut run, &mut out),
47 }
48 }
49 flush(&mut run, &mut out);
50 if markers || n == 1 {
51 let r = (h * 0.08).clamp(1.0, 3.0);
52 let step = (n / 500).max(1);
53 for (j, v) in vals.iter().enumerate().step_by(step) {
54 if let Some(v) = v {
55 out.push(Prim::Circle { cx: fx(j), cy: fy(*v), r, fill: Some(color), stroke: None });
56 }
57 }
58 }
59 }
60 SparklineKind::Column | SparklineKind::WinLoss => {
61 let slot = iw / n as f32;
62 let bw = (slot * 0.8).max(0.25);
63 let win = kind == SparklineKind::WinLoss;
64 let (lo0, hi0) = if win { (-1.0, 1.0) } else { (lo.min(0.0), hi.max(0.0)) };
65 let span = hi0 - lo0;
66 let fy = |v: f64| if span > 0.0 { y0 + ih - ((v - lo0) / span) as f32 * ih } else { y0 + ih };
67 let base = fy(0.0);
68 for (j, v) in vals.iter().enumerate() {
69 let Some(v) = v else { continue };
70 let v = if win { v.signum() * (*v != 0.0) as i32 as f64 } else { *v };
71 if v == 0.0 && win {
72 continue;
73 }
74 let top = fy(v);
75 let (y, hh) = (top.min(base), (top - base).abs().max(0.5));
76 let fill = if v < 0.0 { DOWN } else { color };
77 out.push(Prim::Rect { x: x0 + j as f32 * slot + (slot - bw) / 2.0, y, w: bw, h: hh, fill: Some(fill), stroke: None, radius: 0.0 });
78 }
79 }
80 }
81 finalize(out, w, h)
82}