teksilo_data/chart_change.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! ChartChange — change notifications and stable series identifiers for chart collections.
5//!
6//! [`SeriesId`] is an opaque, stable handle for a series in a [`crate::ChartModel`].
7//! Because `ChartModel` is backed by a slotmap, `SeriesId` values survive arbitrary
8//! series insertions, removals, and reorders — only removing the series itself
9//! invalidates it. [`ChartChange`] describes exactly what mutated (at the series
10//! level or the point level within a series) so that projections
11//! (`ChartWindow`, `ChartAggregate`) and consumers (`ChartSelection`) can refresh
12//! or adjust incrementally instead of rebuilding from scratch.
13//!
14//! Consumers typically receive `ChartChange` values through an observer
15//! registered via [`crate::ChartModel::observe_changes`], which fires
16//! synchronously (before the registering call returns) after each mutation.
17//!
18//! ```ignore
19//! // ChartModel::observe_changes returns an ObserverHandle whose drop
20//! // unregisters the callback — keep it alive for the observer's lifetime.
21//! use teksilo_data::{ChartModel, ChartChange};
22//! let model: ChartModel<String> = ChartModel::new();
23//! let _handle = model.observe_changes(|change| {
24//! println!("{change:?}");
25//! });
26//! model.add_series("Revenue");
27//! // prints: SeriesInserted { index: 0, series: SeriesId(...) }
28//! ```
29
30/// Opaque identifier for a series in a `ChartModel`.
31///
32/// `SeriesId` values are stable across mutations — inserting or removing
33/// other series does not invalidate existing `SeriesId` handles (they are
34/// SlotMap keys).
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub struct SeriesId(slotmap::DefaultKey);
37
38impl SeriesId {
39 pub(crate) fn from_key(key: slotmap::DefaultKey) -> Self {
40 Self(key)
41 }
42
43 pub(crate) fn key(self) -> slotmap::DefaultKey {
44 self.0
45 }
46}
47
48/// Describes a mutation to a chart's series or point data. Emitted by
49/// `ChartModel<T>` automatically.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum ChartChange {
52 /// A series was inserted at the given index.
53 SeriesInserted { index: usize, series: SeriesId },
54
55 /// A series (and all of its points) was removed.
56 SeriesRemoved { series: SeriesId },
57
58 /// A series was moved to a new position among its siblings.
59 SeriesMoved {
60 series: SeriesId,
61 from: usize,
62 to: usize,
63 },
64
65 /// A series' display name changed.
66 SeriesRenamed { series: SeriesId },
67
68 /// A series' explicit color changed (set or cleared). The only variant
69 /// that bumps [`crate::ChartModel::style_version`] rather than
70 /// [`crate::ChartModel::structure_version`].
71 SeriesColorChanged { series: SeriesId },
72
73 /// A series' explicit [`SeriesPattern`](crate::SeriesPattern) changed (set
74 /// or cleared). Paint-only, like
75 /// [`SeriesColorChanged`](Self::SeriesColorChanged): it bumps
76 /// [`crate::ChartModel::style_version`], not `structure_version`.
77 SeriesPatternChanged { series: SeriesId },
78
79 /// A series' visibility flag changed.
80 SeriesVisibilityChanged { series: SeriesId },
81
82 /// Points were inserted; `range` holds the indices of the newly
83 /// inserted points within `series`.
84 PointsInserted {
85 series: SeriesId,
86 range: std::ops::Range<usize>,
87 },
88
89 /// Points were removed; `range` holds the indices they occupied
90 /// *before* removal within `series`.
91 PointsRemoved {
92 series: SeriesId,
93 range: std::ops::Range<usize>,
94 },
95
96 /// A single point's data changed in place without any structural shift.
97 PointUpdated { series: SeriesId, index: usize },
98
99 /// A series' entire point list was replaced; consumers must discard
100 /// cached state for that series and rebuild it.
101 SeriesDataReplaced { series: SeriesId },
102
103 /// The entire chart was replaced. Consumers should discard all state
104 /// and rebuild.
105 Reset,
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn series_id_equality() {
114 use slotmap::SlotMap;
115 let mut sm: SlotMap<slotmap::DefaultKey, ()> = SlotMap::new();
116 let k1 = sm.insert(());
117 let k2 = sm.insert(());
118 let id1 = SeriesId::from_key(k1);
119 let id1_clone = SeriesId::from_key(k1);
120 let id2 = SeriesId::from_key(k2);
121
122 assert_eq!(id1, id1_clone);
123 assert_ne!(id1, id2);
124 }
125}