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}