Skip to main content

teksilo_data/
chart_source.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Shared chart input for a complete model or a live tail projection.
5
6use crate::{ChartChange, ChartModel, ChartWindow, SeriesId, SeriesView};
7use teksilo_core::color_prop::ColorProp;
8use teksilo_core::{ObserverHandle, Signal};
9
10/// A reactive chart input. Conversions retain shared handles and copy no points.
11///
12/// Window point indices (including hover and selection indices) are local to
13/// the visible tail. A window bounds the displayed data, not source storage.
14#[derive(Debug)]
15pub enum ChartSource<T: 'static> {
16    /// All points in the model.
17    Model(ChartModel<T>),
18    /// The last configured number of points in each series.
19    Window(ChartWindow<T>),
20}
21
22impl<T: 'static> Clone for ChartSource<T> {
23    fn clone(&self) -> Self {
24        match self {
25            Self::Model(model) => Self::Model(model.clone()),
26            Self::Window(window) => Self::Window(window.clone()),
27        }
28    }
29}
30
31impl<T: 'static> From<ChartModel<T>> for ChartSource<T> {
32    fn from(model: ChartModel<T>) -> Self {
33        Self::Model(model)
34    }
35}
36impl<T: 'static> From<ChartWindow<T>> for ChartSource<T> {
37    fn from(window: ChartWindow<T>) -> Self {
38        Self::Window(window)
39    }
40}
41
42impl<T: 'static> ChartSource<T> {
43    fn source(&self) -> ChartModel<T> {
44        match self {
45            Self::Model(model) => model.clone(),
46            Self::Window(window) => window.source(),
47        }
48    }
49
50    /// Read the visible series and borrowed point slices.
51    pub fn with_all_series<R>(&self, f: impl FnOnce(&[SeriesView<'_, T>]) -> R) -> R {
52        match self {
53            Self::Model(model) => model.with_all_series(f),
54            Self::Window(window) => window.with_all_series(f),
55        }
56    }
57
58    /// Read one visible series, using window-local point indices.
59    pub fn with_series_view<R>(
60        &self,
61        series: SeriesId,
62        f: impl FnOnce(SeriesView<'_, T>) -> R,
63    ) -> Option<R> {
64        match self {
65            Self::Model(model) => model.with_series_view(series, f),
66            Self::Window(window) => window.with_series_view(series, f),
67        }
68    }
69
70    /// Read series metadata from the source.
71    pub fn with_series<R>(
72        &self,
73        series: SeriesId,
74        f: impl FnOnce(&str, Option<&ColorProp>, bool) -> R,
75    ) -> Option<R> {
76        self.source().with_series(series, f)
77    }
78
79    /// Change source visibility, for interactive legends.
80    pub fn set_series_visible(&self, series: SeriesId, visible: bool) {
81        self.source().set_series_visible(series, visible);
82    }
83
84    /// Ordered series identifiers.
85    pub fn series_ids(&self) -> Vec<SeriesId> {
86        self.source().series_ids()
87    }
88    /// Number of series, including hidden series.
89    pub fn series_count(&self) -> usize {
90        self.source().series_count()
91    }
92    /// Series at the given display position.
93    pub fn series_id_at(&self, index: usize) -> Option<SeriesId> {
94        self.source().series_id_at(index)
95    }
96    /// Sole series, when the source has exactly one.
97    pub fn only_series(&self) -> Option<SeriesId> {
98        self.source().only_series()
99    }
100    /// Number of visible points in a series.
101    pub fn point_count(&self, series: SeriesId) -> usize {
102        self.with_series_view(series, |view| view.points.len())
103            .unwrap_or(0)
104    }
105
106    /// Reactive version for geometry and accessible data.
107    pub fn structure_version(&self) -> Signal<u64> {
108        match self {
109            Self::Model(model) => model.structure_version(),
110            Self::Window(window) => window.structure_version(),
111        }
112    }
113
114    /// Reactive version for colors and patterns.
115    pub fn style_version(&self) -> Signal<u64> {
116        match self {
117            Self::Model(model) => model.style_version(),
118            Self::Window(window) => window.style_version(),
119        }
120    }
121
122    /// Observe changes using indices relative to this input.
123    pub fn observe_changes(&self, f: impl Fn(&ChartChange) + 'static) -> ObserverHandle {
124        match self {
125            Self::Model(model) => model.observe_changes(f),
126            Self::Window(window) => window.observe_changes(f),
127        }
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn window_borrows_points_and_tracks_structure_separately_from_style() {
137        struct NonClone;
138        let model = ChartModel::new();
139        let series = model.add_series("sensor");
140        for value in 0..4 {
141            model.push_point(series, NonClone, value as f32);
142        }
143        let window = ChartWindow::new(model.clone(), 2);
144        let input = ChartSource::from(window.clone());
145        model.with_series_view(series, |original| {
146            input.with_series_view(series, |view| {
147                assert!(std::ptr::eq(&original.points[2], &view.points[0]));
148                assert_eq!(view.points.len(), 2);
149            });
150        });
151        let version = input.structure_version().get();
152        window.set_window_size(1);
153        assert!(input.structure_version().get() > version);
154        assert_eq!(input.point_count(series), 1);
155        let version = input.structure_version().get();
156        model.set_series_pattern(series, crate::SeriesPattern::Dotted);
157        assert_eq!(input.structure_version().get(), version);
158        assert!(input.style_version().get() > 0);
159        input.set_series_visible(series, false);
160        assert_eq!(
161            model.with_series(series, |_, _, visible| visible),
162            Some(false)
163        );
164        model.remove_series(series);
165        assert_eq!(input.series_count(), 0);
166        assert!(input.with_series_view(series, |_| ()).is_none());
167    }
168}