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