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 PieChartConfig {
#[prop_or("center".to_string())]
pub text_align: String,
#[prop_or(true)]
pub show_legend: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DataPoint {
pub name: String,
pub value: i32,
pub color: String,
}
impl DataPoint {
pub fn new(name: &str, value: i32, color: &str) -> Self {
Self {
name: name.into(),
value,
color: color.into(),
}
}
}
#[derive(Clone, Properties, PartialEq, Debug, Eq)]
pub struct PieChartProps {
pub data: Vec<DataPoint>,
#[prop_or(Default::default())]
pub config: PieChartConfig,
}
#[function_component]
pub fn PieChart(props: &PieChartProps) -> 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.8;
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_pie_chart(&context, width, height, &props_clone_resize);
}
};
resize_callback();
let listener = EventListener::new(&window().unwrap(), "resize", move |_event| {
resize_callback();
});
move || drop(listener) });
}
let legend_html = if props.config.show_legend {
html! {
<div style="display: flex; flex-direction: row; gap: 5px; flex-wrap: wrap;">
{ for props.data.iter().map(|data_point| {
html! {
<div style="display: flex; flex-direction: row; align-items: center; gap: 2px;">
<span style="font-size: 10px;">{ &data_point.name }</span>
<div style={format!("background-color: {}; width: 10px; height: 10px; display: inline-block;", &data_point.color)}></div>
</div>
}
})}
</div>
}
} else {
html! {}
};
html! {
<div>
{ legend_html }
<canvas ref={canvas_ref} style="width: 100%; height: 100%;"></canvas>
</div>
}
}
pub fn draw_pie_chart(
context: &CanvasRenderingContext2d,
width: f64,
height: f64,
props: &PieChartProps,
) {
let data = props
.data
.iter()
.map(|data_point| data_point.value)
.collect::<Vec<i32>>();
let colors = props
.data
.iter()
.map(|data_point| data_point.color.clone())
.collect::<Vec<String>>();
let total: f64 = data.iter().sum::<i32>() as f64;
let mut start_angle = 0.0;
for (i, &value) in data.iter().enumerate() {
let slice_angle = value as f64 / total * std::f64::consts::PI * 2.0;
context.begin_path();
context.move_to(width / 2.0, height / 2.0);
context
.arc(
width / 2.0,
height / 2.0,
(width.min(height) / 2.0) - 5.0,
start_angle,
start_angle + slice_angle,
)
.unwrap();
context.close_path();
context.set_fill_style(&JsValue::from_str(colors[i].as_str()));
context.fill();
start_angle += slice_angle;
}
}
#[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_pie_chart() {
let context = mock_context();
let width = 800.0;
let height = 600.0;
let props = PieChartProps {
data: vec![
DataPoint::new("A", 10, ""),
DataPoint::new("B", 20, ""),
DataPoint::new("C", 30, ""),
DataPoint::new("D", 40, ""),
],
config: PieChartConfig::default(),
};
draw_pie_chart(&context, width, height, &props);
assert!(true);
}
}