1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
10pub struct ChartSeries {
11 #[serde(default)]
12 pub name: String,
13 #[serde(default)]
14 pub values: Vec<f64>,
15 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub tone: Option<String>,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
20pub struct Extent {
21 pub min: f64,
22 pub max: f64,
23}
24
25pub fn chart_extent(kind: &str, series: &[ChartSeries], min: Option<f64>, max: Option<f64>) -> Extent {
29 let values: Vec<f64> = series.iter().flat_map(|s| s.values.iter().copied().filter(|v| v.is_finite())).collect();
30 let mut lo = if kind == "sparkline" { values.iter().copied().fold(f64::INFINITY, f64::min) } else { values.iter().copied().fold(0.0, f64::min) };
31 let mut hi = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
32 if kind == "stackedBar" {
33 let n = series.iter().map(|s| s.values.len()).max().unwrap_or(0);
34 hi = f64::NEG_INFINITY;
35 for i in 0..n {
36 let sum: f64 = series.iter().map(|s| s.values.get(i).copied().filter(|v| v.is_finite()).map(|v| v.max(0.0)).unwrap_or(0.0)).sum();
37 hi = hi.max(sum);
38 }
39 }
40 if !lo.is_finite() {
41 lo = 0.0;
42 }
43 if !hi.is_finite() {
44 hi = 0.0;
45 }
46 let lo = min.unwrap_or(lo);
47 let mut hi = max.unwrap_or(hi);
48 if hi <= lo {
49 hi = lo + 1.0;
50 }
51 Extent { min: lo, max: hi }
52}
53
54fn nice_num(x: f64, round: bool) -> f64 {
55 let exp = x.log10().floor();
56 let f = x / 10f64.powf(exp);
57 let nf = if round {
58 if f < 1.5 {
59 1.0
60 } else if f < 3.0 {
61 2.0
62 } else if f < 7.0 {
63 5.0
64 } else {
65 10.0
66 }
67 } else if f <= 1.0 {
68 1.0
69 } else if f <= 2.0 {
70 2.0
71 } else if f <= 5.0 {
72 5.0
73 } else {
74 10.0
75 };
76 nf * 10f64.powf(exp)
77}
78
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80pub struct Ticks {
81 pub min: f64,
82 pub max: f64,
83 pub step: f64,
84 pub ticks: Vec<f64>,
85}
86
87fn fix(v: f64, decimals: i32) -> f64 {
89 let s = format!("{:.*}", decimals.max(0) as usize, v);
90 s.parse().unwrap_or(v)
91}
92
93pub fn nice_ticks(min: f64, max: f64, target: usize) -> Ticks {
97 let lo = min.min(max);
98 let hi = if max == min { min + 1.0 } else { min.max(max) };
99 let range = nice_num(hi - lo, false);
100 let step = nice_num(range / (target.max(2) - 1) as f64, true);
101 let decimals = (-step.log10().floor()).max(0.0) as i32;
102 let nice_min = fix((lo / step).floor() * step, decimals);
103 let nice_max = fix((hi / step).ceil() * step, decimals);
104 let mut ticks = Vec::new();
105 let mut i = 0;
106 let mut v = nice_min;
107 while v <= nice_max + step / 2.0 && i < 100 {
108 ticks.push(fix(v, decimals));
109 i += 1;
110 v = nice_min + i as f64 * step;
111 }
112 Ticks { min: nice_min, max: nice_max, step, ticks }
113}
114
115pub fn series_color(index: usize, tone: Option<&str>) -> String {
118 match tone {
119 Some("danger") => "$color.destructive".into(),
120 Some("neutral") => "$color.mutedForeground".into(),
121 Some(t) => format!("$color.{t}"),
122 None => format!("$color.chart{}", index % 8 + 1),
123 }
124}
125
126pub const DONUT_HOLE: f64 = 0.6;
128pub const WINDOW_THRESHOLD: usize = crate::list::WINDOW_THRESHOLD;
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 fn s(values: &[f64]) -> ChartSeries {
136 ChartSeries { name: "s".into(), values: values.to_vec(), tone: None }
137 }
138
139 #[test]
140 fn extents_per_kind() {
141 assert_eq!(chart_extent("bar", &[s(&[3.0, 8.0])], None, None), Extent { min: 0.0, max: 8.0 });
142 assert_eq!(chart_extent("bar", &[s(&[-2.0, 8.0])], None, None), Extent { min: -2.0, max: 8.0 });
143 assert_eq!(chart_extent("stackedBar", &[s(&[3.0, 8.0]), s(&[4.0, 1.0])], None, None), Extent { min: 0.0, max: 9.0 });
144 assert_eq!(chart_extent("sparkline", &[s(&[3.0, 8.0])], None, None), Extent { min: 3.0, max: 8.0 });
145 assert_eq!(chart_extent("line", &[], None, None), Extent { min: 0.0, max: 1.0 });
146 assert_eq!(chart_extent("line", &[s(&[3.0])], Some(10.0), None), Extent { min: 10.0, max: 11.0 });
147 }
148
149 #[test]
150 fn nice_ticks_are_1_2_5_steps() {
151 let t = nice_ticks(0.0, 8.0, 5);
152 assert_eq!((t.min, t.max, t.step), (0.0, 8.0, 2.0));
153 assert_eq!(t.ticks, vec![0.0, 2.0, 4.0, 6.0, 8.0]);
154 let t = nice_ticks(0.0, 1.0, 5);
155 assert_eq!(t.step, 0.2);
156 assert_eq!(t.ticks, vec![0.0, 0.2, 0.4, 0.6, 0.8, 1.0]);
157 let t = nice_ticks(-3.0, 97.0, 5);
158 assert_eq!(t.ticks.first(), Some(&-20.0));
159 assert_eq!(series_color(9, None), "$color.chart2");
160 assert_eq!(series_color(0, Some("danger")), "$color.destructive");
161 }
162}