Skip to main content

orbital_charts/shared/plots/
heatmap_plot.rs

1//! Heatmap cell plot layer.
2
3use leptos::prelude::*;
4
5use crate::context::{use_chart_context, use_heatmap_plot_context, use_x_scale, use_y_scale};
6use crate::engine::{
7    compute_heatmap_cell_layouts, HEATMAP_CANVAS_THRESHOLD, HEATMAP_CELL_GAP_RATIO,
8};
9use crate::shared::marks::HeatmapCellMark;
10use crate::{AxisClickData, ChartScale};
11
12/// Renders heatmap cells as SVG rects (canvas for dense grids on hydrate).
13#[component]
14pub fn HeatmapPlot(
15    /// Gap ratio between cells.
16    #[prop(default = HEATMAP_CELL_GAP_RATIO)]
17    gap_ratio: f64,
18) -> impl IntoView {
19    let ctx = use_chart_context();
20    let highlight_scope = ctx.highlight_scope;
21    let on_axis = ctx.on_axis_click;
22    let heatmap = use_heatmap_plot_context();
23    let x_scale = use_x_scale("x");
24    let y_scale = use_y_scale("y");
25
26    let cells = heatmap.cells.clone();
27    let color_scale = heatmap.color_scale.clone();
28    let value_min = heatmap.value_min;
29    let value_max = heatmap.value_max;
30    let use_canvas = cells.len() > HEATMAP_CANVAS_THRESHOLD;
31    let plot_height = ctx.drawing_area.plot_height;
32
33    view! {
34        {move || {
35            let (x_band, y_band) = match (&x_scale, &y_scale) {
36                (ChartScale::Band(x), ChartScale::Band(y)) => (x.clone(), y.clone()),
37                _ => return ().into_any(),
38            };
39
40            let layouts = compute_heatmap_cell_layouts(
41                &cells,
42                &x_band,
43                &y_band,
44                &color_scale,
45                value_min,
46                value_max,
47                gap_ratio,
48            );
49
50            if use_canvas {
51                #[cfg(feature = "hydrate")]
52                {
53                    return view! {
54                        <HeatmapCanvasLayer layouts=layouts plot_width=ctx.drawing_area.plot_width plot_height=ctx.drawing_area.plot_height />
55                    }
56                    .into_any();
57                }
58                #[cfg(not(feature = "hydrate"))]
59                {
60                    // SSR fallback to SVG
61                }
62            }
63
64            let x_categories = ctx
65                .x_axes
66                .first()
67                .and_then(|a| a.data.clone())
68                .unwrap_or_default();
69            let axis_id = ctx.x_axes.first().map(|a| a.id.clone()).unwrap_or_else(|| "x".into());
70            let click_bands = on_axis.as_ref().map(|cb| {
71                let bw = x_band.bandwidth();
72                x_categories
73                    .iter()
74                    .enumerate()
75                    .map(|(index, _)| {
76                        let x_center = x_band.scale_by_index(index).unwrap_or(0.0);
77                        let cb = *cb;
78                        let axis_id = axis_id.clone();
79                        view! {
80                            <rect
81                                class="orb-axis-click-band"
82                                x=x_center - bw / 2.0
83                                y=plot_height
84                                width=bw
85                                height=8.0
86                                on:click=move |ev| {
87                                    ev.stop_propagation();
88                                    cb.run((AxisClickData {
89                                        axis_id: axis_id.clone(),
90                                        value: index as f64,
91                                        series_values: vec![],
92                                    },));
93                                }
94                            />
95                        }
96                    })
97                    .collect_view()
98            });
99
100            view! {
101                {layouts
102                    .into_iter()
103                    .map(|layout| view! {
104                        <HeatmapCellMark layout=layout highlight_scope=highlight_scope />
105                    })
106                    .collect_view()}
107                {click_bands}
108            }
109            .into_any()
110        }}
111    }
112}
113
114#[cfg(feature = "hydrate")]
115#[component]
116fn HeatmapCanvasLayer(
117    layouts: Vec<crate::HeatmapCellLayout>,
118    plot_width: f64,
119    plot_height: f64,
120) -> impl IntoView {
121    let canvas_ref = NodeRef::<leptos::html::Canvas>::new();
122
123    Effect::new(move |_| {
124        if let Some(canvas) = canvas_ref.get() {
125            use wasm_bindgen::JsCast;
126            use web_sys::Element;
127
128            let w = plot_width.max(1.0) as u32;
129            let h = plot_height.max(1.0) as u32;
130            let el: &Element = canvas.as_ref();
131            let _ = el.set_attribute("width", &w.to_string());
132            let _ = el.set_attribute("height", &h.to_string());
133            if let Ok(Some(ctx)) = canvas.get_context("2d") {
134                if let Ok(ctx) = ctx.dyn_into::<web_sys::CanvasRenderingContext2d>() {
135                    ctx.clear_rect(0.0, 0.0, w as f64, h as f64);
136                    for cell in &layouts {
137                        ctx.set_fill_style_str(&cell.fill);
138                        ctx.fill_rect(cell.x, cell.y, cell.width, cell.height);
139                    }
140                }
141            }
142        }
143    });
144
145    view! {
146        <foreignObject x="0" y="0" width=plot_width height=plot_height>
147            <canvas
148                node_ref=canvas_ref
149                class="orb-heatmap-canvas"
150                style="width: 100%; height: 100%;"
151            />
152        </foreignObject>
153    }
154}