Skip to main content

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}