Skip to content

Added functionality for callbacks #287

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions examples/wasm-yew-callback-minimal/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"}
9 changes: 9 additions & 0 deletions examples/wasm-yew-callback-minimal/README.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 12 additions & 0 deletions examples/wasm-yew-callback-minimal/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">

<head>
<meta charset="utf-8" />
<title>Plotly Yew</title>
<script src="https://cdn.plot.ly/plotly-2.14.0.min.js"></script>
</head>

<body></body>

</html>
80 changes: 80 additions & 0 deletions examples/wasm-yew-callback-minimal/src/main.rs
Original file line number Diff line number Diff line change
@@ -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::<f64>);
let y = use_state(|| None::<f64>);
let point_numbers = use_state(|| None::<Vec<usize>>);
let point_number = use_state(|| None::<usize>);
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<f64> = (0..50).map(|i| i as f64).collect();
let ys: Vec<f64> = 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<f64> = (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! {
<>
<div id="plot-div"></div>
<div>
<p>{format!("x: {:?}",*x)}</p>
<p>{format!("y: {:?}",*y)}</p>
<p>{format!("curveNumber: {:?}",*curve_number)}</p>
<p>{format!("pointNumber: {:?}",*point_number)}</p>
<p>{format!("pointNumbers: {:?}",*point_numbers)}</p>
<p>{format!("ClickEvent: {:?}",*click_event)}</p>
</div>
</>
}
}

fn main() {
wasm_logger::init(wasm_logger::Config::default());
yew::Renderer::<App>::new().render();
}
2 changes: 2 additions & 0 deletions plotly/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions plotly/src/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
142 changes: 142 additions & 0 deletions plotly/src/callbacks.rs
Original file line number Diff line number Diff line change
@@ -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 `<div>` 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 `<div>`.
///
/// 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 `<div>`.
///
/// # 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 `<div>`.
///
/// # Type Parameters
/// - `F`: A `'static + FnMut(ClickEvent)` closure type to handle the click data.
///
/// # Parameters
/// - `div_id`: The DOM `id` attribute of the Plotly `<div>`.
/// - `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<dyn FnMut(JsValue)>` 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<F>(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<dyn FnMut(JsValue)>);
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<Vec<usize>>,
pub point_number: Option<usize>,
pub x: Option<f64>,
pub y: Option<f64>,
pub z: Option<f64>,
pub lat: Option<f64>,
pub lon: Option<f64>
}


/// 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<ClickPoint> {vec![ClickPoint::default()]}


/// The top-level payload for a Plotly click event.
///
/// - `points`: A `Vec<ClickPoint>` 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<ClickPoint>
}

/// 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![] }
}
}
3 changes: 3 additions & 0 deletions plotly/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading