use crate::axis::{Axis, AxisType};
use crate::dataset::Dataset;
use crate::encode::Encode;
use crate::series::*;
use crate::{Chart, Series};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct ChartDiagnostic {
pub series_name: Option<String>,
pub message: String,
}
#[derive(Debug, Clone)]
pub struct ChartModel {
pub title: Option<String>,
pub x_axis: Axis,
pub y_axis: Axis,
pub x_categories: Vec<String>,
pub y_categories: Vec<String>,
pub x_domain: (f32, f32),
pub y_domain: (f32, f32),
pub series: Vec<ResolvedSeries>,
pub diagnostics: Vec<ChartDiagnostic>,
}
#[derive(Debug, Clone)]
pub enum ResolvedSeries {
Line(ResolvedLineSeries),
Bar(ResolvedBarSeries),
Scatter(scatter::ScatterSeries),
Pie(pie::PieSeries),
Bubble(bubble::BubbleSeries),
Boxplot(boxplot::BoxplotSeries),
Candlestick(candlestick::CandlestickSeries),
Heatmap(heatmap::HeatmapSeries),
CalendarHeatmap(calendar_heatmap::CalendarHeatmapSeries),
Lines(lines::LinesSeries),
Graph(graph::GraphSeries),
Tree(tree::TreeSeries),
Treemap(treemap::TreemapSeries),
Radar(radar::RadarSeries),
Funnel(funnel::FunnelSeries),
Gauge(gauge::GaugeSeries),
Map(map::MapSeries),
Sankey(sankey::SankeySeries),
Parallel(parallel::ParallelSeries),
Sunburst(sunburst::SunburstSeries),
ThemeRiver(theme_river::ThemeRiverSeries),
PictorialBar(pictorial_bar::PictorialBarSeries),
EffectScatter(effect_scatter::EffectScatterSeries),
Liquidfill(liquidfill::LiquidfillSeries),
Wordcloud(wordcloud::WordcloudSeries),
PolarBar(polar::PolarBarSeries),
PolarLine(polar::PolarLineSeries),
SingleAxis(single_axis::SingleAxisSeries),
}
#[derive(Debug, Clone)]
pub struct ResolvedLineSeries {
pub source: line::LineSeries,
pub values: Vec<f32>,
pub categories: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ResolvedBarSeries {
pub source: bar::BarSeries,
pub values: Vec<f32>,
pub categories: Vec<String>,
}
impl ChartModel {
pub fn from_chart(chart: &Chart) -> Self {
let x_axis = chart
.x_axis
.clone()
.unwrap_or_else(|| Axis::category(Vec::new()));
let y_axis = chart.y_axis.clone().unwrap_or_else(Axis::value);
let mut diagnostics = Vec::new();
let mut resolved = Vec::new();
for series in &chart.series {
match series {
Series::Line(line) => resolved.push(ResolvedSeries::Line(resolve_line(
line,
chart.dataset.as_ref(),
))),
Series::Bar(bar) => resolved.push(ResolvedSeries::Bar(resolve_bar(
bar,
chart.dataset.as_ref(),
))),
Series::Scatter(series) => resolved.push(ResolvedSeries::Scatter(series.clone())),
Series::Pie(series) => resolved.push(ResolvedSeries::Pie(series.clone())),
Series::Bubble(series) => resolved.push(ResolvedSeries::Bubble(series.clone())),
Series::Boxplot(series) => resolved.push(ResolvedSeries::Boxplot(series.clone())),
Series::Candlestick(series) => {
resolved.push(ResolvedSeries::Candlestick(series.clone()))
}
Series::Heatmap(series) => resolved.push(ResolvedSeries::Heatmap(series.clone())),
Series::CalendarHeatmap(series) => {
resolved.push(ResolvedSeries::CalendarHeatmap(series.clone()))
}
Series::Lines(series) => resolved.push(ResolvedSeries::Lines(series.clone())),
Series::Graph(series) => resolved.push(ResolvedSeries::Graph(series.clone())),
Series::Tree(series) => resolved.push(ResolvedSeries::Tree(series.clone())),
Series::Treemap(series) => resolved.push(ResolvedSeries::Treemap(series.clone())),
Series::Radar(series) => resolved.push(ResolvedSeries::Radar(series.clone())),
Series::Funnel(series) => resolved.push(ResolvedSeries::Funnel(series.clone())),
Series::Gauge(series) => resolved.push(ResolvedSeries::Gauge(series.clone())),
Series::Map(series) if series.geojson.is_some() => {
resolved.push(ResolvedSeries::Map(series.clone()))
}
Series::Map(series) => diagnostics.push(unsupported(
&series.name,
"Map charts need GeoJSON on the MapSeries before they can be rendered.",
)),
Series::Sankey(series) => resolved.push(ResolvedSeries::Sankey(series.clone())),
Series::Parallel(series) => resolved.push(ResolvedSeries::Parallel(series.clone())),
Series::Sunburst(series) => {
resolved.push(ResolvedSeries::Sunburst(series.clone()))
}
Series::ThemeRiver(series) => {
resolved.push(ResolvedSeries::ThemeRiver(series.clone()))
}
Series::PictorialBar(series) => {
resolved.push(ResolvedSeries::PictorialBar(series.clone()))
}
Series::EffectScatter(series) => {
resolved.push(ResolvedSeries::EffectScatter(series.clone()))
}
Series::Liquidfill(series) => {
resolved.push(ResolvedSeries::Liquidfill(series.clone()))
}
Series::Wordcloud(series) => resolved.push(ResolvedSeries::Wordcloud(series.clone())),
Series::PolarBar(series) => resolved.push(ResolvedSeries::PolarBar(series.clone())),
Series::PolarLine(series) => resolved.push(ResolvedSeries::PolarLine(series.clone())),
Series::SingleAxis(series) => resolved.push(ResolvedSeries::SingleAxis(series.clone())),
Series::Custom(series) => diagnostics.push(unsupported(&series.name, "String-named custom render callbacks are not part of the Fission chart architecture.")),
}
}
let mut x_categories = resolve_x_categories(&x_axis, &resolved);
let y_categories = resolve_y_categories(&y_axis, &resolved);
apply_data_zoom(chart.data_zoom.as_ref(), &mut resolved, &mut x_categories);
let (x_domain, y_domain) =
resolve_domains(&x_axis, &y_axis, &x_categories, &y_categories, &resolved);
Self {
title: chart.title.clone(),
x_axis,
y_axis,
x_categories,
y_categories,
x_domain,
y_domain,
series: resolved,
diagnostics,
}
}
pub fn has_cartesian_series(&self) -> bool {
self.series.iter().any(|series| {
matches!(
series,
ResolvedSeries::Line(_)
| ResolvedSeries::Bar(_)
| ResolvedSeries::Scatter(_)
| ResolvedSeries::Bubble(_)
| ResolvedSeries::Boxplot(_)
| ResolvedSeries::Candlestick(_)
| ResolvedSeries::Heatmap(_)
| ResolvedSeries::PictorialBar(_)
| ResolvedSeries::EffectScatter(_)
)
})
}
}
fn resolve_line(series: &line::LineSeries, dataset: Option<&Dataset>) -> ResolvedLineSeries {
let values = encoded_numbers(dataset, series.encode.as_ref(), "y")
.unwrap_or_else(|| series.data.clone());
let categories = encoded_strings(dataset, series.encode.as_ref(), "x").unwrap_or_default();
ResolvedLineSeries {
source: series.clone(),
values,
categories,
}
}
fn resolve_bar(series: &bar::BarSeries, dataset: Option<&Dataset>) -> ResolvedBarSeries {
let values = encoded_numbers(dataset, series.encode.as_ref(), "y")
.unwrap_or_else(|| series.data.clone());
let categories = encoded_strings(dataset, series.encode.as_ref(), "x").unwrap_or_default();
ResolvedBarSeries {
source: series.clone(),
values,
categories,
}
}
fn encoded_numbers(
dataset: Option<&Dataset>,
encode: Option<&Encode>,
field: &str,
) -> Option<Vec<f32>> {
let dataset = dataset?;
let encode = encode?;
dataset.extract_column_numbers(encode, field)
}
fn encoded_strings(
dataset: Option<&Dataset>,
encode: Option<&Encode>,
field: &str,
) -> Option<Vec<String>> {
let dataset = dataset?;
let encode = encode?;
dataset.extract_column_strings(encode, field)
}
fn resolve_x_categories(axis: &Axis, series: &[ResolvedSeries]) -> Vec<String> {
if axis.axis_type == AxisType::Category && !axis.data.is_empty() {
return axis.data.clone();
}
for series in series {
match series {
ResolvedSeries::Line(line) if !line.categories.is_empty() => {
return line.categories.clone()
}
ResolvedSeries::Bar(bar) if !bar.categories.is_empty() => {
return bar.categories.clone()
}
_ => {}
}
}
let mut max_len = 0usize;
for series in series {
match series {
ResolvedSeries::Line(line) => max_len = max_len.max(line.values.len()),
ResolvedSeries::Bar(bar) => max_len = max_len.max(bar.values.len()),
ResolvedSeries::Boxplot(boxplot) => max_len = max_len.max(boxplot.data.len()),
ResolvedSeries::Candlestick(candle) => max_len = max_len.max(candle.data.len()),
ResolvedSeries::PictorialBar(pic) => max_len = max_len.max(pic.data.len()),
_ => {}
}
}
(0..max_len).map(|idx| (idx + 1).to_string()).collect()
}
fn resolve_y_categories(axis: &Axis, series: &[ResolvedSeries]) -> Vec<String> {
if axis.axis_type == AxisType::Category && !axis.data.is_empty() {
return axis.data.clone();
}
let mut max_len = 0usize;
for series in series {
match series {
ResolvedSeries::Bar(bar)
if bar.source.orientation == bar::BarOrientation::Horizontal =>
{
max_len = max_len.max(bar.values.len())
}
ResolvedSeries::SingleAxis(single_axis) => {
max_len = max_len.max(single_axis.data.len())
}
_ => {}
}
}
(0..max_len).map(|idx| (idx + 1).to_string()).collect()
}
fn apply_data_zoom(
data_zoom: Option<&crate::components::DataZoom>,
series: &mut [ResolvedSeries],
categories: &mut Vec<String>,
) {
let Some(data_zoom) = data_zoom else {
return;
};
if categories.is_empty() {
return;
}
let len = categories.len();
let start = ((data_zoom.start_percent / 100.0).clamp(0.0, 1.0) * len as f32).floor() as usize;
let mut end = ((data_zoom.end_percent / 100.0).clamp(0.0, 1.0) * len as f32).ceil() as usize;
let start = start.min(len.saturating_sub(1));
end = end.max(start + 1).min(len);
*categories = categories[start..end].to_vec();
for series in series {
match series {
ResolvedSeries::Line(line) => {
line.values = slice_vec(&line.values, start, end);
line.categories = slice_vec(&line.categories, start, end);
}
ResolvedSeries::Bar(bar) if bar.source.orientation == bar::BarOrientation::Vertical => {
bar.values = slice_vec(&bar.values, start, end);
bar.categories = slice_vec(&bar.categories, start, end);
}
ResolvedSeries::Boxplot(boxplot) => {
boxplot.data = slice_vec(&boxplot.data, start, end);
}
ResolvedSeries::Candlestick(candle) => {
candle.data = slice_vec(&candle.data, start, end);
}
ResolvedSeries::PictorialBar(pic) => {
pic.data = slice_vec(&pic.data, start, end);
}
_ => {}
}
}
}
fn slice_vec<T: Clone>(values: &[T], start: usize, end: usize) -> Vec<T> {
if values.is_empty() {
return Vec::new();
}
values[start.min(values.len())..end.min(values.len())].to_vec()
}
fn resolve_domains(
x_axis: &Axis,
y_axis: &Axis,
x_categories: &[String],
y_categories: &[String],
series: &[ResolvedSeries],
) -> ((f32, f32), (f32, f32)) {
let mut x_min = f32::MAX;
let mut x_max = f32::MIN;
let mut y_min = f32::MAX;
let mut y_max = f32::MIN;
let mut saw_x = false;
let mut saw_y = false;
let mut bar_stacks: HashMap<(String, usize), f32> = HashMap::new();
let mut line_stacks: HashMap<(String, usize), f32> = HashMap::new();
for series in series {
match series {
ResolvedSeries::Line(line) => {
for (idx, value) in line.values.iter().enumerate() {
let value =
stacked_value(&mut line_stacks, line.source.stack.as_ref(), idx, *value);
y_min = y_min.min(value).min(0.0);
y_max = y_max.max(value).max(0.0);
saw_y = true;
}
}
ResolvedSeries::Bar(bar) => {
for (idx, value) in bar.values.iter().enumerate() {
let value =
stacked_value(&mut bar_stacks, bar.source.stack.as_ref(), idx, *value);
if bar.source.orientation == bar::BarOrientation::Horizontal {
x_min = x_min.min(value).min(0.0);
x_max = x_max.max(value).max(0.0);
saw_x = true;
} else {
y_min = y_min.min(value).min(0.0);
y_max = y_max.max(value).max(0.0);
saw_y = true;
}
}
}
ResolvedSeries::Scatter(scatter) => {
for (x, y) in &scatter.data {
x_min = x_min.min(*x);
x_max = x_max.max(*x);
y_min = y_min.min(*y);
y_max = y_max.max(*y);
saw_x = true;
saw_y = true;
}
}
ResolvedSeries::EffectScatter(scatter) => {
for (x, y) in &scatter.data {
x_min = x_min.min(*x);
x_max = x_max.max(*x);
y_min = y_min.min(*y);
y_max = y_max.max(*y);
saw_x = true;
saw_y = true;
}
}
ResolvedSeries::Bubble(bubble) => {
for (x, y, _) in &bubble.data {
x_min = x_min.min(*x);
x_max = x_max.max(*x);
y_min = y_min.min(*y);
y_max = y_max.max(*y);
saw_x = true;
saw_y = true;
}
}
ResolvedSeries::Boxplot(boxplot) => {
for row in &boxplot.data {
for value in row {
y_min = y_min.min(*value);
y_max = y_max.max(*value);
saw_y = true;
}
}
}
ResolvedSeries::Candlestick(candle) => {
for row in &candle.data {
for value in row {
y_min = y_min.min(*value);
y_max = y_max.max(*value);
saw_y = true;
}
}
}
ResolvedSeries::PictorialBar(pic) => {
for value in &pic.data {
y_min = y_min.min(*value).min(0.0);
y_max = y_max.max(*value).max(0.0);
saw_y = true;
}
}
ResolvedSeries::PolarLine(line) => {
for (_, radius) in &line.data {
y_min = y_min.min(*radius).min(0.0);
y_max = y_max.max(*radius).max(0.0);
saw_y = true;
}
}
ResolvedSeries::SingleAxis(single_axis) => {
for (value, _) in &single_axis.data {
x_min = x_min.min(*value);
x_max = x_max.max(*value);
saw_x = true;
}
}
_ => {}
}
}
let mut x_domain = if x_axis.axis_type == AxisType::Category {
(0.0, x_categories.len().saturating_sub(1).max(1) as f32)
} else if saw_x {
(x_min, x_max)
} else {
(0.0, x_categories.len().saturating_sub(1).max(1) as f32)
};
let mut y_domain = if y_axis.axis_type == AxisType::Category {
(0.0, y_categories.len().saturating_sub(1).max(1) as f32)
} else if saw_y {
(y_min, y_max)
} else {
(0.0, 1.0)
};
if let Some(min) = x_axis.min {
x_domain.0 = min;
}
if let Some(max) = x_axis.max {
x_domain.1 = max;
}
if let Some(min) = y_axis.min {
y_domain.0 = min;
}
if let Some(max) = y_axis.max {
y_domain.1 = max;
}
x_domain = normalize_domain(x_domain);
y_domain = normalize_domain(y_domain);
(x_domain, y_domain)
}
fn stacked_value(
totals: &mut HashMap<(String, usize), f32>,
stack: Option<&String>,
index: usize,
value: f32,
) -> f32 {
if let Some(stack) = stack {
let key = (stack.clone(), index);
let base = *totals.get(&key).unwrap_or(&0.0);
let total = base + value;
totals.insert(key, total);
total
} else {
value
}
}
fn normalize_domain((mut min, mut max): (f32, f32)) -> (f32, f32) {
if !min.is_finite() || !max.is_finite() {
return (0.0, 1.0);
}
if (max - min).abs() < f32::EPSILON {
min -= 1.0;
max += 1.0;
}
(min, max)
}
fn unsupported(series_name: &str, message: &str) -> ChartDiagnostic {
ChartDiagnostic {
series_name: Some(series_name.to_string()),
message: message.to_string(),
}
}