Skip to main content

egui_charts/chart/selection/
mod.rs

1//! Generic selection state for chart elements.
2//!
3//! Provides a shared selection pattern used by series, indicators, and other
4//! selectable chart elements.
5//!
6//! The whole chart tracks a single selection through one
7//! [`SelectionState<ChartElementId>`]. The [`ChartElementId`] enum distinguishes
8//! the kinds of selectable things — a price/volume series, an overlay indicator
9//! drawn on the main chart, or a separate-pane indicator — so one selection
10//! model can express "whatever the user last clicked" regardless of which layer
11//! it lives on.
12
13use std::marker::PhantomData;
14
15/// Unique numeric identifier for a chart series.
16///
17/// Well-known constants: [`MAIN`](Self::MAIN) (candlesticks/bars) and
18/// [`VOLUME`](Self::VOLUME).
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20pub struct SeriesId(pub usize);
21
22impl SeriesId {
23    /// Main chart series (candlesticks/bars)
24    pub const MAIN: SeriesId = SeriesId(0);
25    /// Volume series
26    pub const VOLUME: SeriesId = SeriesId(1);
27
28    /// Get display name for this series
29    pub fn name(&self) -> &'static str {
30        match self.0 {
31            0 => "Main Series",
32            1 => "Volume",
33            _ => "Series",
34        }
35    }
36}
37
38/// Identifies any selectable element across the entire chart.
39///
40/// A single [`SelectionState<ChartElementId>`] tracks the one element the user
41/// has clicked, whether it is a series, an overlay indicator on the main chart,
42/// or a separate-pane indicator (RSI, MACD, …). Indicators are identified by
43/// their index in the indicator registry.
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
45pub enum ChartElementId {
46    /// A price or volume series on the main chart.
47    Series(SeriesId),
48    /// An overlay indicator drawn on the main price chart (SMA, EMA, …),
49    /// identified by its index in the indicator registry.
50    OverlayIndicator(usize),
51    /// A separate-pane indicator below the main chart (RSI, MACD, …),
52    /// identified by its index in the indicator registry.
53    PaneIndicator(usize),
54}
55
56/// Trait bound for IDs that can be used with [`SelectionState`].
57///
58/// Automatically implemented for any type that is `Copy + Eq + Debug`.
59pub trait SelectableId: Copy + Eq + std::fmt::Debug {}
60
61// Blanket implementation for types that meet the requirements
62impl<T: Copy + Eq + std::fmt::Debug> SelectableId for T {}
63
64/// Generic selection state for chart elements, parameterized by an ID type.
65///
66/// Tracks which element is selected, which is hovered, and the bar index
67/// where the selection occurred. Used by both series and indicator selection.
68#[derive(Clone, Debug)]
69pub struct SelectionState<Id: SelectableId> {
70    /// Currently selected element
71    pub selected: Option<Id>,
72    /// Currently hovered element (desktop only)
73    pub hovered: Option<Id>,
74    /// Additional selection metadata (bar index, etc.)
75    pub bar_idx: Option<usize>,
76    _phantom: PhantomData<Id>,
77}
78
79impl<Id: SelectableId> Default for SelectionState<Id> {
80    fn default() -> Self {
81        Self {
82            selected: None,
83            hovered: None,
84            bar_idx: None,
85            _phantom: PhantomData,
86        }
87    }
88}
89
90impl<Id: SelectableId> SelectionState<Id> {
91    /// Create new empty selection state.
92    pub fn new() -> Self {
93        Self::default()
94    }
95
96    /// Select an element.
97    pub fn select(&mut self, id: Id, bar_idx: Option<usize>) {
98        self.selected = Some(id);
99        self.bar_idx = bar_idx;
100    }
101
102    /// Deselect all elements.
103    pub fn deselect(&mut self) {
104        self.selected = None;
105        self.bar_idx = None;
106    }
107
108    /// Set hovered element.
109    pub fn set_hovered(&mut self, id: Option<Id>) {
110        self.hovered = id;
111    }
112
113    /// Check if a specific element is selected.
114    pub fn is_selected(&self, id: Id) -> bool {
115        self.selected == Some(id)
116    }
117
118    /// Check if a specific element is hovered.
119    pub fn is_hovered(&self, id: Id) -> bool {
120        self.hovered == Some(id)
121    }
122
123    /// Check if any element is selected.
124    pub fn has_selection(&self) -> bool {
125        self.selected.is_some()
126    }
127
128    /// Get the currently selected element ID.
129    pub fn selected_id(&self) -> Option<Id> {
130        self.selected
131    }
132
133    /// Get the bar index where selection occurred.
134    pub fn selected_bar(&self) -> Option<usize> {
135        self.bar_idx
136    }
137
138    /// Check if selection state is empty (nothing selected or hovered).
139    pub fn is_empty(&self) -> bool {
140        self.selected.is_none() && self.hovered.is_none()
141    }
142
143    /// Clear all state (selected, hovered, bar_idx).
144    pub fn clear(&mut self) {
145        self.selected = None;
146        self.hovered = None;
147        self.bar_idx = None;
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
156    struct TestId(usize);
157
158    #[test]
159    fn test_select_deselect() {
160        let mut state = SelectionState::<TestId>::new();
161        assert!(!state.has_selection());
162
163        state.select(TestId(1), Some(42));
164        assert!(state.has_selection());
165        assert!(state.is_selected(TestId(1)));
166        assert!(!state.is_selected(TestId(2)));
167        assert_eq!(state.selected_bar(), Some(42));
168
169        state.deselect();
170        assert!(!state.has_selection());
171        assert_eq!(state.selected_bar(), None);
172    }
173
174    #[test]
175    fn test_hover() {
176        let mut state = SelectionState::<TestId>::new();
177        assert!(!state.is_hovered(TestId(1)));
178
179        state.set_hovered(Some(TestId(1)));
180        assert!(state.is_hovered(TestId(1)));
181        assert!(!state.is_hovered(TestId(2)));
182
183        state.set_hovered(None);
184        assert!(!state.is_hovered(TestId(1)));
185    }
186
187    #[test]
188    fn test_chart_element_select_reselect_deselect() {
189        // The unified selection model must move cleanly between the different
190        // kinds of chart elements and back to nothing.
191        let mut state = SelectionState::<ChartElementId>::new();
192        assert!(!state.has_selection());
193
194        // Select a pane indicator.
195        state.select(ChartElementId::PaneIndicator(2), Some(10));
196        assert_eq!(state.selected_id(), Some(ChartElementId::PaneIndicator(2)));
197        assert_eq!(state.selected_bar(), Some(10));
198
199        // Re-select a different element kind — the previous selection is replaced.
200        state.select(ChartElementId::Series(SeriesId::MAIN), Some(20));
201        assert_eq!(
202            state.selected_id(),
203            Some(ChartElementId::Series(SeriesId::MAIN))
204        );
205        assert!(!state.is_selected(ChartElementId::PaneIndicator(2)));
206        assert_eq!(state.selected_bar(), Some(20));
207
208        // Re-select an overlay indicator.
209        state.select(ChartElementId::OverlayIndicator(0), Some(5));
210        assert!(state.is_selected(ChartElementId::OverlayIndicator(0)));
211
212        // Deselect clears everything.
213        state.deselect();
214        assert!(!state.has_selection());
215        assert_eq!(state.selected_id(), None);
216        assert_eq!(state.selected_bar(), None);
217    }
218
219    #[test]
220    fn test_chart_element_ids_are_distinct() {
221        // Series, overlay, and pane indicators with the same numeric index must
222        // never be conflated by the selection model.
223        let overlay = ChartElementId::OverlayIndicator(1);
224        let pane = ChartElementId::PaneIndicator(1);
225        let series = ChartElementId::Series(SeriesId(1));
226        assert_ne!(overlay, pane);
227        assert_ne!(overlay, series);
228        assert_ne!(pane, series);
229    }
230}