Skip to main content

fission_charts/
model.rs

1use crate::axis::{Axis, AxisType};
2use crate::dataset::Dataset;
3use crate::encode::Encode;
4use crate::series::*;
5use crate::{Chart, Series};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, PartialEq)]
9pub struct ChartDiagnostic {
10    pub series_name: Option<String>,
11    pub message: String,
12}
13
14#[derive(Debug, Clone)]
15pub struct ChartModel {
16    pub title: Option<String>,
17    pub x_axis: Axis,
18    pub y_axis: Axis,
19    pub x_categories: Vec<String>,
20    pub y_categories: Vec<String>,
21    pub x_domain: (f32, f32),
22    pub y_domain: (f32, f32),
23    pub series: Vec<ResolvedSeries>,
24    pub diagnostics: Vec<ChartDiagnostic>,
25}
26
27#[derive(Debug, Clone)]
28pub enum ResolvedSeries {
29    Line(ResolvedLineSeries),
30    Bar(ResolvedBarSeries),
31    Scatter(scatter::ScatterSeries),
32    Pie(pie::PieSeries),
33    Bubble(bubble::BubbleSeries),
34    Boxplot(boxplot::BoxplotSeries),
35    Candlestick(candlestick::CandlestickSeries),
36    Heatmap(heatmap::HeatmapSeries),
37    CalendarHeatmap(calendar_heatmap::CalendarHeatmapSeries),
38    Lines(lines::LinesSeries),
39    Graph(graph::GraphSeries),
40    Tree(tree::TreeSeries),
41    Treemap(treemap::TreemapSeries),
42    Radar(radar::RadarSeries),
43    Funnel(funnel::FunnelSeries),
44    Gauge(gauge::GaugeSeries),
45    Map(map::MapSeries),
46    Sankey(sankey::SankeySeries),
47    Parallel(parallel::ParallelSeries),
48    Sunburst(sunburst::SunburstSeries),
49    ThemeRiver(theme_river::ThemeRiverSeries),
50    PictorialBar(pictorial_bar::PictorialBarSeries),
51    EffectScatter(effect_scatter::EffectScatterSeries),
52    Liquidfill(liquidfill::LiquidfillSeries),
53    Wordcloud(wordcloud::WordcloudSeries),
54    PolarBar(polar::PolarBarSeries),
55    PolarLine(polar::PolarLineSeries),
56    SingleAxis(single_axis::SingleAxisSeries),
57}
58
59#[derive(Debug, Clone)]
60pub struct ResolvedLineSeries {
61    pub source: line::LineSeries,
62    pub values: Vec<f32>,
63    pub categories: Vec<String>,
64}
65
66#[derive(Debug, Clone)]
67pub struct ResolvedBarSeries {
68    pub source: bar::BarSeries,
69    pub values: Vec<f32>,
70    pub categories: Vec<String>,
71}
72
73impl ChartModel {
74    pub fn from_chart(chart: &Chart) -> Self {
75        let x_axis = chart
76            .x_axis
77            .clone()
78            .unwrap_or_else(|| Axis::category(Vec::new()));
79        let y_axis = chart.y_axis.clone().unwrap_or_else(Axis::value);
80        let mut diagnostics = Vec::new();
81        let mut resolved = Vec::new();
82
83        for series in &chart.series {
84            match series {
85                Series::Line(line) => resolved.push(ResolvedSeries::Line(resolve_line(
86                    line,
87                    chart.dataset.as_ref(),
88                ))),
89                Series::Bar(bar) => resolved.push(ResolvedSeries::Bar(resolve_bar(
90                    bar,
91                    chart.dataset.as_ref(),
92                ))),
93                Series::Scatter(series) => resolved.push(ResolvedSeries::Scatter(series.clone())),
94                Series::Pie(series) => resolved.push(ResolvedSeries::Pie(series.clone())),
95                Series::Bubble(series) => resolved.push(ResolvedSeries::Bubble(series.clone())),
96                Series::Boxplot(series) => resolved.push(ResolvedSeries::Boxplot(series.clone())),
97                Series::Candlestick(series) => {
98                    resolved.push(ResolvedSeries::Candlestick(series.clone()))
99                }
100                Series::Heatmap(series) => resolved.push(ResolvedSeries::Heatmap(series.clone())),
101                Series::CalendarHeatmap(series) => {
102                    resolved.push(ResolvedSeries::CalendarHeatmap(series.clone()))
103                }
104                Series::Lines(series) => resolved.push(ResolvedSeries::Lines(series.clone())),
105                Series::Graph(series) => resolved.push(ResolvedSeries::Graph(series.clone())),
106                Series::Tree(series) => resolved.push(ResolvedSeries::Tree(series.clone())),
107                Series::Treemap(series) => resolved.push(ResolvedSeries::Treemap(series.clone())),
108                Series::Radar(series) => resolved.push(ResolvedSeries::Radar(series.clone())),
109                Series::Funnel(series) => resolved.push(ResolvedSeries::Funnel(series.clone())),
110                Series::Gauge(series) => resolved.push(ResolvedSeries::Gauge(series.clone())),
111                Series::Map(series) if series.geojson.is_some() => {
112                    resolved.push(ResolvedSeries::Map(series.clone()))
113                }
114                Series::Map(series) => diagnostics.push(unsupported(
115                    &series.name,
116                    "Map charts need GeoJSON on the MapSeries before they can be rendered.",
117                )),
118                Series::Sankey(series) => resolved.push(ResolvedSeries::Sankey(series.clone())),
119                Series::Parallel(series) => resolved.push(ResolvedSeries::Parallel(series.clone())),
120                Series::Sunburst(series) => {
121                    resolved.push(ResolvedSeries::Sunburst(series.clone()))
122                }
123                Series::ThemeRiver(series) => {
124                    resolved.push(ResolvedSeries::ThemeRiver(series.clone()))
125                }
126                Series::PictorialBar(series) => {
127                    resolved.push(ResolvedSeries::PictorialBar(series.clone()))
128                }
129                Series::EffectScatter(series) => {
130                    resolved.push(ResolvedSeries::EffectScatter(series.clone()))
131                }
132                Series::Liquidfill(series) => {
133                    resolved.push(ResolvedSeries::Liquidfill(series.clone()))
134                }
135                Series::Wordcloud(series) => resolved.push(ResolvedSeries::Wordcloud(series.clone())),
136                Series::PolarBar(series) => resolved.push(ResolvedSeries::PolarBar(series.clone())),
137                Series::PolarLine(series) => resolved.push(ResolvedSeries::PolarLine(series.clone())),
138                Series::SingleAxis(series) => resolved.push(ResolvedSeries::SingleAxis(series.clone())),
139                Series::Custom(series) => diagnostics.push(unsupported(&series.name, "String-named custom render callbacks are not part of the Fission chart architecture.")),
140            }
141        }
142
143        let mut x_categories = resolve_x_categories(&x_axis, &resolved);
144        let y_categories = resolve_y_categories(&y_axis, &resolved);
145        apply_data_zoom(chart.data_zoom.as_ref(), &mut resolved, &mut x_categories);
146        let (x_domain, y_domain) =
147            resolve_domains(&x_axis, &y_axis, &x_categories, &y_categories, &resolved);
148
149        Self {
150            title: chart.title.clone(),
151            x_axis,
152            y_axis,
153            x_categories,
154            y_categories,
155            x_domain,
156            y_domain,
157            series: resolved,
158            diagnostics,
159        }
160    }
161
162    pub fn has_cartesian_series(&self) -> bool {
163        self.series.iter().any(|series| {
164            matches!(
165                series,
166                ResolvedSeries::Line(_)
167                    | ResolvedSeries::Bar(_)
168                    | ResolvedSeries::Scatter(_)
169                    | ResolvedSeries::Bubble(_)
170                    | ResolvedSeries::Boxplot(_)
171                    | ResolvedSeries::Candlestick(_)
172                    | ResolvedSeries::Heatmap(_)
173                    | ResolvedSeries::PictorialBar(_)
174                    | ResolvedSeries::EffectScatter(_)
175            )
176        })
177    }
178}
179
180fn resolve_line(series: &line::LineSeries, dataset: Option<&Dataset>) -> ResolvedLineSeries {
181    let values = encoded_numbers(dataset, series.encode.as_ref(), "y")
182        .unwrap_or_else(|| series.data.clone());
183    let categories = encoded_strings(dataset, series.encode.as_ref(), "x").unwrap_or_default();
184    ResolvedLineSeries {
185        source: series.clone(),
186        values,
187        categories,
188    }
189}
190
191fn resolve_bar(series: &bar::BarSeries, dataset: Option<&Dataset>) -> ResolvedBarSeries {
192    let values = encoded_numbers(dataset, series.encode.as_ref(), "y")
193        .unwrap_or_else(|| series.data.clone());
194    let categories = encoded_strings(dataset, series.encode.as_ref(), "x").unwrap_or_default();
195    ResolvedBarSeries {
196        source: series.clone(),
197        values,
198        categories,
199    }
200}
201
202fn encoded_numbers(
203    dataset: Option<&Dataset>,
204    encode: Option<&Encode>,
205    field: &str,
206) -> Option<Vec<f32>> {
207    let dataset = dataset?;
208    let encode = encode?;
209    dataset.extract_column_numbers(encode, field)
210}
211
212fn encoded_strings(
213    dataset: Option<&Dataset>,
214    encode: Option<&Encode>,
215    field: &str,
216) -> Option<Vec<String>> {
217    let dataset = dataset?;
218    let encode = encode?;
219    dataset.extract_column_strings(encode, field)
220}
221
222fn resolve_x_categories(axis: &Axis, series: &[ResolvedSeries]) -> Vec<String> {
223    if axis.axis_type == AxisType::Category && !axis.data.is_empty() {
224        return axis.data.clone();
225    }
226
227    for series in series {
228        match series {
229            ResolvedSeries::Line(line) if !line.categories.is_empty() => {
230                return line.categories.clone()
231            }
232            ResolvedSeries::Bar(bar) if !bar.categories.is_empty() => {
233                return bar.categories.clone()
234            }
235            _ => {}
236        }
237    }
238
239    let mut max_len = 0usize;
240    for series in series {
241        match series {
242            ResolvedSeries::Line(line) => max_len = max_len.max(line.values.len()),
243            ResolvedSeries::Bar(bar) => max_len = max_len.max(bar.values.len()),
244            ResolvedSeries::Boxplot(boxplot) => max_len = max_len.max(boxplot.data.len()),
245            ResolvedSeries::Candlestick(candle) => max_len = max_len.max(candle.data.len()),
246            ResolvedSeries::PictorialBar(pic) => max_len = max_len.max(pic.data.len()),
247            _ => {}
248        }
249    }
250
251    (0..max_len).map(|idx| (idx + 1).to_string()).collect()
252}
253
254fn resolve_y_categories(axis: &Axis, series: &[ResolvedSeries]) -> Vec<String> {
255    if axis.axis_type == AxisType::Category && !axis.data.is_empty() {
256        return axis.data.clone();
257    }
258
259    let mut max_len = 0usize;
260    for series in series {
261        match series {
262            ResolvedSeries::Bar(bar)
263                if bar.source.orientation == bar::BarOrientation::Horizontal =>
264            {
265                max_len = max_len.max(bar.values.len())
266            }
267            ResolvedSeries::SingleAxis(single_axis) => {
268                max_len = max_len.max(single_axis.data.len())
269            }
270            _ => {}
271        }
272    }
273
274    (0..max_len).map(|idx| (idx + 1).to_string()).collect()
275}
276
277fn apply_data_zoom(
278    data_zoom: Option<&crate::components::DataZoom>,
279    series: &mut [ResolvedSeries],
280    categories: &mut Vec<String>,
281) {
282    let Some(data_zoom) = data_zoom else {
283        return;
284    };
285    if categories.is_empty() {
286        return;
287    }
288
289    let len = categories.len();
290    let start = ((data_zoom.start_percent / 100.0).clamp(0.0, 1.0) * len as f32).floor() as usize;
291    let mut end = ((data_zoom.end_percent / 100.0).clamp(0.0, 1.0) * len as f32).ceil() as usize;
292    let start = start.min(len.saturating_sub(1));
293    end = end.max(start + 1).min(len);
294
295    *categories = categories[start..end].to_vec();
296    for series in series {
297        match series {
298            ResolvedSeries::Line(line) => {
299                line.values = slice_vec(&line.values, start, end);
300                line.categories = slice_vec(&line.categories, start, end);
301            }
302            ResolvedSeries::Bar(bar) if bar.source.orientation == bar::BarOrientation::Vertical => {
303                bar.values = slice_vec(&bar.values, start, end);
304                bar.categories = slice_vec(&bar.categories, start, end);
305            }
306            ResolvedSeries::Boxplot(boxplot) => {
307                boxplot.data = slice_vec(&boxplot.data, start, end);
308            }
309            ResolvedSeries::Candlestick(candle) => {
310                candle.data = slice_vec(&candle.data, start, end);
311            }
312            ResolvedSeries::PictorialBar(pic) => {
313                pic.data = slice_vec(&pic.data, start, end);
314            }
315            _ => {}
316        }
317    }
318}
319
320fn slice_vec<T: Clone>(values: &[T], start: usize, end: usize) -> Vec<T> {
321    if values.is_empty() {
322        return Vec::new();
323    }
324    values[start.min(values.len())..end.min(values.len())].to_vec()
325}
326
327fn resolve_domains(
328    x_axis: &Axis,
329    y_axis: &Axis,
330    x_categories: &[String],
331    y_categories: &[String],
332    series: &[ResolvedSeries],
333) -> ((f32, f32), (f32, f32)) {
334    let mut x_min = f32::MAX;
335    let mut x_max = f32::MIN;
336    let mut y_min = f32::MAX;
337    let mut y_max = f32::MIN;
338    let mut saw_x = false;
339    let mut saw_y = false;
340
341    let mut bar_stacks: HashMap<(String, usize), f32> = HashMap::new();
342    let mut line_stacks: HashMap<(String, usize), f32> = HashMap::new();
343
344    for series in series {
345        match series {
346            ResolvedSeries::Line(line) => {
347                for (idx, value) in line.values.iter().enumerate() {
348                    let value =
349                        stacked_value(&mut line_stacks, line.source.stack.as_ref(), idx, *value);
350                    y_min = y_min.min(value).min(0.0);
351                    y_max = y_max.max(value).max(0.0);
352                    saw_y = true;
353                }
354            }
355            ResolvedSeries::Bar(bar) => {
356                for (idx, value) in bar.values.iter().enumerate() {
357                    let value =
358                        stacked_value(&mut bar_stacks, bar.source.stack.as_ref(), idx, *value);
359                    if bar.source.orientation == bar::BarOrientation::Horizontal {
360                        x_min = x_min.min(value).min(0.0);
361                        x_max = x_max.max(value).max(0.0);
362                        saw_x = true;
363                    } else {
364                        y_min = y_min.min(value).min(0.0);
365                        y_max = y_max.max(value).max(0.0);
366                        saw_y = true;
367                    }
368                }
369            }
370            ResolvedSeries::Scatter(scatter) => {
371                for (x, y) in &scatter.data {
372                    x_min = x_min.min(*x);
373                    x_max = x_max.max(*x);
374                    y_min = y_min.min(*y);
375                    y_max = y_max.max(*y);
376                    saw_x = true;
377                    saw_y = true;
378                }
379            }
380            ResolvedSeries::EffectScatter(scatter) => {
381                for (x, y) in &scatter.data {
382                    x_min = x_min.min(*x);
383                    x_max = x_max.max(*x);
384                    y_min = y_min.min(*y);
385                    y_max = y_max.max(*y);
386                    saw_x = true;
387                    saw_y = true;
388                }
389            }
390            ResolvedSeries::Bubble(bubble) => {
391                for (x, y, _) in &bubble.data {
392                    x_min = x_min.min(*x);
393                    x_max = x_max.max(*x);
394                    y_min = y_min.min(*y);
395                    y_max = y_max.max(*y);
396                    saw_x = true;
397                    saw_y = true;
398                }
399            }
400            ResolvedSeries::Boxplot(boxplot) => {
401                for row in &boxplot.data {
402                    for value in row {
403                        y_min = y_min.min(*value);
404                        y_max = y_max.max(*value);
405                        saw_y = true;
406                    }
407                }
408            }
409            ResolvedSeries::Candlestick(candle) => {
410                for row in &candle.data {
411                    for value in row {
412                        y_min = y_min.min(*value);
413                        y_max = y_max.max(*value);
414                        saw_y = true;
415                    }
416                }
417            }
418            ResolvedSeries::PictorialBar(pic) => {
419                for value in &pic.data {
420                    y_min = y_min.min(*value).min(0.0);
421                    y_max = y_max.max(*value).max(0.0);
422                    saw_y = true;
423                }
424            }
425            ResolvedSeries::PolarLine(line) => {
426                for (_, radius) in &line.data {
427                    y_min = y_min.min(*radius).min(0.0);
428                    y_max = y_max.max(*radius).max(0.0);
429                    saw_y = true;
430                }
431            }
432            ResolvedSeries::SingleAxis(single_axis) => {
433                for (value, _) in &single_axis.data {
434                    x_min = x_min.min(*value);
435                    x_max = x_max.max(*value);
436                    saw_x = true;
437                }
438            }
439            _ => {}
440        }
441    }
442
443    let mut x_domain = if x_axis.axis_type == AxisType::Category {
444        (0.0, x_categories.len().saturating_sub(1).max(1) as f32)
445    } else if saw_x {
446        (x_min, x_max)
447    } else {
448        (0.0, x_categories.len().saturating_sub(1).max(1) as f32)
449    };
450    let mut y_domain = if y_axis.axis_type == AxisType::Category {
451        (0.0, y_categories.len().saturating_sub(1).max(1) as f32)
452    } else if saw_y {
453        (y_min, y_max)
454    } else {
455        (0.0, 1.0)
456    };
457
458    if let Some(min) = x_axis.min {
459        x_domain.0 = min;
460    }
461    if let Some(max) = x_axis.max {
462        x_domain.1 = max;
463    }
464    if let Some(min) = y_axis.min {
465        y_domain.0 = min;
466    }
467    if let Some(max) = y_axis.max {
468        y_domain.1 = max;
469    }
470
471    x_domain = normalize_domain(x_domain);
472    y_domain = normalize_domain(y_domain);
473    (x_domain, y_domain)
474}
475
476fn stacked_value(
477    totals: &mut HashMap<(String, usize), f32>,
478    stack: Option<&String>,
479    index: usize,
480    value: f32,
481) -> f32 {
482    if let Some(stack) = stack {
483        let key = (stack.clone(), index);
484        let base = *totals.get(&key).unwrap_or(&0.0);
485        let total = base + value;
486        totals.insert(key, total);
487        total
488    } else {
489        value
490    }
491}
492
493fn normalize_domain((mut min, mut max): (f32, f32)) -> (f32, f32) {
494    if !min.is_finite() || !max.is_finite() {
495        return (0.0, 1.0);
496    }
497    if (max - min).abs() < f32::EPSILON {
498        min -= 1.0;
499        max += 1.0;
500    }
501    (min, max)
502}
503
504fn unsupported(series_name: &str, message: &str) -> ChartDiagnostic {
505    ChartDiagnostic {
506        series_name: Some(series_name.to_string()),
507        message: message.to_string(),
508    }
509}