|
| 1 | +//! SVG-fidelity spike: a minimal sparkline drawn as an inline `<svg><polyline>` |
| 2 | +//! in pure Dioxus (no JS interop), fed from a typed `Vec<f64>`. Proves whether |
| 3 | +//! rsx!-driven SVG gives the fidelity later chart/topology screens need. |
| 4 | +
|
| 5 | +use dioxus::prelude::*; |
| 6 | + |
| 7 | +#[derive(Clone, PartialEq, Props)] |
| 8 | +pub struct SparklineProps { |
| 9 | + /// Y values; X is the index. Normalized to a 100x24 viewBox. |
| 10 | + pub points: Vec<f64>, |
| 11 | +} |
| 12 | + |
| 13 | +#[component] |
| 14 | +pub fn Sparkline(props: SparklineProps) -> Element { |
| 15 | + let pts = &props.points; |
| 16 | + if pts.len() < 2 { |
| 17 | + return rsx! { svg { width: "100", height: "24", "viewBox": "0 0 100 24" } }; |
| 18 | + } |
| 19 | + let max = pts.iter().cloned().fold(f64::MIN, f64::max); |
| 20 | + let min = pts.iter().cloned().fold(f64::MAX, f64::min); |
| 21 | + let span = (max - min).max(f64::EPSILON); |
| 22 | + let n = (pts.len() - 1) as f64; |
| 23 | + let coords: String = pts |
| 24 | + .iter() |
| 25 | + .enumerate() |
| 26 | + .map(|(i, y)| { |
| 27 | + let x = (i as f64 / n) * 100.0; |
| 28 | + let yy = 24.0 - ((y - min) / span) * 24.0; |
| 29 | + format!("{x:.1},{yy:.1}") |
| 30 | + }) |
| 31 | + .collect::<Vec<_>>() |
| 32 | + .join(" "); |
| 33 | + rsx! { |
| 34 | + svg { width: "100", height: "24", "viewBox": "0 0 100 24", |
| 35 | + polyline { |
| 36 | + points: "{coords}", |
| 37 | + fill: "none", |
| 38 | + stroke: "currentColor", |
| 39 | + "stroke-width": "1.5", |
| 40 | + } |
| 41 | + } |
| 42 | + } |
| 43 | +} |
| 44 | + |
| 45 | +#[cfg(test)] |
| 46 | +mod tests { |
| 47 | + use super::*; |
| 48 | + |
| 49 | + fn render(app: fn() -> Element) -> String { |
| 50 | + let mut dom = VirtualDom::new(app); |
| 51 | + dom.rebuild_in_place(); |
| 52 | + dioxus_ssr::render(&dom) |
| 53 | + } |
| 54 | + |
| 55 | + #[test] |
| 56 | + fn renders_polyline_from_points() { |
| 57 | + fn app() -> Element { |
| 58 | + rsx! { Sparkline { points: vec![1.0, 4.0, 2.0, 8.0, 5.0] } } |
| 59 | + } |
| 60 | + let html = render(app); |
| 61 | + assert!(html.contains("<svg")); |
| 62 | + assert!(html.contains("<polyline")); |
| 63 | + assert!(html.contains("points=")); |
| 64 | + } |
| 65 | + |
| 66 | + #[test] |
| 67 | + fn degenerate_input_renders_empty_svg() { |
| 68 | + fn app() -> Element { |
| 69 | + rsx! { Sparkline { points: vec![1.0] } } |
| 70 | + } |
| 71 | + let html = render(app); |
| 72 | + assert!(html.contains("<svg")); |
| 73 | + assert!(!html.contains("<polyline")); |
| 74 | + } |
| 75 | +} |
0 commit comments