use super::types::ChartType;
#[derive(Clone, Debug)]
pub struct ChartSeries {
pub name: String,
pub values: Vec<f64>,
}
impl ChartSeries {
pub fn new(name: &str, values: Vec<f64>) -> Self {
ChartSeries {
name: name.to_string(),
values,
}
}
pub fn len(&self) -> usize {
self.values.len()
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub enum LegendPosition {
#[default]
Right,
Left,
Top,
TopRight,
Bottom,
}
impl LegendPosition {
pub fn as_str(self) -> &'static str {
match self {
LegendPosition::Right => "r",
LegendPosition::Left => "l",
LegendPosition::Top => "t",
LegendPosition::TopRight => "tr",
LegendPosition::Bottom => "b",
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub enum LabelPosition {
#[default]
BestFit,
Center,
InsideBase,
InsideEnd,
OutsideEnd,
Left,
Right,
Top,
Bottom,
}
impl LabelPosition {
pub fn as_str(self) -> &'static str {
match self {
LabelPosition::BestFit => "bestFit",
LabelPosition::Center => "ctr",
LabelPosition::InsideBase => "inBase",
LabelPosition::InsideEnd => "inEnd",
LabelPosition::OutsideEnd => "outEnd",
LabelPosition::Left => "l",
LabelPosition::Right => "r",
LabelPosition::Top => "t",
LabelPosition::Bottom => "b",
}
}
}
#[derive(Clone, Debug, Default)]
pub struct LegendConfig {
pub show: bool,
pub position: LegendPosition,
}
impl LegendConfig {
pub fn shown() -> Self {
Self {
show: true,
position: LegendPosition::Right,
}
}
pub fn hidden() -> Self {
Self {
show: false,
position: LegendPosition::Right,
}
}
pub fn with_position(mut self, position: LegendPosition) -> Self {
self.position = position;
self
}
pub fn with_show(mut self, show: bool) -> Self {
self.show = show;
self
}
}
#[derive(Clone, Debug, Default)]
pub struct AxisConfig {
pub min: Option<f64>,
pub max: Option<f64>,
pub major_unit: Option<f64>,
pub minor_unit: Option<f64>,
pub log_scale: bool,
pub number_format: Option<String>,
pub tick_label_size_pt: Option<u32>,
pub tick_label_bold: bool,
pub tick_label_rotation_deg: Option<i32>,
}
impl AxisConfig {
pub fn with_min(mut self, min: f64) -> Self {
self.min = Some(min);
self
}
pub fn with_max(mut self, max: f64) -> Self {
self.max = Some(max);
self
}
pub fn with_major_unit(mut self, unit: f64) -> Self {
self.major_unit = Some(unit);
self
}
pub fn with_minor_unit(mut self, unit: f64) -> Self {
self.minor_unit = Some(unit);
self
}
pub fn with_log_scale(mut self, base: u32) -> Self {
self.log_scale = (2..=99).contains(&base);
self
}
pub fn with_number_format(mut self, format: impl Into<String>) -> Self {
self.number_format = Some(format.into());
self
}
pub fn with_tick_label_size_pt(mut self, pts: u32) -> Self {
self.tick_label_size_pt = Some(pts.max(8));
self
}
pub fn with_tick_label_bold(mut self, bold: bool) -> Self {
self.tick_label_bold = bold;
self
}
pub fn with_tick_label_rotation(mut self, deg: i32) -> Self {
let d = deg.rem_euclid(360);
self.tick_label_rotation_deg = Some(if d > 180 { d - 360 } else { d });
self
}
}
#[derive(Clone, Debug, Default)]
pub struct DataLabelsConfig {
pub show_value: bool,
pub show_category_name: bool,
pub show_percentage: bool,
pub show_series_name: bool,
pub position: LabelPosition,
}
impl DataLabelsConfig {
pub fn with_show_value(mut self, show: bool) -> Self {
self.show_value = show;
self
}
pub fn with_show_category_name(mut self, show: bool) -> Self {
self.show_category_name = show;
self
}
pub fn with_show_percentage(mut self, show: bool) -> Self {
self.show_percentage = show;
self
}
pub fn with_show_series_name(mut self, show: bool) -> Self {
self.show_series_name = show;
self
}
pub fn with_position(mut self, position: LabelPosition) -> Self {
self.position = position;
self
}
pub fn show_values() -> Self {
Self {
show_value: true,
position: LabelPosition::InsideEnd,
..Default::default()
}
}
pub fn show_percentages_outside() -> Self {
Self {
show_percentage: true,
position: LabelPosition::OutsideEnd,
..Default::default()
}
}
}
#[derive(Clone, Debug)]
pub struct Chart {
pub title: String,
pub chart_type: ChartType,
pub categories: Vec<String>,
pub series: Vec<ChartSeries>,
pub x: u32, pub y: u32, pub width: u32, pub height: u32, pub legend: LegendConfig,
pub value_axis: AxisConfig,
pub category_axis: AxisConfig,
pub data_labels: DataLabelsConfig,
}
impl Chart {
pub fn new(
title: &str,
chart_type: ChartType,
categories: Vec<String>,
x: u32,
y: u32,
width: u32,
height: u32,
) -> Self {
Chart {
title: title.to_string(),
chart_type,
categories,
series: Vec::new(),
x,
y,
width,
height,
legend: LegendConfig::shown(),
value_axis: AxisConfig::default(),
category_axis: AxisConfig::default(),
data_labels: DataLabelsConfig::default(),
}
}
pub fn add_series(mut self, series: ChartSeries) -> Self {
self.series.push(series);
self
}
pub fn category_count(&self) -> usize {
self.categories.len()
}
pub fn series_count(&self) -> usize {
self.series.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chart_series() {
let series = ChartSeries::new("Sales", vec![10.0, 20.0, 30.0]);
assert_eq!(series.name, "Sales");
assert_eq!(series.len(), 3);
assert!(!series.is_empty());
}
#[test]
fn test_chart_add_series() {
let chart = Chart::new(
"Test",
ChartType::Pie,
vec!["A".to_string()],
0,
0,
1000000,
1000000,
)
.add_series(ChartSeries::new("Data", vec![50.0]));
assert_eq!(chart.series_count(), 1);
}
#[test]
fn test_legend_position_str() {
assert_eq!(LegendPosition::Bottom.as_str(), "b");
assert_eq!(LegendPosition::Top.as_str(), "t");
assert_eq!(LegendPosition::Right.as_str(), "r");
assert_eq!(LegendPosition::Left.as_str(), "l");
assert_eq!(LegendPosition::TopRight.as_str(), "tr");
}
#[test]
fn test_label_position_str() {
assert_eq!(LabelPosition::BestFit.as_str(), "bestFit");
assert_eq!(LabelPosition::Center.as_str(), "ctr");
assert_eq!(LabelPosition::InsideEnd.as_str(), "inEnd");
assert_eq!(LabelPosition::OutsideEnd.as_str(), "outEnd");
}
#[test]
fn test_axis_log_scale_rejects_out_of_range() {
let cfg = AxisConfig::default().with_log_scale(1);
assert!(!cfg.log_scale);
let cfg = AxisConfig::default().with_log_scale(10);
assert!(cfg.log_scale);
let cfg = AxisConfig::default().with_log_scale(100);
assert!(!cfg.log_scale);
}
#[test]
fn test_axis_tick_label_rotation_wraps() {
let cfg = AxisConfig::default().with_tick_label_rotation(720);
assert_eq!(cfg.tick_label_rotation_deg, Some(0));
let cfg = AxisConfig::default().with_tick_label_rotation(225);
assert_eq!(cfg.tick_label_rotation_deg, Some(-135));
}
#[test]
fn test_data_labels_presets() {
let v = DataLabelsConfig::show_values();
assert!(v.show_value);
assert!(!v.show_percentage);
assert_eq!(v.position, LabelPosition::InsideEnd);
let p = DataLabelsConfig::show_percentages_outside();
assert!(p.show_percentage);
assert_eq!(p.position, LabelPosition::OutsideEnd);
}
#[test]
fn test_chart_default_legend_is_shown_right() {
let chart = Chart::new(
"Test",
ChartType::Bar,
vec!["A".to_string()],
0,
0,
1000000,
1000000,
);
assert!(chart.legend.show);
assert_eq!(chart.legend.position, LegendPosition::Right);
}
}