1use crate::canvas::Canvas;
3use crate::{Chart, Error, Limits, Result, check_size};
4
5const COLORS: [[u8; 3]; 6] = [[31, 119, 180], [214, 39, 40], [44, 160, 44], [255, 127, 14], [148, 103, 189], [140, 86, 75]];
6const NAMES: [&str; 6] = ["blue", "red", "green", "orange", "purple", "brown"];
7
8fn check(c: &Chart, l: &Limits) -> Result<()> {
9 if c.series.len() > l.max_series {
10 return Err(Error::TooLarge(format!("{} series (at most {})", c.series.len(), l.max_series)));
11 }
12 let most = c.series.iter().map(|s| s.values.len()).chain([c.x.len()]).max().unwrap_or(0);
13 if most > l.max_points {
14 return Err(Error::TooLarge(format!("{most} points (at most {})", l.max_points)));
15 }
16 Ok(())
17}
18
19pub fn chart_png(c: &Chart, width: usize, height: usize, limits: &Limits) -> Result<Vec<u8>> {
22 check_size(width, height, limits)?;
23 check(c, limits)?;
24 let (x, lines, w, h) = (&c.x, &c.series, width, height);
25 let xs: Vec<f64> = x.iter().enumerate().map(|(i, v)| v.filter(|v| v.is_finite()).unwrap_or(i as f64)).collect();
26 let all: Vec<f64> = lines.iter().flat_map(|l| l.values.iter().flatten().copied()).filter(|v| v.is_finite()).collect();
27 if all.is_empty() || xs.is_empty() {
28 return Err(Error::Empty);
29 }
30 let (lo, hi) = all.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), v| (a.min(*v), b.max(*v)));
31 let (xl, xh) = xs.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), v| (a.min(*v), b.max(*v)));
32 let (hi, lo) = if hi > lo { (hi, lo) } else { (hi + 1.0, lo - 1.0) };
33 let xh = if xh > xl { xh } else { xl + 1.0 };
34 let mut c = Canvas::new(w, h, [255; 3]);
35 let m = 12i64;
36 let (pw, ph) = (w as i64 - 2 * m, h as i64 - 2 * m);
37 let grey = [200, 200, 200];
38 for (a, b) in [((m, m), (m + pw, m)), ((m, m + ph), (m + pw, m + ph)), ((m, m), (m, m + ph)), ((m + pw, m), (m + pw, m + ph))] {
39 c.line(a, b, grey);
40 }
41 for (k, l) in lines.iter().enumerate() {
42 let col = COLORS[k % COLORS.len()];
43 let mut prev: Option<(i64, i64)> = None;
44 for (i, v) in l.values.iter().enumerate() {
45 let Some(v) = v.filter(|v| v.is_finite()) else {
46 prev = None;
47 continue;
48 };
49 let px = m + ((xs.get(i).copied().unwrap_or(i as f64) - xl) / (xh - xl) * pw as f64).round() as i64;
50 let py = m + ph - ((v - lo) / (hi - lo) * ph as f64).round() as i64;
51 match prev {
52 Some(p) => c.line(p, (px, py), col),
53 None => c.set(px, py, col),
54 }
55 prev = Some((px, py));
56 }
57 }
58 c.png()
59}
60
61pub fn legend(c: &Chart) -> String {
63 c.series.iter().enumerate().map(|(i, l)| format!("{} ({})", l.name, NAMES[i % NAMES.len()])).collect::<Vec<_>>().join(", ")
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use crate::Series;
70
71 fn decode(b: &[u8]) -> (u32, u32, Vec<u8>) {
72 let mut r = ::png::Decoder::new(std::io::Cursor::new(b)).read_info().unwrap();
73 let mut buf = vec![0; r.output_buffer_size().unwrap()];
74 let i = r.next_frame(&mut buf).unwrap();
75 buf.truncate(i.buffer_size());
76 (i.width, i.height, buf)
77 }
78
79 fn sample() -> Chart {
80 Chart {
81 x: vec![Some(0.0), Some(1.0), Some(2.0)],
82 series: vec![
83 Series { name: "a".into(), values: vec![Some(1.0), None, Some(3.0)] },
84 Series { name: "b".into(), values: vec![Some(2.0); 3] },
85 ],
86 }
87 }
88
89 #[test]
90 fn charts_draw_as_png() {
91 let l = Limits::default();
92 let png = chart_png(&sample(), 200, 100, &l).unwrap();
93 let (w, h, px) = decode(&png);
94 assert_eq!((w, h), (200, 100));
95 let at = |x: usize, y: usize| px[(y * 200 + x) * 3..][..3].to_vec();
96 assert_eq!(at(0, 0), vec![255, 255, 255], "background");
97 assert_eq!(at(12, 12), vec![200, 200, 200], "frame corner");
98 assert_eq!(at(100, 50), vec![214, 39, 40]);
100 assert_eq!(at(12, 88), vec![31, 119, 180]);
102 assert_eq!(legend(&sample()), "a (blue), b (red)");
103 }
104
105 #[test]
106 fn nothing_finite_is_empty_and_limits_hold() {
107 let l = Limits::default();
108 let none =
109 Chart { x: vec![Some(0.0); 3], series: vec![Series { name: "n".into(), values: vec![None, Some(f64::NAN), Some(f64::INFINITY)] }] };
110 assert_eq!(chart_png(&none, 10, 10, &l), Err(Error::Empty));
111 assert_eq!(chart_png(&Chart::default(), 10, 10, &l), Err(Error::Empty));
112 assert!(matches!(chart_png(&sample(), 0, 10, &l), Err(Error::Invalid(_))));
113 assert!(matches!(chart_png(&sample(), 9_000, 10, &l), Err(Error::TooLarge(_))));
114 let small = Limits { max_pixels: 100, ..l };
115 assert!(matches!(chart_png(&sample(), 20, 20, &small), Err(Error::TooLarge(_))));
116 let few = Limits { max_points: 2, ..l };
117 assert!(matches!(chart_png(&sample(), 20, 20, &few), Err(Error::TooLarge(_))));
118 let one = Limits { max_series: 1, ..l };
119 assert!(matches!(chart_png(&sample(), 20, 20, &one), Err(Error::TooLarge(_))));
120 }
121
122 #[test]
123 fn missing_x_values_fall_back_to_the_index() {
124 let l = Limits::default();
125 let mut c = sample();
126 let a = chart_png(&c, 64, 48, &l).unwrap();
127 c.x = vec![None, Some(f64::NAN), Some(2.0)];
128 assert_eq!(chart_png(&c, 64, 48, &l).unwrap(), a);
129 }
130}