1use serde::{Deserialize, Serialize};
4
5use crate::prompt::Tone;
6
7pub const MAX_BARS: usize = 15;
9pub const MAX_COLUMNS: usize = 12;
11pub const MAX_LINE_POINTS: usize = 60;
13pub const MAX_SERIES: usize = 3;
15pub const MAX_CHART_LABEL: usize = 60;
17pub const MAX_CHART_VALUE: f64 = 1e15;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
22#[serde(rename_all = "kebab-case")]
23#[non_exhaustive]
24pub enum ChartKind {
25 #[default]
27 Bar,
28 Column,
30 Line,
32}
33
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36pub struct Series {
37 #[serde(default)]
39 pub name: Option<String>,
40 pub values: Vec<Option<f64>>,
42}
43
44#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
46pub struct Chart {
47 #[serde(default)]
49 pub kind: ChartKind,
50 #[serde(default)]
52 pub title: Option<String>,
53 #[serde(default)]
55 pub unit: Option<String>,
56 pub labels: Vec<String>,
58 pub series: Vec<Series>,
60 #[serde(default)]
62 pub highlight: Option<usize>,
63 #[serde(default)]
65 pub tone: Tone,
66 #[serde(default)]
69 pub locale: Option<String>,
70}
71
72impl Chart {
73 pub fn validate(&self) -> Result<(), String> {
75 let (max_labels, max_series) = match self.kind {
76 ChartKind::Bar => (MAX_BARS, 1),
77 ChartKind::Column => (MAX_COLUMNS, MAX_SERIES),
78 ChartKind::Line => (MAX_LINE_POINTS, MAX_SERIES),
79 };
80 let kind = match self.kind {
81 ChartKind::Bar => "bar",
82 ChartKind::Column => "column",
83 ChartKind::Line => "line",
84 };
85 let min_labels = if self.kind == ChartKind::Line { 2 } else { 1 };
86 if self.labels.len() < min_labels || self.labels.len() > max_labels {
87 return Err(format!(
88 "a {kind} chart has {min_labels} to {max_labels} labels"
89 ));
90 }
91 if self.series.is_empty() || self.series.len() > max_series {
92 return Err(format!("a {kind} chart has 1 to {max_series} series"));
93 }
94 for label in &self.labels {
95 label_ok("chart label", label)?;
96 }
97 if self.series.len() > 1 && self.series.iter().any(|s| s.name.is_none()) {
98 return Err("series need names when there are several".into());
99 }
100 for s in &self.series {
101 if let Some(name) = &s.name {
102 label_ok("series name", name)?;
103 }
104 if s.values.len() != self.labels.len() {
105 return Err("each series has one value per label".into());
106 }
107 if s.values
108 .iter()
109 .flatten()
110 .any(|v| !v.is_finite() || v.abs() > MAX_CHART_VALUE)
111 {
112 return Err(format!(
113 "chart values are numbers no larger than {MAX_CHART_VALUE:e} in size"
114 ));
115 }
116 }
117 if self
118 .series
119 .iter()
120 .all(|s| s.values.iter().all(Option::is_none))
121 {
122 return Err("a chart needs at least one value".into());
123 }
124 if let Some(title) = &self.title {
125 label_ok("chart title", title)?;
126 }
127 if let Some(unit) = &self.unit
128 && unit.chars().count() > 30
129 {
130 return Err("chart unit: at most 30 characters".into());
131 }
132 if self.highlight.is_some_and(|i| i >= self.labels.len()) {
133 return Err("the highlight must be one of the labels".into());
134 }
135 Ok(())
136 }
137}
138
139fn label_ok(what: &str, text: &str) -> Result<(), String> {
140 if text.trim().is_empty() {
141 return Err(format!("{what}s must not be empty"));
142 }
143 if text.chars().count() > MAX_CHART_LABEL {
144 return Err(format!("{what}: at most {MAX_CHART_LABEL} characters"));
145 }
146 Ok(())
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
151pub struct Image {
152 pub name: String,
154 pub alt: String,
156 pub width: u32,
158 pub height: u32,
160 #[serde(skip)]
163 pub png: Vec<u8>,
164}
165
166#[derive(Debug, thiserror::Error)]
168#[non_exhaustive]
169pub enum ChartError {
170 #[error("invalid chart: {0}")]
172 Invalid(String),
173 #[error("no font for {0:?}")]
175 Glyphs(String),
176 #[error("cannot draw the chart: {0}")]
178 Failed(String),
179}
180
181pub trait ChartRenderer: Send + Sync {
184 fn render(&self, chart: &Chart) -> Result<Image, ChartError>;
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 fn chart(kind: ChartKind, labels: &[&str], series: Vec<Vec<Option<f64>>>) -> Chart {
193 Chart {
194 kind,
195 labels: labels.iter().map(|l| (*l).to_string()).collect(),
196 series: series
197 .into_iter()
198 .enumerate()
199 .map(|(i, values)| Series {
200 name: Some(format!("s{i}")),
201 values,
202 })
203 .collect(),
204 ..Chart::default()
205 }
206 }
207
208 #[test]
209 fn limits_by_kind() {
210 let one = chart(ChartKind::Bar, &["a", "b"], vec![vec![Some(1.0), None]]);
211 assert_eq!(one.validate(), Ok(()));
212 let two = chart(
213 ChartKind::Bar,
214 &["a"],
215 vec![vec![Some(1.0)], vec![Some(2.0)]],
216 );
217 assert!(two.validate().is_err());
218 let column = Chart {
219 kind: ChartKind::Column,
220 ..two
221 };
222 assert_eq!(column.validate(), Ok(()));
223 let line = chart(ChartKind::Line, &["a"], vec![vec![Some(1.0)]]);
224 assert!(line.validate().is_err(), "a line needs two points");
225 let bad = chart(ChartKind::Bar, &["a"], vec![vec![Some(f64::NAN)]]);
226 assert!(bad.validate().is_err());
227 let empty = chart(ChartKind::Bar, &["a"], vec![vec![None]]);
228 assert!(empty.validate().is_err());
229 let short = chart(ChartKind::Bar, &["a", "b"], vec![vec![Some(1.0)]]);
230 assert!(short.validate().is_err());
231 }
232}