1use std::cmp::Ordering;
2use std::collections::{BTreeMap, BTreeSet};
3
4use delinea::engine::model::ChartModel;
5use delinea::engine::window::DataWindow;
6use delinea::ids::{AxisId, DatasetId, FieldId};
7use delinea::spec::AxisKind;
8use delinea::{ChartSpec, LinkEvent};
9use fret_runtime::Model;
10use fret_ui::UiHost;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct LinkAxisKey {
14 pub kind: AxisKind,
15 pub dataset: DatasetId,
16 pub field: FieldId,
17}
18
19impl Ord for LinkAxisKey {
20 fn cmp(&self, other: &Self) -> Ordering {
21 let rank = |kind: AxisKind| match kind {
22 AxisKind::X => 0u8,
23 AxisKind::Y => 1u8,
24 };
25
26 (rank(self.kind), self.dataset, self.field).cmp(&(
27 rank(other.kind),
28 other.dataset,
29 other.field,
30 ))
31 }
32}
33
34impl PartialOrd for LinkAxisKey {
35 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
36 Some(self.cmp(other))
37 }
38}
39
40#[derive(Debug, Clone, PartialEq)]
41pub struct AxisPointerLinkAnchor {
42 pub axis: LinkAxisKey,
43 pub value: f64,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq)]
47pub struct BrushSelectionLink2D {
48 pub x_axis: LinkAxisKey,
49 pub y_axis: LinkAxisKey,
50 pub x: DataWindow,
51 pub y: DataWindow,
52}
53
54#[derive(Debug, Default, Clone, Copy)]
55pub struct ChartLinkPolicy {
56 pub brush: bool,
57 pub axis_pointer: bool,
58 pub domain_windows: bool,
59}
60
61#[derive(Debug, Clone)]
62pub struct ChartLinkRouter {
63 axis_to_key: BTreeMap<AxisId, LinkAxisKey>,
64 key_to_axis: BTreeMap<LinkAxisKey, AxisId>,
65}
66
67impl ChartLinkRouter {
68 pub fn with_explicit_axis_map(mut self, map: BTreeMap<AxisId, LinkAxisKey>) -> Self {
69 self.apply_explicit_axis_map(map);
70 self
71 }
72
73 fn apply_explicit_axis_map(&mut self, map: BTreeMap<AxisId, LinkAxisKey>) {
74 let mut explicit_key_to_axis: BTreeMap<LinkAxisKey, Option<AxisId>> = BTreeMap::new();
75
76 for (axis, key) in map {
77 self.axis_to_key.insert(axis, key);
78
79 match explicit_key_to_axis.get(&key).copied().flatten() {
80 None => {
81 explicit_key_to_axis.insert(key, Some(axis));
82 }
83 Some(existing) if existing == axis => {}
84 Some(_) => {
85 explicit_key_to_axis.insert(key, None);
86 }
87 }
88 }
89
90 for (key, axis) in explicit_key_to_axis {
91 match axis {
92 Some(axis) => {
93 self.key_to_axis.insert(key, axis);
94 }
95 None => {
96 self.key_to_axis.remove(&key);
97 }
98 }
99 }
100 }
101
102 pub fn from_spec(spec: &ChartSpec) -> Self {
103 let mut axis_kind_by_id: BTreeMap<AxisId, AxisKind> = BTreeMap::new();
104 for axis in &spec.axes {
105 axis_kind_by_id.insert(axis.id, axis.kind);
106 }
107
108 let mut axis_to_pairs: BTreeMap<AxisId, BTreeSet<(DatasetId, FieldId)>> = BTreeMap::new();
109 for s in &spec.series {
110 axis_to_pairs
111 .entry(s.x_axis)
112 .or_default()
113 .insert((s.dataset, s.encode.x));
114 axis_to_pairs
115 .entry(s.y_axis)
116 .or_default()
117 .insert((s.dataset, s.encode.y));
118 }
119
120 let mut axis_to_key: BTreeMap<AxisId, LinkAxisKey> = BTreeMap::new();
121 let mut key_to_axis: BTreeMap<LinkAxisKey, AxisId> = BTreeMap::new();
122 for (axis, pairs) in axis_to_pairs {
123 let Some(kind) = axis_kind_by_id.get(&axis).copied() else {
124 continue;
125 };
126 if pairs.len() != 1 {
127 continue;
128 }
129 let (dataset, field) = pairs.into_iter().next().unwrap();
130 let key = LinkAxisKey {
131 kind,
132 dataset,
133 field,
134 };
135 axis_to_key.insert(axis, key);
136
137 match key_to_axis.get(&key).copied() {
138 None => {
139 key_to_axis.insert(key, axis);
140 }
141 Some(_) => {
142 key_to_axis.remove(&key);
144 }
145 }
146 }
147
148 Self {
149 axis_to_key,
150 key_to_axis,
151 }
152 }
153
154 pub fn from_model(model: &ChartModel) -> Self {
155 let mut axis_to_pairs: BTreeMap<AxisId, BTreeSet<(DatasetId, FieldId)>> = BTreeMap::new();
156 for s in model.series.values() {
157 axis_to_pairs
158 .entry(s.x_axis)
159 .or_default()
160 .insert((s.dataset, s.encode.x));
161 axis_to_pairs
162 .entry(s.y_axis)
163 .or_default()
164 .insert((s.dataset, s.encode.y));
165 }
166
167 let mut axis_to_key: BTreeMap<AxisId, LinkAxisKey> = BTreeMap::new();
168 let mut key_to_axis: BTreeMap<LinkAxisKey, AxisId> = BTreeMap::new();
169 for (axis, pairs) in axis_to_pairs {
170 let Some(kind) = model.axes.get(&axis).map(|a| a.kind) else {
171 continue;
172 };
173 if pairs.len() != 1 {
174 continue;
175 }
176 let (dataset, field) = pairs.into_iter().next().unwrap();
177 let key = LinkAxisKey {
178 kind,
179 dataset,
180 field,
181 };
182 axis_to_key.insert(axis, key);
183
184 match key_to_axis.get(&key).copied() {
185 None => {
186 key_to_axis.insert(key, axis);
187 }
188 Some(_) => {
189 key_to_axis.remove(&key);
190 }
191 }
192 }
193
194 Self {
195 axis_to_key,
196 key_to_axis,
197 }
198 }
199
200 pub fn axis_key(&self, axis: AxisId) -> Option<LinkAxisKey> {
201 self.axis_to_key.get(&axis).copied()
202 }
203
204 pub fn axis_for_key(&self, key: LinkAxisKey) -> Option<AxisId> {
205 self.key_to_axis.get(&key).copied()
206 }
207}
208
209#[derive(Debug, Clone)]
210pub struct LinkedChartMember {
211 pub router: ChartLinkRouter,
212 pub output: Model<crate::retained::ChartCanvasOutput>,
213}
214
215#[derive(Debug, Clone)]
216struct LinkedChartMemberMemory {
217 last_link_events_revision: u64,
218 ignore_next_link_events_revision: bool,
219 last_domain_windows_by_key: BTreeMap<LinkAxisKey, Option<DataWindow>>,
220 ignore_next_domain_windows: bool,
221}
222
223#[derive(Debug)]
224pub struct LinkedChartGroup {
225 policy: ChartLinkPolicy,
226 members: Vec<LinkedChartMember>,
227 memory: Vec<LinkedChartMemberMemory>,
228 brush: Model<Option<BrushSelectionLink2D>>,
229 axis_pointer: Model<Option<AxisPointerLinkAnchor>>,
230 domain_windows: Model<BTreeMap<LinkAxisKey, Option<DataWindow>>>,
231}
232
233impl LinkedChartGroup {
234 pub fn new(
235 policy: ChartLinkPolicy,
236 brush: Model<Option<BrushSelectionLink2D>>,
237 axis_pointer: Model<Option<AxisPointerLinkAnchor>>,
238 domain_windows: Model<BTreeMap<LinkAxisKey, Option<DataWindow>>>,
239 ) -> Self {
240 Self {
241 policy,
242 members: Vec::new(),
243 memory: Vec::new(),
244 brush,
245 axis_pointer,
246 domain_windows,
247 }
248 }
249
250 pub fn push(&mut self, member: LinkedChartMember) -> &mut Self {
251 self.members.push(member);
252 self.memory.push(LinkedChartMemberMemory {
253 last_link_events_revision: 0,
254 ignore_next_link_events_revision: false,
255 last_domain_windows_by_key: BTreeMap::new(),
256 ignore_next_domain_windows: false,
257 });
258 self
259 }
260
261 pub fn tick<H: UiHost>(&mut self, app: &mut H) -> bool {
262 if self.members.len() <= 1 {
263 return false;
264 }
265
266 let Ok(shared_domain_windows) = self.domain_windows.read(app, |_app, w| w.clone()) else {
267 return false;
268 };
269
270 let mut outputs: Vec<Option<crate::retained::ChartCanvasOutput>> =
271 Vec::with_capacity(self.members.len());
272 for m in &self.members {
273 let out = m.output.read(app, |_app, o| o.clone()).ok();
274 outputs.push(out);
275 }
276
277 for i in 0..self.members.len() {
279 let Some(out) = outputs.get(i).and_then(|o| o.clone()) else {
280 continue;
281 };
282 let mem = &mut self.memory[i];
283 if mem.ignore_next_link_events_revision
284 && out.link_events_revision != mem.last_link_events_revision
285 {
286 mem.last_link_events_revision = out.link_events_revision;
287 mem.ignore_next_link_events_revision = false;
288 }
289 if mem.ignore_next_domain_windows
290 && out.snapshot.domain_windows_by_key != mem.last_domain_windows_by_key
291 {
292 mem.last_domain_windows_by_key = out.snapshot.domain_windows_by_key.clone();
293 mem.ignore_next_domain_windows = false;
294 }
295 }
296
297 let mut source_index: Option<usize> = None;
302 let mut source_events: Option<Vec<LinkEvent>> = None;
303 let mut source_domain_window_updates: Vec<(LinkAxisKey, Option<DataWindow>)> = Vec::new();
304 let mut source_score: i32 = -1;
305
306 for i in 0..self.members.len() {
307 let Some(out) = outputs.get(i).and_then(|o| o.clone()) else {
308 continue;
309 };
310 let mem = &self.memory[i];
311
312 let link_events_changed = !mem.ignore_next_link_events_revision
313 && out.link_events_revision != mem.last_link_events_revision;
314
315 let domain_windows_changed = self.policy.domain_windows
316 && !mem.ignore_next_domain_windows
317 && out.snapshot.domain_windows_by_key != mem.last_domain_windows_by_key;
318
319 if !link_events_changed && !domain_windows_changed {
320 continue;
321 }
322
323 let events = if link_events_changed {
324 out.snapshot.link_events.clone()
325 } else {
326 Vec::new()
327 };
328
329 let mut domain_window_updates: Vec<(LinkAxisKey, Option<DataWindow>)> = Vec::new();
330 if domain_windows_changed {
331 let mut keys: BTreeSet<LinkAxisKey> = BTreeSet::new();
332 keys.extend(mem.last_domain_windows_by_key.keys().copied());
333 keys.extend(out.snapshot.domain_windows_by_key.keys().copied());
334 for key in keys {
335 let prev = mem
336 .last_domain_windows_by_key
337 .get(&key)
338 .copied()
339 .unwrap_or(None);
340 let next = out
341 .snapshot
342 .domain_windows_by_key
343 .get(&key)
344 .copied()
345 .unwrap_or(None);
346 if prev != next {
347 domain_window_updates.push((key, next));
348 }
349 }
350 }
351
352 let mut score = 0i32;
353 if events
354 .iter()
355 .any(|e| matches!(e, LinkEvent::AxisPointerChanged { anchor: Some(_) }))
356 {
357 score += 10;
358 }
359 if events
360 .iter()
361 .any(|e| matches!(e, LinkEvent::DomainWindowChanged { .. }))
362 {
363 score += 5;
364 }
365 if !domain_window_updates.is_empty() {
366 score += 4;
367 score += (domain_window_updates.len().min(8)) as i32;
368 }
369 if events
370 .iter()
371 .any(|e| matches!(e, LinkEvent::BrushSelectionChanged { .. }))
372 {
373 score += 3;
374 }
375
376 if score > source_score {
377 source_score = score;
378 source_index = Some(i);
379 source_events = Some(events.clone());
380 source_domain_window_updates = domain_window_updates;
381 }
382 }
383
384 let Some(source_index) = source_index else {
385 return false;
386 };
387 let source_events = source_events.unwrap_or_default();
388
389 if let Some(out) = outputs.get(source_index).and_then(|o| o.clone()) {
391 self.memory[source_index].last_link_events_revision = out.link_events_revision;
392 self.memory[source_index].last_domain_windows_by_key =
393 out.snapshot.domain_windows_by_key.clone();
394 }
395
396 let source_router = &self.members[source_index].router;
397
398 let mut changed = false;
399
400 if self.policy.axis_pointer {
401 if let Some(anchor) = source_events.iter().rev().find_map(|e| match e {
402 LinkEvent::AxisPointerChanged { anchor } => anchor.as_ref(),
403 _ => None,
404 }) {
405 let next = source_router
406 .axis_key(anchor.axis)
407 .map(|axis| AxisPointerLinkAnchor {
408 axis,
409 value: anchor.value,
410 });
411
412 let Ok(current) = self.axis_pointer.read(app, |_app, a| a.clone()) else {
413 return false;
414 };
415 if current != next {
416 let _ = self.axis_pointer.update(app, |a, _cx| {
417 *a = next;
418 });
419 changed = true;
420 }
421 } else if source_events
422 .iter()
423 .any(|e| matches!(e, LinkEvent::AxisPointerChanged { anchor: None }))
424 {
425 let Ok(current) = self.axis_pointer.read(app, |_app, a| a.clone()) else {
426 return false;
427 };
428 if current.is_some() {
429 let _ = self.axis_pointer.update(app, |a, _cx| {
430 *a = None;
431 });
432 changed = true;
433 }
434 }
435 }
436
437 if self.policy.domain_windows {
438 let mut updates = source_domain_window_updates;
439
440 if updates.is_empty() {
441 updates = source_events
442 .iter()
443 .filter_map(|e| match e {
444 LinkEvent::DomainWindowChanged { axis, window } => {
445 source_router.axis_key(*axis).map(|key| (key, *window))
446 }
447 _ => None,
448 })
449 .collect();
450 }
451
452 if !updates.is_empty() {
453 let current = &shared_domain_windows;
454 let mut next = current.clone();
455 for (key, window) in updates {
456 next.insert(key, window);
457 }
458 if &next != current {
459 let _ = self.domain_windows.update(app, |w, _cx| {
460 *w = next;
461 });
462 changed = true;
463 }
464 }
465 }
466
467 if self.policy.brush
468 && let Some(selection) = source_events.iter().rev().find_map(|e| match e {
469 LinkEvent::BrushSelectionChanged { selection } => Some(*selection),
470 _ => None,
471 })
472 {
473 let next = selection.and_then(|sel| {
474 let x_key = source_router.axis_key(sel.x_axis)?;
475 let y_key = source_router.axis_key(sel.y_axis)?;
476 Some(BrushSelectionLink2D {
477 x_axis: x_key,
478 y_axis: y_key,
479 x: sel.x,
480 y: sel.y,
481 })
482 });
483
484 let Ok(current) = self.brush.read(app, |_app, s| *s) else {
485 return false;
486 };
487 if current != next {
488 let _ = self.brush.update(app, |s, _cx| {
489 *s = next;
490 });
491 changed = true;
492 }
493 }
494
495 if !changed {
496 return false;
497 }
498
499 for i in 0..self.members.len() {
501 if i == source_index {
502 continue;
503 }
504 self.memory[i].ignore_next_link_events_revision = true;
505 self.memory[i].ignore_next_domain_windows = true;
506 }
507
508 true
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515 use delinea::spec::{
516 AxisSpec, ChartSpec, DatasetSpec, FieldSpec, GridSpec, SeriesEncode, SeriesKind, SeriesSpec,
517 };
518
519 fn spec_with_ambiguous_x_key() -> (ChartSpec, LinkAxisKey, AxisId, AxisId) {
520 let chart_id = delinea::ids::ChartId::new(1);
521 let dataset = DatasetId::new(1);
522 let grid = delinea::ids::GridId::new(1);
523 let x1 = AxisId::new(1);
524 let x2 = AxisId::new(2);
525 let y = AxisId::new(3);
526 let x_field = FieldId::new(1);
527 let y_field = FieldId::new(2);
528
529 let key = LinkAxisKey {
530 kind: AxisKind::X,
531 dataset,
532 field: x_field,
533 };
534
535 let spec = ChartSpec {
536 id: chart_id,
537 viewport: None,
538 datasets: vec![DatasetSpec {
539 id: dataset,
540 fields: vec![
541 FieldSpec {
542 id: x_field,
543 column: 0,
544 },
545 FieldSpec {
546 id: y_field,
547 column: 1,
548 },
549 ],
550
551 from: None,
552 transforms: Vec::new(),
553 }],
554 grids: vec![GridSpec { id: grid }],
555 axes: vec![
556 AxisSpec {
557 id: x1,
558 name: None,
559 kind: AxisKind::X,
560 grid,
561 position: None,
562 scale: Default::default(),
563 range: None,
564 },
565 AxisSpec {
566 id: x2,
567 name: None,
568 kind: AxisKind::X,
569 grid,
570 position: None,
571 scale: Default::default(),
572 range: None,
573 },
574 AxisSpec {
575 id: y,
576 name: None,
577 kind: AxisKind::Y,
578 grid,
579 position: None,
580 scale: Default::default(),
581 range: None,
582 },
583 ],
584 data_zoom_x: vec![],
585 data_zoom_y: vec![],
586 tooltip: None,
587 axis_pointer: None,
588 visual_maps: vec![],
589 series: vec![
590 SeriesSpec {
591 id: delinea::ids::SeriesId::new(1),
592 name: None,
593 kind: SeriesKind::Line,
594 dataset,
595 encode: SeriesEncode {
596 x: x_field,
597 y: y_field,
598 y2: None,
599 },
600 x_axis: x1,
601 y_axis: y,
602 stack: None,
603 stack_strategy: Default::default(),
604 bar_layout: Default::default(),
605 area_baseline: None,
606 lod: None,
607 },
608 SeriesSpec {
609 id: delinea::ids::SeriesId::new(2),
610 name: None,
611 kind: SeriesKind::Line,
612 dataset,
613 encode: SeriesEncode {
614 x: x_field,
615 y: y_field,
616 y2: None,
617 },
618 x_axis: x2,
619 y_axis: y,
620 stack: None,
621 stack_strategy: Default::default(),
622 bar_layout: Default::default(),
623 area_baseline: None,
624 lod: None,
625 },
626 ],
627 };
628
629 (spec, key, x1, x2)
630 }
631
632 #[test]
633 fn explicit_axis_map_can_restore_unique_reverse_mapping() {
634 let (spec, key, x1, _x2) = spec_with_ambiguous_x_key();
635 let router = ChartLinkRouter::from_spec(&spec);
636 assert_eq!(router.axis_for_key(key), None);
637
638 let mut explicit = BTreeMap::new();
639 explicit.insert(x1, key);
640 let router = ChartLinkRouter::from_spec(&spec).with_explicit_axis_map(explicit);
641 assert_eq!(router.axis_for_key(key), Some(x1));
642 }
643
644 #[test]
645 fn duplicate_explicit_key_assignment_disables_reverse_mapping() {
646 let (spec, key, x1, x2) = spec_with_ambiguous_x_key();
647 let mut explicit = BTreeMap::new();
648 explicit.insert(x1, key);
649 explicit.insert(x2, key);
650
651 let router = ChartLinkRouter::from_spec(&spec).with_explicit_axis_map(explicit);
652 assert_eq!(router.axis_for_key(key), None);
653 }
654}