use dioxus::prelude::*;
use super::primitives::{generate_svg_area_path, generate_svg_path, CurveType};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum TrendDirection {
#[default]
Auto,
Positive,
Negative,
Neutral,
}
#[component]
pub fn Sparkline(
data: Vec<f64>,
#[props(default)] trend: TrendDirection,
#[props(default = true)] with_fill: bool,
#[props(default)] curve_type: CurveType,
#[props(default = 2.0)] stroke_width: f64,
#[props(default = 120)] width: u32,
#[props(default = 36)] height: u32,
#[props(default)] color: Option<String>,
#[props(default)] class: String,
) -> Element {
if data.is_empty() {
return rsx! {
div { class: "inline-block {class}", style: "width: {width}px; height: {height}px;" }
};
}
let min_val = data.iter().copied().fold(f64::INFINITY, f64::min);
let max_val = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let range = if (max_val - min_val).abs() < f64::EPSILON {
1.0
} else {
max_val - min_val
};
let w = width as f64;
let h = height as f64;
let pad_y = stroke_width + 1.0;
let plot_h = (h - pad_y * 2.0).max(1.0);
let n = data.len();
let x_step = if n > 1 { w / (n - 1) as f64 } else { w };
let coords: Vec<(f64, f64)> = data
.iter()
.enumerate()
.map(|(i, &v)| {
let cx = i as f64 * x_step;
let cy = pad_y + plot_h - ((v - min_val) / range) * plot_h;
(cx, cy)
})
.collect();
let resolved_color = if let Some(c) = color {
c
} else {
match trend {
TrendDirection::Positive => "#00ff9d".to_string(), TrendDirection::Negative => "#ff1744".to_string(), TrendDirection::Neutral => "#a1a1aa".to_string(), TrendDirection::Auto => {
let first = data.first().copied().unwrap_or(0.0);
let last = data.last().copied().unwrap_or(0.0);
if last > first {
"#00ff9d".to_string()
} else if last < first {
"#ff1744".to_string()
} else {
"#a855f7".to_string() }
}
}
};
let line_path = generate_svg_path(&coords, curve_type);
let area_path = generate_svg_area_path(&coords, h, curve_type);
let grad_id = crate::a11y::use_unique_id("spark-grad", None);
let mut hover_index = use_signal(|| None::<usize>);
rsx! {
div {
class: "relative inline-block select-none shrink-0 {class}",
style: "width: {width}px; height: {height}px;",
onmousemove: move |evt| {
let c = evt.element_coordinates();
if c.x < 0.0 || c.x > w || c.y < -5.0 || c.y > h + 5.0 {
hover_index.set(None);
return;
}
let ratio = (c.x / w).clamp(0.0, 1.0);
let idx = (ratio * (n - 1) as f64).round() as usize;
hover_index.set(Some(idx.min(n - 1)));
},
onmouseleave: move |_| {
hover_index.set(None);
},
onpointerleave: move |_| {
hover_index.set(None);
},
svg {
class: "w-full h-full overflow-visible",
view_box: "0 0 {width} {height}",
onmouseleave: move |_| {
hover_index.set(None);
},
onpointerleave: move |_| {
hover_index.set(None);
},
defs {
linearGradient {
id: "{grad_id}",
x1: "0",
y1: "0",
x2: "0",
y2: "1",
stop {
offset: "0%",
stop_color: "{resolved_color}",
stop_opacity: "0.35",
}
stop {
offset: "100%",
stop_color: "{resolved_color}",
stop_opacity: "0.0",
}
}
}
if with_fill {
path {
d: "{area_path}",
fill: "url(#{grad_id})",
pointer_events: "none",
}
}
path {
d: "{line_path}",
fill: "none",
stroke: "{resolved_color}",
stroke_width: "{stroke_width}",
stroke_linecap: "round",
stroke_linejoin: "round",
style: "filter: drop-shadow(0 0 4px {resolved_color}88);",
pointer_events: "none",
}
if let Some(idx) = hover_index() {
if idx < coords.len() {
{
let (cx, cy) = coords[idx];
rsx! {
circle {
cx: "{cx}",
cy: "{cy}",
r: "3.5",
fill: "{resolved_color}",
stroke: "#ffffff",
stroke_width: "1.5",
pointer_events: "none",
}
}
}
}
}
}
if let Some(idx) = hover_index() {
if idx < data.len() {
{
let val = data[idx];
let (cx, cy) = coords[idx];
let is_right = cx > w / 2.0;
let (pos_style, transform) = if is_right {
(format!("left: {cx}px; top: {cy}px;"), "transform: translate(-100%, -50%); margin-left: -8px;")
} else {
(format!("left: {cx}px; top: {cy}px;"), "transform: translateY(-50%); margin-left: 8px;")
};
rsx! {
div {
class: "pointer-events-none absolute z-[1000] px-2 py-0.5 rounded-md bg-zinc-950/95 border border-white/20 text-[11px] font-mono font-bold text-white shadow-xl backdrop-blur-md whitespace-nowrap",
style: "{pos_style} {transform}",
"{val:.2}"
}
}
}
}
}
}
}
}