From fb0d21421a1287dd4a32af351b75a29457427c3e Mon Sep 17 00:00:00 2001 From: h4ck4l1 Date: Fri, 25 Apr 2025 21:38:01 +0000 Subject: [PATCH 1/3] Added functionality for callbacks --- examples/wasm-yew-callback-minimal/Cargo.toml | 12 ++ examples/wasm-yew-callback-minimal/README.md | 9 ++ examples/wasm-yew-callback-minimal/index.html | 12 ++ .../wasm-yew-callback-minimal/src/main.rs | 80 ++++++++++ plotly/Cargo.toml | 2 + plotly/src/bindings.rs | 1 + plotly/src/callbacks.rs | 142 ++++++++++++++++++ plotly/src/lib.rs | 3 + 8 files changed, 261 insertions(+) create mode 100644 examples/wasm-yew-callback-minimal/Cargo.toml create mode 100644 examples/wasm-yew-callback-minimal/README.md create mode 100644 examples/wasm-yew-callback-minimal/index.html create mode 100644 examples/wasm-yew-callback-minimal/src/main.rs create mode 100644 plotly/src/callbacks.rs diff --git a/examples/wasm-yew-callback-minimal/Cargo.toml b/examples/wasm-yew-callback-minimal/Cargo.toml new file mode 100644 index 00000000..12f22f6d --- /dev/null +++ b/examples/wasm-yew-callback-minimal/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "wasm-yew-callback-minimal" +version = "0.1.0" +edition = "2024" + +[dependencies] +plotly = { path = "../../plotly" } +yew = "0.21" +yew-hooks = "0.3" +log = "0.4" +wasm-logger = "0.2" +web-sys = { version = "0.3.77"} \ No newline at end of file diff --git a/examples/wasm-yew-callback-minimal/README.md b/examples/wasm-yew-callback-minimal/README.md new file mode 100644 index 00000000..a62a6681 --- /dev/null +++ b/examples/wasm-yew-callback-minimal/README.md @@ -0,0 +1,9 @@ +# Wasm Yew Minimal + +## Prerequisites + +1. Install [Trunk](https://trunkrs.dev/) using `cargo install --locked trunk`. + +## How to Run + +1. Run `trunk serve --open` in this directory to build and serve the application, opening the default web browser automatically. \ No newline at end of file diff --git a/examples/wasm-yew-callback-minimal/index.html b/examples/wasm-yew-callback-minimal/index.html new file mode 100644 index 00000000..88480a2e --- /dev/null +++ b/examples/wasm-yew-callback-minimal/index.html @@ -0,0 +1,12 @@ + + + + + + Plotly Yew + + + + + + \ No newline at end of file diff --git a/examples/wasm-yew-callback-minimal/src/main.rs b/examples/wasm-yew-callback-minimal/src/main.rs new file mode 100644 index 00000000..f1a82ab2 --- /dev/null +++ b/examples/wasm-yew-callback-minimal/src/main.rs @@ -0,0 +1,80 @@ +use plotly::{Plot,common::Mode, Scatter,Histogram}; +use plotly::callbacks::{ClickEvent}; +use web_sys::js_sys::Math; +use yew::prelude::*; + + +#[function_component(App)] +pub fn plot_component() -> Html { + + let x = use_state(|| None::); + let y = use_state(|| None::); + let point_numbers = use_state(|| None::>); + let point_number = use_state(|| None::); + let curve_number = use_state(|| 0usize); + let click_event = use_state(|| ClickEvent::default()); + + let x_clone = x.clone(); + let y_clone = y.clone(); + let curve_clone = curve_number.clone(); + let point_numbers_clone = point_numbers.clone(); + let point_number_clone = point_number.clone(); + let click_event_clone = click_event.clone(); + + let p = yew_hooks::use_async::<_, _, ()>({ + let id = "plot-div"; + let mut fig = Plot::new(); + let xs: Vec = (0..50).map(|i| i as f64).collect(); + let ys: Vec = xs.iter().map(|x| x.sin()).collect(); + fig.add_trace( + Scatter::new(xs.clone(), ys.clone()) + .mode(Mode::Markers) + .name("Sine markers") + ); + let random_values: Vec = (0..100) + .map(|_| Math::random()) + .collect(); + fig.add_trace( + Histogram::new(random_values) + .name("Random histogram") + ); + let layout = plotly::Layout::new().title("Click Event Callback Example in Yew"); + fig.set_layout(layout); + async move { + plotly::bindings::new_plot(id, &fig).await; + plotly::callbacks::bind_click(id, move |event| { + let pt = &event.points[0]; + x_clone.set(pt.x); + y_clone.set(pt.y); + curve_clone.set(pt.curve_number); + point_numbers_clone.set(pt.point_numbers.clone()); + point_number_clone.set(pt.point_number); + click_event_clone.set(event); + }); + Ok(()) + } + }); + // Only on first render + use_effect_with((), move |_| { + p.run(); + }); + + html! { + <> +
+
+

{format!("x: {:?}",*x)}

+

{format!("y: {:?}",*y)}

+

{format!("curveNumber: {:?}",*curve_number)}

+

{format!("pointNumber: {:?}",*point_number)}

+

{format!("pointNumbers: {:?}",*point_numbers)}

+

{format!("ClickEvent: {:?}",*click_event)}

+
+ + } +} + +fn main() { + wasm_logger::init(wasm_logger::Config::default()); + yew::Renderer::::new().render(); +} \ No newline at end of file diff --git a/plotly/Cargo.toml b/plotly/Cargo.toml index e7bbcee3..2340ef10 100644 --- a/plotly/Cargo.toml +++ b/plotly/Cargo.toml @@ -40,6 +40,8 @@ rand = "0.9" getrandom = { version = "0.3", features = ["wasm_js"] } wasm-bindgen-futures = { version = "0.4" } wasm-bindgen = { version = "0.2" } +serde-wasm-bindgen = {version = "0.6.3"} +web-sys = { version = "0.3.77", features = ["Document", "Window", "HtmlElement"]} [dev-dependencies] csv = "1.1" diff --git a/plotly/src/bindings.rs b/plotly/src/bindings.rs index 9b86c05d..40dcf94f 100644 --- a/plotly/src/bindings.rs +++ b/plotly/src/bindings.rs @@ -25,6 +25,7 @@ extern "C" { pub async fn new_plot(id: &str, plot: &Plot) { let plot_obj = &plot.to_js_object(); + // This will only fail if the Rust Plotly library has produced // plotly-incompatible JSON. An error here should have been handled by the // library, rather than down here. diff --git a/plotly/src/callbacks.rs b/plotly/src/callbacks.rs new file mode 100644 index 00000000..5e3b7fcf --- /dev/null +++ b/plotly/src/callbacks.rs @@ -0,0 +1,142 @@ +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; +use web_sys::{js_sys::Function, window, HtmlElement}; + +/// Provides utilities for binding Plotly.js click events to Rust closures +/// via `wasm-bindgen`. +/// +/// This module defines a `PlotlyDiv` foreign type for the Plotly `
` element, +/// a high-level `bind_click` function to wire up Rust callbacks, and +/// the `ClickPoint`/`ClickEvent` data structures to deserialize event payloads. + +#[wasm_bindgen] +extern "C" { + + /// A wrapper around the JavaScript `HTMLElement` representing a Plotly `
`. + /// + /// This type extends `web_sys::HtmlElement` and exposes Plotly’s + /// `.on(eventName, callback)` method for attaching event listeners. + + #[wasm_bindgen(extends= HtmlElement, js_name=HTMLElement)] + type PlotlyDiv; + + /// Attach a JavaScript event listener to this Plotly `
`. + /// + /// # Parameters + /// - `event`: The Plotly event name (e.g., `"plotly_click"`). + /// - `cb`: A JS `Function` to invoke when the event fires. + /// + /// # Panics + /// This method assumes the underlying element is indeed a Plotly div + /// and that the Plotly.js library has been loaded on the page. + + #[wasm_bindgen(method,structural,js_name=on)] + fn on(this: &PlotlyDiv, event: &str, cb: &Function); +} + +/// Bind a Rust callback to the Plotly `plotly_click` event on a given `
`. +/// +/// # Type Parameters +/// - `F`: A `'static + FnMut(ClickEvent)` closure type to handle the click data. +/// +/// # Parameters +/// - `div_id`: The DOM `id` attribute of the Plotly `
`. +/// - `cb`: A mutable Rust closure that will be called with a `ClickEvent`. +/// +/// # Details +/// 1. Looks up the element by `div_id`, converts it to `PlotlyDiv`. +/// 2. Wraps a `Closure` that deserializes the JS event +/// into our `ClickEvent` type via `serde_wasm_bindgen`. +/// 3. Calls `plot_div.on("plotly_click", …)` to register the listener. +/// 4. Forgets the closure so it lives for the lifetime of the page. +/// +/// # Example +/// ```ignore +/// bind_click("my-plot", |evt| { +/// web_sys::console::log_1(&format!("{:?}", evt).into()); +/// }); +/// ``` + + +pub fn bind_click(div_id: &str, mut cb: F) +where + F: 'static + FnMut(ClickEvent) +{ + + let plot_div: PlotlyDiv = window().unwrap() + .document().unwrap() + .get_element_by_id(div_id).unwrap() + .unchecked_into(); + let closure = Closure::wrap(Box::new(move |event: JsValue| { + let event: ClickEvent = serde_wasm_bindgen::from_value(event) + .expect("\n Couldn't serialize the event \n"); + cb(event); + }) as Box); + plot_div.on("plotly_click", &closure.as_ref().unchecked_ref()); + closure.forget(); +} + + +/// Represents a single point from a Plotly click event. +/// +/// Fields mirror Plotly’s `event.points[i]` properties, all optional +/// where appropriate: +/// +/// - `curve_number`: The zero-based index of the trace that was clicked. +/// - `point_numbers`: An optional list of indices if multiple points were selected. +/// - `point_number`: The index of the specific point clicked (if singular). +/// - `x`, `y`, `z`: Optional numeric coordinates in data space. +/// - `lat`, `lon`: Optional geographic coordinates (for map plots). +/// +/// # Serialization +/// Uses `serde` with `camelCase` field names to match Plotly’s JS API. + + +#[derive(Debug,Deserialize,Serialize,Default)] +#[serde(rename_all = "camelCase")] +pub struct ClickPoint { + pub curve_number: usize, + pub point_numbers: Option>, + pub point_number: Option, + pub x: Option, + pub y: Option, + pub z: Option, + pub lat: Option, + pub lon: Option +} + + +/// Provide a default single-point vector for `ClickEvent::points`. +/// +/// Returns `vec![ClickPoint::default()]` so deserialization always yields +/// at least one element rather than an empty vector. + +fn default_click_event() -> Vec {vec![ClickPoint::default()]} + + +/// The top-level payload for a Plotly click event. +/// +/// - `points`: A `Vec` containing all clicked points. +/// Defaults to the result of `default_click_event` to ensure +/// `points` is non-empty even if Plotly sends no data. +/// +/// # Serialization +/// Uses `serde` with `camelCase` names and a custom default so you can +/// call `event.points` without worrying about missing values. + +#[derive(Debug,Deserialize,Serialize)] +#[serde(rename_all="camelCase",default)] +pub struct ClickEvent { + #[serde(default="default_click_event")] + pub points: Vec +} + +/// A `Default` implementation yielding an empty `points` vector. +/// +/// Useful when you need a zero-event placeholder (e.g., initial state). + +impl Default for ClickEvent { + fn default() -> Self { + ClickEvent { points: vec![] } + } +} \ No newline at end of file diff --git a/plotly/src/lib.rs b/plotly/src/lib.rs index ee082087..e22a1482 100644 --- a/plotly/src/lib.rs +++ b/plotly/src/lib.rs @@ -19,6 +19,9 @@ pub use crate::ndarray::ArrayTraces; #[cfg(target_family = "wasm")] pub mod bindings; +#[cfg(target_family = "wasm")] +pub mod callbacks; + pub mod common; pub mod configuration; pub mod layout; From e033d8400c2c6ef015b5863de561e79048cba109 Mon Sep 17 00:00:00 2001 From: Andrei Gherghescu <8067229+andrei-ng@users.noreply.github.com> Date: Mon, 12 May 2025 20:19:12 +0200 Subject: [PATCH 2/3] fix fmt & clippy Signed-off-by: Andrei Gherghescu <8067229+andrei-ng@users.noreply.github.com> --- .../wasm-yew-callback-minimal/src/main.rs | 19 ++--- plotly/src/bindings.rs | 1 - plotly/src/callbacks.rs | 76 +++++++++---------- 3 files changed, 42 insertions(+), 54 deletions(-) diff --git a/examples/wasm-yew-callback-minimal/src/main.rs b/examples/wasm-yew-callback-minimal/src/main.rs index f1a82ab2..e0d6055c 100644 --- a/examples/wasm-yew-callback-minimal/src/main.rs +++ b/examples/wasm-yew-callback-minimal/src/main.rs @@ -1,12 +1,10 @@ -use plotly::{Plot,common::Mode, Scatter,Histogram}; -use plotly::callbacks::{ClickEvent}; +use plotly::callbacks::ClickEvent; +use plotly::{Histogram, Plot, Scatter, common::Mode}; use web_sys::js_sys::Math; use yew::prelude::*; - #[function_component(App)] pub fn plot_component() -> Html { - let x = use_state(|| None::); let y = use_state(|| None::); let point_numbers = use_state(|| None::>); @@ -29,15 +27,10 @@ pub fn plot_component() -> Html { fig.add_trace( Scatter::new(xs.clone(), ys.clone()) .mode(Mode::Markers) - .name("Sine markers") - ); - let random_values: Vec = (0..100) - .map(|_| Math::random()) - .collect(); - fig.add_trace( - Histogram::new(random_values) - .name("Random histogram") + .name("Sine markers"), ); + let random_values: Vec = (0..100).map(|_| Math::random()).collect(); + fig.add_trace(Histogram::new(random_values).name("Random histogram")); let layout = plotly::Layout::new().title("Click Event Callback Example in Yew"); fig.set_layout(layout); async move { @@ -77,4 +70,4 @@ pub fn plot_component() -> Html { fn main() { wasm_logger::init(wasm_logger::Config::default()); yew::Renderer::::new().render(); -} \ No newline at end of file +} diff --git a/plotly/src/bindings.rs b/plotly/src/bindings.rs index 40dcf94f..9b86c05d 100644 --- a/plotly/src/bindings.rs +++ b/plotly/src/bindings.rs @@ -25,7 +25,6 @@ extern "C" { pub async fn new_plot(id: &str, plot: &Plot) { let plot_obj = &plot.to_js_object(); - // This will only fail if the Rust Plotly library has produced // plotly-incompatible JSON. An error here should have been handled by the // library, rather than down here. diff --git a/plotly/src/callbacks.rs b/plotly/src/callbacks.rs index 5e3b7fcf..6f6257bd 100644 --- a/plotly/src/callbacks.rs +++ b/plotly/src/callbacks.rs @@ -5,14 +5,14 @@ use web_sys::{js_sys::Function, window, HtmlElement}; /// Provides utilities for binding Plotly.js click events to Rust closures /// via `wasm-bindgen`. /// -/// This module defines a `PlotlyDiv` foreign type for the Plotly `
` element, -/// a high-level `bind_click` function to wire up Rust callbacks, and +/// This module defines a `PlotlyDiv` foreign type for the Plotly `
` +/// element, a high-level `bind_click` function to wire up Rust callbacks, and /// the `ClickPoint`/`ClickEvent` data structures to deserialize event payloads. - #[wasm_bindgen] extern "C" { - /// A wrapper around the JavaScript `HTMLElement` representing a Plotly `
`. + /// A wrapper around the JavaScript `HTMLElement` representing a Plotly + /// `
`. /// /// This type extends `web_sys::HtmlElement` and exposes Plotly’s /// `.on(eventName, callback)` method for attaching event listeners. @@ -37,17 +37,18 @@ extern "C" { /// Bind a Rust callback to the Plotly `plotly_click` event on a given `
`. /// /// # Type Parameters -/// - `F`: A `'static + FnMut(ClickEvent)` closure type to handle the click data. +/// - `F`: A `'static + FnMut(ClickEvent)` closure type to handle the click +/// data. /// /// # Parameters /// - `div_id`: The DOM `id` attribute of the Plotly `
`. /// - `cb`: A mutable Rust closure that will be called with a `ClickEvent`. /// /// # Details -/// 1. Looks up the element by `div_id`, converts it to `PlotlyDiv`. -/// 2. Wraps a `Closure` that deserializes the JS event -/// into our `ClickEvent` type via `serde_wasm_bindgen`. -/// 3. Calls `plot_div.on("plotly_click", …)` to register the listener. +/// 1. Looks up the element by `div_id`, converts it to `PlotlyDiv`. +/// 2. Wraps a `Closure` that deserializes the JS event into +/// our `ClickEvent` type via `serde_wasm_bindgen`. +/// 3. Calls `plot_div.on("plotly_click", …)` to register the listener. /// 4. Forgets the closure so it lives for the lifetime of the page. /// /// # Example @@ -56,43 +57,41 @@ extern "C" { /// web_sys::console::log_1(&format!("{:?}", evt).into()); /// }); /// ``` - - pub fn bind_click(div_id: &str, mut cb: F) -where - F: 'static + FnMut(ClickEvent) +where + F: 'static + FnMut(ClickEvent), { - - let plot_div: PlotlyDiv = window().unwrap() - .document().unwrap() - .get_element_by_id(div_id).unwrap() + let plot_div: PlotlyDiv = window() + .unwrap() + .document() + .unwrap() + .get_element_by_id(div_id) + .unwrap() .unchecked_into(); let closure = Closure::wrap(Box::new(move |event: JsValue| { - let event: ClickEvent = serde_wasm_bindgen::from_value(event) - .expect("\n Couldn't serialize the event \n"); + let event: ClickEvent = + serde_wasm_bindgen::from_value(event).expect("\n Couldn't serialize the event \n"); cb(event); }) as Box); - plot_div.on("plotly_click", &closure.as_ref().unchecked_ref()); + plot_div.on("plotly_click", closure.as_ref().unchecked_ref()); closure.forget(); } - /// Represents a single point from a Plotly click event. /// /// Fields mirror Plotly’s `event.points[i]` properties, all optional /// where appropriate: /// /// - `curve_number`: The zero-based index of the trace that was clicked. -/// - `point_numbers`: An optional list of indices if multiple points were selected. +/// - `point_numbers`: An optional list of indices if multiple points were +/// selected. /// - `point_number`: The index of the specific point clicked (if singular). /// - `x`, `y`, `z`: Optional numeric coordinates in data space. /// - `lat`, `lon`: Optional geographic coordinates (for map plots). /// /// # Serialization /// Uses `serde` with `camelCase` field names to match Plotly’s JS API. - - -#[derive(Debug,Deserialize,Serialize,Default)] +#[derive(Debug, Deserialize, Serialize, Default)] #[serde(rename_all = "camelCase")] pub struct ClickPoint { pub curve_number: usize, @@ -102,41 +101,38 @@ pub struct ClickPoint { pub y: Option, pub z: Option, pub lat: Option, - pub lon: Option + pub lon: Option, } - /// Provide a default single-point vector for `ClickEvent::points`. /// /// Returns `vec![ClickPoint::default()]` so deserialization always yields /// at least one element rather than an empty vector. - -fn default_click_event() -> Vec {vec![ClickPoint::default()]} - +fn default_click_event() -> Vec { + vec![ClickPoint::default()] +} /// The top-level payload for a Plotly click event. /// -/// - `points`: A `Vec` containing all clicked points. -/// Defaults to the result of `default_click_event` to ensure -/// `points` is non-empty even if Plotly sends no data. +/// - `points`: A `Vec` containing all clicked points. Defaults to +/// the result of `default_click_event` to ensure `points` is non-empty even +/// if Plotly sends no data. /// /// # Serialization /// Uses `serde` with `camelCase` names and a custom default so you can /// call `event.points` without worrying about missing values. - -#[derive(Debug,Deserialize,Serialize)] -#[serde(rename_all="camelCase",default)] +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", default)] pub struct ClickEvent { - #[serde(default="default_click_event")] - pub points: Vec + #[serde(default = "default_click_event")] + pub points: Vec, } /// A `Default` implementation yielding an empty `points` vector. /// /// Useful when you need a zero-event placeholder (e.g., initial state). - impl Default for ClickEvent { fn default() -> Self { ClickEvent { points: vec![] } } -} \ No newline at end of file +} From fd684ae00fa904e6c196dd74987078c5bc94977f Mon Sep 17 00:00:00 2001 From: Andrei Gherghescu <8067229+andrei-ng@users.noreply.github.com> Date: Mon, 12 May 2025 22:28:22 +0200 Subject: [PATCH 3/3] remove unwrap calls - update changelog Signed-off-by: Andrei Gherghescu <8067229+andrei-ng@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 1 + .../wasm-yew-callback-minimal/src/main.rs | 14 ++++++++----- plotly/src/callbacks.rs | 21 +++++++++++-------- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73799af2..98a0a09d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,7 +115,7 @@ jobs: strategy: fail-fast: false matrix: - example: [wasm-yew-minimal] + example: [wasm-yew-minimal, wasm-yew-callback-minimal] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 724188f9..e0d69dec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ### Changed - [[#277](https://github.com/plotly/plotly.rs/pull/277)] Removed `wasm` feature flag and put evrything behind target specific dependencies. Added `.cargo/config.toml` for configuration flags needed by `getrandom` version 0.3 on `wasm` targets. - [[#281]((https://github.com/plotly/plotly.rs/pull/xxx))] Update to askama 0.13.0 +- [[#287]](https://github.com/plotly/plotly.rs/pull/287) Added functionality for callbacks (using wasm) - [[#289]](https://github.com/plotly/plotly.rs/pull/289) Fixes Kaleido static export for MacOS targets by removing `--disable-gpu` flag for MacOS - [[#290]](https://github.com/plotly/plotly.rs/pull/289) Remove `--disable-gpu` flag for Kaleido static-image generation for all targets. diff --git a/examples/wasm-yew-callback-minimal/src/main.rs b/examples/wasm-yew-callback-minimal/src/main.rs index e0d6055c..d2e4a04b 100644 --- a/examples/wasm-yew-callback-minimal/src/main.rs +++ b/examples/wasm-yew-callback-minimal/src/main.rs @@ -1,5 +1,5 @@ use plotly::callbacks::ClickEvent; -use plotly::{Histogram, Plot, Scatter, common::Mode}; +use plotly::{Histogram, Plot, Scatter, common::Mode, histogram::Bins}; use web_sys::js_sys::Math; use yew::prelude::*; @@ -23,14 +23,18 @@ pub fn plot_component() -> Html { let id = "plot-div"; let mut fig = Plot::new(); let xs: Vec = (0..50).map(|i| i as f64).collect(); - let ys: Vec = xs.iter().map(|x| x.sin()).collect(); + let ys: Vec = xs.iter().map(|x| x.sin() * 5.0).collect(); fig.add_trace( Scatter::new(xs.clone(), ys.clone()) .mode(Mode::Markers) - .name("Sine markers"), + .name("Sine Wave Markers"), + ); + let random_values: Vec = (0..500).map(|_| Math::random() * 100.0).collect(); + fig.add_trace( + Histogram::new(random_values) + .name("Random Data Histogram") + .x_bins(Bins::new(-1.0, 30.0, 5.0)), ); - let random_values: Vec = (0..100).map(|_| Math::random()).collect(); - fig.add_trace(Histogram::new(random_values).name("Random histogram")); let layout = plotly::Layout::new().title("Click Event Callback Example in Yew"); fig.set_layout(layout); async move { diff --git a/plotly/src/callbacks.rs b/plotly/src/callbacks.rs index 6f6257bd..4a47b562 100644 --- a/plotly/src/callbacks.rs +++ b/plotly/src/callbacks.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; -use web_sys::{js_sys::Function, window, HtmlElement}; +use web_sys::{js_sys::Function, HtmlElement}; /// Provides utilities for binding Plotly.js click events to Rust closures /// via `wasm-bindgen`. @@ -61,22 +61,25 @@ pub fn bind_click(div_id: &str, mut cb: F) where F: 'static + FnMut(ClickEvent), { - let plot_div: PlotlyDiv = window() - .unwrap() - .document() - .unwrap() - .get_element_by_id(div_id) - .unwrap() - .unchecked_into(); let closure = Closure::wrap(Box::new(move |event: JsValue| { let event: ClickEvent = - serde_wasm_bindgen::from_value(event).expect("\n Couldn't serialize the event \n"); + serde_wasm_bindgen::from_value(event).expect("Could not serialize the event"); cb(event); }) as Box); + + let plot_div: PlotlyDiv = get_div(div_id).expect("Could not get Div element by Id"); plot_div.on("plotly_click", closure.as_ref().unchecked_ref()); closure.forget(); } +fn get_div(tag: &str) -> Option { + web_sys::window()? + .document()? + .get_element_by_id(tag)? + .dyn_into() + .ok() +} + /// Represents a single point from a Plotly click event. /// /// Fields mirror Plotly’s `event.points[i]` properties, all optional