egui-charts 0.2.0

High-performance financial charting engine for egui — candlesticks, 95 drawing tools, 130+ indicators, and a full design-token theme system
Documentation
//! Generic selection state for chart elements.
//!
//! Provides a shared selection pattern used by series, indicators, and other
//! selectable chart elements.
//!
//! The whole chart tracks a single selection through one
//! [`SelectionState<ChartElementId>`]. The [`ChartElementId`] enum distinguishes
//! the kinds of selectable things — a price/volume series, an overlay indicator
//! drawn on the main chart, or a separate-pane indicator — so one selection
//! model can express "whatever the user last clicked" regardless of which layer
//! it lives on.

use std::marker::PhantomData;

/// Unique numeric identifier for a chart series.
///
/// Well-known constants: [`MAIN`](Self::MAIN) (candlesticks/bars) and
/// [`VOLUME`](Self::VOLUME).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SeriesId(pub usize);

impl SeriesId {
    /// Main chart series (candlesticks/bars)
    pub const MAIN: SeriesId = SeriesId(0);
    /// Volume series
    pub const VOLUME: SeriesId = SeriesId(1);

    /// Get display name for this series
    pub fn name(&self) -> &'static str {
        match self.0 {
            0 => "Main Series",
            1 => "Volume",
            _ => "Series",
        }
    }
}

/// Identifies any selectable element across the entire chart.
///
/// A single [`SelectionState<ChartElementId>`] tracks the one element the user
/// has clicked, whether it is a series, an overlay indicator on the main chart,
/// or a separate-pane indicator (RSI, MACD, …). Indicators are identified by
/// their index in the indicator registry.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ChartElementId {
    /// A price or volume series on the main chart.
    Series(SeriesId),
    /// An overlay indicator drawn on the main price chart (SMA, EMA, …),
    /// identified by its index in the indicator registry.
    OverlayIndicator(usize),
    /// A separate-pane indicator below the main chart (RSI, MACD, …),
    /// identified by its index in the indicator registry.
    PaneIndicator(usize),
}

/// Trait bound for IDs that can be used with [`SelectionState`].
///
/// Automatically implemented for any type that is `Copy + Eq + Debug`.
pub trait SelectableId: Copy + Eq + std::fmt::Debug {}

// Blanket implementation for types that meet the requirements
impl<T: Copy + Eq + std::fmt::Debug> SelectableId for T {}

/// Generic selection state for chart elements, parameterized by an ID type.
///
/// Tracks which element is selected, which is hovered, and the bar index
/// where the selection occurred. Used by both series and indicator selection.
#[derive(Clone, Debug)]
pub struct SelectionState<Id: SelectableId> {
    /// Currently selected element
    pub selected: Option<Id>,
    /// Currently hovered element (desktop only)
    pub hovered: Option<Id>,
    /// Additional selection metadata (bar index, etc.)
    pub bar_idx: Option<usize>,
    _phantom: PhantomData<Id>,
}

impl<Id: SelectableId> Default for SelectionState<Id> {
    fn default() -> Self {
        Self {
            selected: None,
            hovered: None,
            bar_idx: None,
            _phantom: PhantomData,
        }
    }
}

impl<Id: SelectableId> SelectionState<Id> {
    /// Create new empty selection state.
    pub fn new() -> Self {
        Self::default()
    }

    /// Select an element.
    pub fn select(&mut self, id: Id, bar_idx: Option<usize>) {
        self.selected = Some(id);
        self.bar_idx = bar_idx;
    }

    /// Deselect all elements.
    pub fn deselect(&mut self) {
        self.selected = None;
        self.bar_idx = None;
    }

    /// Set hovered element.
    pub fn set_hovered(&mut self, id: Option<Id>) {
        self.hovered = id;
    }

    /// Check if a specific element is selected.
    pub fn is_selected(&self, id: Id) -> bool {
        self.selected == Some(id)
    }

    /// Check if a specific element is hovered.
    pub fn is_hovered(&self, id: Id) -> bool {
        self.hovered == Some(id)
    }

    /// Check if any element is selected.
    pub fn has_selection(&self) -> bool {
        self.selected.is_some()
    }

    /// Get the currently selected element ID.
    pub fn selected_id(&self) -> Option<Id> {
        self.selected
    }

    /// Get the bar index where selection occurred.
    pub fn selected_bar(&self) -> Option<usize> {
        self.bar_idx
    }

    /// Check if selection state is empty (nothing selected or hovered).
    pub fn is_empty(&self) -> bool {
        self.selected.is_none() && self.hovered.is_none()
    }

    /// Clear all state (selected, hovered, bar_idx).
    pub fn clear(&mut self) {
        self.selected = None;
        self.hovered = None;
        self.bar_idx = None;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    struct TestId(usize);

    #[test]
    fn test_select_deselect() {
        let mut state = SelectionState::<TestId>::new();
        assert!(!state.has_selection());

        state.select(TestId(1), Some(42));
        assert!(state.has_selection());
        assert!(state.is_selected(TestId(1)));
        assert!(!state.is_selected(TestId(2)));
        assert_eq!(state.selected_bar(), Some(42));

        state.deselect();
        assert!(!state.has_selection());
        assert_eq!(state.selected_bar(), None);
    }

    #[test]
    fn test_hover() {
        let mut state = SelectionState::<TestId>::new();
        assert!(!state.is_hovered(TestId(1)));

        state.set_hovered(Some(TestId(1)));
        assert!(state.is_hovered(TestId(1)));
        assert!(!state.is_hovered(TestId(2)));

        state.set_hovered(None);
        assert!(!state.is_hovered(TestId(1)));
    }

    #[test]
    fn test_chart_element_select_reselect_deselect() {
        // The unified selection model must move cleanly between the different
        // kinds of chart elements and back to nothing.
        let mut state = SelectionState::<ChartElementId>::new();
        assert!(!state.has_selection());

        // Select a pane indicator.
        state.select(ChartElementId::PaneIndicator(2), Some(10));
        assert_eq!(state.selected_id(), Some(ChartElementId::PaneIndicator(2)));
        assert_eq!(state.selected_bar(), Some(10));

        // Re-select a different element kind — the previous selection is replaced.
        state.select(ChartElementId::Series(SeriesId::MAIN), Some(20));
        assert_eq!(
            state.selected_id(),
            Some(ChartElementId::Series(SeriesId::MAIN))
        );
        assert!(!state.is_selected(ChartElementId::PaneIndicator(2)));
        assert_eq!(state.selected_bar(), Some(20));

        // Re-select an overlay indicator.
        state.select(ChartElementId::OverlayIndicator(0), Some(5));
        assert!(state.is_selected(ChartElementId::OverlayIndicator(0)));

        // Deselect clears everything.
        state.deselect();
        assert!(!state.has_selection());
        assert_eq!(state.selected_id(), None);
        assert_eq!(state.selected_bar(), None);
    }

    #[test]
    fn test_chart_element_ids_are_distinct() {
        // Series, overlay, and pane indicators with the same numeric index must
        // never be conflated by the selection model.
        let overlay = ChartElementId::OverlayIndicator(1);
        let pane = ChartElementId::PaneIndicator(1);
        let series = ChartElementId::Series(SeriesId(1));
        assert_ne!(overlay, pane);
        assert_ne!(overlay, series);
        assert_ne!(pane, series);
    }
}