use crate::canvas::Canvas;
use crate::{Chart, Error, Limits, Result, check_size};
const COLORS: [[u8; 3]; 6] = [[31, 119, 180], [214, 39, 40], [44, 160, 44], [255, 127, 14], [148, 103, 189], [140, 86, 75]];
const NAMES: [&str; 6] = ["blue", "red", "green", "orange", "purple", "brown"];
fn check(c: &Chart, l: &Limits) -> Result<()> {
if c.series.len() > l.max_series {
return Err(Error::TooLarge(format!("{} series (at most {})", c.series.len(), l.max_series)));
}
let most = c.series.iter().map(|s| s.values.len()).chain([c.x.len()]).max().unwrap_or(0);
if most > l.max_points {
return Err(Error::TooLarge(format!("{most} points (at most {})", l.max_points)));
}
Ok(())
}
pub fn chart_png(c: &Chart, width: usize, height: usize, limits: &Limits) -> Result<Vec<u8>> {
check_size(width, height, limits)?;
check(c, limits)?;
let (x, lines, w, h) = (&c.x, &c.series, width, height);
let xs: Vec<f64> = x.iter().enumerate().map(|(i, v)| v.filter(|v| v.is_finite()).unwrap_or(i as f64)).collect();
let all: Vec<f64> = lines.iter().flat_map(|l| l.values.iter().flatten().copied()).filter(|v| v.is_finite()).collect();
if all.is_empty() || xs.is_empty() {
return Err(Error::Empty);
}
let (lo, hi) = all.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), v| (a.min(*v), b.max(*v)));
let (xl, xh) = xs.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), v| (a.min(*v), b.max(*v)));
let (hi, lo) = if hi > lo { (hi, lo) } else { (hi + 1.0, lo - 1.0) };
let xh = if xh > xl { xh } else { xl + 1.0 };
let mut c = Canvas::new(w, h, [255; 3]);
let m = 12i64;
let (pw, ph) = (w as i64 - 2 * m, h as i64 - 2 * m);
let grey = [200, 200, 200];
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))] {
c.line(a, b, grey);
}
for (k, l) in lines.iter().enumerate() {
let col = COLORS[k % COLORS.len()];
let mut prev: Option<(i64, i64)> = None;
for (i, v) in l.values.iter().enumerate() {
let Some(v) = v.filter(|v| v.is_finite()) else {
prev = None;
continue;
};
let px = m + ((xs.get(i).copied().unwrap_or(i as f64) - xl) / (xh - xl) * pw as f64).round() as i64;
let py = m + ph - ((v - lo) / (hi - lo) * ph as f64).round() as i64;
match prev {
Some(p) => c.line(p, (px, py), col),
None => c.set(px, py, col),
}
prev = Some((px, py));
}
}
c.png()
}
pub fn legend(c: &Chart) -> String {
c.series.iter().enumerate().map(|(i, l)| format!("{} ({})", l.name, NAMES[i % NAMES.len()])).collect::<Vec<_>>().join(", ")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Series;
fn decode(b: &[u8]) -> (u32, u32, Vec<u8>) {
let mut r = ::png::Decoder::new(std::io::Cursor::new(b)).read_info().unwrap();
let mut buf = vec![0; r.output_buffer_size().unwrap()];
let i = r.next_frame(&mut buf).unwrap();
buf.truncate(i.buffer_size());
(i.width, i.height, buf)
}
fn sample() -> Chart {
Chart {
x: vec![Some(0.0), Some(1.0), Some(2.0)],
series: vec![
Series { name: "a".into(), values: vec![Some(1.0), None, Some(3.0)] },
Series { name: "b".into(), values: vec![Some(2.0); 3] },
],
}
}
#[test]
fn charts_draw_as_png() {
let l = Limits::default();
let png = chart_png(&sample(), 200, 100, &l).unwrap();
let (w, h, px) = decode(&png);
assert_eq!((w, h), (200, 100));
let at = |x: usize, y: usize| px[(y * 200 + x) * 3..][..3].to_vec();
assert_eq!(at(0, 0), vec![255, 255, 255], "background");
assert_eq!(at(12, 12), vec![200, 200, 200], "frame corner");
assert_eq!(at(100, 50), vec![214, 39, 40]);
assert_eq!(at(12, 88), vec![31, 119, 180]);
assert_eq!(legend(&sample()), "a (blue), b (red)");
}
#[test]
fn nothing_finite_is_empty_and_limits_hold() {
let l = Limits::default();
let none =
Chart { x: vec![Some(0.0); 3], series: vec![Series { name: "n".into(), values: vec![None, Some(f64::NAN), Some(f64::INFINITY)] }] };
assert_eq!(chart_png(&none, 10, 10, &l), Err(Error::Empty));
assert_eq!(chart_png(&Chart::default(), 10, 10, &l), Err(Error::Empty));
assert!(matches!(chart_png(&sample(), 0, 10, &l), Err(Error::Invalid(_))));
assert!(matches!(chart_png(&sample(), 9_000, 10, &l), Err(Error::TooLarge(_))));
let small = Limits { max_pixels: 100, ..l };
assert!(matches!(chart_png(&sample(), 20, 20, &small), Err(Error::TooLarge(_))));
let few = Limits { max_points: 2, ..l };
assert!(matches!(chart_png(&sample(), 20, 20, &few), Err(Error::TooLarge(_))));
let one = Limits { max_series: 1, ..l };
assert!(matches!(chart_png(&sample(), 20, 20, &one), Err(Error::TooLarge(_))));
}
#[test]
fn missing_x_values_fall_back_to_the_index() {
let l = Limits::default();
let mut c = sample();
let a = chart_png(&c, 64, 48, &l).unwrap();
c.x = vec![None, Some(f64::NAN), Some(2.0)];
assert_eq!(chart_png(&c, 64, 48, &l).unwrap(), a);
}
}