use crate::compile::stat_aggregate;
use crate::compile::stat_bin;
use crate::compile::stat_boxplot::{self, BoxPlotSummary};
use crate::compile::stat_smooth;
use crate::error::{ChartError, Result};
use crate::grammar::layer::{Layer, MarkType};
use crate::grammar::position::Position;
use crate::grammar::stat::Stat;
#[derive(Clone, Debug)]
pub struct ResolvedLayer {
pub mark: MarkType,
pub x_data: Vec<f64>,
pub y_data: Vec<f64>,
pub categories: Option<Vec<String>>,
pub y_baseline: Option<Vec<f64>>,
pub boxplot: Option<Vec<BoxPlotSummary>>,
pub inner_radius_fraction: f32,
pub position: Position,
pub is_binned: bool,
pub facet_values: Option<Vec<String>>,
pub layer_idx: usize,
pub heatmap_data: Option<Vec<Vec<f64>>>,
pub row_labels: Option<Vec<String>>,
pub col_labels: Option<Vec<String>>,
pub annotate_cells: bool,
pub label: Option<String>,
pub dodge_width: Option<f64>,
pub error_bars: Option<Vec<f64>>,
}
pub fn resolve_layer(layer: &Layer, layer_idx: usize) -> Result<ResolvedLayer> {
match &layer.stat {
Stat::Identity => resolve_identity(layer, layer_idx),
Stat::Bin { bins } => resolve_bin(layer, layer_idx, *bins),
Stat::BoxPlot => resolve_boxplot(layer, layer_idx),
Stat::Aggregate { func } => resolve_aggregate(layer, layer_idx, *func),
Stat::Smooth { bandwidth } => resolve_smooth(layer, layer_idx, *bandwidth),
}
}
fn resolve_identity(layer: &Layer, layer_idx: usize) -> Result<ResolvedLayer> {
Ok(ResolvedLayer {
mark: layer.mark,
x_data: layer.x_data.clone().unwrap_or_default(),
y_data: layer.y_data.clone().unwrap_or_default(),
categories: layer.categories.clone(),
y_baseline: None,
boxplot: None,
inner_radius_fraction: layer.inner_radius_fraction,
position: layer.position,
is_binned: false,
facet_values: layer.facet_values.clone(),
layer_idx,
heatmap_data: layer.heatmap_data.clone(),
row_labels: layer.row_labels.clone(),
col_labels: layer.col_labels.clone(),
annotate_cells: layer.annotate_cells,
label: layer.label.clone(),
dodge_width: None,
error_bars: layer.error_bars.clone(),
})
}
fn resolve_bin(layer: &Layer, layer_idx: usize, bins: usize) -> Result<ResolvedLayer> {
let x_data = layer.x_data.as_ref().ok_or(ChartError::EmptyData)?;
let bin_count = if bins == 0 {
stat_bin::sturges_bins(x_data.len())
} else {
bins
};
let (centers, counts) = stat_bin::compute_bins(x_data, bin_count);
Ok(ResolvedLayer {
mark: MarkType::Bar, x_data: centers,
y_data: counts,
categories: None,
y_baseline: None,
boxplot: None,
inner_radius_fraction: 0.0,
position: layer.position,
is_binned: true,
facet_values: layer.facet_values.clone(),
layer_idx,
heatmap_data: None,
row_labels: None,
col_labels: None,
annotate_cells: false,
label: layer.label.clone(),
dodge_width: None,
error_bars: None,
})
}
fn resolve_boxplot(layer: &Layer, layer_idx: usize) -> Result<ResolvedLayer> {
let categories = layer
.categories
.as_ref()
.ok_or_else(|| ChartError::InvalidParameter("boxplot requires categories".into()))?;
let y_data = layer.y_data.as_ref().ok_or(ChartError::EmptyData)?;
let summaries = stat_boxplot::compute_boxplot(categories, y_data)?;
let x_data: Vec<f64> = (0..summaries.len()).map(|i| i as f64).collect();
let cat_labels: Vec<String> = summaries.iter().map(|s| s.category.clone()).collect();
let mut all_y = Vec::new();
for s in &summaries {
all_y.push(s.whisker_lo);
all_y.push(s.whisker_hi);
all_y.extend_from_slice(&s.outliers);
}
Ok(ResolvedLayer {
mark: MarkType::Bar, x_data,
y_data: all_y,
categories: Some(cat_labels),
y_baseline: None,
boxplot: Some(summaries),
inner_radius_fraction: 0.0,
position: layer.position,
is_binned: false,
facet_values: layer.facet_values.clone(),
layer_idx,
heatmap_data: None,
row_labels: None,
col_labels: None,
annotate_cells: false,
label: layer.label.clone(),
dodge_width: None,
error_bars: None,
})
}
fn resolve_aggregate(
layer: &Layer,
layer_idx: usize,
func: crate::grammar::stat::AggregateFunc,
) -> Result<ResolvedLayer> {
let categories = layer
.categories
.as_ref()
.ok_or_else(|| ChartError::InvalidParameter("aggregate requires categories".into()))?;
let y_data = layer.y_data.as_ref().ok_or(ChartError::EmptyData)?;
let result = stat_aggregate::compute_aggregate(categories, y_data, func)?;
Ok(ResolvedLayer {
mark: layer.mark,
x_data: result.x_data,
y_data: result.y_data,
categories: Some(result.categories),
y_baseline: None,
boxplot: None,
inner_radius_fraction: 0.0,
position: layer.position,
is_binned: false,
facet_values: layer.facet_values.clone(),
layer_idx,
heatmap_data: None,
row_labels: None,
col_labels: None,
annotate_cells: false,
label: layer.label.clone(),
dodge_width: None,
error_bars: None,
})
}
fn resolve_smooth(layer: &Layer, layer_idx: usize, bandwidth: f64) -> Result<ResolvedLayer> {
let x_data = layer.x_data.as_ref().ok_or(ChartError::EmptyData)?;
let y_data = layer.y_data.as_ref().ok_or(ChartError::EmptyData)?;
let (x_smooth, y_smooth) = stat_smooth::compute_loess(x_data, y_data, bandwidth)?;
Ok(ResolvedLayer {
mark: layer.mark,
x_data: x_smooth,
y_data: y_smooth,
categories: layer.categories.clone(),
y_baseline: None,
boxplot: None,
inner_radius_fraction: 0.0,
position: layer.position,
is_binned: false,
facet_values: layer.facet_values.clone(),
layer_idx,
heatmap_data: None,
row_labels: None,
col_labels: None,
annotate_cells: false,
label: layer.label.clone(),
dodge_width: None,
error_bars: None,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::grammar::stat::AggregateFunc;
#[test]
fn identity_passthrough() {
let layer = Layer::new(MarkType::Point)
.with_x(vec![1.0, 2.0, 3.0])
.with_y(vec![4.0, 5.0, 6.0]);
let resolved = resolve_layer(&layer, 0).unwrap();
assert_eq!(resolved.x_data, vec![1.0, 2.0, 3.0]);
assert_eq!(resolved.y_data, vec![4.0, 5.0, 6.0]);
assert!(!resolved.is_binned);
assert!(resolved.boxplot.is_none());
}
#[test]
fn bin_produces_bars() {
let layer = Layer::new(MarkType::Bar)
.with_x(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0])
.stat(Stat::Bin { bins: 5 });
let resolved = resolve_layer(&layer, 0).unwrap();
assert_eq!(resolved.x_data.len(), 5);
assert_eq!(resolved.y_data.len(), 5);
assert!(resolved.is_binned);
assert!(matches!(resolved.mark, MarkType::Bar));
}
#[test]
fn boxplot_produces_summaries() {
let layer = Layer::new(MarkType::Bar)
.with_y(vec![1.0, 2.0, 3.0, 4.0, 5.0])
.with_categories(vec!["A".into(); 5])
.stat(Stat::BoxPlot);
let resolved = resolve_layer(&layer, 0).unwrap();
assert!(resolved.boxplot.is_some());
let summaries = resolved.boxplot.unwrap();
assert_eq!(summaries.len(), 1);
assert_eq!(summaries[0].category, "A");
}
#[test]
fn aggregate_routing() {
let layer = Layer::new(MarkType::Bar)
.with_y(vec![10.0, 20.0, 30.0])
.with_categories(vec!["A".into(), "B".into(), "A".into()])
.stat(Stat::Aggregate {
func: AggregateFunc::Sum,
});
let resolved = resolve_layer(&layer, 0).unwrap();
assert_eq!(resolved.categories.as_ref().unwrap(), &["A", "B"]);
assert!((resolved.y_data[0] - 40.0).abs() < 1e-10); assert!((resolved.y_data[1] - 20.0).abs() < 1e-10); }
#[test]
fn smooth_produces_output() {
let x: Vec<f64> = (0..20).map(f64::from).collect();
let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
let layer = Layer::new(MarkType::Line)
.with_x(x)
.with_y(y)
.stat(Stat::Smooth { bandwidth: 0.5 });
let resolved = resolve_layer(&layer, 0).unwrap();
assert_eq!(resolved.x_data.len(), 20);
assert_eq!(resolved.y_data.len(), 20);
}
}