use plotters::prelude::*;
#[cfg(feature = "visualization")]
use plotters_svg::SVGBackend;
#[cfg(feature = "visualization")]
use std::borrow::BorrowMut;
use crate::dataframe::DataFrame;
use crate::series::Series;
use crate::VeloxxError;
#[cfg(feature = "visualization")]
use crate::types::Value;
#[derive(Debug, Clone, PartialEq)]
pub enum ChartType {
Line,
Scatter,
Bar,
Histogram,
Heatmap,
}
#[derive(Debug, Clone)]
pub struct PlotConfig {
pub title: String,
pub x_label: String,
pub y_label: String,
pub width: u32,
pub height: u32,
pub show_grid: bool,
pub show_legend: bool,
}
impl Default for PlotConfig {
fn default() -> Self {
Self {
title: "Velox Plot".to_string(),
x_label: "X".to_string(),
y_label: "Y".to_string(),
width: 800,
height: 600,
show_grid: true,
show_legend: true,
}
}
}
#[derive(Debug)]
pub struct Plot<'a> {
dataframe: &'a DataFrame,
chart_type: ChartType,
config: PlotConfig,
x_column: Option<String>,
y_column: Option<String>,
}
impl<'a> Plot<'a> {
pub fn new(dataframe: &'a DataFrame, chart_type: ChartType) -> Self {
Self {
dataframe,
chart_type,
config: PlotConfig::default(),
x_column: None,
y_column: None,
}
}
pub fn with_config(mut self, config: PlotConfig) -> Self {
self.config = config;
self
}
pub fn with_columns(mut self, x_column: &str, y_column: &str) -> Self {
self.x_column = Some(x_column.to_string());
self.y_column = Some(y_column.to_string());
self
}
#[cfg(feature = "visualization")]
pub fn save(&self, filename: &str) -> Result<(), VeloxxError> {
match self.chart_type {
ChartType::Line => self.create_line_plot(filename),
ChartType::Scatter => self.create_scatter_plot(filename),
ChartType::Bar => self.create_bar_plot(filename),
ChartType::Histogram => self.create_histogram(filename),
ChartType::Heatmap => self.create_heatmap(filename),
}
}
#[cfg(not(feature = "visualization"))]
pub fn save(&self, _filename: &str) -> Result<(), VeloxxError> {
Err(VeloxxError::InvalidOperation(
"Visualization feature is not enabled. Enable with --features visualization"
.to_string(),
))
}
#[cfg(feature = "visualization")]
fn create_line_plot(&self, filename: &str) -> Result<(), VeloxxError> {
let backend = SVGBackend::new(filename, (self.config.width, self.config.height));
let root = backend.into_drawing_area();
root.fill(&WHITE).map_err(|e| {
VeloxxError::InvalidOperation(format!("Failed to initialize plot: {}", e))
})?;
let (x_data, y_data) = self.extract_xy_data()?;
if x_data.is_empty() || y_data.is_empty() {
return Err(VeloxxError::InvalidOperation(
"No data available for plotting".to_string(),
));
}
let x_min = x_data.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let x_max = x_data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let y_min = y_data.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let y_max = y_data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let mut chart = ChartBuilder::on(&root)
.caption(&self.config.title, ("sans-serif", 40))
.margin(20)
.x_label_area_size(40)
.y_label_area_size(50)
.build_cartesian_2d(x_min..x_max, y_min..y_max)
.map_err(|e| VeloxxError::InvalidOperation(format!("Failed to build chart: {}", e)))?;
if self.config.show_grid {
chart
.configure_mesh()
.x_desc(&self.config.x_label)
.y_desc(&self.config.y_label)
.draw()
.map_err(|e| {
VeloxxError::InvalidOperation(format!("Failed to draw mesh: {}", e))
})?;
}
chart
.draw_series(LineSeries::new(
x_data.iter().zip(y_data.iter()).map(|(&x, &y)| (x, y)),
&BLUE,
))
.map_err(|e| {
VeloxxError::InvalidOperation(format!("Failed to draw line series: {}", e))
})?
.label("Data")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 10, y)], BLUE));
if self.config.show_legend {
chart.configure_series_labels().draw().map_err(|e| {
VeloxxError::InvalidOperation(format!("Failed to draw legend: {}", e))
})?;
}
root.present()
.map_err(|e| VeloxxError::InvalidOperation(format!("Failed to save plot: {}", e)))?;
Ok(())
}
#[cfg(feature = "visualization")]
fn create_scatter_plot(&self, filename: &str) -> Result<(), VeloxxError> {
let backend = SVGBackend::new(filename, (self.config.width, self.config.height));
let root = backend.into_drawing_area();
root.fill(&WHITE).map_err(|e| {
VeloxxError::InvalidOperation(format!("Failed to initialize plot: {}", e))
})?;
let (x_data, y_data) = self.extract_xy_data()?;
if x_data.is_empty() || y_data.is_empty() {
return Err(VeloxxError::InvalidOperation(
"No data available for plotting".to_string(),
));
}
let x_min = x_data.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let x_max = x_data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let y_min = y_data.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let y_max = y_data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let mut chart = ChartBuilder::on(&root)
.caption(&self.config.title, ("sans-serif", 40))
.margin(20)
.x_label_area_size(40)
.y_label_area_size(50)
.build_cartesian_2d(x_min..x_max, y_min..y_max)
.map_err(|e| VeloxxError::InvalidOperation(format!("Failed to build chart: {}", e)))?;
if self.config.show_grid {
chart
.configure_mesh()
.x_desc(&self.config.x_label)
.y_desc(&self.config.y_label)
.draw()
.map_err(|e| {
VeloxxError::InvalidOperation(format!("Failed to draw mesh: {}", e))
})?;
}
chart
.draw_series(
x_data
.iter()
.zip(y_data.iter())
.map(|(&x, &y)| Circle::new((x, y), 3, BLUE.filled())),
)
.map_err(|e| {
VeloxxError::InvalidOperation(format!("Failed to draw scatter series: {}", e))
})?
.label("Data Points")
.legend(|(x, y)| Circle::new((x + 10, y), 3, BLUE.filled()));
if self.config.show_legend {
chart.configure_series_labels().draw().map_err(|e| {
VeloxxError::InvalidOperation(format!("Failed to draw legend: {}", e))
})?;
}
root.present()
.map_err(|e| VeloxxError::InvalidOperation(format!("Failed to save plot: {}", e)))?;
Ok(())
}
#[cfg(feature = "visualization")]
fn create_bar_plot(&self, filename: &str) -> Result<(), VeloxxError> {
let backend = SVGBackend::new(filename, (self.config.width, self.config.height));
let root = backend.into_drawing_area();
root.fill(&WHITE).map_err(|e| {
VeloxxError::InvalidOperation(format!("Failed to initialize plot: {}", e))
})?;
let (categories, values) = self.extract_categorical_data()?;
if categories.is_empty() || values.is_empty() {
return Err(VeloxxError::InvalidOperation(
"No data available for plotting".to_string(),
));
}
let y_max = values.iter().fold(0.0f64, |a, &b| a.max(b));
let mut chart = ChartBuilder::on(&root)
.caption(&self.config.title, ("sans-serif", 40))
.margin(20)
.x_label_area_size(40)
.y_label_area_size(50)
.build_cartesian_2d(0f64..(categories.len() as f64), 0f64..y_max * 1.1)
.map_err(|e| VeloxxError::InvalidOperation(format!("Failed to build chart: {}", e)))?;
if self.config.show_grid {
chart
.configure_mesh()
.x_desc(&self.config.x_label)
.y_desc(&self.config.y_label)
.draw()
.map_err(|e| {
VeloxxError::InvalidOperation(format!("Failed to draw mesh: {}", e))
})?;
}
chart
.draw_series(values.iter().enumerate().map(|(i, &value)| {
Rectangle::new([(i as f64, 0.0), (i as f64 + 0.8, value)], BLUE.filled())
}))
.map_err(|e| {
VeloxxError::InvalidOperation(format!("Failed to draw bar series: {}", e))
})?
.label("Values")
.legend(|(x, y)| Rectangle::new([(x, y), (x + 10, y + 10)], BLUE.filled()));
if self.config.show_legend {
chart.configure_series_labels().draw().map_err(|e| {
VeloxxError::InvalidOperation(format!("Failed to draw legend: {}", e))
})?;
}
root.present()
.map_err(|e| VeloxxError::InvalidOperation(format!("Failed to save plot: {}", e)))?;
Ok(())
}
#[cfg(feature = "visualization")]
fn create_histogram(&self, filename: &str) -> Result<(), VeloxxError> {
let backend = SVGBackend::new(filename, (self.config.width, self.config.height));
let root = backend.into_drawing_area();
root.fill(&WHITE).map_err(|e| {
VeloxxError::InvalidOperation(format!("Failed to initialize plot: {}", e))
})?;
let data = self.extract_histogram_data()?;
if data.is_empty() {
return Err(VeloxxError::InvalidOperation(
"No data available for plotting".to_string(),
));
}
let x_min = data.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let x_max = data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let mut chart = ChartBuilder::on(&root)
.caption(&self.config.title, ("sans-serif", 40))
.margin(20)
.x_label_area_size(40)
.y_label_area_size(50)
.build_cartesian_2d((x_min..x_max).step(1.0), 0u32..50u32)
.map_err(|e| VeloxxError::InvalidOperation(format!("Failed to build chart: {}", e)))?;
chart
.configure_mesh()
.x_desc(&self.config.x_label)
.y_desc(&self.config.y_label)
.draw()
.map_err(|e| VeloxxError::InvalidOperation(format!("Failed to draw mesh: {}", e)))?;
let hist_data: Vec<f64> = data.to_vec();
let series = Histogram::vertical(chart.borrow_mut())
.style(BLUE.filled())
.data(hist_data.iter().map(|x| (*x, 1)));
chart.draw_series(series).map_err(|e| {
VeloxxError::InvalidOperation(format!("Failed to draw histogram series: {}", e))
})?;
root.present()
.map_err(|e| VeloxxError::InvalidOperation(format!("Failed to save plot: {}", e)))?;
Ok(())
}
#[cfg(feature = "visualization")]
fn create_heatmap(&self, _filename: &str) -> Result<(), VeloxxError> {
Err(VeloxxError::InvalidOperation(
"Heatmap plotting not yet implemented".to_string(),
))
}
fn extract_histogram_data(&self) -> Result<Vec<f64>, VeloxxError> {
let x_col_name = self
.x_column
.as_ref()
.ok_or_else(|| VeloxxError::InvalidOperation("X column not specified".to_string()))?;
let x_series = self
.dataframe
.get_column(x_col_name)
.ok_or_else(|| VeloxxError::ColumnNotFound(x_col_name.clone()))?;
self.series_to_f64_vec(x_series)
}
fn extract_xy_data(&self) -> Result<(Vec<f64>, Vec<f64>), VeloxxError> {
let x_col_name = self
.x_column
.as_ref()
.ok_or_else(|| VeloxxError::InvalidOperation("X column not specified".to_string()))?;
let y_col_name = self
.y_column
.as_ref()
.ok_or_else(|| VeloxxError::InvalidOperation("Y column not specified".to_string()))?;
let x_series = self
.dataframe
.get_column(x_col_name)
.ok_or_else(|| VeloxxError::ColumnNotFound(x_col_name.clone()))?;
let y_series = self
.dataframe
.get_column(y_col_name)
.ok_or_else(|| VeloxxError::ColumnNotFound(y_col_name.clone()))?;
let x_data = self.series_to_f64_vec(x_series)?;
let y_data = self.series_to_f64_vec(y_series)?;
Ok((x_data, y_data))
}
fn extract_categorical_data(&self) -> Result<(Vec<String>, Vec<f64>), VeloxxError> {
let x_col_name = self
.x_column
.as_ref()
.ok_or_else(|| VeloxxError::InvalidOperation("X column not specified".to_string()))?;
let y_col_name = self
.y_column
.as_ref()
.ok_or_else(|| VeloxxError::InvalidOperation("Y column not specified".to_string()))?;
let x_series = self
.dataframe
.get_column(x_col_name)
.ok_or_else(|| VeloxxError::ColumnNotFound(x_col_name.clone()))?;
let y_series = self
.dataframe
.get_column(y_col_name)
.ok_or_else(|| VeloxxError::ColumnNotFound(y_col_name.clone()))?;
let categories = self.series_to_string_vec(x_series)?;
let values = self.series_to_f64_vec(y_series)?;
Ok((categories, values))
}
fn series_to_f64_vec(&self, series: &Series) -> Result<Vec<f64>, VeloxxError> {
let mut result = Vec::new();
for i in 0..series.len() {
if let Some(value) = series.get_value(i) {
match value {
Value::F64(f) => result.push(f),
Value::I32(i) => result.push(i as f64),
_ => {
return Err(VeloxxError::InvalidOperation(
"Cannot convert non-numeric data to f64".to_string(),
));
}
}
}
}
Ok(result)
}
fn series_to_string_vec(&self, series: &Series) -> Result<Vec<String>, VeloxxError> {
let mut result = Vec::new();
for i in 0..series.len() {
if let Some(value) = series.get_value(i) {
result.push(value.to_string());
}
}
Ok(result)
}
}