use serde::{Deserialize, Serialize};
use crate::prompt::Tone;
pub const MAX_BARS: usize = 15;
pub const MAX_COLUMNS: usize = 12;
pub const MAX_LINE_POINTS: usize = 60;
pub const MAX_SERIES: usize = 3;
pub const MAX_CHART_LABEL: usize = 60;
pub const MAX_CHART_VALUE: f64 = 1e15;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum ChartKind {
#[default]
Bar,
Column,
Line,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Series {
#[serde(default)]
pub name: Option<String>,
pub values: Vec<Option<f64>>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct Chart {
#[serde(default)]
pub kind: ChartKind,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub unit: Option<String>,
pub labels: Vec<String>,
pub series: Vec<Series>,
#[serde(default)]
pub highlight: Option<usize>,
#[serde(default)]
pub tone: Tone,
#[serde(default)]
pub locale: Option<String>,
}
impl Chart {
pub fn validate(&self) -> Result<(), String> {
let (max_labels, max_series) = match self.kind {
ChartKind::Bar => (MAX_BARS, 1),
ChartKind::Column => (MAX_COLUMNS, MAX_SERIES),
ChartKind::Line => (MAX_LINE_POINTS, MAX_SERIES),
};
let kind = match self.kind {
ChartKind::Bar => "bar",
ChartKind::Column => "column",
ChartKind::Line => "line",
};
let min_labels = if self.kind == ChartKind::Line { 2 } else { 1 };
if self.labels.len() < min_labels || self.labels.len() > max_labels {
return Err(format!(
"a {kind} chart has {min_labels} to {max_labels} labels"
));
}
if self.series.is_empty() || self.series.len() > max_series {
return Err(format!("a {kind} chart has 1 to {max_series} series"));
}
for label in &self.labels {
label_ok("chart label", label)?;
}
if self.series.len() > 1 && self.series.iter().any(|s| s.name.is_none()) {
return Err("series need names when there are several".into());
}
for s in &self.series {
if let Some(name) = &s.name {
label_ok("series name", name)?;
}
if s.values.len() != self.labels.len() {
return Err("each series has one value per label".into());
}
if s.values
.iter()
.flatten()
.any(|v| !v.is_finite() || v.abs() > MAX_CHART_VALUE)
{
return Err(format!(
"chart values are numbers no larger than {MAX_CHART_VALUE:e} in size"
));
}
}
if self
.series
.iter()
.all(|s| s.values.iter().all(Option::is_none))
{
return Err("a chart needs at least one value".into());
}
if let Some(title) = &self.title {
label_ok("chart title", title)?;
}
if let Some(unit) = &self.unit
&& unit.chars().count() > 30
{
return Err("chart unit: at most 30 characters".into());
}
if self.highlight.is_some_and(|i| i >= self.labels.len()) {
return Err("the highlight must be one of the labels".into());
}
Ok(())
}
}
fn label_ok(what: &str, text: &str) -> Result<(), String> {
if text.trim().is_empty() {
return Err(format!("{what}s must not be empty"));
}
if text.chars().count() > MAX_CHART_LABEL {
return Err(format!("{what}: at most {MAX_CHART_LABEL} characters"));
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Image {
pub name: String,
pub alt: String,
pub width: u32,
pub height: u32,
#[serde(skip)]
pub png: Vec<u8>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ChartError {
#[error("invalid chart: {0}")]
Invalid(String),
#[error("no font for {0:?}")]
Glyphs(String),
#[error("cannot draw the chart: {0}")]
Failed(String),
}
pub trait ChartRenderer: Send + Sync {
fn render(&self, chart: &Chart) -> Result<Image, ChartError>;
}
#[cfg(test)]
mod tests {
use super::*;
fn chart(kind: ChartKind, labels: &[&str], series: Vec<Vec<Option<f64>>>) -> Chart {
Chart {
kind,
labels: labels.iter().map(|l| (*l).to_string()).collect(),
series: series
.into_iter()
.enumerate()
.map(|(i, values)| Series {
name: Some(format!("s{i}")),
values,
})
.collect(),
..Chart::default()
}
}
#[test]
fn limits_by_kind() {
let one = chart(ChartKind::Bar, &["a", "b"], vec![vec![Some(1.0), None]]);
assert_eq!(one.validate(), Ok(()));
let two = chart(
ChartKind::Bar,
&["a"],
vec![vec![Some(1.0)], vec![Some(2.0)]],
);
assert!(two.validate().is_err());
let column = Chart {
kind: ChartKind::Column,
..two
};
assert_eq!(column.validate(), Ok(()));
let line = chart(ChartKind::Line, &["a"], vec![vec![Some(1.0)]]);
assert!(line.validate().is_err(), "a line needs two points");
let bad = chart(ChartKind::Bar, &["a"], vec![vec![Some(f64::NAN)]]);
assert!(bad.validate().is_err());
let empty = chart(ChartKind::Bar, &["a"], vec![vec![None]]);
assert!(empty.validate().is_err());
let short = chart(ChartKind::Bar, &["a", "b"], vec![vec![Some(1.0)]]);
assert!(short.validate().is_err());
}
}