1+ use serde:: { Deserialize , Serialize } ;
2+ use wasm_bindgen:: prelude:: * ;
3+ use web_sys:: { js_sys:: Function , window, HtmlElement } ;
4+
5+ /// Provides utilities for binding Plotly.js click events to Rust closures
6+ /// via `wasm-bindgen`.
7+ ///
8+ /// This module defines a `PlotlyDiv` foreign type for the Plotly `<div>` element,
9+ /// a high-level `bind_click` function to wire up Rust callbacks, and
10+ /// the `ClickPoint`/`ClickEvent` data structures to deserialize event payloads.
11+
12+ #[ wasm_bindgen]
13+ extern "C" {
14+
15+ /// A wrapper around the JavaScript `HTMLElement` representing a Plotly `<div>`.
16+ ///
17+ /// This type extends `web_sys::HtmlElement` and exposes Plotly’s
18+ /// `.on(eventName, callback)` method for attaching event listeners.
19+
20+ #[ wasm_bindgen( extends= HtmlElement , js_name=HTMLElement ) ]
21+ type PlotlyDiv ;
22+
23+ /// Attach a JavaScript event listener to this Plotly `<div>`.
24+ ///
25+ /// # Parameters
26+ /// - `event`: The Plotly event name (e.g., `"plotly_click"`).
27+ /// - `cb`: A JS `Function` to invoke when the event fires.
28+ ///
29+ /// # Panics
30+ /// This method assumes the underlying element is indeed a Plotly div
31+ /// and that the Plotly.js library has been loaded on the page.
32+
33+ #[ wasm_bindgen( method, structural, js_name=on) ]
34+ fn on ( this : & PlotlyDiv , event : & str , cb : & Function ) ;
35+ }
36+
37+ /// Bind a Rust callback to the Plotly `plotly_click` event on a given `<div>`.
38+ ///
39+ /// # Type Parameters
40+ /// - `F`: A `'static + FnMut(ClickEvent)` closure type to handle the click data.
41+ ///
42+ /// # Parameters
43+ /// - `div_id`: The DOM `id` attribute of the Plotly `<div>`.
44+ /// - `cb`: A mutable Rust closure that will be called with a `ClickEvent`.
45+ ///
46+ /// # Details
47+ /// 1. Looks up the element by `div_id`, converts it to `PlotlyDiv`.
48+ /// 2. Wraps a `Closure<dyn FnMut(JsValue)>` that deserializes the JS event
49+ /// into our `ClickEvent` type via `serde_wasm_bindgen`.
50+ /// 3. Calls `plot_div.on("plotly_click", …)` to register the listener.
51+ /// 4. Forgets the closure so it lives for the lifetime of the page.
52+ ///
53+ /// # Example
54+ /// ```ignore
55+ /// bind_click("my-plot", |evt| {
56+ /// web_sys::console::log_1(&format!("{:?}", evt).into());
57+ /// });
58+ /// ```
59+
60+
61+ pub fn bind_click < F > ( div_id : & str , mut cb : F )
62+ where
63+ F : ' static + FnMut ( ClickEvent )
64+ {
65+
66+ let plot_div: PlotlyDiv = window ( ) . unwrap ( )
67+ . document ( ) . unwrap ( )
68+ . get_element_by_id ( div_id) . unwrap ( )
69+ . unchecked_into ( ) ;
70+ let closure = Closure :: wrap ( Box :: new ( move |event : JsValue | {
71+ let event: ClickEvent = serde_wasm_bindgen:: from_value ( event)
72+ . expect ( "\n Couldn't serialize the event \n " ) ;
73+ cb ( event) ;
74+ } ) as Box < dyn FnMut ( JsValue ) > ) ;
75+ plot_div. on ( "plotly_click" , & closure. as_ref ( ) . unchecked_ref ( ) ) ;
76+ closure. forget ( ) ;
77+ }
78+
79+
80+ /// Represents a single point from a Plotly click event.
81+ ///
82+ /// Fields mirror Plotly’s `event.points[i]` properties, all optional
83+ /// where appropriate:
84+ ///
85+ /// - `curve_number`: The zero-based index of the trace that was clicked.
86+ /// - `point_numbers`: An optional list of indices if multiple points were selected.
87+ /// - `point_number`: The index of the specific point clicked (if singular).
88+ /// - `x`, `y`, `z`: Optional numeric coordinates in data space.
89+ /// - `lat`, `lon`: Optional geographic coordinates (for map plots).
90+ ///
91+ /// # Serialization
92+ /// Uses `serde` with `camelCase` field names to match Plotly’s JS API.
93+
94+
95+ #[ derive( Debug , Deserialize , Serialize , Default ) ]
96+ #[ serde( rename_all = "camelCase" ) ]
97+ pub struct ClickPoint {
98+ pub curve_number : usize ,
99+ pub point_numbers : Option < Vec < usize > > ,
100+ pub point_number : Option < usize > ,
101+ pub x : Option < f64 > ,
102+ pub y : Option < f64 > ,
103+ pub z : Option < f64 > ,
104+ pub lat : Option < f64 > ,
105+ pub lon : Option < f64 >
106+ }
107+
108+
109+ /// Provide a default single-point vector for `ClickEvent::points`.
110+ ///
111+ /// Returns `vec![ClickPoint::default()]` so deserialization always yields
112+ /// at least one element rather than an empty vector.
113+
114+ fn default_click_event ( ) -> Vec < ClickPoint > { vec ! [ ClickPoint :: default ( ) ] }
115+
116+
117+ /// The top-level payload for a Plotly click event.
118+ ///
119+ /// - `points`: A `Vec<ClickPoint>` containing all clicked points.
120+ /// Defaults to the result of `default_click_event` to ensure
121+ /// `points` is non-empty even if Plotly sends no data.
122+ ///
123+ /// # Serialization
124+ /// Uses `serde` with `camelCase` names and a custom default so you can
125+ /// call `event.points` without worrying about missing values.
126+
127+ #[ derive( Debug , Deserialize , Serialize ) ]
128+ #[ serde( rename_all="camelCase" , default ) ]
129+ pub struct ClickEvent {
130+ #[ serde( default ="default_click_event" ) ]
131+ pub points : Vec < ClickPoint >
132+ }
133+
134+ /// A `Default` implementation yielding an empty `points` vector.
135+ ///
136+ /// Useful when you need a zero-event placeholder (e.g., initial state).
137+
138+ impl Default for ClickEvent {
139+ fn default ( ) -> Self {
140+ ClickEvent { points : vec ! [ ] }
141+ }
142+ }
0 commit comments