#[cfg(feature = "visualization")]
use plotters::prelude::*;
#[cfg(feature = "visualization")]
use std::collections::HashMap;
#[cfg(feature = "visualization")]
use std::path::Path;
use crate::error::{PandRSError, Result};
#[cfg(feature = "visualization")]
use crate::vis::config::{OutputType, PlotKind, PlotSettings};
#[cfg(feature = "visualization")]
pub use self::backend::plot_boxplot_png;
#[cfg(feature = "visualization")]
pub use self::backend::plot_boxplot_svg;
#[cfg(feature = "visualization")]
pub use self::backend::plot_histogram_png;
#[cfg(feature = "visualization")]
pub use self::backend::plot_histogram_svg;
#[cfg(feature = "visualization")]
pub use self::backend::plot_multi_series_png;
#[cfg(feature = "visualization")]
pub use self::backend::plot_multi_series_svg;
#[cfg(feature = "visualization")]
pub use self::backend::plot_series_xy_png;
#[cfg(feature = "visualization")]
pub use self::backend::plot_series_xy_svg;
#[cfg(feature = "visualization")]
pub mod backend {
use super::*;
pub fn plot_series_xy_png<P: AsRef<Path>>(
x: &[f64],
y: &[f64],
path: P,
settings: &PlotSettings,
series_name: &str,
) -> Result<()> {
let root = BitMapBackend::new(path.as_ref(), (settings.width, settings.height))
.into_drawing_area();
root.fill(&WHITE)?;
let x_min = x.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let x_max = x.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let y_min = y.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let y_max = y.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let x_range = x_max - x_min;
let y_range = y_max - y_min;
let x_min = x_min - x_range * 0.05;
let x_max = x_max + x_range * 0.05;
let y_min = y_min - y_range * 0.05;
let y_max = y_max + y_range * 0.05;
let mut chart = ChartBuilder::on(&root)
.caption(&settings.title, ("sans-serif", 30).into_font())
.margin(10)
.x_label_area_size(30)
.y_label_area_size(30)
.build_cartesian_2d(x_min..x_max, y_min..y_max)?;
if settings.show_grid {
chart
.configure_mesh()
.x_desc(&settings.x_label)
.y_desc(&settings.y_label)
.draw()?;
} else {
chart
.configure_mesh()
.x_desc(&settings.x_label)
.y_desc(&settings.y_label)
.disable_mesh()
.draw()?;
}
let color = RGBColor(
settings.color_palette[0].0,
settings.color_palette[0].1,
settings.color_palette[0].2,
);
match settings.plot_kind {
PlotKind::Line => {
let line_series =
LineSeries::new(x.iter().zip(y.iter()).map(|(&x, &y)| (x, y)), color);
if settings.show_legend {
chart
.draw_series(line_series)?
.label(series_name)
.legend(move |(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], color));
} else {
chart.draw_series(line_series)?;
}
}
PlotKind::Scatter => {
let scatter_series = x
.iter()
.zip(y.iter())
.map(|(&x, &y)| Circle::new((x, y), 3, color.filled()));
if settings.show_legend {
chart
.draw_series(scatter_series)?
.label(series_name)
.legend(move |(x, y)| Circle::new((x, y), 3, color.filled()));
} else {
chart.draw_series(scatter_series)?;
}
}
PlotKind::Bar => {
let num_bars = x.len() as f64;
let bars = x.iter().zip(y.iter()).enumerate().map(|(i, (&x_val, &y))| {
let bar_width = x_range / num_bars * 0.8;
let x0 = x_val - bar_width / 2.0;
let x1 = x_val + bar_width / 2.0;
Rectangle::new([(x0, 0.0), (x1, y)], color.filled())
});
if settings.show_legend {
chart
.draw_series(bars)?
.label(series_name)
.legend(move |(x, y)| {
Rectangle::new([(x, y - 5), (x + 20, y + 5)], color.filled())
});
} else {
chart.draw_series(bars)?;
}
}
PlotKind::Area => {
let area_series = AreaSeries::new(
x.iter().zip(y.iter()).map(|(&x, &y)| (x, y)),
0.0,
color.mix(0.2),
)
.border_style(color);
if settings.show_legend {
chart
.draw_series(area_series)?
.label(series_name)
.legend(move |(x, y)| {
Rectangle::new([(x, y - 5), (x + 20, y + 5)], color.mix(0.2).filled())
});
} else {
chart.draw_series(area_series)?;
}
}
_ => {
return Err(PandRSError::NotImplemented(format!(
"Plot kind {:?} not supported for this function",
settings.plot_kind
)));
}
}
if settings.show_legend {
chart
.configure_series_labels()
.background_style(&WHITE.mix(0.8))
.border_style(&BLACK)
.draw()?;
}
root.present()?;
Ok(())
}
pub fn plot_series_xy_svg<P: AsRef<Path>>(
x: &[f64],
y: &[f64],
path: P,
settings: &PlotSettings,
series_name: &str,
) -> Result<()> {
let root =
SVGBackend::new(path.as_ref(), (settings.width, settings.height)).into_drawing_area();
root.fill(&WHITE)?;
let x_min = x.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let x_max = x.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let y_min = y.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let y_max = y.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let x_range = x_max - x_min;
let y_range = y_max - y_min;
let x_min = x_min - x_range * 0.05;
let x_max = x_max + x_range * 0.05;
let y_min = y_min - y_range * 0.05;
let y_max = y_max + y_range * 0.05;
let mut chart = ChartBuilder::on(&root)
.caption(&settings.title, ("sans-serif", 30).into_font())
.margin(10)
.x_label_area_size(30)
.y_label_area_size(30)
.build_cartesian_2d(x_min..x_max, y_min..y_max)?;
if settings.show_grid {
chart
.configure_mesh()
.x_desc(&settings.x_label)
.y_desc(&settings.y_label)
.draw()?;
} else {
chart
.configure_mesh()
.x_desc(&settings.x_label)
.y_desc(&settings.y_label)
.disable_mesh()
.draw()?;
}
let color = RGBColor(
settings.color_palette[0].0,
settings.color_palette[0].1,
settings.color_palette[0].2,
);
match settings.plot_kind {
PlotKind::Line => {
let line_series =
LineSeries::new(x.iter().zip(y.iter()).map(|(&x, &y)| (x, y)), color);
if settings.show_legend {
chart
.draw_series(line_series)?
.label(series_name)
.legend(move |(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], color));
} else {
chart.draw_series(line_series)?;
}
}
PlotKind::Scatter => {
let scatter_series = x
.iter()
.zip(y.iter())
.map(|(&x, &y)| Circle::new((x, y), 3, color.filled()));
if settings.show_legend {
chart
.draw_series(scatter_series)?
.label(series_name)
.legend(move |(x, y)| Circle::new((x, y), 3, color.filled()));
} else {
chart.draw_series(scatter_series)?;
}
}
_ => {
return Err(PandRSError::NotImplemented(format!(
"Plot kind {:?} not supported for this function in SVG format",
settings.plot_kind
)));
}
}
if settings.show_legend {
chart
.configure_series_labels()
.background_style(&WHITE.mix(0.8))
.border_style(&BLACK)
.draw()?;
}
root.present()?;
Ok(())
}
pub fn plot_multi_series_png<P: AsRef<Path>>(
series_data: Vec<(String, Vec<f64>, Vec<f64>, (u8, u8, u8))>,
path: P,
settings: &PlotSettings,
) -> Result<()> {
let root = BitMapBackend::new(path.as_ref(), (settings.width, settings.height))
.into_drawing_area();
root.fill(&WHITE)?;
let mut x_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for (_, x, y, _) in &series_data {
let x_min_local = x.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let x_max_local = x.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let y_min_local = y.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let y_max_local = y.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
x_min = x_min.min(x_min_local);
x_max = x_max.max(x_max_local);
y_min = y_min.min(y_min_local);
y_max = y_max.max(y_max_local);
}
let x_range = x_max - x_min;
let y_range = y_max - y_min;
let x_min = x_min - x_range * 0.05;
let x_max = x_max + x_range * 0.05;
let y_min = y_min - y_range * 0.05;
let y_max = y_max + y_range * 0.05;
let mut chart = ChartBuilder::on(&root)
.caption(&settings.title, ("sans-serif", 30).into_font())
.margin(10)
.x_label_area_size(30)
.y_label_area_size(30)
.build_cartesian_2d(x_min..x_max, y_min..y_max)?;
if settings.show_grid {
chart
.configure_mesh()
.x_desc(&settings.x_label)
.y_desc(&settings.y_label)
.draw()?;
} else {
chart
.configure_mesh()
.x_desc(&settings.x_label)
.y_desc(&settings.y_label)
.disable_mesh()
.draw()?;
}
for (name, x, y, rgb) in series_data {
let color = RGBColor(rgb.0, rgb.1, rgb.2);
match settings.plot_kind {
PlotKind::Line => {
let line_series =
LineSeries::new(x.iter().zip(y.iter()).map(|(&x, &y)| (x, y)), color);
if settings.show_legend {
chart
.draw_series(line_series)?
.label(&name)
.legend(move |(x, y)| {
PathElement::new(vec![(x, y), (x + 20, y)], color)
});
} else {
chart.draw_series(line_series)?;
}
}
_ => {
return Err(PandRSError::NotImplemented(format!(
"Plot kind {:?} not supported for multiple series",
settings.plot_kind
)));
}
}
}
if settings.show_legend {
chart
.configure_series_labels()
.background_style(&WHITE.mix(0.8))
.border_style(&BLACK)
.draw()?;
}
root.present()?;
Ok(())
}
pub fn plot_multi_series_svg<P: AsRef<Path>>(
series_data: Vec<(String, Vec<f64>, Vec<f64>, (u8, u8, u8))>,
path: P,
settings: &PlotSettings,
) -> Result<()> {
let root =
SVGBackend::new(path.as_ref(), (settings.width, settings.height)).into_drawing_area();
Err(PandRSError::NotImplemented(
"SVG multi-series plotting not fully implemented yet".to_string(),
))
}
pub fn plot_histogram_png<P: AsRef<Path>>(
values: &[f64],
bins: usize,
path: P,
settings: &PlotSettings,
series_name: &str,
) -> Result<()> {
if values.is_empty() {
return Err(PandRSError::Empty("No data to plot".to_string()));
}
let min_val = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max_val = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let bin_width = (max_val - min_val) / bins as f64;
let mut histogram = vec![0; bins];
for &value in values {
let bin = ((value - min_val) / bin_width).floor() as usize;
let bin = bin.min(bins - 1); histogram[bin] += 1;
}
let root = BitMapBackend::new(path.as_ref(), (settings.width, settings.height))
.into_drawing_area();
root.fill(&WHITE)?;
let max_height = *histogram.iter().max().unwrap_or(&1) as f64;
let mut chart = ChartBuilder::on(&root)
.caption(&settings.title, ("sans-serif", 30).into_font())
.margin(10)
.x_label_area_size(30)
.y_label_area_size(30)
.build_cartesian_2d(
(min_val - bin_width * 0.1)..(max_val + bin_width * 0.1),
0.0..(max_height * 1.1),
)?;
if settings.show_grid {
chart
.configure_mesh()
.x_desc(&settings.x_label)
.y_desc("Frequency")
.draw()?;
} else {
chart
.configure_mesh()
.x_desc(&settings.x_label)
.y_desc("Frequency")
.disable_mesh()
.draw()?;
}
let color = RGBColor(
settings.color_palette[0].0,
settings.color_palette[0].1,
settings.color_palette[0].2,
);
let bars = histogram.iter().enumerate().map(|(i, &count)| {
let x0 = min_val + i as f64 * bin_width;
let x1 = x0 + bin_width * 0.8; let y = count as f64;
Rectangle::new([(x0, 0.0), (x1, y)], color.filled())
});
if settings.show_legend {
chart
.draw_series(bars)?
.label(series_name)
.legend(move |(x, y)| {
Rectangle::new([(x, y - 5), (x + 20, y + 5)], color.filled())
});
} else {
chart.draw_series(bars)?;
}
if settings.show_legend {
chart
.configure_series_labels()
.background_style(&WHITE.mix(0.8))
.border_style(&BLACK)
.draw()?;
}
root.present()?;
Ok(())
}
pub fn plot_histogram_svg<P: AsRef<Path>>(
values: &[f64],
bins: usize,
path: P,
settings: &PlotSettings,
series_name: &str,
) -> Result<()> {
Err(PandRSError::NotImplemented(
"SVG histogram plotting not fully implemented yet".to_string(),
))
}
pub fn plot_boxplot_png<P: AsRef<Path>>(
category_map: &HashMap<String, Vec<f64>>,
path: P,
settings: &PlotSettings,
) -> Result<()> {
if category_map.is_empty() {
return Err(PandRSError::Empty("No data to plot".to_string()));
}
let mut categories = Vec::new();
let mut stats = Vec::new();
for (cat, values) in category_map {
if values.is_empty() {
continue;
}
categories.push(cat.clone());
let mut sorted_values = values.clone();
sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let len = sorted_values.len();
let median = if len % 2 == 0 {
(sorted_values[len / 2 - 1] + sorted_values[len / 2]) / 2.0
} else {
sorted_values[len / 2]
};
let q1_idx = len / 4;
let q3_idx = len * 3 / 4;
let q1 = sorted_values[q1_idx];
let q3 = sorted_values[q3_idx];
let iqr = q3 - q1;
let lower_bound = q1 - 1.5 * iqr;
let upper_bound = q3 + 1.5 * iqr;
let min = *sorted_values
.iter()
.find(|&&x| x >= lower_bound)
.unwrap_or(&sorted_values[0]);
let max = *sorted_values
.iter()
.rev()
.find(|&&x| x <= upper_bound)
.unwrap_or(&sorted_values[len - 1]);
stats.push((min, q1, median, q3, max));
}
let root = BitMapBackend::new(path.as_ref(), (settings.width, settings.height))
.into_drawing_area();
root.fill(&WHITE)?;
let y_min = stats
.iter()
.map(|&(min, _, _, _, _)| min)
.fold(f64::INFINITY, |a, b| a.min(b));
let y_max = stats
.iter()
.map(|&(_, _, _, _, max)| max)
.fold(f64::NEG_INFINITY, |a, b| a.max(b));
let y_range = y_max - y_min;
let y_min = y_min - y_range * 0.1;
let y_max = y_max + y_range * 0.1;
let mut chart = ChartBuilder::on(&root)
.caption(&settings.title, ("sans-serif", 30).into_font())
.margin(10)
.x_label_area_size(30)
.y_label_area_size(50)
.build_cartesian_2d((0..categories.len()).into_segmented(), y_min..y_max)?;
if settings.show_grid {
chart
.configure_mesh()
.disable_x_mesh()
.x_labels(categories.len())
.x_label_formatter(&|idx| match idx {
plotters::prelude::SegmentValue::Exact(i) => {
if *i < categories.len() {
categories[*i].to_string()
} else {
"".to_string()
}
}
plotters::prelude::SegmentValue::CenterOf(i) => {
if *i < categories.len() {
categories[*i].to_string()
} else {
"".to_string()
}
}
_ => "".to_string(),
})
.y_desc(&settings.y_label)
.draw()?;
} else {
chart
.configure_mesh()
.disable_mesh()
.x_labels(categories.len())
.x_label_formatter(&|idx| match idx {
plotters::prelude::SegmentValue::Exact(i) => {
if *i < categories.len() {
categories[*i].to_string()
} else {
"".to_string()
}
}
plotters::prelude::SegmentValue::CenterOf(i) => {
if *i < categories.len() {
categories[*i].to_string()
} else {
"".to_string()
}
}
_ => "".to_string(),
})
.y_desc(&settings.y_label)
.draw()?;
}
let category_color = |idx: usize| {
let color_idx = idx % settings.color_palette.len();
let (r, g, b) = settings.color_palette[color_idx];
RGBColor(r, g, b)
};
for (i, &(min, q1, median, q3, max)) in stats.iter().enumerate() {
let color = category_color(i);
chart.draw_series(std::iter::once(Rectangle::new(
[
(SegmentValue::CenterOf(i), q1),
(SegmentValue::CenterOf(i), q3),
],
color.mix(0.3).filled(),
)))?;
chart.draw_series(std::iter::once(PathElement::new(
vec![
(SegmentValue::CenterOf(i), median),
(SegmentValue::CenterOf(i), median),
],
color.stroke_width(3),
)))?;
chart.draw_series(std::iter::once(PathElement::new(
vec![
(SegmentValue::CenterOf(i), min),
(SegmentValue::CenterOf(i), q1),
],
color.stroke_width(1),
)))?;
chart.draw_series(std::iter::once(PathElement::new(
vec![
(SegmentValue::CenterOf(i), q3),
(SegmentValue::CenterOf(i), max),
],
color.stroke_width(1),
)))?;
let width = 0.2;
let left_x = (i as f64 - width) as usize;
let right_x = (i as f64 + width) as usize;
chart.draw_series(std::iter::once(PathElement::new(
vec![
(SegmentValue::Exact(left_x), min),
(SegmentValue::Exact(right_x), min),
],
color.stroke_width(1),
)))?;
chart.draw_series(std::iter::once(PathElement::new(
vec![
(SegmentValue::Exact(left_x), max),
(SegmentValue::Exact(right_x), max),
],
color.stroke_width(1),
)))?;
}
root.present()?;
Ok(())
}
pub fn plot_boxplot_svg<P: AsRef<Path>>(
category_map: &HashMap<String, Vec<f64>>,
path: P,
settings: &PlotSettings,
) -> Result<()> {
Err(PandRSError::NotImplemented(
"SVG box plot not fully implemented yet".to_string(),
))
}
}
#[cfg(not(feature = "visualization"))]
pub mod backend {
use super::*;
use std::collections::HashMap;
use std::path::Path;
macro_rules! visualization_not_enabled {
($name:ident, $($arg:ident: $type:ty),*) => {
pub fn $name<P: AsRef<Path>>($($arg: $type,)* _path: P) -> Result<()> {
Err(PandRSError::FeatureNotAvailable("Visualization feature is not enabled. Recompile with --feature visualization".to_string()))
}
};
}
visualization_not_enabled!(plot_series_xy_png, _x: &[f64], _y: &[f64], _settings: &crate::vis::config::PlotSettings, _series_name: &str);
visualization_not_enabled!(plot_series_xy_svg, _x: &[f64], _y: &[f64], _settings: &crate::vis::config::PlotSettings, _series_name: &str);
visualization_not_enabled!(plot_multi_series_png, _series_data: Vec<(String, Vec<f64>, Vec<f64>, (u8, u8, u8))>, _settings: &crate::vis::config::PlotSettings);
visualization_not_enabled!(plot_multi_series_svg, _series_data: Vec<(String, Vec<f64>, Vec<f64>, (u8, u8, u8))>, _settings: &crate::vis::config::PlotSettings);
visualization_not_enabled!(plot_histogram_png, _values: &[f64], _bins: usize, _settings: &crate::vis::config::PlotSettings, _series_name: &str);
visualization_not_enabled!(plot_histogram_svg, _values: &[f64], _bins: usize, _settings: &crate::vis::config::PlotSettings, _series_name: &str);
visualization_not_enabled!(plot_boxplot_png, _category_map: &HashMap<String, Vec<f64>>, _settings: &crate::vis::config::PlotSettings);
visualization_not_enabled!(plot_boxplot_svg, _category_map: &HashMap<String, Vec<f64>>, _settings: &crate::vis::config::PlotSettings);
}