1use crate::axis::{Axis, AxisType};
2use crate::components::{
3 AxisPointer, ChartGraphic, ChartGraphicKind, ChartTimeline, DataZoom, MarkArea, MarkLine,
4 MarkPoint, VisualMap,
5};
6use crate::grid::Grid;
7use crate::interaction::{ChartHit, ChartInteraction, ChartInteractionEvent, ChartInteractionKind};
8use crate::layout::math::{arc, catmull_rom_to_bezier, pie_slice};
9use crate::layout::scale::LinearScale;
10use crate::legend::Legend;
11use crate::model::{ChartModel, ResolvedBarSeries, ResolvedLineSeries, ResolvedSeries};
12use crate::series::graph::GraphEdge;
13use crate::series::Series;
14use crate::tooltip::Tooltip;
15use fission_core::event::{InputEvent, PointerEvent};
16use fission_core::internal::{
17 CustomEventResult, CustomHitResult, CustomRenderObject, InternalRenderNode,
18};
19use fission_core::op::Color;
20use fission_core::ui::{Container, Widget};
21use fission_core::{
22 Action, ActionEnvelope, AnimationPropertyId, AnimationRequest, AnimationStartValue,
23 EasingFunction, WidgetId,
24};
25use fission_ir::op::{Fill, LayoutOp, LineCap, LineJoin, PaintOp, Stroke};
26use fission_layout::{LayoutPoint, LayoutRect};
27use serde::{Deserialize, Serialize};
28use std::collections::{BTreeMap, HashMap};
29use std::sync::Arc;
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct Chart {
33 pub id: Option<WidgetId>,
34 pub width: Option<f32>,
35 pub height: Option<f32>,
36 pub title: Option<String>,
37 pub tooltip: Option<Tooltip>,
38 pub legend: Option<Legend>,
39 pub grid: Option<Grid>,
40 pub x_axis: Option<Axis>,
41 pub y_axis: Option<Axis>,
42 pub series: Vec<Series>,
43 pub dataset: Option<crate::dataset::Dataset>,
44 pub visual_map: Option<VisualMap>,
45 pub data_zoom: Option<DataZoom>,
46 pub axis_pointer: Option<AxisPointer>,
47 pub mark_points: Vec<MarkPoint>,
48 pub mark_lines: Vec<MarkLine>,
49 pub mark_areas: Vec<MarkArea>,
50 pub graphics: Vec<ChartGraphic>,
51 pub timeline: Option<ChartTimeline>,
52 pub theme: Option<ChartTheme>,
53 pub interaction: ChartInteraction,
54 pub animation: crate::animation::ChartAnimation,
55 pub animate: bool,
56}
57
58impl Default for Chart {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64impl Chart {
65 pub fn new() -> Self {
66 Self {
67 id: None,
68 width: None,
69 height: None,
70 title: None,
71 tooltip: None,
72 legend: None,
73 grid: None,
74 x_axis: None,
75 y_axis: None,
76 series: Vec::new(),
77 dataset: None,
78 visual_map: None,
79 data_zoom: None,
80 axis_pointer: None,
81 mark_points: Vec::new(),
82 mark_lines: Vec::new(),
83 mark_areas: Vec::new(),
84 graphics: Vec::new(),
85 timeline: None,
86 theme: None,
87 interaction: ChartInteraction::default(),
88 animation: crate::animation::ChartAnimation::default(),
89 animate: false,
90 }
91 }
92
93 pub fn id(mut self, id: WidgetId) -> Self {
94 self.id = Some(id);
95 self
96 }
97
98 pub fn width(mut self, w: f32) -> Self {
99 self.width = Some(w);
100 self
101 }
102
103 pub fn height(mut self, h: f32) -> Self {
104 self.height = Some(h);
105 self
106 }
107
108 pub fn dataset(mut self, ds: crate::dataset::Dataset) -> Self {
109 self.dataset = Some(ds);
110 self
111 }
112
113 pub fn title(mut self, title: &str) -> Self {
114 self.title = Some(title.to_string());
115 self
116 }
117
118 pub fn tooltip(mut self, tooltip: Tooltip) -> Self {
119 self.tooltip = Some(tooltip);
120 self
121 }
122
123 pub fn legend(mut self, legend: Legend) -> Self {
124 self.legend = Some(legend);
125 self
126 }
127
128 pub fn x_axis(mut self, axis: Axis) -> Self {
129 self.x_axis = Some(axis);
130 self
131 }
132
133 pub fn y_axis(mut self, axis: Axis) -> Self {
134 self.y_axis = Some(axis);
135 self
136 }
137
138 pub fn series(mut self, series: Vec<Series>) -> Self {
139 self.series = series;
140 self
141 }
142
143 pub fn grid(mut self, grid: Grid) -> Self {
144 self.grid = Some(grid);
145 self
146 }
147
148 pub fn visual_map(mut self, visual_map: VisualMap) -> Self {
149 self.visual_map = Some(visual_map);
150 self
151 }
152
153 pub fn data_zoom(mut self, data_zoom: DataZoom) -> Self {
154 self.data_zoom = Some(data_zoom);
155 self
156 }
157
158 pub fn axis_pointer(mut self, axis_pointer: AxisPointer) -> Self {
159 self.axis_pointer = Some(axis_pointer);
160 self
161 }
162
163 pub fn mark_point(mut self, mark_point: MarkPoint) -> Self {
164 self.mark_points.push(mark_point);
165 self
166 }
167
168 pub fn mark_line(mut self, mark_line: MarkLine) -> Self {
169 self.mark_lines.push(mark_line);
170 self
171 }
172
173 pub fn mark_area(mut self, mark_area: MarkArea) -> Self {
174 self.mark_areas.push(mark_area);
175 self
176 }
177
178 pub fn graphic(mut self, graphic: ChartGraphic) -> Self {
179 self.graphics.push(graphic);
180 self
181 }
182
183 pub fn timeline(mut self, timeline: ChartTimeline) -> Self {
184 self.timeline = Some(timeline);
185 self
186 }
187
188 pub fn theme(mut self, theme: ChartTheme) -> Self {
189 self.theme = Some(theme);
190 self
191 }
192
193 pub fn animate(mut self, animate: bool) -> Self {
194 self.animate = animate;
195 self.animation.enabled = animate;
196 self
197 }
198
199 pub fn animation(mut self, animation: crate::animation::ChartAnimation) -> Self {
200 self.animate = animation.enabled;
201 self.animation = animation;
202 self
203 }
204
205 pub fn interaction(mut self, interaction: ChartInteraction) -> Self {
206 self.interaction = interaction;
207 self
208 }
209
210 pub fn emit_interaction_events(mut self, emit: bool) -> Self {
211 self.interaction = self.interaction.emit_events(emit);
212 self
213 }
214
215 pub fn hit_test(&self, width: f32, height: f32, point: LayoutPoint) -> Option<ChartHit> {
216 let model = ChartModel::from_chart(self);
217 let area = chart_area_for_size(self, width, height);
218 hit_test_chart(&model, &area, point)
219 }
220}
221
222impl From<Chart> for Widget {
223 fn from(component: Chart) -> Self {
224 let (ctx, _) = fission_core::build::current::<()>();
225 let this = &component;
226 if this.animation.enabled {
227 ctx.anim_for(this.animation_id()).request(AnimationRequest {
228 property: chart_animation_property(),
229 from: AnimationStartValue::Explicit(0.0),
230 to: 1.0,
231 duration_ms: this.animation.duration_ms,
232 repeat: this.animation.repeat,
233 delay_ms: this.animation.delay_ms,
234 frame_interval_ms: Some(16),
235 easing: chart_easing(this.animation.easing),
236 });
237 }
238
239 let render_object = if this.interaction.enabled {
240 Some(Arc::new(ChartRenderObject {
241 chart: this.clone(),
242 }) as Arc<dyn CustomRenderObject>)
243 } else {
244 None
245 };
246 let mut container = Container::new(fission_core::internal::custom_render_widget(
247 InternalRenderNode {
248 debug_tag: "fission_charts::Chart".into(),
249 lowerer: Some(Arc::new(ChartInternalLowerer {
250 chart: this.clone(),
251 })),
252 render_object,
253 },
254 ));
255 if let Some(w) = this.width {
256 container = container.width(w);
257 } else {
258 container = container.flex_grow(1.0);
259 }
260 if let Some(h) = this.height {
261 container = container.height(h);
262 } else if this.width.is_none() {
263 container = container.flex_grow(1.0);
264 }
265 container.into()
266 }
267}
268
269impl Chart {
270 fn animation_id(&self) -> WidgetId {
271 self.id.unwrap_or_else(|| {
272 let title = self.title.as_deref().unwrap_or("untitled");
273 WidgetId::explicit(&format!("fission_charts::Chart::{title}"))
274 })
275 }
276}
277
278fn chart_animation_property() -> AnimationPropertyId {
279 AnimationPropertyId::custom("fission_charts::progress")
280}
281
282fn chart_easing(easing: crate::animation::ChartEasing) -> EasingFunction {
283 match easing {
284 crate::animation::ChartEasing::Linear => EasingFunction::Linear,
285 crate::animation::ChartEasing::EaseIn => EasingFunction::EaseIn,
286 crate::animation::ChartEasing::EaseOut => EasingFunction::EaseOut,
287 crate::animation::ChartEasing::EaseInOut => EasingFunction::EaseInOut,
288 }
289}
290
291#[derive(Debug)]
292pub struct ChartInternalLowerer {
293 pub chart: Chart,
294}
295
296#[derive(Debug)]
297struct ChartRenderObject {
298 chart: Chart,
299}
300
301impl CustomRenderObject for ChartRenderObject {
302 fn hit_test(&self, local_point: LayoutPoint, node_rect: LayoutRect) -> CustomHitResult {
303 if local_point.x >= 0.0
304 && local_point.y >= 0.0
305 && local_point.x < node_rect.width()
306 && local_point.y < node_rect.height()
307 {
308 CustomHitResult::inside(None)
309 } else {
310 CustomHitResult::miss()
311 }
312 }
313
314 fn handle_event(
315 &self,
316 node_id: fission_ir::WidgetId,
317 event: &InputEvent,
318 node_rect: LayoutRect,
319 ) -> CustomEventResult {
320 if !self.chart.interaction.emit_events {
321 return CustomEventResult::ignored();
322 }
323
324 let Some((kind, point, modifiers)) = chart_event_point(event) else {
325 return CustomEventResult::ignored();
326 };
327 let local = LayoutPoint::new(point.x - node_rect.x(), point.y - node_rect.y());
328 let hit = self
329 .chart
330 .hit_test(node_rect.width(), node_rect.height(), local);
331 let event = ChartInteractionEvent {
332 chart_id: self.chart.title.clone(),
333 kind,
334 local_x: local.x,
335 local_y: local.y,
336 modifiers,
337 hit,
338 };
339 let envelope = ActionEnvelope {
340 id: ChartInteractionEvent::static_id(),
341 payload: event.encode(),
342 };
343 CustomEventResult::consumed_with(vec![(node_id, envelope)])
344 }
345}
346
347#[derive(Debug, Clone, Copy)]
348struct ChartArea {
349 outer_w: f32,
350 outer_h: f32,
351 plot: LayoutRect,
352}
353
354#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
355pub struct ChartTheme {
356 pub background: Color,
357 pub plot_background: Color,
358 pub grid_line: Color,
359 pub axis_line: Color,
360 pub label: Color,
361 pub title: Color,
362 pub diagnostic: Color,
363 pub palette: Vec<Color>,
364}
365
366impl Default for ChartTheme {
367 fn default() -> Self {
368 Self::light()
369 }
370}
371
372impl ChartTheme {
373 pub fn light() -> Self {
374 Self {
375 background: color(255, 255, 255, 255),
376 plot_background: color(250, 252, 255, 255),
377 grid_line: color(226, 232, 240, 255),
378 axis_line: color(148, 163, 184, 255),
379 label: color(71, 85, 105, 255),
380 title: color(15, 23, 42, 255),
381 diagnostic: color(180, 83, 9, 255),
382 palette: vec![
383 color(84, 112, 198, 255),
384 color(145, 204, 117, 255),
385 color(250, 200, 88, 255),
386 color(238, 102, 102, 255),
387 color(115, 192, 222, 255),
388 color(154, 96, 180, 255),
389 color(234, 124, 204, 255),
390 color(59, 162, 114, 255),
391 ],
392 }
393 }
394
395 pub fn dark() -> Self {
396 Self {
397 background: color(15, 23, 42, 255),
398 plot_background: color(17, 24, 39, 255),
399 grid_line: color(51, 65, 85, 255),
400 axis_line: color(100, 116, 139, 255),
401 label: color(203, 213, 225, 255),
402 title: color(248, 250, 252, 255),
403 diagnostic: color(251, 191, 36, 255),
404 palette: vec![
405 color(96, 165, 250, 255),
406 color(45, 212, 191, 255),
407 color(251, 191, 36, 255),
408 color(248, 113, 113, 255),
409 color(56, 189, 248, 255),
410 color(192, 132, 252, 255),
411 color(244, 114, 182, 255),
412 color(74, 222, 128, 255),
413 ],
414 }
415 }
416
417 fn from_env(env: &fission_core::Env) -> Self {
418 let colors = &env.theme.tokens.colors;
419 let dark = color_luma(colors.background) < 128.0;
420 let mut theme = if dark { Self::dark() } else { Self::light() };
421 theme.background = colors.surface;
422 theme.plot_background = if dark {
423 mix_color(colors.surface, colors.background, 0.5)
424 } else {
425 mix_color(colors.surface, Color::WHITE, 0.55)
426 };
427 theme.grid_line = colors.border;
428 theme.axis_line = colors.text_secondary;
429 theme.label = colors.text_secondary;
430 theme.title = colors.text_primary;
431 if env.theme.tokens.data_visualization.palette.is_empty() {
432 theme.palette[0] = colors.primary;
433 theme.palette[1] = colors.secondary;
434 } else {
435 theme.palette = env.theme.tokens.data_visualization.palette.clone();
436 }
437 theme
438 }
439}
440
441#[cfg(test)]
442mod chart_theme_tests {
443 use super::*;
444
445 #[test]
446 fn chart_theme_uses_generated_data_visualization_palette() {
447 let mut env = fission_core::Env::default();
448 env.theme.tokens.data_visualization.palette = vec![
449 color(1, 2, 3, 255),
450 color(4, 5, 6, 255),
451 color(7, 8, 9, 255),
452 ];
453
454 let theme = ChartTheme::from_env(&env);
455
456 assert_eq!(theme.palette, env.theme.tokens.data_visualization.palette);
457 }
458}
459
460impl fission_core::internal::InternalLowerer for ChartInternalLowerer {
461 fn lower_dyn(
462 &self,
463 cx: &mut fission_core::internal::InternalLoweringCx,
464 ) -> fission_ir::WidgetId {
465 let model = ChartModel::from_chart(&self.chart);
466 let theme = self
467 .chart
468 .theme
469 .clone()
470 .unwrap_or_else(|| ChartTheme::from_env(cx.env));
471 let area = chart_area(&self.chart, cx);
472 let mut root = fission_core::internal::InternalIrBuilder::new(
473 cx.next_node_id(),
474 fission_ir::Op::Layout(LayoutOp::ZStack),
475 );
476
477 draw_background(cx, &mut root, &area, &theme);
478 draw_title(cx, &mut root, &model, &area, &theme);
479 if model.has_cartesian_series() {
480 draw_cartesian_axes(cx, &mut root, &model, &area, &theme);
481 }
482
483 draw_mark_areas(cx, &mut root, &model, &self.chart, &area);
484 render_series(cx, &mut root, &model, &self.chart, &area, &theme);
485 draw_mark_lines(cx, &mut root, &model, &self.chart, &area, &theme);
486 draw_mark_points(cx, &mut root, &model, &self.chart, &area, &theme);
487 draw_legend(cx, &mut root, &model, &self.chart, &area, &theme);
488 draw_visual_map(cx, &mut root, &self.chart, &area, &theme);
489 draw_data_zoom(cx, &mut root, &self.chart, &area, &theme);
490 draw_brush(cx, &mut root, &self.chart, &area, &theme);
491 draw_graphics(cx, &mut root, &self.chart, &area, &theme);
492 draw_timeline(cx, &mut root, &self.chart, &area, &theme);
493 draw_toolbox(cx, &mut root, &self.chart, &area, &theme);
494 draw_diagnostics(cx, &mut root, &model, &area, &theme);
495
496 root.build(cx)
497 }
498}
499
500fn chart_area(chart: &Chart, cx: &fission_core::internal::InternalLoweringCx) -> ChartArea {
501 let outer_w = chart.width.unwrap_or_else(|| {
502 let available_w = cx.env.viewport_size.width;
503 (available_w - 380.0).max(360.0)
504 });
505 let outer_h = chart.height.unwrap_or_else(|| {
506 let available_h = cx.env.viewport_size.height;
507 (available_h - 200.0).max(320.0)
508 });
509 chart_area_for_size(chart, outer_w, outer_h)
510}
511
512fn chart_area_for_size(chart: &Chart, outer_w: f32, outer_h: f32) -> ChartArea {
513 let grid = chart.grid.clone().unwrap_or_default();
514 let left = grid.left.unwrap_or(70.0);
515 let top = grid
516 .top
517 .unwrap_or(if chart.title.is_some() { 58.0 } else { 38.0 });
518 let right = grid
519 .right
520 .unwrap_or(if chart.legend.is_some() { 130.0 } else { 44.0 });
521 let bottom = grid.bottom.unwrap_or(if chart.data_zoom.is_some() {
522 78.0
523 } else {
524 54.0
525 });
526 ChartArea {
527 outer_w,
528 outer_h,
529 plot: LayoutRect::new(
530 left,
531 top,
532 (outer_w - left - right).max(1.0),
533 (outer_h - top - bottom).max(1.0),
534 ),
535 }
536}
537
538fn chart_event_point(event: &InputEvent) -> Option<(ChartInteractionKind, LayoutPoint, u8)> {
539 match event {
540 InputEvent::Pointer(PointerEvent::Move { point, modifiers }) => {
541 Some((ChartInteractionKind::Hover, *point, *modifiers))
542 }
543 InputEvent::Pointer(PointerEvent::Down {
544 point, modifiers, ..
545 }) => Some((ChartInteractionKind::Press, *point, *modifiers)),
546 InputEvent::Pointer(PointerEvent::Up {
547 point, modifiers, ..
548 }) => Some((ChartInteractionKind::Release, *point, *modifiers)),
549 InputEvent::Pointer(PointerEvent::Scroll {
550 point, modifiers, ..
551 }) => Some((ChartInteractionKind::Scroll, *point, *modifiers)),
552 _ => None,
553 }
554}
555
556#[derive(Debug, Clone, Copy)]
557struct ChartAnimationFrame {
558 enabled: bool,
559 progress: f32,
560 stagger_fraction: f32,
561}
562
563impl ChartAnimationFrame {
564 fn from_chart(chart: &Chart, cx: &fission_core::internal::InternalLoweringCx) -> Self {
565 if !chart.animation.enabled {
566 return Self::complete();
567 }
568
569 let progress = cx
570 .runtime_state
571 .animation
572 .values
573 .get(&(chart.animation_id(), chart_animation_property()))
574 .copied()
575 .unwrap_or(1.0)
576 .clamp(0.0, 1.0);
577 let duration = chart.animation.duration_ms.max(1) as f32;
578 let stagger_fraction = (chart.animation.stagger_ms as f32 / duration).clamp(0.0, 0.18);
579
580 Self {
581 enabled: true,
582 progress,
583 stagger_fraction,
584 }
585 }
586
587 fn complete() -> Self {
588 Self {
589 enabled: false,
590 progress: 1.0,
591 stagger_fraction: 0.0,
592 }
593 }
594
595 fn series_progress(self, series_index: usize) -> f32 {
596 if !self.enabled {
597 return 1.0;
598 }
599 self.staggered_progress(series_index, self.stagger_fraction)
600 }
601
602 fn item_progress(self, series_progress: f32, item_index: usize) -> f32 {
603 if !self.enabled {
604 return 1.0;
605 }
606 let item_stagger = (self.stagger_fraction * 0.55).min(0.08);
607 Self {
608 progress: series_progress,
609 ..self
610 }
611 .staggered_progress(item_index, item_stagger)
612 }
613
614 fn staggered_progress(self, index: usize, step: f32) -> f32 {
615 let delay = (index as f32 * step).min(0.86);
616 if self.progress <= delay {
617 0.0
618 } else {
619 ((self.progress - delay) / (1.0 - delay)).clamp(0.0, 1.0)
620 }
621 }
622}
623
624fn render_series(
625 cx: &mut fission_core::internal::InternalLoweringCx,
626 root: &mut fission_core::internal::InternalIrBuilder,
627 model: &ChartModel,
628 chart: &Chart,
629 area: &ChartArea,
630 theme: &ChartTheme,
631) {
632 let x_scale = LinearScale::nice(model.x_domain.0, model.x_domain.1, 6);
633 let y_scale = LinearScale::nice(model.y_domain.0, model.y_domain.1, 6);
634 let bar_groups = count_bar_groups(&model.series);
635 let mut bar_group_index = 0usize;
636 let mut bar_stacks: HashMap<(String, usize), f32> = HashMap::new();
637 let mut line_stacks: HashMap<(String, usize), f32> = HashMap::new();
638 let animation = ChartAnimationFrame::from_chart(chart, cx);
639
640 for (series_index, series) in model.series.iter().enumerate() {
641 match series {
642 ResolvedSeries::Bar(bar) => {
643 let group_index = if bar.source.stack.is_none() {
644 let idx = bar_group_index;
645 bar_group_index += 1;
646 idx
647 } else {
648 0
649 };
650 render_bar(
651 cx,
652 root,
653 bar,
654 &mut bar_stacks,
655 model,
656 area,
657 &x_scale,
658 &y_scale,
659 theme,
660 group_index,
661 bar_groups,
662 animation,
663 series_index,
664 );
665 }
666 ResolvedSeries::Line(line) => render_line(
667 cx,
668 root,
669 line,
670 &mut line_stacks,
671 model,
672 area,
673 &x_scale,
674 &y_scale,
675 theme,
676 animation,
677 series_index,
678 ),
679 ResolvedSeries::Scatter(scatter) => render_scatter(
680 cx,
681 root,
682 &scatter.data,
683 scatter.color,
684 chart.visual_map.as_ref(),
685 area,
686 &x_scale,
687 &y_scale,
688 theme,
689 false,
690 animation,
691 series_index,
692 ),
693 ResolvedSeries::Bubble(bubble) => render_bubble(
694 cx,
695 root,
696 bubble,
697 chart.visual_map.as_ref(),
698 area,
699 &x_scale,
700 &y_scale,
701 animation,
702 series_index,
703 ),
704 ResolvedSeries::EffectScatter(effect) => render_scatter(
705 cx,
706 root,
707 &effect.data,
708 effect.color,
709 chart.visual_map.as_ref(),
710 area,
711 &x_scale,
712 &y_scale,
713 theme,
714 true,
715 animation,
716 series_index,
717 ),
718 ResolvedSeries::Pie(pie) => {
719 render_pie(cx, root, pie, area, theme, animation, series_index)
720 }
721 ResolvedSeries::Boxplot(boxplot) => render_boxplot(
722 cx,
723 root,
724 boxplot,
725 model,
726 area,
727 &y_scale,
728 theme,
729 animation,
730 series_index,
731 ),
732 ResolvedSeries::Candlestick(candle) => render_candlestick(
733 cx,
734 root,
735 candle,
736 model,
737 area,
738 &y_scale,
739 animation,
740 series_index,
741 ),
742 ResolvedSeries::Heatmap(heatmap) => render_heatmap(
743 cx,
744 root,
745 heatmap,
746 model,
747 chart.visual_map.as_ref(),
748 area,
749 theme,
750 animation,
751 series_index,
752 ),
753 ResolvedSeries::CalendarHeatmap(calendar) => render_calendar_heatmap(
754 cx,
755 root,
756 calendar,
757 chart.visual_map.as_ref(),
758 area,
759 theme,
760 animation,
761 series_index,
762 ),
763 ResolvedSeries::Lines(lines) => {
764 render_lines(cx, root, lines, area, theme, animation, series_index)
765 }
766 ResolvedSeries::Graph(graph) => {
767 render_graph(cx, root, graph, area, theme, animation, series_index)
768 }
769 ResolvedSeries::Tree(tree) => {
770 render_tree(cx, root, tree, area, theme, animation, series_index)
771 }
772 ResolvedSeries::Treemap(treemap) => {
773 render_treemap(cx, root, treemap, area, theme, animation, series_index)
774 }
775 ResolvedSeries::Radar(radar) => {
776 render_radar(cx, root, radar, area, theme, animation, series_index)
777 }
778 ResolvedSeries::Funnel(funnel) => {
779 render_funnel(cx, root, funnel, area, theme, animation, series_index)
780 }
781 ResolvedSeries::Gauge(gauge) => {
782 render_gauge(cx, root, gauge, area, theme, animation, series_index)
783 }
784 ResolvedSeries::Map(map) => render_map(
785 cx,
786 root,
787 map,
788 chart.visual_map.as_ref(),
789 area,
790 theme,
791 animation,
792 series_index,
793 ),
794 ResolvedSeries::Sankey(sankey) => {
795 render_sankey(cx, root, sankey, area, theme, animation, series_index)
796 }
797 ResolvedSeries::Parallel(parallel) => {
798 render_parallel(cx, root, parallel, area, theme, animation, series_index)
799 }
800 ResolvedSeries::Sunburst(sunburst) => {
801 render_sunburst(cx, root, sunburst, area, theme, animation, series_index)
802 }
803 ResolvedSeries::ThemeRiver(river) => {
804 render_theme_river(cx, root, river, area, theme, animation, series_index)
805 }
806 ResolvedSeries::PictorialBar(pic) => render_pictorial_bar(
807 cx,
808 root,
809 pic,
810 model,
811 area,
812 &y_scale,
813 theme,
814 animation,
815 series_index,
816 ),
817 ResolvedSeries::Liquidfill(liquid) => {
818 render_liquidfill(cx, root, liquid, area, theme, animation, series_index)
819 }
820 ResolvedSeries::Wordcloud(words) => {
821 render_wordcloud(cx, root, words, area, theme, animation, series_index)
822 }
823 ResolvedSeries::PolarBar(polar) => {
824 render_polar_bar(cx, root, polar, area, theme, animation, series_index)
825 }
826 ResolvedSeries::PolarLine(polar) => {
827 render_polar_line(cx, root, polar, area, theme, animation, series_index)
828 }
829 ResolvedSeries::SingleAxis(single_axis) => {
830 render_single_axis(cx, root, single_axis, area, theme, animation, series_index)
831 }
832 }
833 }
834}
835
836fn hit_test_chart(model: &ChartModel, area: &ChartArea, point: LayoutPoint) -> Option<ChartHit> {
837 if !area.plot.contains(point) {
838 return None;
839 }
840
841 let x_scale = LinearScale::nice(model.x_domain.0, model.x_domain.1, 6);
842 let y_scale = LinearScale::nice(model.y_domain.0, model.y_domain.1, 6);
843 let threshold = 10.0;
844 let bar_groups = count_bar_groups(&model.series);
845 let mut bar_group_index = 0usize;
846 let mut bar_stacks: HashMap<(String, usize), f32> = HashMap::new();
847 let mut line_stacks: HashMap<(String, usize), f32> = HashMap::new();
848 let mut direct_hit = None;
849
850 for (series_index, series) in model.series.iter().enumerate() {
851 match series {
852 ResolvedSeries::Bar(bar) => {
853 let group_index = if bar.source.stack.is_none() {
854 let idx = bar_group_index;
855 bar_group_index += 1;
856 idx
857 } else {
858 0
859 };
860 let band = band_width(model, area);
861 let group_count = bar_groups.max(1) as f32;
862 let bar_w = if bar.source.stack.is_some() {
863 band * 0.64
864 } else {
865 (band * 0.72 / group_count).max(2.0)
866 };
867 let group_offset = if bar.source.stack.is_some() {
868 0.0
869 } else {
870 (group_index as f32 - (group_count - 1.0) / 2.0) * bar_w
871 };
872
873 for (idx, value) in bar.values.iter().enumerate() {
874 let base = stack_base(&bar_stacks, bar.source.stack.as_ref(), idx);
875 let total = base + *value;
876 if let Some(stack) = bar.source.stack.as_ref() {
877 bar_stacks.insert((stack.clone(), idx), total);
878 }
879 let rect = if bar.source.orientation
880 == crate::series::bar::BarOrientation::Horizontal
881 {
882 let band = category_band_width(
883 model.y_categories.len().max(bar.values.len()),
884 area.plot.height(),
885 );
886 let bar_h = if bar.source.stack.is_some() {
887 band * 0.64
888 } else {
889 (band * 0.72 / group_count).max(2.0)
890 };
891 let group_offset_y = if bar.source.stack.is_some() {
892 0.0
893 } else {
894 (group_index as f32 - (group_count - 1.0) / 2.0) * bar_h
895 };
896 let y = map_category_y(idx, model, area) + group_offset_y;
897 let x0 = map_x(base, area, &x_scale);
898 let x1 = map_x(total, area, &x_scale);
899 LayoutRect::new(
900 x0.min(x1),
901 y - bar_h / 2.0,
902 (x1 - x0).abs().max(1.0),
903 bar_h,
904 )
905 } else {
906 let x = map_category_x(idx, model, area) + group_offset;
907 let y0 = map_y(base, area, &y_scale);
908 let y1 = map_y(total, area, &y_scale);
909 LayoutRect::new(
910 x - bar_w / 2.0,
911 y0.min(y1),
912 bar_w,
913 (y0 - y1).abs().max(1.0),
914 )
915 };
916 if rect.contains(point) {
917 direct_hit = Some(ChartHit::series_item(
918 series_index,
919 bar.source.name.clone(),
920 idx,
921 Some(idx as f32),
922 Some(total),
923 ));
924 }
925 }
926 }
927 ResolvedSeries::Line(line) => {
928 for (idx, value) in line.values.iter().enumerate() {
929 let base = stack_base(&line_stacks, line.source.stack.as_ref(), idx);
930 let total = base + *value;
931 if let Some(stack) = line.source.stack.as_ref() {
932 line_stacks.insert((stack.clone(), idx), total);
933 }
934 let x = map_category_x(idx, model, area);
935 let y = map_y(total, area, &y_scale);
936 if distance(point, (x, y)) <= threshold {
937 direct_hit = Some(ChartHit::series_item(
938 series_index,
939 line.source.name.clone(),
940 idx,
941 Some(idx as f32),
942 Some(total),
943 ));
944 }
945 }
946 }
947 ResolvedSeries::Scatter(scatter) => {
948 if let Some(hit) = hit_test_points(
949 series_index,
950 &scatter.name,
951 &scatter.data,
952 area,
953 &x_scale,
954 &y_scale,
955 point,
956 threshold,
957 ) {
958 direct_hit = Some(hit);
959 }
960 }
961 ResolvedSeries::Bubble(bubble) => {
962 let max_size = bubble
963 .data
964 .iter()
965 .map(|(_, _, size)| *size)
966 .fold(1.0_f32, f32::max);
967 for (idx, (xv, yv, size)) in bubble.data.iter().enumerate() {
968 let x = map_x(*xv, area, &x_scale);
969 let y = map_y(*yv, area, &y_scale);
970 let t = (*size / max_size).clamp(0.0, 1.0).sqrt();
971 let radius = bubble.min_radius + (bubble.max_radius - bubble.min_radius) * t;
972 if distance(point, (x, y)) <= radius.max(threshold) {
973 direct_hit = Some(ChartHit::series_item(
974 series_index,
975 bubble.name.clone(),
976 idx,
977 Some(*xv),
978 Some(*yv),
979 ));
980 }
981 }
982 }
983 ResolvedSeries::EffectScatter(scatter) => {
984 if let Some(hit) = hit_test_points(
985 series_index,
986 &scatter.name,
987 &scatter.data,
988 area,
989 &x_scale,
990 &y_scale,
991 point,
992 threshold * 1.6,
993 ) {
994 direct_hit = Some(hit);
995 }
996 }
997 ResolvedSeries::Pie(pie) => {
998 if let Some(hit) = hit_test_pie(series_index, pie, area, point) {
999 direct_hit = Some(hit);
1000 }
1001 }
1002 ResolvedSeries::Heatmap(heatmap) => {
1003 let max_x = heatmap.data.iter().map(|d| d.0).max().unwrap_or(0) + 1;
1004 let max_y = heatmap.data.iter().map(|d| d.1).max().unwrap_or(0) + 1;
1005 let cell_w = area.plot.width() / max_x.max(1) as f32;
1006 let cell_h = area.plot.height() / max_y.max(1) as f32;
1007 for (idx, (x_idx, y_idx, value)) in heatmap.data.iter().enumerate() {
1008 let rect = LayoutRect::new(
1009 area.plot.x() + *x_idx as f32 * cell_w,
1010 area.plot.bottom() - (*y_idx as f32 + 1.0) * cell_h,
1011 cell_w,
1012 cell_h,
1013 );
1014 if rect.contains(point) {
1015 direct_hit = Some(ChartHit::series_item(
1016 series_index,
1017 heatmap.name.clone(),
1018 idx,
1019 Some(*x_idx as f32),
1020 Some(*value),
1021 ));
1022 }
1023 }
1024 }
1025 _ => {}
1026 }
1027 }
1028
1029 direct_hit
1030 .or_else(|| nearest_cartesian_hit(model, area, point))
1031 .or_else(|| Some(ChartHit::plot_area()))
1032}
1033
1034fn draw_background(
1035 cx: &mut fission_core::internal::InternalLoweringCx,
1036 root: &mut fission_core::internal::InternalIrBuilder,
1037 area: &ChartArea,
1038 theme: &ChartTheme,
1039) {
1040 add_rect(
1041 cx,
1042 root,
1043 LayoutRect::new(0.0, 0.0, area.outer_w, area.outer_h),
1044 theme.background,
1045 None,
1046 14.0,
1047 );
1048 add_rect(
1049 cx,
1050 root,
1051 area.plot,
1052 theme.plot_background,
1053 Some(stroke(theme.grid_line, 1.0)),
1054 8.0,
1055 );
1056}
1057
1058fn draw_title(
1059 cx: &mut fission_core::internal::InternalLoweringCx,
1060 root: &mut fission_core::internal::InternalIrBuilder,
1061 model: &ChartModel,
1062 _area: &ChartArea,
1063 theme: &ChartTheme,
1064) {
1065 if let Some(title) = model.title.as_ref() {
1066 add_text(cx, root, title, 18.0, theme.title, 20.0, 18.0, 360.0, 28.0);
1067 }
1068}
1069
1070fn draw_cartesian_axes(
1071 cx: &mut fission_core::internal::InternalLoweringCx,
1072 root: &mut fission_core::internal::InternalIrBuilder,
1073 model: &ChartModel,
1074 area: &ChartArea,
1075 theme: &ChartTheme,
1076) {
1077 let y_scale = LinearScale::nice(model.y_domain.0, model.y_domain.1, 6);
1078 for tick in &y_scale.ticks {
1079 let y = map_y(*tick, area, &y_scale);
1080 if model.y_axis.split_line {
1081 add_path(
1082 cx,
1083 root,
1084 &format!("M {} {} L {} {}", area.plot.x(), y, area.plot.right(), y),
1085 None,
1086 Some(stroke(theme.grid_line, 1.0)),
1087 );
1088 }
1089 add_text(
1090 cx,
1091 root,
1092 &format_tick(*tick),
1093 11.0,
1094 theme.label,
1095 8.0,
1096 y - 7.0,
1097 area.plot.x() - 14.0,
1098 14.0,
1099 );
1100 }
1101
1102 add_path(
1103 cx,
1104 root,
1105 &format!(
1106 "M {} {} L {} {}",
1107 area.plot.x(),
1108 area.plot.bottom(),
1109 area.plot.right(),
1110 area.plot.bottom()
1111 ),
1112 None,
1113 Some(stroke(theme.axis_line, 1.0)),
1114 );
1115 add_path(
1116 cx,
1117 root,
1118 &format!(
1119 "M {} {} L {} {}",
1120 area.plot.x(),
1121 area.plot.y(),
1122 area.plot.x(),
1123 area.plot.bottom()
1124 ),
1125 None,
1126 Some(stroke(theme.axis_line, 1.0)),
1127 );
1128
1129 if model.x_axis.axis_type == AxisType::Category && !model.x_categories.is_empty() {
1130 let band = band_width(model, area);
1131 for (idx, label) in model.x_categories.iter().enumerate() {
1132 let x = map_category_x(idx, model, area);
1133 add_text(
1134 cx,
1135 root,
1136 label,
1137 11.0,
1138 theme.label,
1139 x - band / 2.0,
1140 area.plot.bottom() + 8.0,
1141 band,
1142 18.0,
1143 );
1144 }
1145 } else if model.y_axis.axis_type == AxisType::Category && !model.y_categories.is_empty() {
1146 let x_scale = LinearScale::nice(model.x_domain.0, model.x_domain.1, 6);
1147 for tick in &x_scale.ticks {
1148 let x = map_x(*tick, area, &x_scale);
1149 add_text(
1150 cx,
1151 root,
1152 &format_tick(*tick),
1153 11.0,
1154 theme.label,
1155 x - 24.0,
1156 area.plot.bottom() + 8.0,
1157 48.0,
1158 18.0,
1159 );
1160 }
1161 let band = category_band_width(model.y_categories.len(), area.plot.height());
1162 for (idx, label) in model.y_categories.iter().enumerate() {
1163 let y = map_category_y(idx, model, area);
1164 add_text(
1165 cx,
1166 root,
1167 label,
1168 11.0,
1169 theme.label,
1170 8.0,
1171 y - band / 2.0,
1172 area.plot.x() - 14.0,
1173 band.max(16.0),
1174 );
1175 }
1176 } else {
1177 let x_scale = LinearScale::nice(model.x_domain.0, model.x_domain.1, 6);
1178 for tick in &x_scale.ticks {
1179 let x = map_x(*tick, area, &x_scale);
1180 add_text(
1181 cx,
1182 root,
1183 &format_tick(*tick),
1184 11.0,
1185 theme.label,
1186 x - 24.0,
1187 area.plot.bottom() + 8.0,
1188 48.0,
1189 18.0,
1190 );
1191 }
1192 }
1193}
1194
1195fn render_bar(
1196 cx: &mut fission_core::internal::InternalLoweringCx,
1197 root: &mut fission_core::internal::InternalIrBuilder,
1198 bar: &ResolvedBarSeries,
1199 stacks: &mut HashMap<(String, usize), f32>,
1200 model: &ChartModel,
1201 area: &ChartArea,
1202 x_scale: &LinearScale,
1203 y_scale: &LinearScale,
1204 _theme: &ChartTheme,
1205 group_index: usize,
1206 group_count: usize,
1207 animation: ChartAnimationFrame,
1208 series_index: usize,
1209) {
1210 let series_progress = animation.series_progress(series_index);
1211 if bar.source.orientation == crate::series::bar::BarOrientation::Horizontal {
1212 render_horizontal_bar(
1213 cx,
1214 root,
1215 bar,
1216 stacks,
1217 model,
1218 area,
1219 x_scale,
1220 group_index,
1221 group_count,
1222 animation,
1223 series_progress,
1224 );
1225 return;
1226 }
1227
1228 let band = band_width(model, area);
1229 let group_count = group_count.max(1) as f32;
1230 let bar_w = if bar.source.stack.is_some() {
1231 band * 0.64
1232 } else {
1233 (band * 0.72 / group_count).max(2.0)
1234 };
1235 let group_offset = if bar.source.stack.is_some() {
1236 0.0
1237 } else {
1238 (group_index as f32 - (group_count - 1.0) / 2.0) * bar_w
1239 };
1240
1241 for (idx, value) in bar.values.iter().enumerate() {
1242 let item_progress = animation.item_progress(series_progress, idx);
1243 if item_progress <= f32::EPSILON {
1244 continue;
1245 }
1246 let base = stack_base(stacks, bar.source.stack.as_ref(), idx);
1247 let total = base + *value * item_progress;
1248 if bar.source.stack.is_some() {
1249 stacks.insert((bar.source.stack.clone().unwrap(), idx), total);
1250 }
1251 let x = map_category_x(idx, model, area) + group_offset;
1252 let y0 = map_y(base, area, y_scale);
1253 let y1 = map_y(total, area, y_scale);
1254 let top = y0.min(y1);
1255 let height = (y0 - y1).abs().max(1.0);
1256 if let Some(background) = bar.source.background {
1257 add_rect(
1258 cx,
1259 root,
1260 LayoutRect::new(x - bar_w / 2.0, area.plot.y(), bar_w, area.plot.height()),
1261 background,
1262 None,
1263 bar.source.border_radius.unwrap_or(4.0),
1264 );
1265 }
1266 add_rect(
1267 cx,
1268 root,
1269 LayoutRect::new(x - bar_w / 2.0, top, bar_w, height),
1270 bar.source.color,
1271 None,
1272 bar.source.border_radius.unwrap_or(4.0),
1273 );
1274 }
1275}
1276
1277#[allow(clippy::too_many_arguments)]
1278fn render_horizontal_bar(
1279 cx: &mut fission_core::internal::InternalLoweringCx,
1280 root: &mut fission_core::internal::InternalIrBuilder,
1281 bar: &ResolvedBarSeries,
1282 stacks: &mut HashMap<(String, usize), f32>,
1283 model: &ChartModel,
1284 area: &ChartArea,
1285 x_scale: &LinearScale,
1286 group_index: usize,
1287 group_count: usize,
1288 animation: ChartAnimationFrame,
1289 series_progress: f32,
1290) {
1291 let band = category_band_width(
1292 model.y_categories.len().max(bar.values.len()),
1293 area.plot.height(),
1294 );
1295 let group_count = group_count.max(1) as f32;
1296 let bar_h = if bar.source.stack.is_some() {
1297 band * 0.64
1298 } else {
1299 (band * 0.72 / group_count).max(2.0)
1300 };
1301 let group_offset = if bar.source.stack.is_some() {
1302 0.0
1303 } else {
1304 (group_index as f32 - (group_count - 1.0) / 2.0) * bar_h
1305 };
1306
1307 for (idx, value) in bar.values.iter().enumerate() {
1308 let item_progress = animation.item_progress(series_progress, idx);
1309 if item_progress <= f32::EPSILON {
1310 continue;
1311 }
1312 let base = stack_base(stacks, bar.source.stack.as_ref(), idx);
1313 let total = base + *value * item_progress;
1314 if bar.source.stack.is_some() {
1315 stacks.insert((bar.source.stack.clone().unwrap(), idx), total);
1316 }
1317 let y = map_category_y(idx, model, area) + group_offset;
1318 let x0 = map_x(base, area, x_scale);
1319 let x1 = map_x(total, area, x_scale);
1320 let left = x0.min(x1);
1321 let width = (x1 - x0).abs().max(1.0);
1322 if let Some(background) = bar.source.background {
1323 add_rect(
1324 cx,
1325 root,
1326 LayoutRect::new(area.plot.x(), y - bar_h / 2.0, area.plot.width(), bar_h),
1327 background,
1328 None,
1329 bar.source.border_radius.unwrap_or(4.0),
1330 );
1331 }
1332 add_rect(
1333 cx,
1334 root,
1335 LayoutRect::new(left, y - bar_h / 2.0, width, bar_h),
1336 bar.source.color,
1337 None,
1338 bar.source.border_radius.unwrap_or(4.0),
1339 );
1340 }
1341}
1342
1343fn render_line(
1344 cx: &mut fission_core::internal::InternalLoweringCx,
1345 root: &mut fission_core::internal::InternalIrBuilder,
1346 line: &ResolvedLineSeries,
1347 stacks: &mut HashMap<(String, usize), f32>,
1348 model: &ChartModel,
1349 area: &ChartArea,
1350 _x_scale: &LinearScale,
1351 y_scale: &LinearScale,
1352 _theme: &ChartTheme,
1353 animation: ChartAnimationFrame,
1354 series_index: usize,
1355) {
1356 if line.values.is_empty() {
1357 return;
1358 }
1359 let series_progress = animation.series_progress(series_index);
1360 if series_progress <= f32::EPSILON {
1361 return;
1362 }
1363 let mut points = Vec::new();
1364 let mut base_points = Vec::new();
1365 for (idx, value) in line.values.iter().enumerate() {
1366 let base = stack_base(stacks, line.source.stack.as_ref(), idx);
1367 let total = base + *value;
1368 if line.source.stack.is_some() {
1369 stacks.insert((line.source.stack.clone().unwrap(), idx), total);
1370 }
1371 let x = map_category_x(idx, model, area);
1372 points.push((x, map_y(total, area, y_scale)));
1373 base_points.push((x, map_y(base, area, y_scale)));
1374 }
1375
1376 let revealed_points = reveal_points(&points, series_progress);
1377 let revealed_base_points = reveal_points(&base_points, series_progress);
1378
1379 if let Some(area_color) = line.source.area_style {
1380 if revealed_points.len() > 1 && revealed_base_points.len() > 1 {
1381 let mut area_path = path_for_line(
1382 &revealed_points,
1383 line.source.smooth,
1384 line.source.step.as_deref(),
1385 );
1386 for (x, y) in revealed_base_points.iter().rev() {
1387 area_path.push_str(&format!(" L {} {}", x, y));
1388 }
1389 area_path.push_str(" Z");
1390 let fill = Fill::LinearGradient {
1391 start: (area.plot.x(), area.plot.y()),
1392 end: (area.plot.x(), area.plot.bottom()),
1393 stops: vec![(0.0, area_color), (1.0, area_color.with_alpha(16))],
1394 };
1395 add_path(cx, root, &area_path, Some(fill), None);
1396 }
1397 }
1398
1399 if revealed_points.len() > 1 {
1400 add_path(
1401 cx,
1402 root,
1403 &path_for_line(
1404 &revealed_points,
1405 line.source.smooth,
1406 line.source.step.as_deref(),
1407 ),
1408 None,
1409 Some(stroke(line.source.color, 2.4)),
1410 );
1411 }
1412 for (idx, (x, y)) in revealed_points.into_iter().enumerate() {
1413 let item_progress = animation.item_progress(series_progress, idx);
1414 if item_progress <= f32::EPSILON {
1415 continue;
1416 }
1417 let radius = 3.0 * item_progress.sqrt();
1418 add_rect(
1419 cx,
1420 root,
1421 LayoutRect::new(x - radius, y - radius, radius * 2.0, radius * 2.0),
1422 fade_color(line.source.color, item_progress),
1423 Some(stroke(Color::WHITE, 1.0)),
1424 radius,
1425 );
1426 }
1427}
1428
1429fn render_scatter(
1430 cx: &mut fission_core::internal::InternalLoweringCx,
1431 root: &mut fission_core::internal::InternalIrBuilder,
1432 data: &[(f32, f32)],
1433 color: Color,
1434 visual_map: Option<&VisualMap>,
1435 area: &ChartArea,
1436 x_scale: &LinearScale,
1437 y_scale: &LinearScale,
1438 _theme: &ChartTheme,
1439 effect: bool,
1440 animation: ChartAnimationFrame,
1441 series_index: usize,
1442) {
1443 let series_progress = animation.series_progress(series_index);
1444 if series_progress <= f32::EPSILON {
1445 return;
1446 }
1447
1448 for (idx, (xv, yv)) in data.iter().enumerate() {
1449 let item_progress = animation.item_progress(series_progress, idx);
1450 if item_progress <= f32::EPSILON {
1451 continue;
1452 }
1453 let x = map_x(*xv, area, x_scale);
1454 let y = map_y(*yv, area, y_scale);
1455 let fill = visual_map
1456 .map(|map| visual_color(map, *yv))
1457 .unwrap_or(color);
1458 if effect {
1459 for (scale, alpha) in [(2.2, 45), (1.55, 72), (1.0, 220)] {
1460 let r = 7.0 * scale * item_progress.sqrt();
1461 add_rect(
1462 cx,
1463 root,
1464 LayoutRect::new(x - r, y - r, r * 2.0, r * 2.0),
1465 fill.with_alpha(((alpha as f32) * item_progress).round() as u8),
1466 None,
1467 r,
1468 );
1469 }
1470 } else {
1471 let r = 5.5 * item_progress.sqrt();
1472 add_rect(
1473 cx,
1474 root,
1475 LayoutRect::new(x - r, y - r, r * 2.0, r * 2.0),
1476 fade_color(fill, item_progress),
1477 Some(stroke(Color::WHITE, 1.0)),
1478 r,
1479 );
1480 }
1481 }
1482}
1483
1484fn render_bubble(
1485 cx: &mut fission_core::internal::InternalLoweringCx,
1486 root: &mut fission_core::internal::InternalIrBuilder,
1487 bubble: &crate::series::bubble::BubbleSeries,
1488 visual_map: Option<&VisualMap>,
1489 area: &ChartArea,
1490 x_scale: &LinearScale,
1491 y_scale: &LinearScale,
1492 animation: ChartAnimationFrame,
1493 series_index: usize,
1494) {
1495 let max_size = bubble
1496 .data
1497 .iter()
1498 .map(|(_, _, size)| *size)
1499 .fold(1.0_f32, f32::max);
1500 let series_progress = animation.series_progress(series_index);
1501 if series_progress <= f32::EPSILON {
1502 return;
1503 }
1504
1505 for (idx, (xv, yv, size)) in bubble.data.iter().enumerate() {
1506 let item_progress = animation.item_progress(series_progress, idx);
1507 if item_progress <= f32::EPSILON {
1508 continue;
1509 }
1510 let x = map_x(*xv, area, x_scale);
1511 let y = map_y(*yv, area, y_scale);
1512 let t = (*size / max_size).clamp(0.0, 1.0).sqrt();
1513 let radius = (bubble.min_radius + (bubble.max_radius - bubble.min_radius) * t)
1514 * item_progress.sqrt();
1515 let fill = visual_map
1516 .map(|map| visual_color(map, *size))
1517 .unwrap_or_else(|| bubble.color.with_alpha(185));
1518 add_rect(
1519 cx,
1520 root,
1521 LayoutRect::new(x - radius, y - radius, radius * 2.0, radius * 2.0),
1522 fade_color(fill, item_progress),
1523 Some(stroke(Color::WHITE, 1.2)),
1524 radius,
1525 );
1526 if radius > 14.0 {
1527 add_text(
1528 cx,
1529 root,
1530 &(idx + 1).to_string(),
1531 10.0,
1532 Color::WHITE,
1533 x - 10.0,
1534 y - 6.0,
1535 20.0,
1536 12.0,
1537 );
1538 }
1539 }
1540}
1541
1542fn render_pie(
1543 cx: &mut fission_core::internal::InternalLoweringCx,
1544 root: &mut fission_core::internal::InternalIrBuilder,
1545 pie: &crate::series::pie::PieSeries,
1546 area: &ChartArea,
1547 theme: &ChartTheme,
1548 animation: ChartAnimationFrame,
1549 series_index: usize,
1550) {
1551 let total: f32 = pie.data.iter().map(|(_, value)| *value).sum();
1552 if total <= 0.0 {
1553 return;
1554 }
1555 let series_progress = animation.series_progress(series_index);
1556 if series_progress <= f32::EPSILON {
1557 return;
1558 }
1559 let cx_pie = area.plot.x() + area.plot.width() * 0.45;
1560 let cy_pie = area.plot.y() + area.plot.height() * 0.52;
1561 let max_r = area.plot.width().min(area.plot.height()) * 0.38;
1562 let inner = pie.inner_radius.max(0.0).min(max_r * 0.85);
1563 let max_value = pie
1564 .data
1565 .iter()
1566 .map(|(_, value)| *value)
1567 .fold(1.0_f32, f32::max);
1568 let mut angle = -std::f32::consts::PI / 2.0;
1569 let mut remaining_reveal = std::f32::consts::TAU * series_progress;
1570 for (idx, (label, value)) in pie.data.iter().enumerate() {
1571 let sweep = (*value / total) * std::f32::consts::TAU;
1572 let revealed_sweep = sweep.min(remaining_reveal.max(0.0));
1573 if revealed_sweep <= f32::EPSILON {
1574 break;
1575 }
1576 let end = angle + revealed_sweep;
1577 let mut outer = max_r;
1578 if let Some(rose_type) = pie.rose_type.as_deref() {
1579 let normalized = (*value / max_value).clamp(0.0, 1.0);
1580 outer = match rose_type {
1581 "area" => max_r * (0.42 + 0.58 * normalized.sqrt()),
1582 "radius" => max_r * (0.42 + 0.58 * normalized),
1583 _ => max_r,
1584 };
1585 }
1586 add_path(
1587 cx,
1588 root,
1589 &pie_slice(cx_pie, cy_pie, inner, outer, angle, end),
1590 Some(Fill::Solid(theme.palette[idx % theme.palette.len()])),
1591 Some(stroke(Color::WHITE, 1.2)),
1592 );
1593 let mid = angle + revealed_sweep / 2.0;
1594 let lx = cx_pie + (outer + 20.0) * mid.cos();
1595 let ly = cy_pie + (outer + 20.0) * mid.sin();
1596 if series_progress > 0.92 || revealed_sweep >= sweep * 0.92 {
1597 add_text(
1598 cx,
1599 root,
1600 label,
1601 11.0,
1602 theme.label,
1603 lx - 36.0,
1604 ly - 7.0,
1605 72.0,
1606 14.0,
1607 );
1608 }
1609 angle += sweep;
1610 remaining_reveal -= sweep;
1611 }
1612}
1613
1614fn render_boxplot(
1615 cx: &mut fission_core::internal::InternalLoweringCx,
1616 root: &mut fission_core::internal::InternalIrBuilder,
1617 boxplot: &crate::series::boxplot::BoxplotSeries,
1618 model: &ChartModel,
1619 area: &ChartArea,
1620 y_scale: &LinearScale,
1621 _theme: &ChartTheme,
1622 animation: ChartAnimationFrame,
1623 series_index: usize,
1624) {
1625 let series_progress = animation.series_progress(series_index);
1626 if series_progress <= f32::EPSILON {
1627 return;
1628 }
1629 let band = band_width(model, area);
1630 let box_w = band * 0.46;
1631 for (idx, row) in boxplot.data.iter().enumerate() {
1632 if row.len() < 5 {
1633 continue;
1634 }
1635 let item_progress = animation.item_progress(series_progress, idx);
1636 if item_progress <= f32::EPSILON {
1637 continue;
1638 }
1639 let x = map_category_x(idx, model, area);
1640 let median_anchor = map_y(row[2], area, y_scale);
1641 let min_y = interpolate(median_anchor, map_y(row[0], area, y_scale), item_progress);
1642 let q1_y = interpolate(median_anchor, map_y(row[1], area, y_scale), item_progress);
1643 let med_y = map_y(row[2], area, y_scale);
1644 let q3_y = interpolate(median_anchor, map_y(row[3], area, y_scale), item_progress);
1645 let max_y = interpolate(median_anchor, map_y(row[4], area, y_scale), item_progress);
1646 add_rect(
1647 cx,
1648 root,
1649 LayoutRect::new(
1650 x - box_w / 2.0,
1651 q3_y.min(q1_y),
1652 box_w,
1653 (q1_y - q3_y).abs().max(1.0),
1654 ),
1655 fade_color(boxplot.color.with_alpha(70), item_progress),
1656 Some(fade_stroke(stroke(boxplot.color, 1.5), item_progress)),
1657 1.0,
1658 );
1659 add_path(
1660 cx,
1661 root,
1662 &format!(
1663 "M {} {} L {} {} M {} {} L {} {} M {} {} L {} {} M {} {} L {} {}",
1664 x,
1665 min_y,
1666 x,
1667 q1_y.max(q3_y),
1668 x,
1669 max_y,
1670 x,
1671 q1_y.min(q3_y),
1672 x - box_w / 2.0,
1673 min_y,
1674 x + box_w / 2.0,
1675 min_y,
1676 x - box_w / 2.0,
1677 max_y,
1678 x + box_w / 2.0,
1679 max_y
1680 ),
1681 None,
1682 Some(fade_stroke(stroke(boxplot.color, 1.2), item_progress)),
1683 );
1684 add_path(
1685 cx,
1686 root,
1687 &format!(
1688 "M {} {} L {} {}",
1689 x - box_w / 2.0,
1690 med_y,
1691 x + box_w / 2.0,
1692 med_y
1693 ),
1694 None,
1695 Some(fade_stroke(stroke(boxplot.color, 2.0), item_progress)),
1696 );
1697 }
1698}
1699
1700fn render_candlestick(
1701 cx: &mut fission_core::internal::InternalLoweringCx,
1702 root: &mut fission_core::internal::InternalIrBuilder,
1703 candle: &crate::series::candlestick::CandlestickSeries,
1704 model: &ChartModel,
1705 area: &ChartArea,
1706 y_scale: &LinearScale,
1707 animation: ChartAnimationFrame,
1708 series_index: usize,
1709) {
1710 let series_progress = animation.series_progress(series_index);
1711 if series_progress <= f32::EPSILON {
1712 return;
1713 }
1714 let band = band_width(model, area);
1715 let box_w = band * 0.5;
1716 for (idx, row) in candle.data.iter().enumerate() {
1717 if row.len() < 4 {
1718 continue;
1719 }
1720 let item_progress = animation.item_progress(series_progress, idx);
1721 if item_progress <= f32::EPSILON {
1722 continue;
1723 }
1724 let open = row[0];
1725 let close = row[1];
1726 let low = row[2];
1727 let high = row[3];
1728 let up = close >= open;
1729 let color = if up {
1730 candle.color_up
1731 } else {
1732 candle.color_down
1733 };
1734 let x = map_category_x(idx, model, area);
1735 let center_y = map_y((open + close) / 2.0, area, y_scale);
1736 let open_y = interpolate(center_y, map_y(open, area, y_scale), item_progress);
1737 let close_y = interpolate(center_y, map_y(close, area, y_scale), item_progress);
1738 let high_y = interpolate(center_y, map_y(high, area, y_scale), item_progress);
1739 let low_y = interpolate(center_y, map_y(low, area, y_scale), item_progress);
1740 add_path(
1741 cx,
1742 root,
1743 &format!("M {} {} L {} {}", x, high_y, x, low_y),
1744 None,
1745 Some(fade_stroke(stroke(color, 1.4), item_progress)),
1746 );
1747 add_rect(
1748 cx,
1749 root,
1750 LayoutRect::new(
1751 x - box_w / 2.0,
1752 open_y.min(close_y),
1753 box_w,
1754 (open_y - close_y).abs().max(1.0),
1755 ),
1756 fade_color(if up { Color::WHITE } else { color }, item_progress),
1757 Some(fade_stroke(stroke(color, 1.4), item_progress)),
1758 0.0,
1759 );
1760 }
1761}
1762
1763fn render_heatmap(
1764 cx: &mut fission_core::internal::InternalLoweringCx,
1765 root: &mut fission_core::internal::InternalIrBuilder,
1766 heatmap: &crate::series::heatmap::HeatmapSeries,
1767 model: &ChartModel,
1768 visual_map: Option<&VisualMap>,
1769 area: &ChartArea,
1770 theme: &ChartTheme,
1771 animation: ChartAnimationFrame,
1772 series_index: usize,
1773) {
1774 let series_progress = animation.series_progress(series_index);
1775 if series_progress <= f32::EPSILON {
1776 return;
1777 }
1778 let max_x = heatmap.data.iter().map(|d| d.0).max().unwrap_or(0) + 1;
1779 let max_y = heatmap.data.iter().map(|d| d.1).max().unwrap_or(0) + 1;
1780 let cell_w = area.plot.width() / max_x.max(1) as f32;
1781 let cell_h = area.plot.height() / max_y.max(1) as f32;
1782 let max_val = heatmap.data.iter().map(|d| d.2).fold(1.0_f32, f32::max);
1783 for (idx, (x_idx, y_idx, val)) in heatmap.data.iter().enumerate() {
1784 let item_progress = animation.item_progress(series_progress, idx);
1785 if item_progress <= f32::EPSILON {
1786 continue;
1787 }
1788 let x = area.plot.x() + *x_idx as f32 * cell_w;
1789 let y = area.plot.bottom() - (*y_idx as f32 + 1.0) * cell_h;
1790 let fill = visual_map
1791 .map(|map| visual_color(map, *val))
1792 .unwrap_or_else(|| heat_color(*val / max_val));
1793 let rect = scale_rect_from_center(
1794 LayoutRect::new(x, y, cell_w, cell_h),
1795 0.82 + item_progress * 0.18,
1796 );
1797 add_rect(
1798 cx,
1799 root,
1800 rect,
1801 fade_color(fill, item_progress),
1802 Some(fade_stroke(stroke(Color::WHITE, 1.0), item_progress)),
1803 0.0,
1804 );
1805 }
1806 if model.x_axis.axis_type == AxisType::Category {
1807 for (idx, label) in model.x_axis.data.iter().enumerate() {
1808 add_text(
1809 cx,
1810 root,
1811 label,
1812 10.0,
1813 theme.label,
1814 area.plot.x() + idx as f32 * cell_w,
1815 area.plot.bottom() + 8.0,
1816 cell_w,
1817 14.0,
1818 );
1819 }
1820 }
1821}
1822
1823fn render_calendar_heatmap(
1824 cx: &mut fission_core::internal::InternalLoweringCx,
1825 root: &mut fission_core::internal::InternalIrBuilder,
1826 calendar: &crate::series::calendar_heatmap::CalendarHeatmapSeries,
1827 visual_map: Option<&VisualMap>,
1828 area: &ChartArea,
1829 theme: &ChartTheme,
1830 animation: ChartAnimationFrame,
1831 series_index: usize,
1832) {
1833 use chrono::{Datelike, Duration, NaiveDate};
1834
1835 let series_progress = animation.series_progress(series_index);
1836 if series_progress <= f32::EPSILON {
1837 return;
1838 }
1839
1840 let parsed: Vec<(NaiveDate, f32)> = calendar
1841 .data
1842 .iter()
1843 .filter_map(|(date, value)| {
1844 NaiveDate::parse_from_str(date, "%Y-%m-%d")
1845 .ok()
1846 .map(|date| (date, *value))
1847 })
1848 .collect();
1849 if parsed.is_empty() {
1850 return;
1851 }
1852
1853 let min_date = parsed.iter().map(|(date, _)| *date).min().unwrap();
1854 let max_date = parsed.iter().map(|(date, _)| *date).max().unwrap();
1855 let start = calendar
1856 .start
1857 .as_ref()
1858 .and_then(|date| NaiveDate::parse_from_str(date, "%Y-%m-%d").ok())
1859 .unwrap_or(min_date);
1860 let end = calendar
1861 .end
1862 .as_ref()
1863 .and_then(|date| NaiveDate::parse_from_str(date, "%Y-%m-%d").ok())
1864 .unwrap_or(max_date)
1865 .max(start);
1866
1867 let start_weekday = start.weekday().num_days_from_monday() as i64;
1868 let days = (end - start).num_days().max(0) + 1;
1869 let weeks = ((start_weekday + days + 6) / 7).max(1) as usize;
1870 let cell = (area.plot.width() / weeks as f32)
1871 .min(area.plot.height() / 7.0)
1872 .max(4.0);
1873 let x0 = area.plot.x();
1874 let y0 = area.plot.y() + (area.plot.height() - cell * 7.0) / 2.0;
1875 let values: HashMap<NaiveDate, f32> = parsed.into_iter().collect();
1876 let max_value = values.values().copied().fold(1.0_f32, f32::max);
1877
1878 let mut date = start;
1879 let mut idx = 0usize;
1880 while date <= end {
1881 let offset = (date - start).num_days() + start_weekday;
1882 let week = (offset / 7) as f32;
1883 let day = date.weekday().num_days_from_monday() as f32;
1884 let value = values.get(&date).copied().unwrap_or(0.0);
1885 let fill = visual_map
1886 .map(|map| visual_color(map, value))
1887 .unwrap_or_else(|| heat_color(value / max_value));
1888 let item_progress = animation.item_progress(series_progress, idx);
1889 let rect = scale_rect_from_center(
1890 LayoutRect::new(x0 + week * cell, y0 + day * cell, cell - 2.0, cell - 2.0),
1891 0.82 + item_progress * 0.18,
1892 );
1893 add_rect(
1894 cx,
1895 root,
1896 rect,
1897 fade_color(
1898 fill.with_alpha(if value > 0.0 { 230 } else { 55 }),
1899 item_progress,
1900 ),
1901 Some(fade_stroke(stroke(Color::WHITE, 0.8), item_progress)),
1902 2.0,
1903 );
1904 date += Duration::days(1);
1905 idx += 1;
1906 }
1907
1908 for (idx, label) in ["Mon", "Wed", "Fri", "Sun"].iter().enumerate() {
1909 let day = [0.0, 2.0, 4.0, 6.0][idx];
1910 add_text(
1911 cx,
1912 root,
1913 label,
1914 10.0,
1915 theme.label,
1916 x0 - 34.0,
1917 y0 + day * cell - 2.0,
1918 28.0,
1919 12.0,
1920 );
1921 }
1922 add_text(
1923 cx,
1924 root,
1925 &format!("{} to {}", start.format("%b %Y"), end.format("%b %Y")),
1926 11.0,
1927 theme.label,
1928 x0,
1929 y0 + cell * 7.0 + 8.0,
1930 area.plot.width(),
1931 16.0,
1932 );
1933}
1934
1935fn render_graph(
1936 cx: &mut fission_core::internal::InternalLoweringCx,
1937 root: &mut fission_core::internal::InternalIrBuilder,
1938 graph: &crate::series::graph::GraphSeries,
1939 area: &ChartArea,
1940 theme: &ChartTheme,
1941 animation: ChartAnimationFrame,
1942 series_index: usize,
1943) {
1944 let series_progress = animation.series_progress(series_index);
1945 if series_progress <= f32::EPSILON {
1946 return;
1947 }
1948 let positions = crate::layout::force_graph::ForceGraphLayout::compute_positions(
1949 &graph.nodes,
1950 &graph.edges,
1951 area.plot.width(),
1952 area.plot.height(),
1953 80,
1954 );
1955 render_edges(
1956 cx,
1957 root,
1958 &graph.edges,
1959 &positions,
1960 area,
1961 theme,
1962 animation,
1963 series_progress,
1964 );
1965 for (idx, node) in graph.nodes.iter().enumerate() {
1966 let item_progress = animation.item_progress(series_progress, idx + graph.edges.len());
1967 if item_progress <= f32::EPSILON {
1968 continue;
1969 }
1970 if let Some((x, y)) = positions.get(&node.id) {
1971 let r = (7.0 + node.value.sqrt().min(24.0)) * item_progress.sqrt();
1972 let px = area.plot.x() + *x;
1973 let py = area.plot.y() + *y;
1974 add_rect(
1975 cx,
1976 root,
1977 LayoutRect::new(px - r, py - r, r * 2.0, r * 2.0),
1978 fade_color(theme.palette[idx % theme.palette.len()], item_progress),
1979 Some(fade_stroke(stroke(Color::WHITE, 1.0), item_progress)),
1980 r,
1981 );
1982 if item_progress > 0.82 {
1983 add_text(
1984 cx,
1985 root,
1986 &node.name,
1987 10.0,
1988 theme.label,
1989 px + r + 4.0,
1990 py - 7.0,
1991 100.0,
1992 14.0,
1993 );
1994 }
1995 }
1996 }
1997}
1998
1999fn render_lines(
2000 cx: &mut fission_core::internal::InternalLoweringCx,
2001 root: &mut fission_core::internal::InternalIrBuilder,
2002 lines: &crate::series::lines::LinesSeries,
2003 area: &ChartArea,
2004 theme: &ChartTheme,
2005 animation: ChartAnimationFrame,
2006 series_index: usize,
2007) {
2008 if lines.data.is_empty() {
2009 return;
2010 }
2011 let series_progress = animation.series_progress(series_index);
2012 if series_progress <= f32::EPSILON {
2013 return;
2014 }
2015
2016 let mut min_x = f32::MAX;
2017 let mut max_x = f32::MIN;
2018 let mut min_y = f32::MAX;
2019 let mut max_y = f32::MIN;
2020 let mut max_value = 1.0_f32;
2021 for segment in &lines.data {
2022 for (x, y) in [segment.from, segment.to] {
2023 min_x = min_x.min(x);
2024 max_x = max_x.max(x);
2025 min_y = min_y.min(y);
2026 max_y = max_y.max(y);
2027 }
2028 max_value = max_value.max(segment.value);
2029 }
2030 let (min_x, max_x) = normalize_bounds(min_x, max_x);
2031 let (min_y, max_y) = normalize_bounds(min_y, max_y);
2032
2033 for (idx, segment) in lines.data.iter().enumerate() {
2034 let item_progress = animation.item_progress(series_progress, idx);
2035 if item_progress <= f32::EPSILON {
2036 continue;
2037 }
2038 let from = map_lines_point(segment.from, min_x, max_x, min_y, max_y, area);
2039 let full_to = map_lines_point(segment.to, min_x, max_x, min_y, max_y, area);
2040 let to = interpolate_point(from, full_to, item_progress);
2041 let intensity = (segment.value / max_value).clamp(0.0, 1.0);
2042 let stroke_color = fade_color(
2043 mix_color(lines.color.with_alpha(110), lines.color, intensity),
2044 item_progress,
2045 );
2046 let control_x = (from.0 + to.0) / 2.0;
2047 let control_y = (from.1 + to.1) / 2.0 - 36.0 * intensity;
2048 let path = format!(
2049 "M {} {} C {} {} {} {} {} {}",
2050 from.0, from.1, control_x, control_y, control_x, control_y, to.0, to.1
2051 );
2052 add_path(
2053 cx,
2054 root,
2055 &path,
2056 None,
2057 Some(stroke(stroke_color, 1.6 + 2.2 * intensity)),
2058 );
2059 if item_progress > 0.72 {
2060 draw_arrow_head(cx, root, from, to, stroke_color);
2061 }
2062
2063 if lines.effect {
2064 let mid = quadratic_midpoint(from, (control_x, control_y), to);
2065 let radius = 4.0 + 5.0 * intensity;
2066 add_rect(
2067 cx,
2068 root,
2069 LayoutRect::new(mid.0 - radius, mid.1 - radius, radius * 2.0, radius * 2.0),
2070 stroke_color.with_alpha(130),
2071 Some(stroke(Color::WHITE.with_alpha(150), 1.0)),
2072 radius,
2073 );
2074 }
2075 }
2076
2077 add_text(
2078 cx,
2079 root,
2080 "lines",
2081 10.0,
2082 theme.label,
2083 area.plot.x() + 8.0,
2084 area.plot.y() + 8.0,
2085 56.0,
2086 14.0,
2087 );
2088}
2089
2090fn render_tree(
2091 cx: &mut fission_core::internal::InternalLoweringCx,
2092 root: &mut fission_core::internal::InternalIrBuilder,
2093 tree: &crate::series::tree::TreeSeries,
2094 area: &ChartArea,
2095 theme: &ChartTheme,
2096 animation: ChartAnimationFrame,
2097 series_index: usize,
2098) {
2099 if tree.data.is_empty() {
2100 return;
2101 }
2102 let series_progress = animation.series_progress(series_index);
2103 if series_progress <= f32::EPSILON {
2104 return;
2105 }
2106
2107 let leaf_count = tree.data.iter().map(tree_leaf_count).sum::<usize>().max(1);
2108 let depth = tree
2109 .data
2110 .iter()
2111 .map(treemap_depth)
2112 .max()
2113 .unwrap_or(1)
2114 .max(1);
2115 let mut next_leaf = 0usize;
2116 let mut nodes = Vec::<TreeRenderNode>::new();
2117 let mut edges = Vec::<((f32, f32), (f32, f32))>::new();
2118
2119 for root_node in &tree.data {
2120 if tree.radial {
2121 layout_radial_tree_node(
2122 root_node,
2123 0,
2124 depth,
2125 leaf_count,
2126 &mut next_leaf,
2127 area,
2128 &mut nodes,
2129 &mut edges,
2130 );
2131 } else {
2132 layout_tree_node(
2133 root_node,
2134 0,
2135 depth,
2136 leaf_count,
2137 &mut next_leaf,
2138 area,
2139 &mut nodes,
2140 &mut edges,
2141 );
2142 }
2143 }
2144
2145 for (idx, (from, to)) in edges.iter().enumerate() {
2146 let item_progress = animation.item_progress(series_progress, idx);
2147 if item_progress <= f32::EPSILON {
2148 continue;
2149 }
2150 let to = interpolate_point(*from, *to, item_progress);
2151 let path = if tree.radial {
2152 format!("M {} {} L {} {}", from.0, from.1, to.0, to.1)
2153 } else {
2154 let mid_x = (from.0 + to.0) / 2.0;
2155 format!(
2156 "M {} {} C {} {} {} {} {} {}",
2157 from.0, from.1, mid_x, from.1, mid_x, to.1, to.0, to.1
2158 )
2159 };
2160 add_path(
2161 cx,
2162 root,
2163 &path,
2164 None,
2165 Some(fade_stroke(
2166 stroke(theme.axis_line.with_alpha(150), 1.3),
2167 item_progress,
2168 )),
2169 );
2170 }
2171
2172 for (idx, node) in nodes.iter().enumerate() {
2173 let item_progress = animation.item_progress(series_progress, idx + edges.len());
2174 if item_progress <= f32::EPSILON {
2175 continue;
2176 }
2177 let radius = (if node.depth == 0 { 8.0 } else { 6.0 }) * item_progress.sqrt();
2178 let color = theme.palette[idx % theme.palette.len()];
2179 add_rect(
2180 cx,
2181 root,
2182 LayoutRect::new(node.x - radius, node.y - radius, radius * 2.0, radius * 2.0),
2183 fade_color(color, item_progress),
2184 Some(fade_stroke(stroke(Color::WHITE, 1.0), item_progress)),
2185 radius,
2186 );
2187 if item_progress > 0.82 && (!tree.radial || node.depth > 0) {
2188 add_text(
2189 cx,
2190 root,
2191 &node.name,
2192 10.0,
2193 theme.label,
2194 node.x + radius + 5.0,
2195 node.y - 7.0,
2196 110.0,
2197 14.0,
2198 );
2199 }
2200 }
2201}
2202
2203fn render_treemap(
2204 cx: &mut fission_core::internal::InternalLoweringCx,
2205 root: &mut fission_core::internal::InternalIrBuilder,
2206 treemap: &crate::series::treemap::TreemapSeries,
2207 area: &ChartArea,
2208 theme: &ChartTheme,
2209 animation: ChartAnimationFrame,
2210 series_index: usize,
2211) {
2212 let series_progress = animation.series_progress(series_index);
2213 if series_progress <= f32::EPSILON {
2214 return;
2215 }
2216 let layout = crate::layout::treemap::TreemapLayout::squarify(&treemap.data, area.plot);
2217 for (idx, (node, rect)) in layout.iter().enumerate() {
2218 let item_progress = animation.item_progress(series_progress, idx);
2219 if item_progress <= f32::EPSILON {
2220 continue;
2221 }
2222 let rect = scale_rect_from_center(*rect, 0.86 + item_progress * 0.14);
2223 add_rect(
2224 cx,
2225 root,
2226 rect,
2227 fade_color(theme.palette[idx % theme.palette.len()], item_progress),
2228 Some(fade_stroke(stroke(Color::WHITE, 2.0), item_progress)),
2229 3.0,
2230 );
2231 if item_progress > 0.82 && rect.width() > 58.0 && rect.height() > 24.0 {
2232 add_text(
2233 cx,
2234 root,
2235 &node.name,
2236 11.0,
2237 Color::WHITE,
2238 rect.x() + 6.0,
2239 rect.y() + 6.0,
2240 rect.width() - 12.0,
2241 16.0,
2242 );
2243 }
2244 }
2245}
2246
2247fn render_radar(
2248 cx: &mut fission_core::internal::InternalLoweringCx,
2249 root: &mut fission_core::internal::InternalIrBuilder,
2250 radar: &crate::series::radar::RadarSeries,
2251 area: &ChartArea,
2252 theme: &ChartTheme,
2253 animation: ChartAnimationFrame,
2254 series_index: usize,
2255) {
2256 let axes = radar.data.first().map(|data| data.len()).unwrap_or(0);
2257 if axes == 0 {
2258 return;
2259 }
2260 let series_progress = animation.series_progress(series_index);
2261 if series_progress <= f32::EPSILON {
2262 return;
2263 }
2264 let center = (
2265 area.plot.x() + area.plot.width() / 2.0,
2266 area.plot.y() + area.plot.height() / 2.0,
2267 );
2268 let r = area.plot.width().min(area.plot.height()) * 0.38;
2269 for ring in 1..=5 {
2270 let rr = r * ring as f32 / 5.0;
2271 let mut path = String::new();
2272 for axis in 0..axes {
2273 let angle = radar_angle(axis, axes);
2274 let x = center.0 + rr * angle.cos();
2275 let y = center.1 + rr * angle.sin();
2276 if axis == 0 {
2277 path.push_str(&format!("M {} {}", x, y));
2278 } else {
2279 path.push_str(&format!(" L {} {}", x, y));
2280 }
2281 }
2282 path.push_str(" Z");
2283 add_path(cx, root, &path, None, Some(stroke(theme.grid_line, 1.0)));
2284 }
2285 for axis in 0..axes {
2286 let angle = radar_angle(axis, axes);
2287 add_path(
2288 cx,
2289 root,
2290 &format!(
2291 "M {} {} L {} {}",
2292 center.0,
2293 center.1,
2294 center.0 + r * angle.cos(),
2295 center.1 + r * angle.sin()
2296 ),
2297 None,
2298 Some(stroke(theme.axis_line, 1.0)),
2299 );
2300 }
2301 for (idx, data) in radar.data.iter().enumerate() {
2302 let item_progress = animation.item_progress(series_progress, idx);
2303 if item_progress <= f32::EPSILON {
2304 continue;
2305 }
2306 let mut path = String::new();
2307 for (axis, value) in data.iter().enumerate() {
2308 let angle = radar_angle(axis, axes);
2309 let rr = r * (*value / 100.0).clamp(0.0, 1.0) * item_progress;
2310 let x = center.0 + rr * angle.cos();
2311 let y = center.1 + rr * angle.sin();
2312 if axis == 0 {
2313 path.push_str(&format!("M {} {}", x, y));
2314 } else {
2315 path.push_str(&format!(" L {} {}", x, y));
2316 }
2317 }
2318 path.push_str(" Z");
2319 let c = theme.palette[idx % theme.palette.len()];
2320 add_path(
2321 cx,
2322 root,
2323 &path,
2324 Some(Fill::Solid(fade_color(c.with_alpha(70), item_progress))),
2325 Some(fade_stroke(stroke(c, 2.0), item_progress)),
2326 );
2327 }
2328}
2329
2330fn render_polar_bar(
2331 cx: &mut fission_core::internal::InternalLoweringCx,
2332 root: &mut fission_core::internal::InternalIrBuilder,
2333 polar: &crate::series::polar::PolarBarSeries,
2334 area: &ChartArea,
2335 theme: &ChartTheme,
2336 animation: ChartAnimationFrame,
2337 series_index: usize,
2338) {
2339 if polar.data.is_empty() {
2340 return;
2341 }
2342 let series_progress = animation.series_progress(series_index);
2343 if series_progress <= f32::EPSILON {
2344 return;
2345 }
2346
2347 let center = (
2348 area.plot.x() + area.plot.width() / 2.0,
2349 area.plot.y() + area.plot.height() / 2.0,
2350 );
2351 let max_r = area.plot.width().min(area.plot.height()) * 0.43;
2352 let inner = polar.inner_radius.min(max_r * 0.72);
2353 let max_value = polar
2354 .data
2355 .iter()
2356 .map(|(_, value)| *value)
2357 .fold(1.0_f32, f32::max);
2358 let slot = std::f32::consts::TAU / polar.data.len() as f32;
2359
2360 for ring in 1..=4 {
2361 let r = inner + (max_r - inner) * ring as f32 / 4.0;
2362 add_path(
2363 cx,
2364 root,
2365 &circle_path(center.0, center.1, r),
2366 None,
2367 Some(stroke(theme.grid_line, 1.0)),
2368 );
2369 }
2370
2371 for (idx, (label, value)) in polar.data.iter().enumerate() {
2372 let item_progress = animation.item_progress(series_progress, idx);
2373 if item_progress <= f32::EPSILON {
2374 continue;
2375 }
2376 let start = -std::f32::consts::PI / 2.0 + idx as f32 * slot + slot * 0.10;
2377 let end = start + slot * 0.80 * item_progress;
2378 let outer = inner + (max_r - inner) * (*value / max_value).clamp(0.0, 1.0) * item_progress;
2379 let c = mix_color(
2380 polar.color.with_alpha(150),
2381 theme.palette[idx % theme.palette.len()],
2382 0.35,
2383 );
2384 add_path(
2385 cx,
2386 root,
2387 &pie_slice(center.0, center.1, inner, outer, start, end),
2388 Some(Fill::Solid(fade_color(c, item_progress))),
2389 Some(fade_stroke(stroke(Color::WHITE, 1.0), item_progress)),
2390 );
2391 let mid = (start + end) / 2.0;
2392 if item_progress > 0.86 {
2393 add_text(
2394 cx,
2395 root,
2396 label,
2397 10.0,
2398 theme.label,
2399 center.0 + (max_r + 16.0) * mid.cos() - 28.0,
2400 center.1 + (max_r + 16.0) * mid.sin() - 7.0,
2401 56.0,
2402 14.0,
2403 );
2404 }
2405 }
2406}
2407
2408fn render_polar_line(
2409 cx: &mut fission_core::internal::InternalLoweringCx,
2410 root: &mut fission_core::internal::InternalIrBuilder,
2411 polar: &crate::series::polar::PolarLineSeries,
2412 area: &ChartArea,
2413 theme: &ChartTheme,
2414 animation: ChartAnimationFrame,
2415 series_index: usize,
2416) {
2417 if polar.data.is_empty() {
2418 return;
2419 }
2420 let series_progress = animation.series_progress(series_index);
2421 if series_progress <= f32::EPSILON {
2422 return;
2423 }
2424
2425 let center = (
2426 area.plot.x() + area.plot.width() / 2.0,
2427 area.plot.y() + area.plot.height() / 2.0,
2428 );
2429 let max_r = area.plot.width().min(area.plot.height()) * 0.42;
2430 let max_value = polar
2431 .data
2432 .iter()
2433 .map(|(_, radius)| *radius)
2434 .fold(1.0_f32, f32::max);
2435 for ring in 1..=4 {
2436 let r = max_r * ring as f32 / 4.0;
2437 add_path(
2438 cx,
2439 root,
2440 &circle_path(center.0, center.1, r),
2441 None,
2442 Some(stroke(theme.grid_line, 1.0)),
2443 );
2444 }
2445 for axis in 0..8 {
2446 let angle = -std::f32::consts::PI / 2.0 + axis as f32 / 8.0 * std::f32::consts::TAU;
2447 add_path(
2448 cx,
2449 root,
2450 &format!(
2451 "M {} {} L {} {}",
2452 center.0,
2453 center.1,
2454 center.0 + max_r * angle.cos(),
2455 center.1 + max_r * angle.sin()
2456 ),
2457 None,
2458 Some(stroke(theme.grid_line, 0.8)),
2459 );
2460 }
2461
2462 let points: Vec<(f32, f32)> = polar
2463 .data
2464 .iter()
2465 .map(|(angle_degrees, radius)| {
2466 let angle = angle_degrees.to_radians() - std::f32::consts::PI / 2.0;
2467 let r = max_r * (*radius / max_value).clamp(0.0, 1.0);
2468 (center.0 + r * angle.cos(), center.1 + r * angle.sin())
2469 })
2470 .collect();
2471 let revealed_points = reveal_points(&points, series_progress);
2472 add_path(
2473 cx,
2474 root,
2475 &path_for_line(&revealed_points, polar.smooth, None),
2476 None,
2477 Some(fade_stroke(stroke(polar.color, 2.4), series_progress)),
2478 );
2479 for (idx, (x, y)) in revealed_points.into_iter().enumerate() {
2480 let item_progress = animation.item_progress(series_progress, idx);
2481 if item_progress <= f32::EPSILON {
2482 continue;
2483 }
2484 let r = 4.0 * item_progress.sqrt();
2485 add_rect(
2486 cx,
2487 root,
2488 LayoutRect::new(x - r, y - r, r * 2.0, r * 2.0),
2489 fade_color(polar.color, item_progress),
2490 Some(fade_stroke(stroke(Color::WHITE, 1.0), item_progress)),
2491 r,
2492 );
2493 }
2494}
2495
2496fn render_single_axis(
2497 cx: &mut fission_core::internal::InternalLoweringCx,
2498 root: &mut fission_core::internal::InternalIrBuilder,
2499 single_axis: &crate::series::single_axis::SingleAxisSeries,
2500 area: &ChartArea,
2501 theme: &ChartTheme,
2502 animation: ChartAnimationFrame,
2503 series_index: usize,
2504) {
2505 if single_axis.data.is_empty() {
2506 return;
2507 }
2508 let series_progress = animation.series_progress(series_index);
2509 if series_progress <= f32::EPSILON {
2510 return;
2511 }
2512
2513 let min = single_axis
2514 .data
2515 .iter()
2516 .map(|(value, _)| *value)
2517 .fold(f32::MAX, f32::min);
2518 let max = single_axis
2519 .data
2520 .iter()
2521 .map(|(value, _)| *value)
2522 .fold(f32::MIN, f32::max);
2523 let scale = LinearScale::nice(min, max, 6);
2524 let axis_y = area.plot.y() + area.plot.height() * 0.55;
2525 add_path(
2526 cx,
2527 root,
2528 &format!(
2529 "M {} {} L {} {}",
2530 area.plot.x(),
2531 axis_y,
2532 area.plot.right(),
2533 axis_y
2534 ),
2535 None,
2536 Some(stroke(theme.axis_line, 1.2)),
2537 );
2538 for tick in &scale.ticks {
2539 let x = map_x(*tick, area, &scale);
2540 add_path(
2541 cx,
2542 root,
2543 &format!("M {} {} L {} {}", x, axis_y - 5.0, x, axis_y + 5.0),
2544 None,
2545 Some(stroke(theme.axis_line, 1.0)),
2546 );
2547 add_text(
2548 cx,
2549 root,
2550 &format_tick(*tick),
2551 10.0,
2552 theme.label,
2553 x - 20.0,
2554 axis_y + 10.0,
2555 40.0,
2556 14.0,
2557 );
2558 }
2559 let max_size = single_axis
2560 .data
2561 .iter()
2562 .map(|(_, size)| *size)
2563 .fold(1.0_f32, f32::max);
2564 for (idx, (value, size)) in single_axis.data.iter().enumerate() {
2565 let item_progress = animation.item_progress(series_progress, idx);
2566 if item_progress <= f32::EPSILON {
2567 continue;
2568 }
2569 let x = map_x(*value, area, &scale);
2570 let lane = idx % 5;
2571 let y = axis_y - 32.0 + lane as f32 * 16.0;
2572 let r = (4.0 + 12.0 * (*size / max_size).clamp(0.0, 1.0).sqrt()) * item_progress.sqrt();
2573 add_rect(
2574 cx,
2575 root,
2576 LayoutRect::new(x - r, y - r, r * 2.0, r * 2.0),
2577 fade_color(single_axis.color.with_alpha(170), item_progress),
2578 Some(fade_stroke(stroke(Color::WHITE, 1.0), item_progress)),
2579 r,
2580 );
2581 }
2582}
2583
2584fn render_funnel(
2585 cx: &mut fission_core::internal::InternalLoweringCx,
2586 root: &mut fission_core::internal::InternalIrBuilder,
2587 funnel: &crate::series::funnel::FunnelSeries,
2588 area: &ChartArea,
2589 theme: &ChartTheme,
2590 animation: ChartAnimationFrame,
2591 series_index: usize,
2592) {
2593 if funnel.data.is_empty() {
2594 return;
2595 }
2596 let series_progress = animation.series_progress(series_index);
2597 if series_progress <= f32::EPSILON {
2598 return;
2599 }
2600 let max = funnel.data.iter().map(|(_, v)| *v).fold(1.0_f32, f32::max);
2601 let step_h = area.plot.height() / funnel.data.len() as f32;
2602 let cx_mid = area.plot.x() + area.plot.width() / 2.0;
2603 for (idx, (label, value)) in funnel.data.iter().enumerate() {
2604 let item_progress = animation.item_progress(series_progress, idx);
2605 if item_progress <= f32::EPSILON {
2606 continue;
2607 }
2608 let y = area.plot.y() + idx as f32 * step_h;
2609 let top_w = if idx == 0 {
2610 area.plot.width()
2611 } else {
2612 area.plot.width() * funnel.data[idx - 1].1 / max
2613 } * item_progress;
2614 let bot_w = area.plot.width() * *value / max * item_progress;
2615 let path = format!(
2616 "M {} {} L {} {} L {} {} L {} {} Z",
2617 cx_mid - top_w / 2.0,
2618 y,
2619 cx_mid + top_w / 2.0,
2620 y,
2621 cx_mid + bot_w / 2.0,
2622 y + step_h,
2623 cx_mid - bot_w / 2.0,
2624 y + step_h
2625 );
2626 add_path(
2627 cx,
2628 root,
2629 &path,
2630 Some(Fill::Solid(fade_color(
2631 theme.palette[idx % theme.palette.len()],
2632 item_progress,
2633 ))),
2634 Some(fade_stroke(stroke(Color::WHITE, 1.5), item_progress)),
2635 );
2636 if item_progress > 0.82 {
2637 add_text(
2638 cx,
2639 root,
2640 label,
2641 12.0,
2642 Color::WHITE,
2643 cx_mid - 50.0,
2644 y + step_h / 2.0 - 8.0,
2645 100.0,
2646 16.0,
2647 );
2648 }
2649 }
2650}
2651
2652fn render_gauge(
2653 cx: &mut fission_core::internal::InternalLoweringCx,
2654 root: &mut fission_core::internal::InternalIrBuilder,
2655 gauge: &crate::series::gauge::GaugeSeries,
2656 area: &ChartArea,
2657 theme: &ChartTheme,
2658 animation: ChartAnimationFrame,
2659 series_index: usize,
2660) {
2661 let center = (
2662 area.plot.x() + area.plot.width() / 2.0,
2663 area.plot.y() + area.plot.height() * 0.68,
2664 );
2665 let r = area.plot.width().min(area.plot.height()) * 0.42;
2666 add_path(
2667 cx,
2668 root,
2669 &arc(
2670 center.0,
2671 center.1,
2672 r,
2673 std::f32::consts::PI,
2674 std::f32::consts::TAU,
2675 ),
2676 None,
2677 Some(stroke(theme.grid_line, 18.0)),
2678 );
2679 if let Some((label, value)) = gauge.data.first() {
2680 let series_progress = animation.series_progress(series_index);
2681 if series_progress <= f32::EPSILON {
2682 return;
2683 }
2684 let pct = (*value / 100.0).clamp(0.0, 1.0);
2685 let angle = std::f32::consts::PI + pct * std::f32::consts::PI * series_progress;
2686 add_path(
2687 cx,
2688 root,
2689 &arc(center.0, center.1, r, std::f32::consts::PI, angle),
2690 None,
2691 Some(stroke(theme.palette[0], 18.0)),
2692 );
2693 add_path(
2694 cx,
2695 root,
2696 &format!(
2697 "M {} {} L {} {}",
2698 center.0,
2699 center.1,
2700 center.0 + r * 0.78 * angle.cos(),
2701 center.1 + r * 0.78 * angle.sin()
2702 ),
2703 None,
2704 Some(stroke(theme.title, 3.5)),
2705 );
2706 add_rect(
2707 cx,
2708 root,
2709 LayoutRect::new(center.0 - 7.0, center.1 - 7.0, 14.0, 14.0),
2710 theme.title,
2711 None,
2712 7.0,
2713 );
2714 add_text(
2715 cx,
2716 root,
2717 &format!("{} {:.0}", label, value),
2718 18.0,
2719 theme.title,
2720 center.0 - 70.0,
2721 center.1 + 20.0,
2722 140.0,
2723 24.0,
2724 );
2725 }
2726}
2727
2728fn render_map(
2729 cx: &mut fission_core::internal::InternalLoweringCx,
2730 root: &mut fission_core::internal::InternalIrBuilder,
2731 map: &crate::series::map::MapSeries,
2732 visual_map: Option<&VisualMap>,
2733 area: &ChartArea,
2734 theme: &ChartTheme,
2735 animation: ChartAnimationFrame,
2736 series_index: usize,
2737) {
2738 let series_progress = animation.series_progress(series_index);
2739 if series_progress <= f32::EPSILON {
2740 return;
2741 }
2742 let regions =
2743 crate::layout::map::MapLayout::compute_geojson(map, area.plot.width(), area.plot.height());
2744 if regions.is_empty() {
2745 return;
2746 }
2747 let values: Vec<f32> = regions.iter().filter_map(|region| region.value).collect();
2748 let min = values.iter().copied().fold(f32::MAX, f32::min);
2749 let max = values.iter().copied().fold(f32::MIN, f32::max);
2750 let denom = (max - min).max(f32::EPSILON);
2751
2752 for (idx, region) in regions.iter().enumerate() {
2753 let item_progress = animation.item_progress(series_progress, idx);
2754 if item_progress <= f32::EPSILON {
2755 continue;
2756 }
2757 let fill = if let Some(value) = region.value {
2758 visual_map
2759 .map(|map| visual_color(map, value))
2760 .unwrap_or_else(|| {
2761 mix_color(
2762 theme.palette[idx % theme.palette.len()].with_alpha(90),
2763 theme.palette[idx % theme.palette.len()],
2764 ((value - min) / denom).clamp(0.0, 1.0),
2765 )
2766 })
2767 } else {
2768 color(226, 232, 240, 255)
2769 };
2770 let shifted = translate_path(®ion.path, area.plot.x(), area.plot.y());
2771 add_path(
2772 cx,
2773 root,
2774 &shifted,
2775 Some(Fill::Solid(fade_color(fill, item_progress))),
2776 Some(fade_stroke(stroke(Color::WHITE, 1.4), item_progress)),
2777 );
2778 if let Some((x, y, width, height)) = path_bounds(&shifted) {
2779 if item_progress > 0.82 && width > 42.0 && height > 18.0 {
2780 add_text(
2781 cx,
2782 root,
2783 ®ion.name,
2784 10.0,
2785 theme.title,
2786 x + 4.0,
2787 y + height / 2.0 - 7.0,
2788 width - 8.0,
2789 14.0,
2790 );
2791 }
2792 }
2793 }
2794}
2795
2796fn render_sankey(
2797 cx: &mut fission_core::internal::InternalLoweringCx,
2798 root: &mut fission_core::internal::InternalIrBuilder,
2799 sankey: &crate::series::sankey::SankeySeries,
2800 area: &ChartArea,
2801 theme: &ChartTheme,
2802 animation: ChartAnimationFrame,
2803 series_index: usize,
2804) {
2805 let series_progress = animation.series_progress(series_index);
2806 if series_progress <= f32::EPSILON {
2807 return;
2808 }
2809 let (rects, paths) = crate::layout::sankey::SankeyLayout::compute(
2810 &sankey.nodes,
2811 &sankey.edges,
2812 area.plot.width(),
2813 area.plot.height(),
2814 );
2815 for (idx, (_, _, path)) in paths.iter().enumerate() {
2816 let item_progress = animation.item_progress(series_progress, idx);
2817 if item_progress <= f32::EPSILON {
2818 continue;
2819 }
2820 add_path(
2821 cx,
2822 root,
2823 &translate_path(path, area.plot.x(), area.plot.y()),
2824 Some(Fill::Solid(fade_color(
2825 theme.palette[idx % theme.palette.len()].with_alpha(115),
2826 item_progress,
2827 ))),
2828 None,
2829 );
2830 }
2831 for (idx, node) in sankey.nodes.iter().enumerate() {
2832 let item_progress = animation.item_progress(series_progress, idx + paths.len());
2833 if item_progress <= f32::EPSILON {
2834 continue;
2835 }
2836 if let Some(rect) = rects.get(&node.id) {
2837 let shifted = scale_rect_from_center(
2838 LayoutRect::new(
2839 area.plot.x() + rect.x(),
2840 area.plot.y() + rect.y(),
2841 rect.width(),
2842 rect.height(),
2843 ),
2844 0.86 + item_progress * 0.14,
2845 );
2846 add_rect(
2847 cx,
2848 root,
2849 shifted,
2850 fade_color(theme.palette[idx % theme.palette.len()], item_progress),
2851 None,
2852 3.0,
2853 );
2854 if item_progress > 0.82 {
2855 add_text(
2856 cx,
2857 root,
2858 &node.name,
2859 11.0,
2860 theme.label,
2861 shifted.right() + 6.0,
2862 shifted.y() + 4.0,
2863 100.0,
2864 14.0,
2865 );
2866 }
2867 }
2868 }
2869}
2870
2871fn render_sunburst(
2872 cx: &mut fission_core::internal::InternalLoweringCx,
2873 root: &mut fission_core::internal::InternalIrBuilder,
2874 sunburst: &crate::series::sunburst::SunburstSeries,
2875 area: &ChartArea,
2876 theme: &ChartTheme,
2877 animation: ChartAnimationFrame,
2878 series_index: usize,
2879) {
2880 if sunburst.data.is_empty() {
2881 return;
2882 }
2883 let series_progress = animation.series_progress(series_index);
2884 if series_progress <= f32::EPSILON {
2885 return;
2886 }
2887 let center = (
2888 area.plot.x() + area.plot.width() / 2.0,
2889 area.plot.y() + area.plot.height() / 2.0,
2890 );
2891 let depth = sunburst
2892 .data
2893 .iter()
2894 .map(treemap_depth)
2895 .max()
2896 .unwrap_or(1)
2897 .max(1);
2898 let radius = area.plot.width().min(area.plot.height()) * 0.44;
2899 let ring = radius / depth as f32;
2900 let total: f32 = sunburst.data.iter().map(treemap_weight).sum();
2901 if total <= 0.0 {
2902 return;
2903 }
2904 let mut angle = -std::f32::consts::PI / 2.0;
2905 let mut index = 0usize;
2906 for node in &sunburst.data {
2907 let sweep = treemap_weight(node) / total * std::f32::consts::TAU * series_progress;
2908 render_sunburst_node(
2909 cx,
2910 root,
2911 node,
2912 center,
2913 ring,
2914 0,
2915 angle,
2916 angle + sweep,
2917 theme,
2918 &mut index,
2919 animation,
2920 series_progress,
2921 );
2922 angle += sweep;
2923 }
2924}
2925
2926#[allow(clippy::too_many_arguments)]
2927fn render_sunburst_node(
2928 cx: &mut fission_core::internal::InternalLoweringCx,
2929 root: &mut fission_core::internal::InternalIrBuilder,
2930 node: &crate::series::treemap::TreemapNode,
2931 center: (f32, f32),
2932 ring: f32,
2933 depth: usize,
2934 start: f32,
2935 end: f32,
2936 theme: &ChartTheme,
2937 index: &mut usize,
2938 animation: ChartAnimationFrame,
2939 series_progress: f32,
2940) {
2941 if end <= start {
2942 return;
2943 }
2944 let item_index = *index;
2945 let item_progress = animation.item_progress(series_progress, item_index);
2946 if item_progress <= f32::EPSILON {
2947 *index += 1;
2948 return;
2949 }
2950 let inner = depth as f32 * ring;
2951 let outer = inner + ring * 0.94;
2952 let color = theme.palette[item_index % theme.palette.len()];
2953 *index += 1;
2954 add_path(
2955 cx,
2956 root,
2957 &pie_slice(center.0, center.1, inner, outer, start, end),
2958 Some(Fill::Solid(fade_color(
2959 color.with_alpha(215),
2960 item_progress,
2961 ))),
2962 Some(fade_stroke(stroke(Color::WHITE, 1.0), item_progress)),
2963 );
2964 if item_progress > 0.82 && end - start > 0.22 && outer > 28.0 {
2965 let mid = (start + end) / 2.0;
2966 let label_r = inner + (outer - inner) * 0.52;
2967 add_text(
2968 cx,
2969 root,
2970 &node.name,
2971 10.0,
2972 Color::WHITE,
2973 center.0 + label_r * mid.cos() - 30.0,
2974 center.1 + label_r * mid.sin() - 7.0,
2975 60.0,
2976 14.0,
2977 );
2978 }
2979 let child_total: f32 = node.children.iter().map(treemap_weight).sum();
2980 if child_total <= 0.0 {
2981 return;
2982 }
2983 let mut child_start = start;
2984 for child in &node.children {
2985 let child_sweep = treemap_weight(child) / child_total * (end - start);
2986 render_sunburst_node(
2987 cx,
2988 root,
2989 child,
2990 center,
2991 ring,
2992 depth + 1,
2993 child_start,
2994 child_start + child_sweep,
2995 theme,
2996 index,
2997 animation,
2998 series_progress,
2999 );
3000 child_start += child_sweep;
3001 }
3002}
3003
3004fn render_parallel(
3005 cx: &mut fission_core::internal::InternalLoweringCx,
3006 root: &mut fission_core::internal::InternalIrBuilder,
3007 parallel: &crate::series::parallel::ParallelSeries,
3008 area: &ChartArea,
3009 theme: &ChartTheme,
3010 animation: ChartAnimationFrame,
3011 series_index: usize,
3012) {
3013 let axes = parallel.data.first().map(|row| row.len()).unwrap_or(0);
3014 if axes < 2 {
3015 return;
3016 }
3017 let series_progress = animation.series_progress(series_index);
3018 if series_progress <= f32::EPSILON {
3019 return;
3020 }
3021 let step = area.plot.width() / (axes - 1) as f32;
3022 for axis in 0..axes {
3023 let x = area.plot.x() + axis as f32 * step;
3024 add_path(
3025 cx,
3026 root,
3027 &format!("M {} {} L {} {}", x, area.plot.y(), x, area.plot.bottom()),
3028 None,
3029 Some(stroke(theme.axis_line, 1.0)),
3030 );
3031 }
3032 for (idx, row) in parallel.data.iter().enumerate() {
3033 let item_progress = animation.item_progress(series_progress, idx);
3034 if item_progress <= f32::EPSILON {
3035 continue;
3036 }
3037 let points: Vec<(f32, f32)> = row
3038 .iter()
3039 .enumerate()
3040 .map(|(axis, value)| {
3041 let x = area.plot.x() + axis as f32 * step;
3042 let y = area.plot.bottom() - (*value / 100.0).clamp(0.0, 1.0) * area.plot.height();
3043 (x, y)
3044 })
3045 .collect();
3046 let path = path_for_points(&reveal_points(&points, item_progress));
3047 add_path(
3048 cx,
3049 root,
3050 &path,
3051 None,
3052 Some(fade_stroke(
3053 stroke(
3054 theme.palette[idx % theme.palette.len()].with_alpha(170),
3055 2.0,
3056 ),
3057 item_progress,
3058 )),
3059 );
3060 }
3061}
3062
3063fn render_theme_river(
3064 cx: &mut fission_core::internal::InternalLoweringCx,
3065 root: &mut fission_core::internal::InternalIrBuilder,
3066 river: &crate::series::theme_river::ThemeRiverSeries,
3067 area: &ChartArea,
3068 theme: &ChartTheme,
3069 animation: ChartAnimationFrame,
3070 series_index: usize,
3071) {
3072 if river.data.is_empty() {
3073 return;
3074 }
3075 let series_progress = animation.series_progress(series_index);
3076 if series_progress <= f32::EPSILON {
3077 return;
3078 }
3079 let mut by_time: BTreeMap<String, HashMap<String, f32>> = BTreeMap::new();
3080 let mut categories = Vec::<String>::new();
3081 for (time, value, category) in &river.data {
3082 by_time
3083 .entry(time.clone())
3084 .or_default()
3085 .insert(category.clone(), *value);
3086 if !categories.iter().any(|existing| existing == category) {
3087 categories.push(category.clone());
3088 }
3089 }
3090 let times: Vec<String> = by_time.keys().cloned().collect();
3091 if times.len() < 2 || categories.is_empty() {
3092 return;
3093 }
3094
3095 let totals: Vec<f32> = times
3096 .iter()
3097 .map(|time| by_time[time].values().sum::<f32>())
3098 .collect();
3099 let max_total = totals.iter().copied().fold(1.0_f32, f32::max);
3100 let scale = area.plot.height() * 0.72 / max_total.max(f32::EPSILON);
3101 let step = area.plot.width() / (times.len() - 1) as f32;
3102 let mut bases = vec![0.0_f32; times.len()];
3103
3104 add_path(
3105 cx,
3106 root,
3107 &format!(
3108 "M {} {} L {} {}",
3109 area.plot.x(),
3110 area.plot.y() + area.plot.height() / 2.0,
3111 area.plot.right(),
3112 area.plot.y() + area.plot.height() / 2.0
3113 ),
3114 None,
3115 Some(stroke(theme.grid_line, 1.0)),
3116 );
3117
3118 for (cat_idx, category) in categories.iter().enumerate() {
3119 let item_progress = animation.item_progress(series_progress, cat_idx);
3120 if item_progress <= f32::EPSILON {
3121 continue;
3122 }
3123 let mut top = Vec::new();
3124 let mut bottom = Vec::new();
3125 for (idx, time) in times.iter().enumerate() {
3126 let value = by_time[time].get(category).copied().unwrap_or(0.0).max(0.0);
3127 let total = totals[idx];
3128 let baseline = area.plot.y() + area.plot.height() / 2.0 + total * scale / 2.0;
3129 let x = area.plot.x() + idx as f32 * step;
3130 let y_top = baseline - (bases[idx] + value) * scale;
3131 let y_bottom = baseline - bases[idx] * scale;
3132 top.push((x, y_top));
3133 bottom.push((x, y_bottom));
3134 bases[idx] += value;
3135 }
3136 let top = reveal_points(&top, item_progress);
3137 let bottom = reveal_points(&bottom, item_progress);
3138 if top.len() < 2 || bottom.len() < 2 {
3139 continue;
3140 }
3141 let mut path = path_for_points(&top);
3142 for (x, y) in bottom.iter().rev() {
3143 path.push_str(&format!(" L {} {}", x, y));
3144 }
3145 path.push_str(" Z");
3146 let color = theme.palette[cat_idx % theme.palette.len()];
3147 add_path(
3148 cx,
3149 root,
3150 &path,
3151 Some(Fill::Solid(fade_color(
3152 color.with_alpha(150),
3153 item_progress,
3154 ))),
3155 Some(fade_stroke(stroke(color, 1.0), item_progress)),
3156 );
3157 }
3158
3159 for (idx, time) in times.iter().enumerate() {
3160 if idx % ((times.len() / 4).max(1)) == 0 {
3161 add_text(
3162 cx,
3163 root,
3164 time,
3165 10.0,
3166 theme.label,
3167 area.plot.x() + idx as f32 * step - 30.0,
3168 area.plot.bottom() + 8.0,
3169 60.0,
3170 14.0,
3171 );
3172 }
3173 }
3174}
3175
3176fn render_pictorial_bar(
3177 cx: &mut fission_core::internal::InternalLoweringCx,
3178 root: &mut fission_core::internal::InternalIrBuilder,
3179 pic: &crate::series::pictorial_bar::PictorialBarSeries,
3180 model: &ChartModel,
3181 area: &ChartArea,
3182 y_scale: &LinearScale,
3183 _theme: &ChartTheme,
3184 animation: ChartAnimationFrame,
3185 series_index: usize,
3186) {
3187 let series_progress = animation.series_progress(series_index);
3188 if series_progress <= f32::EPSILON {
3189 return;
3190 }
3191 for (idx, value) in pic.data.iter().enumerate() {
3192 let item_progress = animation.item_progress(series_progress, idx);
3193 if item_progress <= f32::EPSILON {
3194 continue;
3195 }
3196 let x = map_category_x(idx, model, area);
3197 let y0 = map_y(0.0, area, y_scale);
3198 let y1 = map_y(*value, area, y_scale);
3199 let count = ((*value).abs() / 20.0).ceil().max(1.0) as usize;
3200 let visible_units = (count as f32 * item_progress).ceil() as usize;
3201 let step = (y0 - y1) / count as f32;
3202 for unit in 0..visible_units.min(count) {
3203 let unit_progress = ((item_progress * count as f32) - unit as f32).clamp(0.0, 1.0);
3204 if unit_progress <= f32::EPSILON {
3205 continue;
3206 }
3207 let y = y0 - (unit as f32 + 0.5) * step;
3208 let half = 7.0 * unit_progress.sqrt();
3209 let top = 9.0 * unit_progress.sqrt();
3210 let bottom = 8.0 * unit_progress.sqrt();
3211 let path = if pic.symbol == "rect" {
3212 format!(
3213 "M {} {} L {} {} L {} {} L {} {} Z",
3214 x - half,
3215 y - half,
3216 x + half,
3217 y - half,
3218 x + half,
3219 y + half,
3220 x - half,
3221 y + half
3222 )
3223 } else {
3224 format!(
3225 "M {} {} L {} {} L {} {} Z",
3226 x,
3227 y - top,
3228 x + bottom,
3229 y + bottom,
3230 x - bottom,
3231 y + bottom
3232 )
3233 };
3234 add_path(
3235 cx,
3236 root,
3237 &path,
3238 Some(Fill::Solid(fade_color(pic.color, unit_progress))),
3239 None,
3240 );
3241 }
3242 }
3243}
3244
3245fn render_liquidfill(
3246 cx: &mut fission_core::internal::InternalLoweringCx,
3247 root: &mut fission_core::internal::InternalIrBuilder,
3248 liquid: &crate::series::liquidfill::LiquidfillSeries,
3249 area: &ChartArea,
3250 theme: &ChartTheme,
3251 animation: ChartAnimationFrame,
3252 series_index: usize,
3253) {
3254 let series_progress = animation.series_progress(series_index);
3255 let value = liquid.data.first().copied().unwrap_or(0.0).clamp(0.0, 1.0) * series_progress;
3256 let center = (
3257 area.plot.x() + area.plot.width() / 2.0,
3258 area.plot.y() + area.plot.height() / 2.0,
3259 );
3260 let r = area.plot.width().min(area.plot.height()) * 0.34;
3261 add_rect(
3262 cx,
3263 root,
3264 LayoutRect::new(center.0 - r, center.1 - r, r * 2.0, r * 2.0),
3265 color(232, 244, 255, 255),
3266 Some(stroke(liquid.color, 2.0)),
3267 r,
3268 );
3269 let water_y = center.1 + r - value * r * 2.0;
3270 let path = format!(
3271 "M {} {} C {} {} {} {} {} {} L {} {} L {} {} Z",
3272 center.0 - r,
3273 water_y,
3274 center.0 - r * 0.45,
3275 water_y - 16.0,
3276 center.0 + r * 0.45,
3277 water_y + 16.0,
3278 center.0 + r,
3279 water_y,
3280 center.0 + r,
3281 center.1 + r,
3282 center.0 - r,
3283 center.1 + r
3284 );
3285 add_path(
3286 cx,
3287 root,
3288 &path,
3289 Some(Fill::Solid(fade_color(
3290 liquid.color.with_alpha(190),
3291 series_progress,
3292 ))),
3293 None,
3294 );
3295 add_text(
3296 cx,
3297 root,
3298 &format!("{:.0}%", value * 100.0),
3299 24.0,
3300 theme.title,
3301 center.0 - 40.0,
3302 center.1 - 14.0,
3303 80.0,
3304 28.0,
3305 );
3306}
3307
3308fn render_wordcloud(
3309 cx: &mut fission_core::internal::InternalLoweringCx,
3310 root: &mut fission_core::internal::InternalIrBuilder,
3311 wordcloud: &crate::series::wordcloud::WordcloudSeries,
3312 area: &ChartArea,
3313 theme: &ChartTheme,
3314 animation: ChartAnimationFrame,
3315 series_index: usize,
3316) {
3317 let series_progress = animation.series_progress(series_index);
3318 if series_progress <= f32::EPSILON {
3319 return;
3320 }
3321 let layout = crate::layout::wordcloud::WordcloudLayout::compute(
3322 &wordcloud.data,
3323 area.plot.width(),
3324 area.plot.height(),
3325 );
3326 for (idx, (word, size, x, y)) in layout.iter().enumerate() {
3327 let item_progress = animation.item_progress(series_progress, idx);
3328 if item_progress <= f32::EPSILON {
3329 continue;
3330 }
3331 add_text(
3332 cx,
3333 root,
3334 word,
3335 (*size * (0.78 + item_progress * 0.22)).max(1.0),
3336 fade_color(theme.palette[idx % theme.palette.len()], item_progress),
3337 area.plot.x() + x + (*size * (1.0 - item_progress) * 0.08),
3338 area.plot.y() + y,
3339 180.0,
3340 size + 8.0,
3341 );
3342 }
3343}
3344
3345fn draw_legend(
3346 cx: &mut fission_core::internal::InternalLoweringCx,
3347 root: &mut fission_core::internal::InternalIrBuilder,
3348 model: &ChartModel,
3349 chart: &Chart,
3350 area: &ChartArea,
3351 theme: &ChartTheme,
3352) {
3353 if chart.legend.is_none() {
3354 return;
3355 }
3356 let mut y = area.plot.y();
3357 let x = area.plot.right() + 18.0;
3358 for (idx, name) in series_names(model).iter().enumerate() {
3359 add_rect(
3360 cx,
3361 root,
3362 LayoutRect::new(x, y + 3.0, 10.0, 10.0),
3363 theme.palette[idx % theme.palette.len()],
3364 None,
3365 2.0,
3366 );
3367 add_text(cx, root, name, 11.0, theme.label, x + 16.0, y, 110.0, 16.0);
3368 y += 20.0;
3369 }
3370}
3371
3372fn draw_mark_areas(
3373 cx: &mut fission_core::internal::InternalLoweringCx,
3374 root: &mut fission_core::internal::InternalIrBuilder,
3375 model: &ChartModel,
3376 chart: &Chart,
3377 area: &ChartArea,
3378) {
3379 if chart.mark_areas.is_empty() || !model.has_cartesian_series() {
3380 return;
3381 }
3382 let y_scale = LinearScale::nice(model.y_domain.0, model.y_domain.1, 6);
3383 for mark in &chart.mark_areas {
3384 let y0 = map_y(mark.y_min, area, &y_scale);
3385 let y1 = map_y(mark.y_max, area, &y_scale);
3386 add_rect(
3387 cx,
3388 root,
3389 LayoutRect::new(
3390 area.plot.x(),
3391 y0.min(y1),
3392 area.plot.width(),
3393 (y0 - y1).abs().max(1.0),
3394 ),
3395 mark.color,
3396 None,
3397 0.0,
3398 );
3399 }
3400}
3401
3402fn draw_mark_lines(
3403 cx: &mut fission_core::internal::InternalLoweringCx,
3404 root: &mut fission_core::internal::InternalIrBuilder,
3405 model: &ChartModel,
3406 chart: &Chart,
3407 area: &ChartArea,
3408 theme: &ChartTheme,
3409) {
3410 if chart.mark_lines.is_empty() || !model.has_cartesian_series() {
3411 return;
3412 }
3413 let y_scale = LinearScale::nice(model.y_domain.0, model.y_domain.1, 6);
3414 for mark in &chart.mark_lines {
3415 let y = map_y(mark.y, area, &y_scale);
3416 add_path(
3417 cx,
3418 root,
3419 &format!("M {} {} L {} {}", area.plot.x(), y, area.plot.right(), y),
3420 None,
3421 Some(stroke(mark.color, mark.width)),
3422 );
3423 add_text(
3424 cx,
3425 root,
3426 &mark.name,
3427 10.0,
3428 theme.label,
3429 area.plot.right() - 90.0,
3430 y - 16.0,
3431 86.0,
3432 14.0,
3433 );
3434 }
3435}
3436
3437fn draw_mark_points(
3438 cx: &mut fission_core::internal::InternalLoweringCx,
3439 root: &mut fission_core::internal::InternalIrBuilder,
3440 model: &ChartModel,
3441 chart: &Chart,
3442 area: &ChartArea,
3443 theme: &ChartTheme,
3444) {
3445 if chart.mark_points.is_empty() || !model.has_cartesian_series() {
3446 return;
3447 }
3448 let x_scale = LinearScale::nice(model.x_domain.0, model.x_domain.1, 6);
3449 let y_scale = LinearScale::nice(model.y_domain.0, model.y_domain.1, 6);
3450 for mark in &chart.mark_points {
3451 let x = if model.x_axis.axis_type == AxisType::Category {
3452 mark.x
3453 .map(|x| map_category_x(x.round().max(0.0) as usize, model, area))
3454 .unwrap_or(area.plot.x() + area.plot.width() / 2.0)
3455 } else {
3456 map_x(mark.x.unwrap_or(model.x_domain.0), area, &x_scale)
3457 };
3458 let y = map_y(mark.y, area, &y_scale);
3459 add_rect(
3460 cx,
3461 root,
3462 LayoutRect::new(x - 5.0, y - 5.0, 10.0, 10.0),
3463 mark.color,
3464 Some(stroke(Color::WHITE, 1.0)),
3465 5.0,
3466 );
3467 add_text(
3468 cx,
3469 root,
3470 &mark.name,
3471 10.0,
3472 theme.label,
3473 x + 8.0,
3474 y - 8.0,
3475 90.0,
3476 14.0,
3477 );
3478 }
3479}
3480
3481fn draw_visual_map(
3482 cx: &mut fission_core::internal::InternalLoweringCx,
3483 root: &mut fission_core::internal::InternalIrBuilder,
3484 chart: &Chart,
3485 area: &ChartArea,
3486 theme: &ChartTheme,
3487) {
3488 let Some(map) = chart.visual_map.as_ref() else {
3489 return;
3490 };
3491 let x = area.plot.right() + 24.0;
3492 let y = area.plot.bottom() - 110.0;
3493 let h = 90.0;
3494 add_rect(
3495 cx,
3496 root,
3497 LayoutRect::new(x, y, 12.0, h),
3498 color(255, 255, 255, 255),
3499 Some(stroke(theme.grid_line, 1.0)),
3500 2.0,
3501 );
3502 for i in 0..18 {
3503 let t = i as f32 / 17.0;
3504 add_rect(
3505 cx,
3506 root,
3507 LayoutRect::new(
3508 x + 1.0,
3509 y + h - (i as f32 + 1.0) * h / 18.0,
3510 10.0,
3511 h / 18.0 + 0.5,
3512 ),
3513 visual_color_at(map, t),
3514 None,
3515 0.0,
3516 );
3517 }
3518 add_text(
3519 cx,
3520 root,
3521 &format_tick(map.max),
3522 10.0,
3523 theme.label,
3524 x + 18.0,
3525 y - 2.0,
3526 70.0,
3527 14.0,
3528 );
3529 add_text(
3530 cx,
3531 root,
3532 &format_tick(map.min),
3533 10.0,
3534 theme.label,
3535 x + 18.0,
3536 y + h - 12.0,
3537 70.0,
3538 14.0,
3539 );
3540}
3541
3542fn draw_data_zoom(
3543 cx: &mut fission_core::internal::InternalLoweringCx,
3544 root: &mut fission_core::internal::InternalIrBuilder,
3545 chart: &Chart,
3546 area: &ChartArea,
3547 theme: &ChartTheme,
3548) {
3549 let Some(zoom) = chart.data_zoom.as_ref() else {
3550 return;
3551 };
3552 let x = area.plot.x();
3553 let y = area.plot.bottom() + 36.0;
3554 let w = area.plot.width();
3555 add_rect(
3556 cx,
3557 root,
3558 LayoutRect::new(x, y, w, 8.0),
3559 theme.grid_line,
3560 None,
3561 4.0,
3562 );
3563 let start = (zoom.start_percent / 100.0).clamp(0.0, 1.0);
3564 let end = (zoom.end_percent / 100.0).clamp(start, 1.0);
3565 add_rect(
3566 cx,
3567 root,
3568 LayoutRect::new(x + w * start, y - 2.0, w * (end - start), 12.0),
3569 theme.palette[0].with_alpha(180),
3570 None,
3571 6.0,
3572 );
3573}
3574
3575fn draw_brush(
3576 cx: &mut fission_core::internal::InternalLoweringCx,
3577 root: &mut fission_core::internal::InternalIrBuilder,
3578 chart: &Chart,
3579 area: &ChartArea,
3580 theme: &ChartTheme,
3581) {
3582 let Some(brush) = chart.interaction.brush.as_ref() else {
3583 return;
3584 };
3585 let Some((x, y, width, height)) = brush.preview_rect else {
3586 return;
3587 };
3588 let rect = LayoutRect::new(
3589 area.plot.x() + x * area.plot.width(),
3590 area.plot.y() + y * area.plot.height(),
3591 width * area.plot.width(),
3592 height * area.plot.height(),
3593 );
3594 add_rect(
3595 cx,
3596 root,
3597 rect,
3598 theme.palette[0].with_alpha(42),
3599 Some(stroke(theme.palette[0].with_alpha(190), 1.4)),
3600 3.0,
3601 );
3602}
3603
3604fn draw_graphics(
3605 cx: &mut fission_core::internal::InternalLoweringCx,
3606 root: &mut fission_core::internal::InternalIrBuilder,
3607 chart: &Chart,
3608 area: &ChartArea,
3609 theme: &ChartTheme,
3610) {
3611 for graphic in &chart.graphics {
3612 let x = area.plot.x() + graphic.x * area.plot.width();
3613 let y = area.plot.y() + graphic.y * area.plot.height();
3614 let width = graphic.width * area.plot.width();
3615 let height = graphic.height * area.plot.height();
3616 match graphic.kind {
3617 ChartGraphicKind::Rect => add_rect(
3618 cx,
3619 root,
3620 LayoutRect::new(x, y, width, height),
3621 graphic.color,
3622 graphic.stroke.map(|color| stroke(color, 1.0)),
3623 4.0,
3624 ),
3625 ChartGraphicKind::Circle => {
3626 let r = width.min(height) / 2.0;
3627 add_rect(
3628 cx,
3629 root,
3630 LayoutRect::new(x - r, y - r, r * 2.0, r * 2.0),
3631 graphic.color,
3632 graphic.stroke.map(|color| stroke(color, 1.0)),
3633 r,
3634 );
3635 }
3636 ChartGraphicKind::Text => {
3637 if let Some(text) = graphic.text.as_ref() {
3638 add_text(cx, root, text, 12.0, graphic.color, x, y, width, height);
3639 }
3640 }
3641 ChartGraphicKind::Line => add_path(
3642 cx,
3643 root,
3644 &format!("M {} {} L {} {}", x, y, x + width, y + height),
3645 None,
3646 Some(stroke(graphic.color, 1.8)),
3647 ),
3648 }
3649 }
3650 if !chart.graphics.is_empty() {
3651 add_text(
3652 cx,
3653 root,
3654 "graphic layer",
3655 10.0,
3656 theme.label,
3657 area.plot.x() + 8.0,
3658 area.plot.y() + 8.0,
3659 110.0,
3660 14.0,
3661 );
3662 }
3663}
3664
3665fn draw_timeline(
3666 cx: &mut fission_core::internal::InternalLoweringCx,
3667 root: &mut fission_core::internal::InternalIrBuilder,
3668 chart: &Chart,
3669 area: &ChartArea,
3670 theme: &ChartTheme,
3671) {
3672 let Some(timeline) = chart.timeline.as_ref() else {
3673 return;
3674 };
3675 if timeline.labels.is_empty() {
3676 return;
3677 }
3678
3679 let x = area.plot.x();
3680 let y = area.outer_h - 30.0;
3681 let w = area.plot.width();
3682 add_path(
3683 cx,
3684 root,
3685 &format!("M {} {} L {} {}", x, y, x + w, y),
3686 None,
3687 Some(stroke(theme.grid_line, 2.0)),
3688 );
3689 let denom = timeline.labels.len().saturating_sub(1).max(1) as f32;
3690 for (idx, label) in timeline.labels.iter().enumerate() {
3691 let px = x + idx as f32 / denom * w;
3692 let active = idx == timeline.current_index.min(timeline.labels.len() - 1);
3693 let r = if active { 6.0 } else { 4.0 };
3694 add_rect(
3695 cx,
3696 root,
3697 LayoutRect::new(px - r, y - r, r * 2.0, r * 2.0),
3698 if active {
3699 theme.palette[0]
3700 } else {
3701 theme.axis_line
3702 },
3703 Some(stroke(Color::WHITE, 1.0)),
3704 r,
3705 );
3706 add_text(
3707 cx,
3708 root,
3709 label,
3710 10.0,
3711 theme.label,
3712 px - 28.0,
3713 y + 8.0,
3714 56.0,
3715 14.0,
3716 );
3717 }
3718}
3719
3720fn draw_toolbox(
3721 cx: &mut fission_core::internal::InternalLoweringCx,
3722 root: &mut fission_core::internal::InternalIrBuilder,
3723 chart: &Chart,
3724 area: &ChartArea,
3725 theme: &ChartTheme,
3726) {
3727 if chart.interaction.toolbox_actions.is_empty() {
3728 return;
3729 }
3730
3731 let mut x = area.plot.right() - chart.interaction.toolbox_actions.len() as f32 * 54.0;
3732 let y = 18.0;
3733 for action in &chart.interaction.toolbox_actions {
3734 let label = match action {
3735 crate::interaction::ChartToolAction::Restore => "reset",
3736 crate::interaction::ChartToolAction::SaveImage => "save",
3737 crate::interaction::ChartToolAction::DataZoom => "zoom",
3738 crate::interaction::ChartToolAction::Brush => "brush",
3739 };
3740 add_rect(
3741 cx,
3742 root,
3743 LayoutRect::new(x, y, 48.0, 22.0),
3744 theme.plot_background,
3745 Some(stroke(theme.grid_line, 1.0)),
3746 5.0,
3747 );
3748 add_text(
3749 cx,
3750 root,
3751 label,
3752 10.0,
3753 theme.label,
3754 x + 5.0,
3755 y + 4.0,
3756 38.0,
3757 14.0,
3758 );
3759 x += 54.0;
3760 }
3761}
3762
3763fn draw_diagnostics(
3764 cx: &mut fission_core::internal::InternalLoweringCx,
3765 root: &mut fission_core::internal::InternalIrBuilder,
3766 model: &ChartModel,
3767 area: &ChartArea,
3768 theme: &ChartTheme,
3769) {
3770 for (idx, diagnostic) in model.diagnostics.iter().enumerate() {
3771 let text = if let Some(name) = diagnostic.series_name.as_ref() {
3772 format!("{}: {}", name, diagnostic.message)
3773 } else {
3774 diagnostic.message.clone()
3775 };
3776 add_text(
3777 cx,
3778 root,
3779 &text,
3780 12.0,
3781 theme.diagnostic,
3782 area.plot.x() + 12.0,
3783 area.plot.y() + 16.0 + idx as f32 * 18.0,
3784 area.plot.width() - 24.0,
3785 16.0,
3786 );
3787 }
3788}
3789
3790fn count_bar_groups(series: &[ResolvedSeries]) -> usize {
3791 series
3792 .iter()
3793 .filter(|series| matches!(series, ResolvedSeries::Bar(bar) if bar.source.stack.is_none()))
3794 .count()
3795 .max(1)
3796}
3797
3798fn stack_base(stacks: &HashMap<(String, usize), f32>, stack: Option<&String>, idx: usize) -> f32 {
3799 stack
3800 .and_then(|name| stacks.get(&(name.clone(), idx)).copied())
3801 .unwrap_or(0.0)
3802}
3803
3804fn path_for_line(points: &[(f32, f32)], smooth: bool, step: Option<&str>) -> String {
3805 if points.is_empty() {
3806 return String::new();
3807 }
3808 if smooth {
3809 return catmull_rom_to_bezier(points);
3810 }
3811 let mut path = format!("M {} {}", points[0].0, points[0].1);
3812 for pair in points.windows(2) {
3813 let (px, py) = pair[0];
3814 let (x, y) = pair[1];
3815 match step {
3816 Some("start") => path.push_str(&format!(" L {} {} L {} {}", px, y, x, y)),
3817 Some("end") => path.push_str(&format!(" L {} {} L {} {}", x, py, x, y)),
3818 Some("middle") => {
3819 let mx = px + (x - px) / 2.0;
3820 path.push_str(&format!(" L {} {} L {} {} L {} {}", mx, py, mx, y, x, y));
3821 }
3822 _ => path.push_str(&format!(" L {} {}", x, y)),
3823 }
3824 }
3825 path
3826}
3827
3828fn reveal_points(points: &[(f32, f32)], progress: f32) -> Vec<(f32, f32)> {
3829 if points.is_empty() || progress <= f32::EPSILON {
3830 return Vec::new();
3831 }
3832 if progress >= 1.0 || points.len() == 1 {
3833 return points.to_vec();
3834 }
3835
3836 let span = progress.clamp(0.0, 1.0) * (points.len() - 1) as f32;
3837 let last_full = span.floor() as usize;
3838 let mut out = points[..=last_full].to_vec();
3839 if last_full + 1 < points.len() {
3840 let t = span - last_full as f32;
3841 let (ax, ay) = points[last_full];
3842 let (bx, by) = points[last_full + 1];
3843 out.push((ax + (bx - ax) * t, ay + (by - ay) * t));
3844 }
3845 out
3846}
3847
3848fn path_for_points(points: &[(f32, f32)]) -> String {
3849 if points.is_empty() {
3850 return String::new();
3851 }
3852 let mut path = format!("M {} {}", points[0].0, points[0].1);
3853 for (x, y) in points.iter().skip(1) {
3854 path.push_str(&format!(" L {} {}", x, y));
3855 }
3856 path
3857}
3858
3859fn circle_path(cx: f32, cy: f32, r: f32) -> String {
3860 format!(
3861 "M {} {} A {} {} 0 1 0 {} {} A {} {} 0 1 0 {} {}",
3862 cx + r,
3863 cy,
3864 r,
3865 r,
3866 cx - r,
3867 cy,
3868 r,
3869 r,
3870 cx + r,
3871 cy
3872 )
3873}
3874
3875fn treemap_weight(node: &crate::series::treemap::TreemapNode) -> f32 {
3876 let child_total: f32 = node.children.iter().map(treemap_weight).sum();
3877 if child_total > 0.0 {
3878 child_total
3879 } else {
3880 node.value.max(0.0)
3881 }
3882}
3883
3884fn treemap_depth(node: &crate::series::treemap::TreemapNode) -> usize {
3885 1 + node.children.iter().map(treemap_depth).max().unwrap_or(0)
3886}
3887
3888fn path_bounds(path: &str) -> Option<(f32, f32, f32, f32)> {
3889 let tokens: Vec<&str> = path.split_whitespace().collect();
3890 let mut min_x = f32::MAX;
3891 let mut max_x = f32::MIN;
3892 let mut min_y = f32::MAX;
3893 let mut max_y = f32::MIN;
3894 let mut idx = 0usize;
3895 while idx < tokens.len() {
3896 let token = tokens[idx];
3897 idx += 1;
3898 let coord_count = match token {
3899 "M" | "L" => 2,
3900 "C" => 6,
3901 "Z" => 0,
3902 _ => continue,
3903 };
3904 let mut coords = Vec::with_capacity(coord_count);
3905 for _ in 0..coord_count {
3906 if let Some(raw) = tokens.get(idx) {
3907 coords.push(raw.parse::<f32>().ok()?);
3908 idx += 1;
3909 }
3910 }
3911 for pair in coords.chunks(2) {
3912 if let [x, y] = pair {
3913 min_x = min_x.min(*x);
3914 max_x = max_x.max(*x);
3915 min_y = min_y.min(*y);
3916 max_y = max_y.max(*y);
3917 }
3918 }
3919 }
3920 if min_x == f32::MAX {
3921 None
3922 } else {
3923 Some((min_x, min_y, max_x - min_x, max_y - min_y))
3924 }
3925}
3926
3927fn hit_test_points(
3928 series_index: usize,
3929 series_name: &str,
3930 data: &[(f32, f32)],
3931 area: &ChartArea,
3932 x_scale: &LinearScale,
3933 y_scale: &LinearScale,
3934 point: LayoutPoint,
3935 threshold: f32,
3936) -> Option<ChartHit> {
3937 for (idx, (xv, yv)) in data.iter().enumerate() {
3938 let x = map_x(*xv, area, x_scale);
3939 let y = map_y(*yv, area, y_scale);
3940 if distance(point, (x, y)) <= threshold {
3941 return Some(ChartHit::series_item(
3942 series_index,
3943 series_name.to_string(),
3944 idx,
3945 Some(*xv),
3946 Some(*yv),
3947 ));
3948 }
3949 }
3950 None
3951}
3952
3953fn nearest_cartesian_hit(
3954 model: &ChartModel,
3955 area: &ChartArea,
3956 point: LayoutPoint,
3957) -> Option<ChartHit> {
3958 let y_scale = LinearScale::nice(model.y_domain.0, model.y_domain.1, 6);
3959 let mut best: Option<(f32, ChartHit)> = None;
3960
3961 for (series_index, series) in model.series.iter().enumerate() {
3962 match series {
3963 ResolvedSeries::Line(line) => {
3964 for (idx, value) in line.values.iter().enumerate() {
3965 let x = map_category_x(idx, model, area);
3966 let y = map_y(*value, area, &y_scale);
3967 let dx = (point.x - x).abs();
3968 let dy = (point.y - y).abs() * 0.25;
3969 let score = dx + dy;
3970 let hit = ChartHit::series_item(
3971 series_index,
3972 line.source.name.clone(),
3973 idx,
3974 Some(idx as f32),
3975 Some(*value),
3976 );
3977 if best
3978 .as_ref()
3979 .map_or(true, |(best_score, _)| score < *best_score)
3980 {
3981 best = Some((score, hit));
3982 }
3983 }
3984 }
3985 ResolvedSeries::Bar(bar) => {
3986 for (idx, value) in bar.values.iter().enumerate() {
3987 let x = map_category_x(idx, model, area);
3988 let score = (point.x - x).abs();
3989 let hit = ChartHit::series_item(
3990 series_index,
3991 bar.source.name.clone(),
3992 idx,
3993 Some(idx as f32),
3994 Some(*value),
3995 );
3996 if best
3997 .as_ref()
3998 .map_or(true, |(best_score, _)| score < *best_score)
3999 {
4000 best = Some((score, hit));
4001 }
4002 }
4003 }
4004 _ => {}
4005 }
4006 }
4007
4008 best.and_then(|(score, hit)| {
4009 let max_distance = (band_width(model, area) * 0.55).max(16.0);
4010 if score <= max_distance {
4011 Some(hit)
4012 } else {
4013 None
4014 }
4015 })
4016}
4017
4018fn hit_test_pie(
4019 series_index: usize,
4020 pie: &crate::series::pie::PieSeries,
4021 area: &ChartArea,
4022 point: LayoutPoint,
4023) -> Option<ChartHit> {
4024 let total: f32 = pie.data.iter().map(|(_, value)| *value).sum();
4025 if total <= 0.0 {
4026 return None;
4027 }
4028
4029 let center = (
4030 area.plot.x() + area.plot.width() * 0.45,
4031 area.plot.y() + area.plot.height() * 0.52,
4032 );
4033 let max_r = area.plot.width().min(area.plot.height()) * 0.38;
4034 let dx = point.x - center.0;
4035 let dy = point.y - center.1;
4036 let radius = (dx * dx + dy * dy).sqrt();
4037 if radius > max_r {
4038 return None;
4039 }
4040 let inner = pie.inner_radius.max(0.0).min(max_r * 0.85);
4041 if radius < inner {
4042 return None;
4043 }
4044
4045 let mut angle = dy.atan2(dx);
4046 if angle < -std::f32::consts::PI / 2.0 {
4047 angle += std::f32::consts::TAU;
4048 }
4049 let mut start = -std::f32::consts::PI / 2.0;
4050 for (idx, (label, value)) in pie.data.iter().enumerate() {
4051 let sweep = (*value / total) * std::f32::consts::TAU;
4052 let end = start + sweep;
4053 if angle >= start && angle <= end {
4054 let _ = label;
4055 return Some(ChartHit::series_item(
4056 series_index,
4057 pie.name.clone(),
4058 idx,
4059 None,
4060 Some(*value),
4061 ));
4062 }
4063 start = end;
4064 }
4065 None
4066}
4067
4068fn distance(point: LayoutPoint, other: (f32, f32)) -> f32 {
4069 let dx = point.x - other.0;
4070 let dy = point.y - other.1;
4071 (dx * dx + dy * dy).sqrt()
4072}
4073
4074fn band_width(model: &ChartModel, area: &ChartArea) -> f32 {
4075 let count = model.x_categories.len().max(1) as f32;
4076 area.plot.width() / count
4077}
4078
4079fn category_band_width(count: usize, extent: f32) -> f32 {
4080 extent / count.max(1) as f32
4081}
4082
4083fn map_category_x(idx: usize, model: &ChartModel, area: &ChartArea) -> f32 {
4084 area.plot.x() + band_width(model, area) * (idx as f32 + 0.5)
4085}
4086
4087fn map_category_y(idx: usize, model: &ChartModel, area: &ChartArea) -> f32 {
4088 let count = model.y_categories.len().max(1);
4089 area.plot.y() + category_band_width(count, area.plot.height()) * (idx as f32 + 0.5)
4090}
4091
4092fn map_x(value: f32, area: &ChartArea, scale: &LinearScale) -> f32 {
4093 scale.map(value, area.plot.x(), area.plot.right())
4094}
4095
4096fn map_y(value: f32, area: &ChartArea, scale: &LinearScale) -> f32 {
4097 scale.map(value, area.plot.bottom(), area.plot.y())
4098}
4099
4100fn series_names(model: &ChartModel) -> Vec<String> {
4101 model
4102 .series
4103 .iter()
4104 .map(|series| match series {
4105 ResolvedSeries::Line(s) => s.source.name.clone(),
4106 ResolvedSeries::Bar(s) => s.source.name.clone(),
4107 ResolvedSeries::Scatter(s) => s.name.clone(),
4108 ResolvedSeries::Pie(s) => s.name.clone(),
4109 ResolvedSeries::Bubble(s) => s.name.clone(),
4110 ResolvedSeries::Boxplot(s) => s.name.clone(),
4111 ResolvedSeries::Candlestick(s) => s.name.clone(),
4112 ResolvedSeries::Heatmap(s) => s.name.clone(),
4113 ResolvedSeries::CalendarHeatmap(s) => s.name.clone(),
4114 ResolvedSeries::Lines(s) => s.name.clone(),
4115 ResolvedSeries::Graph(s) => s.name.clone(),
4116 ResolvedSeries::Tree(s) => s.name.clone(),
4117 ResolvedSeries::Treemap(s) => s.name.clone(),
4118 ResolvedSeries::Radar(s) => s.name.clone(),
4119 ResolvedSeries::Funnel(s) => s.name.clone(),
4120 ResolvedSeries::Gauge(s) => s.name.clone(),
4121 ResolvedSeries::Map(s) => s.name.clone(),
4122 ResolvedSeries::Sankey(s) => s.name.clone(),
4123 ResolvedSeries::Parallel(s) => s.name.clone(),
4124 ResolvedSeries::Sunburst(s) => s.name.clone(),
4125 ResolvedSeries::ThemeRiver(s) => s.name.clone(),
4126 ResolvedSeries::PictorialBar(s) => s.name.clone(),
4127 ResolvedSeries::EffectScatter(s) => s.name.clone(),
4128 ResolvedSeries::Liquidfill(s) => s.name.clone(),
4129 ResolvedSeries::Wordcloud(s) => s.name.clone(),
4130 ResolvedSeries::PolarBar(s) => s.name.clone(),
4131 ResolvedSeries::PolarLine(s) => s.name.clone(),
4132 ResolvedSeries::SingleAxis(s) => s.name.clone(),
4133 })
4134 .collect()
4135}
4136
4137fn render_edges(
4138 cx: &mut fission_core::internal::InternalLoweringCx,
4139 root: &mut fission_core::internal::InternalIrBuilder,
4140 edges: &[GraphEdge],
4141 positions: &HashMap<String, (f32, f32)>,
4142 area: &ChartArea,
4143 theme: &ChartTheme,
4144 animation: ChartAnimationFrame,
4145 series_progress: f32,
4146) {
4147 for (idx, edge) in edges.iter().enumerate() {
4148 let item_progress = animation.item_progress(series_progress, idx);
4149 if item_progress <= f32::EPSILON {
4150 continue;
4151 }
4152 if let (Some(a), Some(b)) = (positions.get(&edge.source), positions.get(&edge.target)) {
4153 let from = (area.plot.x() + a.0, area.plot.y() + a.1);
4154 let to = interpolate_point(
4155 from,
4156 (area.plot.x() + b.0, area.plot.y() + b.1),
4157 item_progress,
4158 );
4159 add_path(
4160 cx,
4161 root,
4162 &format!("M {} {} L {} {}", from.0, from.1, to.0, to.1),
4163 None,
4164 Some(fade_stroke(
4165 stroke(theme.axis_line.with_alpha(140), 1.2),
4166 item_progress,
4167 )),
4168 );
4169 }
4170 }
4171}
4172
4173fn radar_angle(axis: usize, axes: usize) -> f32 {
4174 axis as f32 / axes as f32 * std::f32::consts::TAU - std::f32::consts::PI / 2.0
4175}
4176
4177fn visual_color(map: &VisualMap, value: f32) -> Color {
4178 let denom = (map.max - map.min).max(f32::EPSILON);
4179 visual_color_at(map, ((value - map.min) / denom).clamp(0.0, 1.0))
4180}
4181
4182fn visual_color_at(map: &VisualMap, t: f32) -> Color {
4183 let colors = if map.in_range_colors.is_empty() {
4184 vec![
4185 color(49, 130, 206, 255),
4186 color(252, 211, 77, 255),
4187 color(220, 38, 38, 255),
4188 ]
4189 } else {
4190 map.in_range_colors.clone()
4191 };
4192 if colors.len() == 1 {
4193 return colors[0];
4194 }
4195 let scaled = t.clamp(0.0, 1.0) * (colors.len() - 1) as f32;
4196 let idx = scaled.floor() as usize;
4197 let next = (idx + 1).min(colors.len() - 1);
4198 let local = scaled - idx as f32;
4199 mix_color(colors[idx], colors[next], local)
4200}
4201
4202fn heat_color(t: f32) -> Color {
4203 mix_color(
4204 color(59, 130, 246, 255),
4205 color(239, 68, 68, 255),
4206 t.clamp(0.0, 1.0),
4207 )
4208}
4209
4210fn mix_color(a: Color, b: Color, t: f32) -> Color {
4211 let mix = |x: u8, y: u8| x as f32 + (y as f32 - x as f32) * t;
4212 color(
4213 mix(a.r, b.r) as u8,
4214 mix(a.g, b.g) as u8,
4215 mix(a.b, b.b) as u8,
4216 mix(a.a, b.a) as u8,
4217 )
4218}
4219
4220fn fade_color(color: Color, progress: f32) -> Color {
4221 color.with_alpha(((color.a as f32) * progress.clamp(0.0, 1.0)).round() as u8)
4222}
4223
4224fn fade_fill(fill: Fill, progress: f32) -> Fill {
4225 match fill {
4226 Fill::Solid(color) => Fill::Solid(fade_color(color, progress)),
4227 Fill::LinearGradient { start, end, stops } => Fill::LinearGradient {
4228 start,
4229 end,
4230 stops: stops
4231 .into_iter()
4232 .map(|(offset, color)| (offset, fade_color(color, progress)))
4233 .collect(),
4234 },
4235 Fill::RadialGradient {
4236 center,
4237 radius,
4238 stops,
4239 } => Fill::RadialGradient {
4240 center,
4241 radius,
4242 stops: stops
4243 .into_iter()
4244 .map(|(offset, color)| (offset, fade_color(color, progress)))
4245 .collect(),
4246 },
4247 }
4248}
4249
4250fn fade_stroke(mut stroke: Stroke, progress: f32) -> Stroke {
4251 stroke.fill = fade_fill(stroke.fill, progress);
4252 stroke
4253}
4254
4255fn interpolate(a: f32, b: f32, progress: f32) -> f32 {
4256 a + (b - a) * progress.clamp(0.0, 1.0)
4257}
4258
4259fn interpolate_point(from: (f32, f32), to: (f32, f32), progress: f32) -> (f32, f32) {
4260 (
4261 interpolate(from.0, to.0, progress),
4262 interpolate(from.1, to.1, progress),
4263 )
4264}
4265
4266fn scale_rect_from_center(rect: LayoutRect, progress: f32) -> LayoutRect {
4267 let progress = progress.clamp(0.0, 1.0);
4268 let width = (rect.width() * progress).max(1.0);
4269 let height = (rect.height() * progress).max(1.0);
4270 LayoutRect::new(
4271 rect.x() + (rect.width() - width) / 2.0,
4272 rect.y() + (rect.height() - height) / 2.0,
4273 width,
4274 height,
4275 )
4276}
4277
4278fn color_luma(color: Color) -> f32 {
4279 color.r as f32 * 0.2126 + color.g as f32 * 0.7152 + color.b as f32 * 0.0722
4280}
4281
4282fn translate_path(path: &str, dx: f32, dy: f32) -> String {
4283 if dx == 0.0 && dy == 0.0 {
4284 path.to_string()
4285 } else {
4286 let tokens: Vec<&str> = path.split_whitespace().collect();
4289 let mut result = String::new();
4290 let mut idx = 0;
4291 while idx < tokens.len() {
4292 let cmd = tokens[idx];
4293 result.push_str(cmd);
4294 idx += 1;
4295 let coord_count = match cmd {
4296 "M" | "L" => 2,
4297 "C" => 6,
4298 "Z" => 0,
4299 _ => 0,
4300 };
4301 for coord_idx in 0..coord_count {
4302 if let Some(raw) = tokens.get(idx) {
4303 let offset = if coord_idx % 2 == 0 { dx } else { dy };
4304 let value = raw.parse::<f32>().unwrap_or(0.0) + offset;
4305 result.push_str(&format!(" {}", value));
4306 idx += 1;
4307 }
4308 }
4309 result.push(' ');
4310 }
4311 result
4312 }
4313}
4314
4315#[derive(Debug, Clone)]
4316struct TreeRenderNode {
4317 name: String,
4318 x: f32,
4319 y: f32,
4320 depth: usize,
4321}
4322
4323fn tree_leaf_count(node: &crate::series::treemap::TreemapNode) -> usize {
4324 if node.children.is_empty() {
4325 1
4326 } else {
4327 node.children.iter().map(tree_leaf_count).sum()
4328 }
4329}
4330
4331#[allow(clippy::too_many_arguments)]
4332fn layout_tree_node(
4333 node: &crate::series::treemap::TreemapNode,
4334 depth_index: usize,
4335 depth_count: usize,
4336 leaf_count: usize,
4337 next_leaf: &mut usize,
4338 area: &ChartArea,
4339 nodes: &mut Vec<TreeRenderNode>,
4340 edges: &mut Vec<((f32, f32), (f32, f32))>,
4341) -> (f32, f32) {
4342 let x_denom = depth_count.saturating_sub(1).max(1) as f32;
4343 let x = area.plot.x() + depth_index as f32 / x_denom * area.plot.width();
4344 let mut child_points = Vec::new();
4345 let y = if node.children.is_empty() {
4346 let y = area.plot.y() + (*next_leaf as f32 + 0.5) / leaf_count as f32 * area.plot.height();
4347 *next_leaf += 1;
4348 y
4349 } else {
4350 let mut sum = 0.0;
4351 for child in &node.children {
4352 let child_point = layout_tree_node(
4353 child,
4354 depth_index + 1,
4355 depth_count,
4356 leaf_count,
4357 next_leaf,
4358 area,
4359 nodes,
4360 edges,
4361 );
4362 child_points.push(child_point);
4363 let (_, child_y) = child_point;
4364 sum += child_y;
4365 }
4366 sum / node.children.len().max(1) as f32
4367 };
4368
4369 let point = (x, y);
4370 for child_point in child_points {
4371 edges.push((point, child_point));
4372 }
4373 nodes.push(TreeRenderNode {
4374 name: node.name.clone(),
4375 x,
4376 y,
4377 depth: depth_index,
4378 });
4379 point
4380}
4381
4382#[allow(clippy::too_many_arguments)]
4383fn layout_radial_tree_node(
4384 node: &crate::series::treemap::TreemapNode,
4385 depth_index: usize,
4386 depth_count: usize,
4387 leaf_count: usize,
4388 next_leaf: &mut usize,
4389 area: &ChartArea,
4390 nodes: &mut Vec<TreeRenderNode>,
4391 edges: &mut Vec<((f32, f32), (f32, f32))>,
4392) -> (f32, f32) {
4393 let center = (
4394 area.plot.x() + area.plot.width() / 2.0,
4395 area.plot.y() + area.plot.height() / 2.0,
4396 );
4397 let radius = area.plot.width().min(area.plot.height()) * 0.44;
4398 let mut child_points = Vec::new();
4399 let point = if node.children.is_empty() {
4400 let angle = -std::f32::consts::PI / 2.0
4401 + (*next_leaf as f32 + 0.5) / leaf_count as f32 * std::f32::consts::TAU;
4402 *next_leaf += 1;
4403 let r = depth_index as f32 / depth_count.saturating_sub(1).max(1) as f32 * radius;
4404 (center.0 + r * angle.cos(), center.1 + r * angle.sin())
4405 } else {
4406 let mut points = Vec::new();
4407 for child in &node.children {
4408 let child_point = layout_radial_tree_node(
4409 child,
4410 depth_index + 1,
4411 depth_count,
4412 leaf_count,
4413 next_leaf,
4414 area,
4415 nodes,
4416 edges,
4417 );
4418 points.push(child_point);
4419 child_points.push(child_point);
4420 }
4421 if depth_index == 0 {
4422 center
4423 } else {
4424 let avg_x = points.iter().map(|point| point.0).sum::<f32>() / points.len() as f32;
4425 let avg_y = points.iter().map(|point| point.1).sum::<f32>() / points.len() as f32;
4426 let angle = (avg_y - center.1).atan2(avg_x - center.0);
4427 let r = depth_index as f32 / depth_count.saturating_sub(1).max(1) as f32 * radius;
4428 (center.0 + r * angle.cos(), center.1 + r * angle.sin())
4429 }
4430 };
4431
4432 nodes.push(TreeRenderNode {
4433 name: node.name.clone(),
4434 x: point.0,
4435 y: point.1,
4436 depth: depth_index,
4437 });
4438 for child_point in child_points {
4439 edges.push((point, child_point));
4440 }
4441 point
4442}
4443
4444fn map_lines_point(
4445 point: (f32, f32),
4446 min_x: f32,
4447 max_x: f32,
4448 min_y: f32,
4449 max_y: f32,
4450 area: &ChartArea,
4451) -> (f32, f32) {
4452 let x_t = ((point.0 - min_x) / (max_x - min_x).max(f32::EPSILON)).clamp(0.0, 1.0);
4453 let y_t = ((point.1 - min_y) / (max_y - min_y).max(f32::EPSILON)).clamp(0.0, 1.0);
4454 (
4455 area.plot.x() + x_t * area.plot.width(),
4456 area.plot.bottom() - y_t * area.plot.height(),
4457 )
4458}
4459
4460fn quadratic_midpoint(from: (f32, f32), control: (f32, f32), to: (f32, f32)) -> (f32, f32) {
4461 (
4462 0.25 * from.0 + 0.5 * control.0 + 0.25 * to.0,
4463 0.25 * from.1 + 0.5 * control.1 + 0.25 * to.1,
4464 )
4465}
4466
4467fn draw_arrow_head(
4468 cx: &mut fission_core::internal::InternalLoweringCx,
4469 root: &mut fission_core::internal::InternalIrBuilder,
4470 from: (f32, f32),
4471 to: (f32, f32),
4472 fill: Color,
4473) {
4474 let angle = (to.1 - from.1).atan2(to.0 - from.0);
4475 let size = 8.0;
4476 let left = (
4477 to.0 - size * (angle - 0.45).cos(),
4478 to.1 - size * (angle - 0.45).sin(),
4479 );
4480 let right = (
4481 to.0 - size * (angle + 0.45).cos(),
4482 to.1 - size * (angle + 0.45).sin(),
4483 );
4484 let path = format!(
4485 "M {} {} L {} {} L {} {} Z",
4486 to.0, to.1, left.0, left.1, right.0, right.1
4487 );
4488 add_path(cx, root, &path, Some(Fill::Solid(fill)), None);
4489}
4490
4491fn normalize_bounds(min: f32, max: f32) -> (f32, f32) {
4492 if !min.is_finite() || !max.is_finite() {
4493 return (0.0, 1.0);
4494 }
4495 if (max - min).abs() < f32::EPSILON {
4496 (min - 1.0, max + 1.0)
4497 } else {
4498 (min, max)
4499 }
4500}
4501
4502fn add_rect(
4503 cx: &mut fission_core::internal::InternalLoweringCx,
4504 root: &mut fission_core::internal::InternalIrBuilder,
4505 rect: LayoutRect,
4506 fill: Color,
4507 stroke_value: Option<Stroke>,
4508 radius: f32,
4509) {
4510 add_positioned_paint(
4511 cx,
4512 root,
4513 rect,
4514 fission_ir::Op::Paint(PaintOp::DrawRect {
4515 fill: Some(Fill::Solid(fill)),
4516 stroke: stroke_value,
4517 corner_radius: radius,
4518 shadow: None,
4519 }),
4520 );
4521}
4522
4523fn add_text(
4524 cx: &mut fission_core::internal::InternalLoweringCx,
4525 root: &mut fission_core::internal::InternalIrBuilder,
4526 text: &str,
4527 size: f32,
4528 color: Color,
4529 left: f32,
4530 top: f32,
4531 width: f32,
4532 height: f32,
4533) {
4534 add_positioned_paint(
4535 cx,
4536 root,
4537 LayoutRect::new(left, top, width.max(1.0), height.max(1.0)),
4538 fission_ir::Op::Paint(PaintOp::DrawText {
4539 text: text.to_string(),
4540 size,
4541 color,
4542 underline: false,
4543 wrap: false,
4544 caret_index: None,
4545 caret_color: None,
4546 caret_width: None,
4547 caret_height: None,
4548 caret_radius: None,
4549 paragraph_style: None,
4550 }),
4551 );
4552}
4553
4554fn add_positioned_paint(
4555 cx: &mut fission_core::internal::InternalLoweringCx,
4556 root: &mut fission_core::internal::InternalIrBuilder,
4557 rect: LayoutRect,
4558 op: fission_ir::Op,
4559) {
4560 let paint_id = cx.next_node_id();
4561 let mut pos = fission_core::internal::InternalIrBuilder::new(
4562 cx.next_node_id(),
4563 fission_ir::Op::Layout(LayoutOp::Positioned {
4564 left: Some(rect.x()),
4565 top: Some(rect.y()),
4566 right: None,
4567 bottom: None,
4568 width: Some(rect.width()),
4569 height: Some(rect.height()),
4570 }),
4571 );
4572 pos.add_child(cx.insert_node(paint_id, op, vec![]));
4573 root.add_child(pos.build(cx));
4574}
4575
4576fn add_path(
4577 cx: &mut fission_core::internal::InternalLoweringCx,
4578 root: &mut fission_core::internal::InternalIrBuilder,
4579 path: &str,
4580 fill: Option<Fill>,
4581 stroke_value: Option<Stroke>,
4582) {
4583 let id = cx.next_node_id();
4584 root.add_child(cx.insert_node(
4585 id,
4586 fission_ir::Op::Paint(PaintOp::DrawPath {
4587 path: path.to_string(),
4588 fill,
4589 stroke: stroke_value,
4590 }),
4591 vec![],
4592 ));
4593}
4594
4595fn stroke(color: Color, width: f32) -> Stroke {
4596 Stroke {
4597 fill: Fill::Solid(color),
4598 width,
4599 dash_array: None,
4600 line_cap: LineCap::Round,
4601 line_join: LineJoin::Round,
4602 }
4603}
4604
4605fn format_tick(value: f32) -> String {
4606 if value.abs() >= 1000.0 {
4607 format!("{:.1}k", value / 1000.0)
4608 } else if value.fract().abs() < 0.001 {
4609 format!("{:.0}", value)
4610 } else {
4611 format!("{:.1}", value)
4612 }
4613}
4614
4615fn color(r: u8, g: u8, b: u8, a: u8) -> Color {
4616 Color { r, g, b, a }
4617}