use dioxus::prelude::*;
use std::f64::consts::PI;
use super::primitives::ChartContainer;
#[component]
pub fn GaugeChart(
value: f64,
#[props(default = 0.0)] min: f64,
#[props(default = 100.0)] max: f64,
#[props(default = 14)] thickness: u32,
#[props(default)] label: Option<String>,
#[props(default)] sublabel: Option<String>,
#[props(default)] color: Option<String>,
#[props(default)] thresholds: Vec<(f64, String)>,
#[props(default = 220)] height: u32,
#[props(default)] class: String,
) -> Element {
let (lo, hi) = if min <= max { (min, max) } else { (max, min) };
let clamped_val = value.clamp(lo, hi);
let range = if (max - min).abs() < f64::EPSILON { 1.0 } else { max - min };
let fraction = (clamped_val - min) / range;
let svg_w = 260.0;
let svg_h = 200.0;
let center_x = svg_w / 2.0;
let center_y = 135.0;
let radius = 85.0;
let start_angle = 150.0 * (PI / 180.0);
let total_sweep = 240.0 * (PI / 180.0);
let current_sweep = fraction * total_sweep;
let end_angle = start_angle + total_sweep;
let value_angle = start_angle + current_sweep;
let resolved_color = if let Some(c) = color {
c
} else if !thresholds.is_empty() {
let mut chosen = "#a855f7".to_string();
for (thresh, col) in &thresholds {
if clamped_val >= *thresh {
chosen = col.clone();
}
}
chosen
} else {
"#a855f7".to_string()
};
let bg_x1 = center_x + radius * start_angle.cos();
let bg_y1 = center_y + radius * start_angle.sin();
let bg_x2 = center_x + radius * end_angle.cos();
let bg_y2 = center_y + radius * end_angle.sin();
let bg_arc_d = format!(
"M {:.2} {:.2} A {:.2} {:.2} 0 1 1 {:.2} {:.2}",
bg_x1, bg_y1, radius, radius, bg_x2, bg_y2
);
let val_x2 = center_x + radius * value_angle.cos();
let val_y2 = center_y + radius * value_angle.sin();
let large_arc = if current_sweep > PI { 1 } else { 0 };
let val_arc_d = format!(
"M {:.2} {:.2} A {:.2} {:.2} 0 {} 1 {:.2} {:.2}",
bg_x1, bg_y1, radius, radius, large_arc, val_x2, val_y2
);
rsx! {
div { class: "flex flex-col items-center select-none {class}",
ChartContainer {
view_box: "0 0 {svg_w} {svg_h}",
height: height,
path {
d: "{bg_arc_d}",
fill: "none",
stroke: "rgba(255, 255, 255, 0.08)",
stroke_width: "{thickness}",
stroke_linecap: "round",
}
if fraction > 0.001 {
path {
d: "{val_arc_d}",
fill: "none",
stroke: "{resolved_color}",
stroke_width: "{thickness}",
stroke_linecap: "round",
style: "filter: drop-shadow(0 0 8px {resolved_color}aa);",
}
}
text {
x: "{center_x}",
y: "{center_y - 8.0}",
text_anchor: "middle",
fill: "#ffffff",
font_size: "28",
font_weight: "bold",
font_family: "monospace",
"{clamped_val:.1}"
}
if let Some(lbl) = &label {
text {
x: "{center_x}",
y: "{center_y + 14.0}",
text_anchor: "middle",
fill: "rgba(255, 255, 255, 0.7)",
font_size: "12",
font_family: "monospace",
"{lbl}"
}
}
if let Some(sub) = &sublabel {
text {
x: "{center_x}",
y: "{center_y + 30.0}",
text_anchor: "middle",
fill: "rgba(255, 255, 255, 0.4)",
font_size: "10.5",
font_family: "monospace",
"{sub}"
}
}
text {
x: "{bg_x1 - 4.0}",
y: "{bg_y1 + 16.0}",
text_anchor: "middle",
fill: "rgba(255, 255, 255, 0.35)",
font_size: "10",
font_family: "monospace",
"{min:.0}"
}
text {
x: "{bg_x2 + 4.0}",
y: "{bg_y2 + 16.0}",
text_anchor: "middle",
fill: "rgba(255, 255, 255, 0.35)",
font_size: "10",
font_family: "monospace",
"{max:.0}"
}
}
}
}
}
#[component]
pub fn RadialBar(
value: f64,
#[props(default = 0.0)] min: f64,
#[props(default = 100.0)] max: f64,
#[props(default = 10)] thickness: u32,
#[props(default = 60.0)] radius: f64,
#[props(default)] color: Option<String>,
#[props(default)] label: Option<String>,
#[props(default = 160)] height: u32,
#[props(default)] class: String,
) -> Element {
let (lo, hi) = if min <= max { (min, max) } else { (max, min) };
let clamped_val = value.clamp(lo, hi);
let range = if (max - min).abs() < f64::EPSILON { 1.0 } else { max - min };
let fraction = (clamped_val - min) / range;
let circumference = 2.0 * PI * radius;
let stroke_offset = circumference * (1.0 - fraction);
let resolved_color = color.unwrap_or_else(|| "#00f0ff".to_string());
let size = (radius * 2.0 + thickness as f64 * 2.0 + 20.0).max(120.0);
let center = size / 2.0;
rsx! {
div { class: "flex flex-col items-center select-none {class}",
ChartContainer {
view_box: "0 0 {size} {size}",
height: height,
circle {
cx: "{center}",
cy: "{center}",
r: "{radius}",
fill: "none",
stroke: "rgba(255, 255, 255, 0.08)",
stroke_width: "{thickness}",
}
circle {
cx: "{center}",
cy: "{center}",
r: "{radius}",
fill: "none",
stroke: "{resolved_color}",
stroke_width: "{thickness}",
stroke_dasharray: "{circumference}",
stroke_dashoffset: "{stroke_offset}",
stroke_linecap: "round",
transform: "rotate(-90 {center} {center})",
style: "filter: drop-shadow(0 0 6px {resolved_color}aa); transition: stroke-dashoffset 0.3s ease;",
}
text {
x: "{center}",
y: if label.is_some() { "{center - 2.0}" } else { "{center + 5.0}" },
text_anchor: "middle",
fill: "#ffffff",
font_size: "18",
font_weight: "bold",
font_family: "monospace",
"{(fraction * 100.0):.0}%"
}
if let Some(lbl) = &label {
text {
x: "{center}",
y: "{center + 14.0}",
text_anchor: "middle",
fill: "rgba(255, 255, 255, 0.45)",
font_size: "10.5",
font_family: "monospace",
"{lbl}"
}
}
}
}
}
}