Skip to main content

orbital_charts/shared/layers/
axis_click_layer.rs

1//! Axis click routing for category band axes.
2
3use leptos::prelude::*;
4
5use crate::context::{is_series_visible, use_chart_context, use_x_scale, use_y_scale};
6use crate::engine::projected_for_plot_type;
7use crate::{AxisClickData, ChartOrientation, ChartScale, ChartType};
8
9/// Transparent band rects for category axis click handling.
10#[component]
11pub fn AxisClickLayer(
12    /// Category axis id (defaults to `"x"` vertical / `"y"` horizontal).
13    #[prop(default = None)]
14    axis_id: Option<String>,
15    /// Bar orientation override.
16    #[prop(default = None)]
17    orientation: Option<ChartOrientation>,
18) -> impl IntoView {
19    let ctx = use_chart_context();
20    let on_axis = ctx.on_axis_click;
21    let orient = orientation.unwrap_or(ctx.orientation);
22    let projected = ctx.projected.clone();
23    let series_defs = ctx.series.clone();
24    let plot_width = ctx.drawing_area.plot_width;
25    let plot_height = ctx.drawing_area.plot_height;
26
27    let category_axis_id = axis_id.unwrap_or_else(|| match orient {
28        ChartOrientation::Vertical => "x".to_string(),
29        ChartOrientation::Horizontal => "y".to_string(),
30    });
31
32    let category_scale = match orient {
33        ChartOrientation::Vertical => use_x_scale(category_axis_id.clone()),
34        ChartOrientation::Horizontal => use_y_scale(category_axis_id.clone()),
35    };
36
37    view! {
38        {move || {
39            if on_axis.is_none() {
40                return ().into_any();
41            }
42            let Some(data) = projected.as_ref() else {
43                return ().into_any();
44            };
45            let plot_data = projected_for_plot_type(data, &series_defs, ChartType::Bar);
46            if plot_data.categories.is_empty() {
47                return ().into_any();
48            }
49
50            let bands = axis_click_bands(
51                orient,
52                &plot_data.categories,
53                &category_scale,
54                plot_width,
55                plot_height,
56            );
57
58            bands
59                .into_iter()
60                .map(|band| {
61                    let axis_id = category_axis_id.clone();
62                    let on_axis = on_axis;
63                    let projected = projected.clone();
64                    let series_defs = series_defs.clone();
65                    view! {
66                        <rect
67                            class="orb-axis-click-band"
68                            x=band.x
69                            y=band.y
70                            width=band.w
71                            height=band.h
72                            on:click=move |ev| {
73                                ev.stop_propagation();
74                                if let Some(cb) = &on_axis {
75                                    let series_values = projected
76                                        .as_ref()
77                                        .map(|p| {
78                                            let plot_data = projected_for_plot_type(
79                                                p,
80                                                &series_defs,
81                                                ChartType::Bar,
82                                            );
83                                            plot_data
84                                                .series
85                                                .iter()
86                                                .filter(|s| is_series_visible(&s.id))
87                                                .map(|s| {
88                                                    (
89                                                        s.id.clone(),
90                                                        s.data.get(band.index).copied().unwrap_or(0.0),
91                                                    )
92                                                })
93                                                .collect()
94                                        })
95                                        .unwrap_or_default();
96                                    cb.run((AxisClickData {
97                                        axis_id: axis_id.clone(),
98                                        value: band.index as f64,
99                                        series_values,
100                                    },));
101                                }
102                            }
103                        />
104                    }
105                })
106                .collect_view()
107                .into_any()
108        }}
109    }
110}
111
112struct ClickBand {
113    x: f64,
114    y: f64,
115    w: f64,
116    h: f64,
117    index: usize,
118}
119
120fn axis_click_bands(
121    orient: ChartOrientation,
122    categories: &[String],
123    category_scale: &ChartScale,
124    plot_width: f64,
125    plot_height: f64,
126) -> Vec<ClickBand> {
127    let ChartScale::Band(band) = category_scale else {
128        return Vec::new();
129    };
130    let bw = band.bandwidth();
131    categories
132        .iter()
133        .enumerate()
134        .filter_map(|(i, cat)| {
135            band.scale(cat).map(|center| match orient {
136                ChartOrientation::Vertical => ClickBand {
137                    x: center - bw / 2.0,
138                    y: 0.0,
139                    w: bw,
140                    h: plot_height,
141                    index: i,
142                },
143                ChartOrientation::Horizontal => ClickBand {
144                    x: 0.0,
145                    y: center - bw / 2.0,
146                    w: plot_width,
147                    h: bw,
148                    index: i,
149                },
150            })
151        })
152        .collect()
153}