orbital_charts/shared/plots/
heatmap_plot.rs1use 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#[component]
14pub fn HeatmapPlot(
15 #[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 }
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}