use gloo::events::EventListener;
use web_sys::{
wasm_bindgen::{JsCast, JsValue},
window, CanvasRenderingContext2d, HtmlCanvasElement,
};
use yew::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, Properties, Default)]
pub struct BarChartConfig {
#[prop_or_default]
pub bar_color: String,
#[prop_or_default]
pub grid_color: String,
#[prop_or_default]
pub axis_color: String,
}
impl BarChartConfig {
pub fn new(bar_color: &str, grid_color: &str, axis_color: &str) -> Self {
Self {
bar_color: bar_color.into(),
grid_color: grid_color.into(),
axis_color: axis_color.into(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Properties)]
pub struct DataPoint {
pub name: String,
pub value: i32,
}
impl DataPoint {
pub fn new(name: &str, value: i32) -> Self {
Self {
name: name.into(),
value,
}
}
}
#[derive(Clone, Properties, PartialEq, Debug, Eq)]
pub struct BarChartProps {
pub data: Vec<DataPoint>,
#[prop_or(Default::default())]
pub config: BarChartConfig,
}
#[function_component]
pub fn BarChart(props: &BarChartProps) -> Html {
let canvas_ref = use_node_ref();
{
let canvas_ref = canvas_ref.clone();
let props_clone = props.clone();
use_effect_with((), move |_| {
let canvas = canvas_ref
.cast::<HtmlCanvasElement>()
.expect("Failed to get canvas element");
let context = canvas
.get_context("2d")
.unwrap()
.unwrap()
.dyn_into::<CanvasRenderingContext2d>()
.unwrap();
let props_clone_resize = props_clone.clone();
let resize_callback = {
let canvas_ref = canvas_ref.clone();
move || {
let canvas = canvas_ref
.cast::<HtmlCanvasElement>()
.expect("Failed to get canvas element");
let device_pixel_ratio = window().unwrap().device_pixel_ratio();
let parent = canvas.parent_element().unwrap();
let width = parent.client_width() as f64;
let height = width * 0.6;
canvas.set_width((width * device_pixel_ratio) as u32);
canvas.set_height((height * device_pixel_ratio) as u32);
context
.scale(device_pixel_ratio, device_pixel_ratio)
.unwrap();
draw_bar_chart(&context, width, height, &props_clone_resize);
}
};
resize_callback();
let listener = EventListener::new(&window().unwrap(), "resize", move |_event| {
resize_callback();
});
move || drop(listener) });
}
html! {
<canvas ref={canvas_ref} style="width: 90%; height: 90%;"></canvas>
}
}
fn draw_bar_chart(
context: &CanvasRenderingContext2d,
width: f64,
height: f64,
props: &BarChartProps,
) {
let data = props
.data
.iter()
.map(|point| point.value)
.collect::<Vec<i32>>();
let num_bars = (data.len() + 2) as f64; let total_spacing = width * 0.1; let total_bar_width = width - total_spacing;
let bar_width = total_bar_width / (num_bars * 3.0); let bar_spacing = total_spacing / (num_bars - 1.0);
let axis_padding = 50.0;
context.clear_rect(0.0, 0.0, width, height);
let max_value = *data.iter().max().unwrap() as f64 * 1.2; let num_grid_lines = 5;
let step_value = max_value / num_grid_lines as f64;
let step_height = (height - axis_padding * 2.0) / num_grid_lines as f64;
context.set_stroke_style(&JsValue::from_str("#cccccc"));
context.set_line_width(1.0);
context.set_fill_style(&JsValue::from_str("black"));
context.set_text_align("right");
context.set_text_baseline("middle");
for i in 0..=num_grid_lines {
let y = height - axis_padding - i as f64 * step_height;
context.begin_path();
context.move_to(axis_padding, y);
context.line_to(width, y);
context.stroke();
let label = (i as f64 * step_value).round();
context
.fill_text(&format!("{}", label), axis_padding - 10.0, y)
.unwrap();
}
let bar_color = props.config.bar_color.clone();
context.set_fill_style(&JsValue::from_str(bar_color.as_str()));
for (i, &value) in data.iter().enumerate() {
let x = axis_padding + i as f64 * (bar_width + bar_spacing);
let y = height - axis_padding - value as f64 * ((height - axis_padding * 2.0) / max_value);
context.fill_rect(x, y, bar_width, height - axis_padding - y);
}
context.set_fill_style(&JsValue::from_str("black"));
context.set_text_align("center");
context.set_text_baseline("middle");
let labels = props
.data
.iter()
.map(|point| &point.name)
.collect::<Vec<&String>>();
for (i, &label) in labels.iter().enumerate() {
let x = axis_padding + i as f64 * (bar_width + bar_spacing) + bar_width / 2.0;
let y = height - axis_padding / 2.0;
context.fill_text(label, x, y).unwrap();
}
}
#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
fn mock_context() -> CanvasRenderingContext2d {
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document
.create_element("canvas")
.unwrap()
.dyn_into::<web_sys::HtmlCanvasElement>()
.unwrap();
canvas
.get_context("2d")
.unwrap()
.unwrap()
.dyn_into::<CanvasRenderingContext2d>()
.unwrap()
}
#[wasm_bindgen_test]
fn test_draw_bar_chart() {
let context = mock_context();
let width = 500.0;
let height = 400.0;
let data = vec![
DataPoint::new("A", 10),
DataPoint::new("B", 20),
DataPoint::new("C", 15),
];
let props = BarChartProps {
data,
config: BarChartConfig::new("blue", "gray", "black"),
};
draw_bar_chart(&context, width, height, &props);
assert!(true);
}
}