use dioxus::prelude::*;
use crate::types::{Labels, Point};
use crate::utils::{normalize_series, polar_to_cartesian};
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum LabelPosition {
Inside,
Outside,
Center,
}
#[derive(Clone, PartialEq, Props)]
pub struct PieChartProps {
series: Vec<f32>,
#[props(optional)]
labels: Option<Labels>,
#[props(default = "100%".to_string(), into)]
width: String,
#[props(default = "100%".to_string(), into)]
height: String,
#[props(default = 600)]
viewbox_width: i32,
#[props(default = 400)]
viewbox_height: i32,
#[props(default = true)]
show_labels: bool,
#[props(default=LabelPosition::Inside)]
label_position: LabelPosition,
#[props(default)]
label_offset: f32,
#[props(optional)]
label_interpolation: Option<fn(f32) -> String>,
#[props(default)]
start_angle: f32,
#[props(optional)]
total: Option<f32>,
#[props(optional)]
show_ratio: Option<f32>,
#[props(default)]
padding: f32,
#[props(default = false)]
donut: bool,
#[props(default = 40.0)]
donut_width: f32,
#[props(default = "dx-pie-chart".to_string(), into)]
class_chart: String,
#[props(default = "dx-series".to_string(), into)]
class_series: String,
#[props(default = "dx-slice".to_string(), into)]
class_slice: String,
#[props(default = "dx-label".to_string(), into)]
class_label: String,
}
#[allow(non_snake_case)]
pub fn PieChart(props: PieChartProps) -> Element {
if props.series.is_empty() {
return rsx!("Pie chart error: empty series");
}
let center = Point::new(
props.viewbox_width as f32 / 2.0,
props.viewbox_height as f32 / 2.0,
);
let center_min = center.x.min(center.y);
let radius = center_min - 30.0 - props.padding;
let label_radius = match props.label_position {
LabelPosition::Inside => radius / 2.0 + props.label_offset,
LabelPosition::Outside => radius + props.label_offset,
LabelPosition::Center => 0.0 + props.label_offset,
};
let normalized_series = normalize_series(&props.series);
let normalized_sum: f32 = normalized_series.iter().sum();
let values_total: f32 = if let Some(r) = props.show_ratio {
1.0 / r.clamp(0.0001, 1.0) * normalized_sum
} else if let Some(v) = props.total {
(normalized_sum / props.series.iter().sum::<f32>() * v).max(normalized_sum)
} else {
normalized_sum
};
let mut m_start_angle = props.start_angle;
let mut color_var = 255.0;
let mut class_index = 0;
let mut label_positions = Vec::<Point>::new();
let normalized_series_rsx = normalized_series.iter().filter_map(|v| {
if *v != 0.0 {
let mut end_angle = if values_total > 0.0 {
m_start_angle + (v / values_total) * 360.0
} else {
0.0
};
let overlap_start_angle = if class_index != 0 {
(m_start_angle - 0.4).max(0.0)
} else {
m_start_angle
};
if end_angle - overlap_start_angle >= 359.99 {
end_angle = overlap_start_angle + 359.99
}
let start_position = polar_to_cartesian(center, radius, overlap_start_angle);
let end_position = polar_to_cartesian(center, radius, end_angle);
let large_arc = i32::from(end_angle - m_start_angle > 180.0);
let dpath = if props.donut {
let donut_radius = radius - props.donut_width;
let start_inside_position = polar_to_cartesian(center, donut_radius, overlap_start_angle);
let end_inside_position = polar_to_cartesian(center, donut_radius, end_angle);
let large_arc_inside = large_arc;
format!("M{end_position}\
A{radius},{radius},0,{large_arc},0,{start_position}\
L{start_inside_position}\
A{donut_radius},{donut_radius},0,{large_arc_inside},1,{end_inside_position}Z")
} else {
format!("M{end_position}\
A{radius},{radius},0,{large_arc},0,{start_position}\
L{center}Z")
};
let element = rsx! {
g {
class: "{props.class_series} {props.class_series}-{class_index}",
path {
d: "{dpath}",
class: "{props.class_slice}",
fill: "rgb({color_var}, 40, 40)",
},
}
};
label_positions.push(polar_to_cartesian(center, label_radius, m_start_angle + (end_angle - m_start_angle) / 2.0));
color_var -= 75.0 * (1.0 / (class_index + 1) as f32);
class_index += 1;
m_start_angle = end_angle;
Some(element)
} else {
label_positions.push(Point::new(-1.0, -1.0));
None
}
});
rsx! {
div {
svg {
view_box: "0 0 {props.viewbox_width} {props.viewbox_height}",
width: "{props.width}",
height: "{props.height}",
class: "{props.class_chart}",
preserve_aspect_ratio: "xMidYMid meet",
xmlns: "http://www.w3.org/2000/svg",
{normalized_series_rsx}
if let Some(ref labels) = props.labels {
g {
{
label_positions.iter().zip(labels.iter()).filter_map(|(position, label)| {
if position.x > 0.0 {
Some(rsx! {
text {
dx: "{position.x}",
dy: "{position.y}",
text_anchor: "middle",
class: "{props.class_label}",
alignment_baseline: "middle",
"{label}"
}
})
} else {
None
}
})
}
}
} else if props.show_labels {
g {
{
label_positions.iter().zip(props.series.iter()).filter_map(|(position, value)| {
let label = if let Some(func) = props.label_interpolation {
func(*value)
} else {
value.to_string()
};
if position.x > 0.0 {
Some(rsx! {
text {
dx: "{position.x}",
dy: "{position.y}",
text_anchor: "middle",
class: "{props.class_label}",
alignment_baseline: "middle",
"{label}"
}
})
} else {
None
}
})
}
}
}
}
}
}
}