orbital_charts/shared/plots/
line_plot.rs1use std::collections::HashMap;
4
5use leptos::prelude::*;
6
7use crate::context::{
8 is_series_visible, use_chart_context, use_hidden_series, use_x_scale, use_y_scale,
9 ChartInteractionContext,
10};
11use crate::engine::{
12 build_line_path, data_fingerprint, projected_for_plot_type, resolve_series_color,
13 resolve_stack_config, stack_series, PlotPoint,
14};
15use crate::shared::marks::LineStroke;
16use crate::{ChartScale, ChartType, CurveType, SeriesDef, StackOffset};
17
18const LAZY_MARKER_THRESHOLD: usize = 500;
20
21fn scale_y(scale: &ChartScale, value: f64) -> f64 {
22 match scale {
23 ChartScale::Linear(l) => l.scale(value),
24 _ => value,
25 }
26}
27
28fn collect_line_markers(
29 projected: &crate::engine::ProjectedChartData,
30 series_defs: &[SeriesDef],
31 x_scale: &ChartScale,
32 y_scale: &ChartScale,
33) -> Vec<(f64, f64, String, usize)> {
34 let plot_data = projected_for_plot_type(projected, series_defs, ChartType::Line);
35 if plot_data.series.is_empty() {
36 return Vec::new();
37 }
38
39 let stack_groups: HashMap<String, String> = series_defs
40 .iter()
41 .filter_map(|s| s.stack_group.as_ref().map(|g| (s.id.clone(), g.clone())))
42 .collect();
43 let has_stack = plot_data
44 .series
45 .iter()
46 .any(|s| stack_groups.contains_key(&s.id));
47
48 let (offset, order) = if has_stack {
49 let group = stack_groups
50 .values()
51 .next()
52 .cloned()
53 .unwrap_or_else(|| "stack".into());
54 resolve_stack_config(&group, series_defs, ChartType::Line)
55 } else {
56 (StackOffset::None, crate::StackOrder::None)
57 };
58
59 let render_series = if has_stack {
60 stack_series(&plot_data.series, &stack_groups, offset, order)
61 } else {
62 plot_data.series.clone()
63 };
64
65 let total_points = render_series.iter().map(|s| s.data.len()).sum::<usize>();
66 if total_points > LAZY_MARKER_THRESHOLD {
67 return Vec::new();
68 }
69
70 let mut line_markers = Vec::new();
71 for s in render_series.iter().filter(|s| is_series_visible(&s.id)) {
72 for (i, cat) in plot_data.categories.iter().enumerate() {
73 let x = category_x(x_scale, cat, i, plot_data.categories.len());
74 let y_val = s.data.get(i).copied();
75 if let Some(y) = y_val.filter(|v| !v.is_nan()).map(|v| scale_y(y_scale, v)) {
76 line_markers.push((x, y, s.id.clone(), i));
77 }
78 }
79 }
80 line_markers
81}
82
83#[component]
85pub fn LinePlot() -> impl IntoView {
86 let ctx = use_chart_context();
87 let interaction = expect_context::<ChartInteractionContext>();
88 let hidden = use_hidden_series();
89 let skip = ctx.skip_animation;
90 let highlight_scope = ctx.highlight_scope;
91 let projected = ctx.projected.clone();
92 let series_defs = ctx.series.clone();
93 let palette = ctx.palette.clone();
94 let x_scale = use_x_scale("x");
95 let y_scale = use_y_scale("y");
96
97 let projected_for_markers = projected.clone();
98 let series_defs_for_markers = series_defs.clone();
99 let x_scale_for_markers = x_scale.clone();
100 let y_scale_for_markers = y_scale.clone();
101 let line_markers = Memo::new(move |_| {
102 let _ = hidden.get();
103 let Some(data) = projected_for_markers.as_ref() else {
104 return Vec::new();
105 };
106 collect_line_markers(
107 data,
108 &series_defs_for_markers,
109 &x_scale_for_markers,
110 &y_scale_for_markers,
111 )
112 });
113
114 Effect::new(move |_| {
115 interaction.plot_line_markers.set(line_markers.get());
116 });
117
118 view! {
119 {move || {
120 let Some(data) = projected.as_ref() else {
121 return ().into_any();
122 };
123
124 let plot_data = projected_for_plot_type(data, &series_defs, ChartType::Line);
125 if plot_data.series.is_empty() {
126 return ().into_any();
127 }
128
129 let stack_groups: HashMap<String, String> = series_defs
130 .iter()
131 .filter_map(|s| s.stack_group.as_ref().map(|g| (s.id.clone(), g.clone())))
132 .collect();
133 let has_stack = plot_data
134 .series
135 .iter()
136 .any(|s| stack_groups.contains_key(&s.id));
137
138 let (offset, order) = if has_stack {
139 let group = stack_groups
140 .values()
141 .next()
142 .cloned()
143 .unwrap_or_else(|| "stack".into());
144 resolve_stack_config(&group, &series_defs, ChartType::Line)
145 } else {
146 (StackOffset::None, crate::StackOrder::None)
147 };
148
149 let render_series = if has_stack {
150 stack_series(&plot_data.series, &stack_groups, offset, order)
151 } else {
152 plot_data.series.clone()
153 };
154
155 let total_points = render_series.iter().map(|s| s.data.len()).sum::<usize>();
156 let lazy_markers = total_points > LAZY_MARKER_THRESHOLD;
157
158 render_series
159 .iter()
160 .enumerate()
161 .filter(|(_, s)| is_series_visible(&s.id))
162 .map(|(idx, s)| {
163 let def = series_defs
164 .iter()
165 .find(|d| d.id == s.id)
166 .cloned()
167 .unwrap_or_default();
168 let color = resolve_series_color(idx, &def, &palette);
169 let curve = def.curve.unwrap_or(CurveType::Linear);
170 let connect_nulls = def.connect_nulls.unwrap_or(false);
171 let show_markers = def.show_markers.unwrap_or(false) && !lazy_markers;
172 let scope = def.highlight_scope.or(highlight_scope);
173
174 let points: Vec<PlotPoint> = plot_data
175 .categories
176 .iter()
177 .enumerate()
178 .map(|(i, cat)| {
179 let x = category_x(&x_scale, cat, i, plot_data.categories.len());
180 let y_val = s.data.get(i).copied();
181 PlotPoint {
182 x,
183 y: y_val.and_then(|v| {
184 if v.is_nan() {
185 None
186 } else {
187 Some(scale_y(&y_scale, v))
188 }
189 }),
190 }
191 })
192 .collect();
193
194 let line = build_line_path(&points, curve, connect_nulls);
195 let marker_triples: Vec<(f64, f64, usize)> = line
196 .markers
197 .iter()
198 .enumerate()
199 .map(|(i, (x, y))| (*x, *y, i))
200 .collect();
201 let d: Signal<String> = Signal::from(line.d.clone());
202 let stroke: Signal<String> = Signal::from(color.clone());
203 let key: Signal<String> =
204 Signal::from(format!("{}-{}", s.id, data_fingerprint(&s.data)));
205
206 view! {
207 <LineStroke
208 d=d
209 stroke=stroke
210 markers=marker_triples
211 series_id=s.id.clone()
212 show_markers=show_markers
213 skip_animation=skip
214 draw_key=key
215 highlight_scope=scope
216 />
217 }
218 })
219 .collect_view()
220 .into_any()
221 }}
222 }
223}
224
225fn category_x(scale: &ChartScale, category: &str, index: usize, count: usize) -> f64 {
226 match scale {
227 ChartScale::Band(b) => b.scale(category).unwrap_or_else(|| {
228 if count > 1 {
229 (b.step() * index as f64) + b.bandwidth() / 2.0
230 } else {
231 b.bandwidth() / 2.0
232 }
233 }),
234 ChartScale::Linear(l) => l.scale(index as f64),
235 }
236}