use std::collections::HashMap;
use crate::model::{DataItem, ResolvedSeries};
#[derive(Debug, Clone)]
pub struct TransformedItem {
pub original: DataItem,
pub display_value: f64,
pub baseline: f64,
pub data_index: usize,
}
#[derive(Debug, Clone)]
pub struct TransformedSeries {
pub series_index: usize,
pub items: Vec<TransformedItem>,
}
pub trait DataTransformer {
fn transform(&self, all_series: &[ResolvedSeries]) -> Vec<TransformedSeries>;
}
pub struct IdentityTransformer;
impl DataTransformer for IdentityTransformer {
fn transform(&self, all_series: &[ResolvedSeries]) -> Vec<TransformedSeries> {
all_series
.iter()
.enumerate()
.map(|(idx, series)| {
let items = extract_data(series);
TransformedSeries {
series_index: idx,
items: items
.iter()
.enumerate()
.map(|(i, item)| TransformedItem {
original: item.clone(),
display_value: item.value,
baseline: 0.0,
data_index: i,
})
.collect(),
}
})
.collect()
}
}
pub struct StackedTransformer {
pub stack_filter: Option<String>,
}
impl StackedTransformer {
pub fn new(stack_filter: Option<String>) -> Self {
Self { stack_filter }
}
}
impl DataTransformer for StackedTransformer {
fn transform(&self, all_series: &[ResolvedSeries]) -> Vec<TransformedSeries> {
let mut stack_groups: HashMap<Option<String>, Vec<(usize, &ResolvedSeries)>> =
HashMap::new();
for (idx, series) in all_series.iter().enumerate() {
let stack_name = extract_stack(series);
if let Some(ref filter) = self.stack_filter
&& stack_name.as_ref() != Some(filter)
{
continue;
}
stack_groups
.entry(stack_name)
.or_default()
.push((idx, series));
}
let mut result: Vec<TransformedSeries> = Vec::with_capacity(all_series.len());
for (idx, series) in all_series.iter().enumerate() {
let stack_name = extract_stack(series);
if stack_name.is_none() || !should_stack(series, &self.stack_filter) {
result.push(transform_identity(idx, series));
}
}
for (_stack_name, group) in stack_groups {
if group.is_empty() {
continue;
}
let data_len = extract_data(group[0].1).len();
let mut cumulative: Vec<f64> = vec![0.0; data_len];
for (idx, series) in group {
let items = extract_data(series);
let transformed_items: Vec<TransformedItem> = items
.iter()
.enumerate()
.map(|(i, item)| {
let baseline = cumulative.get(i).copied().unwrap_or(0.0);
let value = item.value.max(0.0); cumulative[i] = baseline + value;
TransformedItem {
original: item.clone(),
display_value: cumulative[i],
baseline,
data_index: i,
}
})
.collect();
result.push(TransformedSeries {
series_index: idx,
items: transformed_items,
});
}
}
result.sort_by_key(|s| s.series_index);
result
}
}
pub struct PercentStackedTransformer {
pub stack_filter: Option<String>,
}
impl PercentStackedTransformer {
pub fn new(stack_filter: Option<String>) -> Self {
Self { stack_filter }
}
}
impl DataTransformer for PercentStackedTransformer {
fn transform(&self, all_series: &[ResolvedSeries]) -> Vec<TransformedSeries> {
let stacked = StackedTransformer {
stack_filter: self.stack_filter.clone(),
}
.transform(all_series);
let mut max_totals: Vec<f64> = Vec::new();
for series in &stacked {
for item in &series.items {
if item.data_index >= max_totals.len() {
max_totals.resize(item.data_index + 1, 0.0);
}
max_totals[item.data_index] = max_totals[item.data_index].max(item.display_value);
}
}
stacked
.into_iter()
.map(|mut series| {
for item in &mut series.items {
let total = max_totals.get(item.data_index).copied().unwrap_or(1.0);
if total > 0.0 {
item.display_value = (item.display_value / total) * 100.0;
item.baseline = (item.baseline / total) * 100.0;
}
}
series
})
.collect()
}
}
fn extract_data(series: &ResolvedSeries) -> &[DataItem] {
match series {
ResolvedSeries::Bar(s) => &s.data,
ResolvedSeries::Line(s) => &s.data,
ResolvedSeries::Scatter(_) => &[], ResolvedSeries::Pie(s) => &s.data,
ResolvedSeries::Radar(_) => &[],
ResolvedSeries::PolarBar(s) => &s.data,
ResolvedSeries::PolarScatter(_) => &[],
ResolvedSeries::Bubble(_) => &[],
ResolvedSeries::Gauge(_) => &[],
ResolvedSeries::Candlestick(_) => &[],
ResolvedSeries::Table(_) => &[],
}
}
fn extract_stack(series: &ResolvedSeries) -> Option<String> {
match series {
ResolvedSeries::Bar(s) => s.stack.clone(),
ResolvedSeries::Line(s) => s.stack.clone(),
_ => None,
}
}
fn should_stack(series: &ResolvedSeries, filter: &Option<String>) -> bool {
let stack = extract_stack(series);
match filter {
Some(f) => stack.as_ref() == Some(f),
None => stack.is_some(),
}
}
fn transform_identity(idx: usize, series: &ResolvedSeries) -> TransformedSeries {
let items = extract_data(series);
TransformedSeries {
series_index: idx,
items: items
.iter()
.enumerate()
.map(|(i, item)| TransformedItem {
original: item.clone(),
display_value: item.value,
baseline: 0.0,
data_index: i,
})
.collect(),
}
}