Skip to main content

plotting/
canvas.rs

1//! Canvas renderer for the browser. Uses `plotters-canvas::CanvasBackend`
2//! and the shared `draw_on` dispatch in `crate::draw`, so a chart
3//! rendered into a `<canvas>` looks identical to its SVG twin.
4//!
5//! Only compiles under feature `canvas`, which is enabled by the
6//! `nomisync-frontend` WASM crate.
7
8use plotters::prelude::*;
9use plotters_canvas::CanvasBackend;
10use wasm_bindgen::JsValue;
11use web_sys::HtmlCanvasElement;
12
13use crate::draw::draw_on;
14use crate::spec::ChartSpec;
15
16/// Render `spec` into `canvas` via `plotters-canvas`. Sizes the
17/// backing canvas to `device_pixel_ratio` so the chart stays crisp on
18/// `HiDPI` screens; the caller is expected to set the CSS width/height
19/// to the logical size before calling.
20///
21/// # Errors
22///
23/// Returns `Err(JsValue)` when the canvas element cannot be wrapped
24/// (missing 2D context or the shared drawing dispatch fails).
25pub fn render_canvas(spec: &ChartSpec, canvas: &HtmlCanvasElement) -> Result<(), JsValue> {
26    apply_dpr(canvas)?;
27
28    let backend = CanvasBackend::with_canvas_object(canvas.clone())
29        .ok_or_else(|| JsValue::from_str("plotters-canvas: failed to get 2D context"))?;
30    let root = backend.into_drawing_area();
31
32    draw_on(&root, spec).map_err(|e| JsValue::from_str(&format!("draw failed: {e}")))?;
33    root.present()
34        .map_err(|e| JsValue::from_str(&format!("present failed: {e}")))?;
35    Ok(())
36}
37
38/// Multiply the backing store by the window's device pixel ratio so
39/// strokes and text stay crisp. CSS width/height (the logical size)
40/// must be set by the caller — this function only touches the
41/// `width`/`height` attributes on the canvas element.
42fn apply_dpr(canvas: &HtmlCanvasElement) -> Result<(), JsValue> {
43    let dpr = web_sys::window()
44        .ok_or_else(|| JsValue::from_str("no window"))?
45        .device_pixel_ratio();
46    if dpr <= 0.0 || !dpr.is_finite() {
47        return Ok(());
48    }
49
50    let logical_w = f64::from(canvas.client_width().max(1));
51    let logical_h = f64::from(canvas.client_height().max(1));
52    let backing_w = (logical_w * dpr).round().max(1.0) as u32;
53    let backing_h = (logical_h * dpr).round().max(1.0) as u32;
54
55    // Only resize the backing store when the canvas is attached and has
56    // non-zero CSS dimensions. Otherwise leave the author's width/height
57    // attributes untouched.
58    if logical_w > 0.0 && logical_h > 0.0 {
59        canvas.set_width(backing_w);
60        canvas.set_height(backing_h);
61    }
62    Ok(())
63}