1use std::collections::HashMap;
4
5use crate::engine::ProjectedSeries;
6use crate::{ChartType, SeriesDef, StackOffset, StackOrder};
7
8pub fn is_composition_mode(series_defs: &[SeriesDef]) -> bool {
10 series_defs.iter().any(|s| s.chart_type.is_some())
11}
12
13pub fn stack_groups_from_series(series_defs: &[SeriesDef]) -> HashMap<String, String> {
15 series_defs
16 .iter()
17 .filter_map(|s| s.stack_group.as_ref().map(|g| (s.id.clone(), g.clone())))
18 .collect()
19}
20
21pub fn resolve_stack_config(
23 group: &str,
24 series_defs: &[SeriesDef],
25 default_chart_type: ChartType,
26) -> (StackOffset, StackOrder) {
27 let members: Vec<_> = series_defs
28 .iter()
29 .filter(|s| s.stack_group.as_deref() == Some(group))
30 .collect();
31 let offset = members
32 .iter()
33 .find_map(|s| s.stack_offset)
34 .unwrap_or(match default_chart_type {
35 ChartType::Bar => StackOffset::Diverging,
36 _ => StackOffset::None,
37 });
38 let order = members
39 .iter()
40 .find_map(|s| s.stack_order)
41 .unwrap_or(StackOrder::None);
42 (offset, order)
43}
44
45pub fn resolve_chart_stack_offset(
47 series_defs: &[SeriesDef],
48 default_chart_type: ChartType,
49) -> StackOffset {
50 let mut groups: Vec<&str> = series_defs
51 .iter()
52 .filter_map(|s| s.stack_group.as_deref())
53 .collect();
54 groups.sort_unstable();
55 groups.dedup();
56 groups
57 .first()
58 .map(|group| resolve_stack_config(group, series_defs, default_chart_type).0)
59 .unwrap_or(StackOffset::None)
60}
61
62pub fn apply_stack_order(
64 indices: &[usize],
65 series: &[ProjectedSeries],
66 order: StackOrder,
67) -> Vec<usize> {
68 match order {
69 StackOrder::None => indices.to_vec(),
70 StackOrder::Reverse => indices.iter().rev().copied().collect(),
71 StackOrder::Appearance => {
72 let mut ordered = indices.to_vec();
73 ordered.sort_by_key(|&idx| {
74 series[idx]
75 .data
76 .iter()
77 .enumerate()
78 .filter_map(|(row, &value)| value.is_finite().then_some((value, row)))
79 .max_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal))
80 .map(|(_, row)| row)
81 .unwrap_or(usize::MAX)
82 });
83 ordered
84 }
85 StackOrder::Ascending => {
86 let mut ordered = indices.to_vec();
87 ordered.sort_by(|&a, &b| {
88 total_abs(&series[a].data)
89 .partial_cmp(&total_abs(&series[b].data))
90 .unwrap_or(std::cmp::Ordering::Equal)
91 });
92 ordered
93 }
94 StackOrder::Descending => {
95 let mut ordered = indices.to_vec();
96 ordered.sort_by(|&a, &b| {
97 total_abs(&series[b].data)
98 .partial_cmp(&total_abs(&series[a].data))
99 .unwrap_or(std::cmp::Ordering::Equal)
100 });
101 ordered
102 }
103 }
104}
105
106fn total_abs(data: &[f64]) -> f64 {
107 data.iter().filter(|v| v.is_finite()).map(|v| v.abs()).sum()
108}
109
110pub fn stack_series(
112 series: &[ProjectedSeries],
113 stack_groups: &HashMap<String, String>,
114 offset: StackOffset,
115 order: StackOrder,
116) -> Vec<ProjectedSeries> {
117 if series.is_empty() {
118 return Vec::new();
119 }
120
121 let row_count = series.first().map(|s| s.data.len()).unwrap_or(0);
122 let mut groups: HashMap<String, Vec<usize>> = HashMap::new();
123 for (idx, s) in series.iter().enumerate() {
124 if let Some(group) = stack_groups.get(&s.id) {
125 groups.entry(group.clone()).or_default().push(idx);
126 }
127 }
128
129 let mut result: Vec<ProjectedSeries> = series
130 .iter()
131 .map(|s| ProjectedSeries {
132 id: s.id.clone(),
133 label: s.label.clone(),
134 data: if stack_groups.contains_key(&s.id) {
135 vec![0.0; row_count]
136 } else {
137 s.data.clone()
138 },
139 })
140 .collect();
141
142 for indices in groups.values() {
143 let ordered = apply_stack_order(indices, series, order);
144 for row in 0..row_count {
145 match offset {
146 StackOffset::Diverging => stack_row_diverging(&ordered, series, &mut result, row),
147 StackOffset::Expand => stack_row_expand(&ordered, series, &mut result, row),
148 StackOffset::None => stack_row_none(&ordered, series, &mut result, row),
149 }
150 }
151 }
152
153 result
154}
155
156fn stack_row_none(
157 ordered: &[usize],
158 series: &[ProjectedSeries],
159 result: &mut [ProjectedSeries],
160 row: usize,
161) {
162 let mut cumulative = 0.0;
163 for &idx in ordered {
164 let raw = series[idx].data.get(row).copied().unwrap_or(0.0);
165 cumulative += raw;
166 result[idx].data[row] = cumulative;
167 }
168}
169
170fn stack_row_expand(
171 ordered: &[usize],
172 series: &[ProjectedSeries],
173 result: &mut [ProjectedSeries],
174 row: usize,
175) {
176 let row_total: f64 = ordered
177 .iter()
178 .map(|&idx| series[idx].data.get(row).copied().unwrap_or(0.0))
179 .sum();
180 if row_total.abs() <= f64::EPSILON {
181 for &idx in ordered {
182 result[idx].data[row] = 0.0;
183 }
184 return;
185 }
186 let mut cumulative = 0.0;
187 for &idx in ordered {
188 let raw = series[idx].data.get(row).copied().unwrap_or(0.0);
189 cumulative += raw;
190 result[idx].data[row] = cumulative / row_total;
191 }
192}
193
194fn stack_row_diverging(
195 ordered: &[usize],
196 series: &[ProjectedSeries],
197 result: &mut [ProjectedSeries],
198 row: usize,
199) {
200 let mut pos_top = 0.0;
201 let mut neg_ceiling = 0.0;
202 for &idx in ordered {
203 let raw = series[idx].data.get(row).copied().unwrap_or(0.0);
204 if raw >= 0.0 {
205 pos_top += raw;
206 result[idx].data[row] = pos_top;
207 } else {
208 let y1 = neg_ceiling;
209 neg_ceiling += raw;
210 result[idx].data[row] = y1;
211 }
212 }
213}
214
215pub fn stack_segment_bottom(
217 raw_series: &[ProjectedSeries],
218 stacked: &[ProjectedSeries],
219 stack_groups: &HashMap<String, String>,
220 _series_defs: &[SeriesDef],
221 series_id: &str,
222 row: usize,
223 offset: StackOffset,
224 order: StackOrder,
225) -> f64 {
226 let Some(group) = stack_groups.get(series_id) else {
227 return 0.0;
228 };
229
230 let indices: Vec<usize> = stacked
231 .iter()
232 .enumerate()
233 .filter_map(|(idx, s)| (stack_groups.get(&s.id) == Some(group)).then_some(idx))
234 .collect();
235 let ordered = apply_stack_order(&indices, raw_series, order);
236
237 let raw = raw_series
238 .iter()
239 .find(|s| s.id == series_id)
240 .and_then(|s| s.data.get(row))
241 .copied()
242 .unwrap_or(0.0);
243
244 match offset {
245 StackOffset::Diverging => {
246 if raw >= 0.0 {
247 let mut bottom = 0.0;
248 for &idx in &ordered {
249 if raw_series[idx].id == series_id {
250 break;
251 }
252 let prior_raw = raw_series[idx].data.get(row).copied().unwrap_or(0.0);
253 if prior_raw >= 0.0 {
254 bottom = stacked[idx].data.get(row).copied().unwrap_or(bottom);
255 }
256 }
257 bottom
258 } else {
259 stacked
260 .iter()
261 .find(|s| s.id == series_id)
262 .and_then(|s| s.data.get(row))
263 .copied()
264 .unwrap_or(0.0)
265 + raw
266 }
267 }
268 _ => {
269 let mut bottom = 0.0;
270 for s in stacked {
271 if stack_groups.get(&s.id) != Some(group) {
272 continue;
273 }
274 if s.id == series_id {
275 break;
276 }
277 bottom = s.data.get(row).copied().unwrap_or(bottom);
278 }
279 bottom
280 }
281 }
282}
283
284pub fn stack_segment_bottom_for_series(
286 raw_series: &[ProjectedSeries],
287 stacked: &[ProjectedSeries],
288 stack_groups: &HashMap<String, String>,
289 series_defs: &[SeriesDef],
290 series_id: &str,
291 row: usize,
292) -> f64 {
293 let Some(group) = stack_groups.get(series_id) else {
294 return 0.0;
295 };
296 let chart_type = series_defs
297 .iter()
298 .find(|s| s.id == series_id)
299 .and_then(|s| s.chart_type)
300 .unwrap_or(ChartType::Bar);
301 let (offset, order) = resolve_stack_config(group, series_defs, chart_type);
302 stack_segment_bottom(
303 raw_series,
304 stacked,
305 stack_groups,
306 series_defs,
307 series_id,
308 row,
309 offset,
310 order,
311 )
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 fn sample() -> (Vec<ProjectedSeries>, HashMap<String, String>) {
319 let series = vec![
320 ProjectedSeries {
321 id: "a".into(),
322 label: "A".into(),
323 data: vec![10.0, 20.0],
324 },
325 ProjectedSeries {
326 id: "b".into(),
327 label: "B".into(),
328 data: vec![5.0, 10.0],
329 },
330 ];
331 let groups = HashMap::from([("a".into(), "g".into()), ("b".into(), "g".into())]);
332 (series, groups)
333 }
334
335 #[test]
336 fn stack_cumulative_values() {
337 let (series, groups) = sample();
338 let stacked = stack_series(&series, &groups, StackOffset::None, StackOrder::None);
339 assert_eq!(stacked[0].data, vec![10.0, 20.0]);
340 assert_eq!(stacked[1].data, vec![15.0, 30.0]);
341 }
342
343 #[test]
344 fn expand_normalizes_to_one() {
345 let (series, groups) = sample();
346 let stacked = stack_series(&series, &groups, StackOffset::Expand, StackOrder::None);
347 assert!((stacked[1].data[0] - 1.0).abs() < f64::EPSILON);
348 let seg_a = stacked[0].data[0]
349 - stack_segment_bottom(
350 &series,
351 &stacked,
352 &groups,
353 &[],
354 "a",
355 0,
356 StackOffset::Expand,
357 StackOrder::None,
358 );
359 let seg_b = stacked[1].data[0]
360 - stack_segment_bottom(
361 &series,
362 &stacked,
363 &groups,
364 &[],
365 "b",
366 0,
367 StackOffset::Expand,
368 StackOrder::None,
369 );
370 assert!((seg_a + seg_b - 1.0).abs() < f64::EPSILON);
371 }
372
373 #[test]
374 fn diverging_splits_signs() {
375 let series = vec![
376 ProjectedSeries {
377 id: "pos".into(),
378 label: "Pos".into(),
379 data: vec![10.0],
380 },
381 ProjectedSeries {
382 id: "neg".into(),
383 label: "Neg".into(),
384 data: vec![-5.0],
385 },
386 ];
387 let groups = HashMap::from([("pos".into(), "g".into()), ("neg".into(), "g".into())]);
388 let stacked = stack_series(&series, &groups, StackOffset::Diverging, StackOrder::None);
389 assert_eq!(stacked[0].data, vec![10.0]);
390 assert_eq!(stacked[1].data, vec![0.0]);
391 let neg_bottom = stack_segment_bottom(
392 &series,
393 &stacked,
394 &groups,
395 &[],
396 "neg",
397 0,
398 StackOffset::Diverging,
399 StackOrder::None,
400 );
401 assert_eq!(neg_bottom, -5.0);
402 }
403
404 #[test]
405 fn stack_order_reverse() {
406 let (series, groups) = sample();
407 let stacked = stack_series(&series, &groups, StackOffset::None, StackOrder::Reverse);
408 assert_eq!(stacked[0].data, vec![15.0, 30.0]);
409 assert_eq!(stacked[1].data, vec![5.0, 10.0]);
410 }
411
412 #[test]
413 fn resolve_stack_config_bar_defaults_diverging() {
414 let defs = vec![SeriesDef {
415 id: "a".into(),
416 stack_group: Some("g".into()),
417 chart_type: Some(ChartType::Bar),
418 ..Default::default()
419 }];
420 let (offset, order) = resolve_stack_config("g", &defs, ChartType::Bar);
421 assert_eq!(offset, StackOffset::Diverging);
422 assert_eq!(order, StackOrder::None);
423 }
424}