use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[non_exhaustive]
pub enum ChartType {
Bar,
Column,
Bar3D,
Column3D,
Line,
Line3D,
Pie,
Pie3D,
Doughnut,
OfPie,
Area,
Area3D,
Scatter,
Bubble,
Radar,
Surface,
Surface3D,
Stock,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
pub enum ChartGrouping {
#[default]
Clustered,
Stacked,
PercentStacked,
Standard,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub enum StockVariant {
Hlc,
Ohlc,
Vhlc,
Vohlc,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub enum BarShape {
Box,
Cylinder,
Cone,
Pyramid,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub enum ScatterStyle {
Dots,
LineMarker,
SmoothMarker,
Line,
Smooth,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub enum RadarStyle {
Standard,
Marker,
Filled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub enum OfPieType {
Pie,
Bar,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
pub enum LegendPosition {
#[default]
Right,
Bottom,
Top,
Left,
None,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub enum ChartData {
Category {
categories: Vec<String>,
series: Vec<ChartSeries>,
},
Xy {
series: Vec<XySeries>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ChartSeries {
pub name: String,
pub values: Vec<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct XySeries {
pub name: String,
pub x_values: Vec<f64>,
pub y_values: Vec<f64>,
}
impl ChartData {
pub fn category(cats: &[&str], series: &[(&str, &[f64])]) -> Self {
Self::Category {
categories: cats.iter().map(|s| (*s).to_string()).collect(),
series: series
.iter()
.map(|(name, vals)| ChartSeries {
name: (*name).to_string(),
values: vals.to_vec(),
})
.collect(),
}
}
pub fn xy(series: &[(&str, &[f64], &[f64])]) -> Self {
Self::Xy {
series: series
.iter()
.map(|(name, xs, ys)| XySeries {
name: (*name).to_string(),
x_values: xs.to_vec(),
y_values: ys.to_vec(),
})
.collect(),
}
}
pub fn has_no_series(&self) -> bool {
match self {
Self::Category { series, .. } => series.is_empty(),
Self::Xy { series } => series.is_empty(),
}
}
#[deprecated(since = "0.2.0", note = "Use `has_no_series()` instead")]
pub fn is_empty(&self) -> bool {
self.has_no_series()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chart_type_all_18_variants() {
let variants = [
ChartType::Bar,
ChartType::Column,
ChartType::Bar3D,
ChartType::Column3D,
ChartType::Line,
ChartType::Line3D,
ChartType::Pie,
ChartType::Pie3D,
ChartType::Doughnut,
ChartType::OfPie,
ChartType::Area,
ChartType::Area3D,
ChartType::Scatter,
ChartType::Bubble,
ChartType::Radar,
ChartType::Surface,
ChartType::Surface3D,
ChartType::Stock,
];
assert_eq!(variants.len(), 18);
for (i, a) in variants.iter().enumerate() {
for (j, b) in variants.iter().enumerate() {
if i != j {
assert_ne!(a, b, "variants {i} and {j} should be distinct");
}
}
}
}
#[test]
fn chart_data_category_convenience() {
let data = ChartData::category(
&["Q1", "Q2", "Q3", "Q4"],
&[("Sales", &[100.0, 150.0, 200.0, 250.0]), ("Costs", &[80.0, 90.0, 100.0, 110.0])],
);
match &data {
ChartData::Category { categories, series } => {
assert_eq!(categories.len(), 4);
assert_eq!(series.len(), 2);
assert_eq!(series[0].name, "Sales");
assert_eq!(series[1].values, &[80.0, 90.0, 100.0, 110.0]);
}
_ => panic!("expected Category"),
}
}
#[test]
fn chart_data_xy_convenience() {
let data = ChartData::xy(&[("Points", &[1.0, 2.0, 3.0], &[10.0, 20.0, 30.0])]);
match &data {
ChartData::Xy { series } => {
assert_eq!(series.len(), 1);
assert_eq!(series[0].name, "Points");
assert_eq!(series[0].x_values, &[1.0, 2.0, 3.0]);
assert_eq!(series[0].y_values, &[10.0, 20.0, 30.0]);
}
_ => panic!("expected Xy"),
}
}
#[test]
fn chart_data_has_no_series() {
let empty_cat = ChartData::category(&["A"], &[]);
assert!(empty_cat.has_no_series());
let non_empty = ChartData::category(&["A"], &[("S", &[1.0])]);
assert!(!non_empty.has_no_series());
let empty_xy = ChartData::Xy { series: vec![] };
assert!(empty_xy.has_no_series());
}
#[test]
#[allow(deprecated)]
fn chart_data_is_empty_deprecated_alias() {
let empty = ChartData::category(&["A"], &[]);
assert!(empty.is_empty());
assert_eq!(empty.is_empty(), empty.has_no_series());
}
#[test]
fn serde_roundtrip_chart_data() {
let data = ChartData::category(&["A", "B"], &[("S1", &[1.0, 2.0])]);
let json = serde_json::to_string(&data).unwrap();
let back: ChartData = serde_json::from_str(&json).unwrap();
assert_eq!(data, back);
}
#[test]
fn serde_roundtrip_xy_data() {
let data = ChartData::xy(&[("P", &[1.0, 2.0], &[3.0, 4.0])]);
let json = serde_json::to_string(&data).unwrap();
let back: ChartData = serde_json::from_str(&json).unwrap();
assert_eq!(data, back);
}
#[test]
fn chart_grouping_default() {
assert_eq!(ChartGrouping::default(), ChartGrouping::Clustered);
}
#[test]
fn legend_position_default() {
assert_eq!(LegendPosition::default(), LegendPosition::Right);
}
#[test]
fn chart_type_copy_clone() {
let ct = ChartType::Pie;
let ct2 = ct;
assert_eq!(ct, ct2);
}
}