microBioRust_heatmap/
lib.rs

1//! # A Heatmap in Rust web assembly calling d3.js
2//!
3//! You will need to use wasm-pack to build instead of cargo 
4//!  wasm-pack build --target web
5//! And some way of serving locally 
6//!  http-server .
7//! It requires the index.html in the static directory
8//! Currently working with fixed data
9//! and a rusty colour theme
10
11use wasm_bindgen::prelude::*;
12use std::error::Error;
13use serde_wasm_bindgen;
14use serde::{Serialize, Deserialize};
15use wasm_bindgen::JsValue;
16use web_sys::{window, Document, HtmlCanvasElement, CanvasRenderingContext2d};
17use web_sys::console;
18use std::rc::Rc;
19use std::cell::RefCell;
20
21
22pub fn draw_responsive_heatmap(
23    context: &CanvasRenderingContext2d,
24    values: Vec<Vec<i32>>,
25    x_labels: Vec<String>,
26    y_labels: Vec<String>,
27    canvas_width: f64,
28    canvas_height: f64,
29    device_pixel_ratio: f64,
30) {
31    let rows = values.len();
32    let cols = values[0].len();
33    console::log_1(&JsValue::from_str(&format!("up in the draw function")));
34    // Get canvas dimensions 
35    // Calculate dynamic padding and box size
36    let adj_canvas_width = canvas_width * device_pixel_ratio;
37    let adj_canvas_height = canvas_height * device_pixel_ratio;
38    let padding_left = adj_canvas_width * 0.05;
39    let padding_top = adj_canvas_height * 0.05;
40    let padding_bottom = adj_canvas_height * 0.05;
41    let padding_right = adj_canvas_width * 0.05;
42
43  //  let box_width = (adj_canvas_width - padding_left - padding_right) / (cols as f64 * 1.1);
44  //  let box_height = (adj_canvas_height - padding_top - padding_bottom) / (rows as f64 * 1.1);
45   
46    let box_width = 30.0;
47    let box_height = 30.0;
48    // Clear the canvas
49    console::log_1(&JsValue::from_str(&format!("pad left {} pad bottom {}",&padding_left, &padding_bottom)));
50    context.clear_rect(0.0, 0.0, adj_canvas_width, adj_canvas_height);
51    println!("cleared rec");
52    // Draw the heatmap
53    for row in 0..rows {
54        for col in 0..cols {
55            let value = values[row][col];
56
57            // Set color based on value
58            let color = match value {
59                0 => "#fee0d2",
60                1 => "#fc9272",
61                2 => "#de2d26",
62                _ => "#FFFFFF",
63            };
64            context.set_fill_style(&JsValue::from_str(color));
65
66            let x = padding_left + (col as f64 * box_width);
67            let y = padding_top + (row as f64 * box_height);
68            context.fill_rect(x, y, box_width, box_height);
69
70            // Draw box borders
71            context.set_stroke_style(&JsValue::from_str("#FFFFFF"));
72            context.set_line_width(2.0 / device_pixel_ratio);
73            
74            if row < rows - 1 {
75                context.begin_path();
76                context.move_to(x, y + box_height);
77                context.line_to(x + box_width, y + box_height);
78                context.stroke();
79            }
80
81            if col < cols - 1 {
82                context.begin_path();
83                context.move_to(x + box_width, y);
84                context.line_to(x + box_width, y + box_height);
85                context.stroke();
86            }
87        }
88    }
89    console::log_1(&JsValue::from_str(&format!(
90    "after the rows and cols padding bottom: {}, height: {}",
91         &padding_bottom,
92         &(box_height * rows as f64),
93            )));
94
95    // Draw X-axis
96    context.begin_path();
97    context.set_stroke_style(&JsValue::from_str("#000000"));
98    context.move_to(padding_left, (box_height * rows as f64) + padding_bottom);
99    context.line_to((box_height * rows as f64) + padding_bottom, (box_height * rows as f64) + padding_left);
100    context.stroke();
101    
102    // Draw Y-axis
103    context.begin_path();
104    context.move_to(padding_left, padding_top);
105    context.line_to(padding_left, (box_height * rows as f64) + padding_bottom);
106    context.stroke();
107
108    // Draw X-axis ticks and labels
109    let label_font_size = (box_height * 0.3).min(box_width * 0.3).max(12.0);
110    context.set_font(&format!("{}px Arial", label_font_size));
111    context.set_text_align("center");
112    context.set_text_baseline("top");
113    
114    for col in 0..cols {
115        let x = padding_left + col as f64 * box_width + box_width / 2.0;
116        let y = (box_height * rows as f64) + padding_bottom + 5.0;  // Position below the heatmap
117        context.fill_text(&x_labels[col], x, y).unwrap();
118
119        // Draw ticks
120        context.begin_path();
121        context.move_to(x, (box_height * rows as f64) + padding_bottom);
122        context.line_to(x, (box_height * rows as f64) + padding_bottom + 5.0);
123        context.stroke();
124    }
125
126    // Draw Y-axis ticks and labels
127    context.set_text_align("right");
128    context.set_text_baseline("middle");
129    
130    for row in 0..rows {
131        let x = padding_left - 10.0;  // Position to the left of the heatmap
132        let y = padding_top + row as f64 * box_height + box_height / 2.0;
133        context.fill_text(&y_labels[row], x, y).unwrap();
134
135        // Draw ticks
136        context.begin_path();
137        context.move_to(padding_left, y);
138        context.line_to(padding_left - 5.0, y);
139        context.stroke();
140    }
141    console::log_1(&JsValue::from_str(&format!(
142    "at the end of draw funct Canvas width: {}, height: {}",
143        &adj_canvas_width,
144        &adj_canvas_height
145         )));
146}
147
148#[derive(Serialize, Deserialize, Clone, Debug)]
149pub struct HeatmapData {
150   values: Vec<Vec<i32>>,
151   x_labels: Vec<String>,
152   y_labels: Vec<String>,
153}
154
155impl HeatmapData {
156    // Constructor method
157    pub fn new() -> Self {
158        HeatmapData {
159            values: vec![vec![0]],
160            x_labels: Vec::new(),
161            y_labels: Vec::new(),
162        }
163    }
164}
165
166
167//returns a JsValue to javascript
168#[wasm_bindgen(start)]
169pub fn start() -> Result<(), JsValue> {
170    // Get the window and document
171    console::log_1(&JsValue::from_str(&format!("literal start")));
172    let window = window().expect("should have a window in this context");
173    let window = Rc::new(window);
174    let window_clone = Rc::clone(&window);
175    let document = window.document().expect("should have a document on window");
176    console::log_1(&JsValue::from_str(&format!("up in the start of the function")));
177    // Get the canvas element
178    let canvas = document
179        .get_element_by_id("heatmap")
180        .expect("Canvas element not found")
181        .dyn_into::<HtmlCanvasElement>()?;
182    console::log_1(&JsValue::from_str(&format!("called the canvas")));
183    let heatmap_values = vec![
184        vec![2, 1, 0, 1, 0],  // row 1
185        vec![1, 2, 0, 0, 1],  // row 2
186        vec![2, 0, 1, 2, 1],  // row 3
187        vec![0, 0, 0, 2, 0],  // row 4
188        vec![1, 2, 0, 1, 1], // row 5
189    ];
190    console::log_1(&JsValue::from_str(&format!("called the heatmap vals")));
191    let x_labels: Vec<String> = vec!["A", "B", "C", "D", "E"].iter().map(|s| s.to_string()).collect();
192    let y_labels: Vec<String> = vec!["R1", "R2", "R3", "R4", "R5"].iter().map(|s| s.to_string()).collect();
193    
194    let num_rows = heatmap_values.len();      // Should be 5
195    let num_cols = heatmap_values[0].len();   // Should be 5
196    let mut heatmap_data = HeatmapData::new();
197    heatmap_data = HeatmapData { values: heatmap_values.clone(), x_labels: x_labels.clone(), y_labels: y_labels.clone() };
198    let box_size = 100.0;
199    let device_pixel_ratio = window.device_pixel_ratio();
200    console::log_1(&JsValue::from_str(&format!("num rows are {:?} num cols are {:?}", &num_rows, &num_cols)));
201    
202    // Dynamically set canvas size based on number of rows and columns
203    let canvas_width = num_cols as f64 * box_size;  // 6 columns * 50px
204    let canvas_height = num_rows as f64 * box_size; // 6 rows * 50px
205    canvas.set_width(canvas_width as u32);
206    canvas.set_height(canvas_height as u32);
207    console::log_1(&JsValue::from_str(&format!(
208        "Canvas width: {}, height: {}",
209        canvas.width(),
210        canvas.height()
211         )));
212
213    let context = canvas
214        .get_context("2d")?
215        .unwrap()
216        .dyn_into::<CanvasRenderingContext2d>()?;
217
218    // Define the heatmap matrix (3x3) with values representing different colors
219    context.scale(device_pixel_ratio, device_pixel_ratio);
220    
221    draw_responsive_heatmap(
222            &context,
223            heatmap_values.clone(),
224            x_labels.clone(),
225            y_labels.clone(),
226            canvas_width,
227            canvas_height,
228            device_pixel_ratio,
229        );
230   
231    let closure = Closure::wrap(Box::new(move || {
232        let new_width = window_clone.inner_width().unwrap().as_f64().unwrap();
233        let new_height = window_clone.inner_height().unwrap().as_f64().unwrap();
234    
235        // Calculate canvas size based on window size
236        let canvas_new_width = (num_cols as f64 * box_size).min(new_width);
237        let canvas_new_height = (num_rows as f64 * box_size).min(new_height);
238    
239        canvas.set_width(canvas_new_width as u32);
240        canvas.set_height(canvas_new_height as u32);
241    
242        // Reset the scaling
243        context.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0).unwrap();
244        context.scale(device_pixel_ratio, device_pixel_ratio).unwrap();
245
246        draw_responsive_heatmap(
247           &context,
248           heatmap_values.clone(),
249           x_labels.clone(),
250           y_labels.clone(),
251           canvas_new_width.into(),
252           canvas_new_height.into(),
253           device_pixel_ratio,
254        );
255    }) as Box<dyn FnMut()>);
256    
257    window.add_event_listener_with_callback("resize", closure.as_ref().unchecked_ref()).unwrap();
258    closure.forget();
259
260    Ok(())
261}