Skip to main content

orbital_charts/context/
interaction.rs

1//! Chart interaction state (hover / highlight / legend visibility).
2
3use std::collections::HashSet;
4
5use leptos::callback::Callback;
6use leptos::prelude::*;
7
8use crate::engine::BarGeometry;
9use crate::ChartItemId;
10
11/// Interaction context for item hover, highlight, legend, and pointer tracking.
12#[derive(Clone, Copy)]
13pub struct ChartInteractionContext {
14    /// Currently hovered chart item.
15    pub hovered_item: RwSignal<Option<ChartItemId>>,
16    /// Highlighted item (controlled or derived from hover).
17    pub highlighted_item: RwSignal<Option<ChartItemId>>,
18    /// Series ids hidden via legend toggle.
19    pub hidden_series: RwSignal<HashSet<String>>,
20    /// Pointer position in plot coordinates.
21    pub pointer_plot: RwSignal<Option<(f64, f64)>>,
22    /// Category index under the pointer for axis tooltips and band highlight.
23    pub axis_data_index: RwSignal<Option<usize>>,
24    /// Latest bar geometries for pointer hit testing.
25    pub plot_bars: RwSignal<Vec<BarGeometry>>,
26    /// Line marker positions for pointer hit testing.
27    pub plot_line_markers: RwSignal<Vec<(f64, f64, String, usize)>>,
28    /// Optional callback when highlight changes.
29    pub on_highlight_change: Option<Callback<(Option<ChartItemId>,), ()>>,
30}
31
32impl ChartInteractionContext {
33    /// Create a new interaction context with default signals.
34    pub fn new(on_highlight_change: Option<Callback<(Option<ChartItemId>,), ()>>) -> Self {
35        Self {
36            hovered_item: RwSignal::new(None),
37            highlighted_item: RwSignal::new(None),
38            hidden_series: RwSignal::new(HashSet::new()),
39            pointer_plot: RwSignal::new(None),
40            axis_data_index: RwSignal::new(None),
41            plot_bars: RwSignal::new(Vec::new()),
42            plot_line_markers: RwSignal::new(Vec::new()),
43            on_highlight_change,
44        }
45    }
46}
47
48impl Default for ChartInteractionContext {
49    fn default() -> Self {
50        Self::new(None)
51    }
52}
53
54/// Access the hovered item signal.
55pub fn use_hovered_item() -> RwSignal<Option<ChartItemId>> {
56    expect_context::<ChartInteractionContext>().hovered_item
57}
58
59/// Set the hovered item (or clear with `None`).
60pub fn set_hovered_item(item: Option<ChartItemId>) {
61    let ctx = expect_context::<ChartInteractionContext>();
62    ctx.hovered_item.set(item);
63    sync_highlight_from_hover(ctx);
64}
65
66/// Access the highlighted item signal.
67pub fn use_highlighted_item() -> RwSignal<Option<ChartItemId>> {
68    expect_context::<ChartInteractionContext>().highlighted_item
69}
70
71/// Set highlighted item and notify listeners.
72pub fn set_highlighted_item(item: Option<ChartItemId>) {
73    let ctx = expect_context::<ChartInteractionContext>();
74    if let Some(cb) = ctx.on_highlight_change.as_ref() {
75        cb.run((item.clone(),));
76    }
77    ctx.highlighted_item.set(item);
78}
79
80/// Access hidden series ids.
81pub fn use_hidden_series() -> RwSignal<HashSet<String>> {
82    expect_context::<ChartInteractionContext>().hidden_series
83}
84
85/// Toggle visibility for a series id in the legend.
86pub fn toggle_series_visibility(series_id: &str) {
87    use_hidden_series().update(|set| {
88        if set.contains(series_id) {
89            set.remove(series_id);
90        } else {
91            set.insert(series_id.to_string());
92        }
93    });
94}
95
96/// Returns true when the series is visible (not hidden by legend).
97pub fn is_series_visible(series_id: &str) -> bool {
98    !use_hidden_series().with(|set| set.contains(series_id))
99}
100
101/// Pointer position in plot coordinates.
102pub fn use_pointer_plot() -> ReadSignal<Option<(f64, f64)>> {
103    expect_context::<ChartInteractionContext>()
104        .pointer_plot
105        .read_only()
106}
107
108/// Category index under pointer for axis interactions.
109pub fn use_axis_data_index() -> ReadSignal<Option<usize>> {
110    expect_context::<ChartInteractionContext>()
111        .axis_data_index
112        .read_only()
113}
114
115fn sync_highlight_from_hover(ctx: ChartInteractionContext) {
116    let item = ctx.hovered_item.get();
117    if let Some(cb) = ctx.on_highlight_change.as_ref() {
118        cb.run((item.clone(),));
119    }
120    ctx.highlighted_item.set(item);
121}
122
123/// Provide interaction context to descendants.
124#[component]
125pub fn ChartInteractionProvider(
126    /// Optional external highlight signal for controlled mode.
127    #[prop(default = None)]
128    highlighted_item: Option<RwSignal<Option<ChartItemId>>>,
129    /// Fired when highlight changes.
130    #[prop(default = None)]
131    on_highlight_change: Option<Callback<(Option<ChartItemId>,), ()>>,
132    children: Children,
133) -> impl IntoView {
134    let mut ctx = ChartInteractionContext::new(on_highlight_change);
135    if let Some(external) = highlighted_item {
136        ctx.highlighted_item = external;
137    }
138    provide_context(ctx);
139    children()
140}