use leptos::html;
use leptos::prelude::*;
use lodviz_core::core::data::DataPoint;
use lodviz_core::core::scale::{LinearScale, Scale};
use std::ops::Deref;
use wasm_bindgen::JsCast;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RenderMode {
Svg,
Canvas,
#[default]
Auto,
}
impl RenderMode {
pub fn resolve(&self, total_points: usize, series_count: usize) -> ResolvedRenderMode {
match self {
Self::Svg => ResolvedRenderMode::Svg,
Self::Canvas => ResolvedRenderMode::Canvas,
Self::Auto => {
if total_points > 5000 || series_count > 10 {
ResolvedRenderMode::Canvas
} else {
ResolvedRenderMode::Svg
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResolvedRenderMode {
Svg,
Canvas,
}
pub type SeriesRenderData = Vec<(String, Vec<DataPoint>, String, bool)>;
#[component]
pub fn CanvasLineRenderer(
width: Signal<f64>,
height: Signal<f64>,
series: Signal<SeriesRenderData>,
x_scale: Signal<LinearScale>,
y_scale: Signal<LinearScale>,
#[prop(default = 2.0)]
line_width: f64,
) -> impl IntoView {
let canvas_ref = NodeRef::<html::Canvas>::new();
let dpr = web_sys::window()
.and_then(|w| w.device_pixel_ratio().into())
.unwrap_or(1.0);
let canvas_width = Signal::derive(move || (width.get() * dpr) as u32);
let canvas_height = Signal::derive(move || (height.get() * dpr) as u32);
Effect::new(move |_| {
let Some(canvas_el) = canvas_ref.get() else {
return;
};
let w = width.get();
let h = height.get();
let canvas: HtmlCanvasElement = canvas_el.deref().clone().unchecked_into();
let ctx: CanvasRenderingContext2d = canvas
.get_context("2d")
.ok()
.flatten()
.expect("Canvas 2D context")
.unchecked_into();
let _ = ctx.scale(dpr, dpr);
ctx.clear_rect(0.0, 0.0, w, h);
let x_scale_val = x_scale.get();
let y_scale_val = y_scale.get();
for (_, points, color, visible) in series.get() {
if !visible || points.is_empty() {
continue;
}
ctx.begin_path();
ctx.set_stroke_style_str(&color);
ctx.set_line_width(line_width);
ctx.set_line_cap("round");
ctx.set_line_join("round");
for (i, point) in points.iter().enumerate() {
let x = x_scale_val.map(point.x);
let y = y_scale_val.map(point.y);
if i == 0 {
ctx.move_to(x, y);
} else {
ctx.line_to(x, y);
}
}
ctx.stroke();
}
});
view! {
<canvas
node_ref=canvas_ref
width=move || canvas_width.get()
height=move || canvas_height.get()
style=move || {
format!(
"width: {}px; height: {}px; position: absolute; top: 0; left: 0; pointer-events: none;",
width.get(),
height.get(),
)
}
></canvas>
}
}
#[component]
pub fn CanvasScatterRenderer(
width: Signal<f64>,
height: Signal<f64>,
series: Signal<SeriesRenderData>,
x_scale: Signal<LinearScale>,
y_scale: Signal<LinearScale>,
#[prop(default = 4.0)]
point_radius: f64,
#[prop(default = 0.7)]
opacity: f64,
) -> impl IntoView {
let canvas_ref = NodeRef::<html::Canvas>::new();
let dpr = web_sys::window()
.and_then(|w| w.device_pixel_ratio().into())
.unwrap_or(1.0);
let canvas_width = Signal::derive(move || (width.get() * dpr) as u32);
let canvas_height = Signal::derive(move || (height.get() * dpr) as u32);
Effect::new(move |_| {
let Some(canvas_el) = canvas_ref.get() else {
return;
};
let w = width.get();
let h = height.get();
let canvas: HtmlCanvasElement = canvas_el.deref().clone().unchecked_into();
let ctx: CanvasRenderingContext2d = canvas
.get_context("2d")
.ok()
.flatten()
.expect("Canvas 2D context")
.unchecked_into();
let _ = ctx.scale(dpr, dpr);
ctx.clear_rect(0.0, 0.0, w, h);
let x_scale_val = x_scale.get();
let y_scale_val = y_scale.get();
ctx.set_global_alpha(opacity);
for (_, points, color, visible) in series.get() {
if !visible || points.is_empty() {
continue;
}
ctx.set_fill_style_str(&color);
for point in points.iter() {
let x = x_scale_val.map(point.x);
let y = y_scale_val.map(point.y);
ctx.begin_path();
let _ = ctx.arc(x, y, point_radius, 0.0, std::f64::consts::TAU);
ctx.fill();
}
}
});
view! {
<canvas
node_ref=canvas_ref
width=move || canvas_width.get()
height=move || canvas_height.get()
style=move || {
format!(
"width: {}px; height: {}px; position: absolute; top: 0; left: 0; pointer-events: none;",
width.get(),
height.get(),
)
}
></canvas>
}
}