Skip to content

Commit 4b563eb

Browse files
h4ck4l1andrei-ng
authored andcommitted
Added functionality for callbacks
1 parent 1ecb8ac commit 4b563eb

File tree

8 files changed

+261
-0
lines changed

8 files changed

+261
-0
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "wasm-yew-callback-minimal"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
plotly = { path = "../../plotly" }
8+
yew = "0.21"
9+
yew-hooks = "0.3"
10+
log = "0.4"
11+
wasm-logger = "0.2"
12+
web-sys = { version = "0.3.77"}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Wasm Yew Minimal
2+
3+
## Prerequisites
4+
5+
1. Install [Trunk](https://trunkrs.dev/) using `cargo install --locked trunk`.
6+
7+
## How to Run
8+
9+
1. Run `trunk serve --open` in this directory to build and serve the application, opening the default web browser automatically.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="utf-8" />
6+
<title>Plotly Yew</title>
7+
<script src="https://cdn.plot.ly/plotly-2.14.0.min.js"></script>
8+
</head>
9+
10+
<body></body>
11+
12+
</html>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use plotly::{Plot,common::Mode, Scatter,Histogram};
2+
use plotly::callbacks::{ClickEvent};
3+
use web_sys::js_sys::Math;
4+
use yew::prelude::*;
5+
6+
7+
#[function_component(App)]
8+
pub fn plot_component() -> Html {
9+
10+
let x = use_state(|| None::<f64>);
11+
let y = use_state(|| None::<f64>);
12+
let point_numbers = use_state(|| None::<Vec<usize>>);
13+
let point_number = use_state(|| None::<usize>);
14+
let curve_number = use_state(|| 0usize);
15+
let click_event = use_state(|| ClickEvent::default());
16+
17+
let x_clone = x.clone();
18+
let y_clone = y.clone();
19+
let curve_clone = curve_number.clone();
20+
let point_numbers_clone = point_numbers.clone();
21+
let point_number_clone = point_number.clone();
22+
let click_event_clone = click_event.clone();
23+
24+
let p = yew_hooks::use_async::<_, _, ()>({
25+
let id = "plot-div";
26+
let mut fig = Plot::new();
27+
let xs: Vec<f64> = (0..50).map(|i| i as f64).collect();
28+
let ys: Vec<f64> = xs.iter().map(|x| x.sin()).collect();
29+
fig.add_trace(
30+
Scatter::new(xs.clone(), ys.clone())
31+
.mode(Mode::Markers)
32+
.name("Sine markers")
33+
);
34+
let random_values: Vec<f64> = (0..100)
35+
.map(|_| Math::random())
36+
.collect();
37+
fig.add_trace(
38+
Histogram::new(random_values)
39+
.name("Random histogram")
40+
);
41+
let layout = plotly::Layout::new().title("Click Event Callback Example in Yew");
42+
fig.set_layout(layout);
43+
async move {
44+
plotly::bindings::new_plot(id, &fig).await;
45+
plotly::callbacks::bind_click(id, move |event| {
46+
let pt = &event.points[0];
47+
x_clone.set(pt.x);
48+
y_clone.set(pt.y);
49+
curve_clone.set(pt.curve_number);
50+
point_numbers_clone.set(pt.point_numbers.clone());
51+
point_number_clone.set(pt.point_number);
52+
click_event_clone.set(event);
53+
});
54+
Ok(())
55+
}
56+
});
57+
// Only on first render
58+
use_effect_with((), move |_| {
59+
p.run();
60+
});
61+
62+
html! {
63+
<>
64+
<div id="plot-div"></div>
65+
<div>
66+
<p>{format!("x: {:?}",*x)}</p>
67+
<p>{format!("y: {:?}",*y)}</p>
68+
<p>{format!("curveNumber: {:?}",*curve_number)}</p>
69+
<p>{format!("pointNumber: {:?}",*point_number)}</p>
70+
<p>{format!("pointNumbers: {:?}",*point_numbers)}</p>
71+
<p>{format!("ClickEvent: {:?}",*click_event)}</p>
72+
</div>
73+
</>
74+
}
75+
}
76+
77+
fn main() {
78+
wasm_logger::init(wasm_logger::Config::default());
79+
yew::Renderer::<App>::new().render();
80+
}

plotly/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ rand = "0.9"
4040
getrandom = { version = "0.3", features = ["wasm_js"] }
4141
wasm-bindgen-futures = { version = "0.4" }
4242
wasm-bindgen = { version = "0.2" }
43+
serde-wasm-bindgen = {version = "0.6.3"}
44+
web-sys = { version = "0.3.77", features = ["Document", "Window", "HtmlElement"]}
4345

4446
[dev-dependencies]
4547
csv = "1.1"

plotly/src/bindings.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ extern "C" {
2525
pub async fn new_plot(id: &str, plot: &Plot) {
2626
let plot_obj = &plot.to_js_object();
2727

28+
2829
// This will only fail if the Rust Plotly library has produced
2930
// plotly-incompatible JSON. An error here should have been handled by the
3031
// library, rather than down here.

plotly/src/callbacks.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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+
}

plotly/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ pub use crate::ndarray::ArrayTraces;
1919
#[cfg(target_family = "wasm")]
2020
pub mod bindings;
2121

22+
#[cfg(target_family = "wasm")]
23+
pub mod callbacks;
24+
2225
pub mod common;
2326
pub mod configuration;
2427
pub mod layout;

0 commit comments

Comments
 (0)