use super::constants::*;
use super::parser::PieDiagram;
use super::templates::{self, build_style, esc, fmt, fmt_value};
use crate::text::measure;
use crate::theme::Theme;
use std::f64::consts::PI;
struct ArcDatum {
label: String,
value: f64,
start_angle: f64, end_angle: f64,
}
fn create_pie_arcs(sections: &indexmap::IndexMap<String, f64>) -> Vec<ArcDatum> {
let sum: f64 = sections.values().sum();
if sum == 0.0 {
return Vec::new();
}
let filtered: Vec<(&String, &f64)> = sections
.iter()
.filter(|(_, v)| ((*v) / sum) * 100.0 >= 1.0)
.collect();
let filtered_sum: f64 = filtered.iter().map(|(_, v)| **v).sum();
let mut arcs = Vec::new();
let mut current_angle = 0.0_f64;
for (label, value) in &filtered {
let fraction = **value / filtered_sum;
let sweep = fraction * 2.0 * PI;
let start = current_angle;
let end = current_angle + sweep;
arcs.push(ArcDatum {
label: (*label).clone(),
value: **value,
start_angle: start,
end_angle: end,
});
current_angle = end;
}
arcs
}
fn arc_path(start_angle: f64, end_angle: f64, radius: f64) -> String {
let x0 = start_angle.sin() * radius;
let y0 = -start_angle.cos() * radius; let x1 = end_angle.sin() * radius;
let y1 = -end_angle.cos() * radius;
let sweep = end_angle - start_angle;
let large_arc = if sweep > PI { 1 } else { 0 };
format!(
"M{},{:.3}A{},{},0,{},1,{},{:.3}L0,0Z",
fmt(x0),
y0,
fmt(radius),
fmt(radius),
large_arc,
fmt(x1),
y1,
)
}
fn arc_centroid(start_angle: f64, end_angle: f64, radius: f64) -> (f64, f64) {
let mid = (start_angle + end_angle) / 2.0;
let x = mid.sin() * radius;
let y = -mid.cos() * radius;
(x, y)
}
fn slice_color(index: usize) -> &'static str {
PIE_COLORS[index % PIE_COLORS.len()]
}
pub fn render(diag: &PieDiagram, theme: Theme, _use_foreign_object: bool) -> String {
let vars = theme.resolve();
let ff = vars.font_family;
let radius = (PIE_WIDTH.min(HEIGHT) / 2.0) - MARGIN; let label_radius = radius * TEXT_POSITION;
let arcs = create_pie_arcs(&diag.sections);
let total_sum: f64 = diag.sections.values().sum();
let filtered_arcs: Vec<&ArcDatum> = arcs
.iter()
.filter(|a| (a.value / total_sum * 100.0).round() as u64 != 0)
.collect();
let all_sections: Vec<(&String, &f64)> = diag.sections.iter().collect();
let legend_text_width = all_sections
.iter()
.map(|(label, value)| {
let text = if diag.show_data {
format!("{} [{}]", label, fmt_value(**value))
} else {
(*label).clone()
};
measure(&text, LEGEND_FONT_SIZE).0
})
.fold(0.0_f64, f64::max)
* LEGEND_TEXT_SCALE;
let chart_and_legend_width =
PIE_WIDTH + MARGIN + LEGEND_RECT_SIZE + LEGEND_SPACING + legend_text_width;
let title_text_str = diag.title.as_deref().unwrap_or("");
let title_width = if title_text_str.is_empty() {
0.0
} else {
measure(title_text_str, TITLE_FONT_SIZE).0
};
let title_left = PIE_WIDTH / 2.0 - title_width / 2.0;
let title_right = PIE_WIDTH / 2.0 + title_width / 2.0;
let view_box_x = 0.0_f64.min(title_left);
let view_box_right = chart_and_legend_width.max(title_right);
let total_width = view_box_right - view_box_x;
let id = "mermaid-pie";
let style = build_style(id, ff);
let group_tx = PIE_WIDTH / 2.0; let group_ty = HEIGHT / 2.0;
let mut svg_parts: Vec<String> = Vec::new();
svg_parts.push(templates::svg_root(
id,
&fmt(view_box_x),
&fmt(total_width),
&fmt(HEIGHT),
&fmt(total_width),
));
svg_parts.push(format!("<style>{}</style>", style));
svg_parts.push("<g></g>".to_string());
svg_parts.push(templates::main_group(&fmt(group_tx), &fmt(group_ty)));
svg_parts.push(templates::outer_circle(&fmt(
radius + OUTER_STROKE_WIDTH / 2.0
)));
for (i, arc) in filtered_arcs.iter().enumerate() {
let color_idx = diag.sections.get_index_of(&arc.label).unwrap_or(i);
let color = slice_color(color_idx);
let d = arc_path(arc.start_angle, arc.end_angle, radius);
svg_parts.push(templates::pie_slice(&d, color));
}
for arc in &filtered_arcs {
let pct = (arc.value / total_sum * 100.0).round() as u64;
let (cx, cy) = arc_centroid(arc.start_angle, arc.end_angle, label_radius);
svg_parts.push(templates::slice_label(&fmt(cx), &fmt(cy), pct));
}
let title_y = -((HEIGHT - 50.0) / 2.0); svg_parts.push(templates::title_text(&fmt(title_y), &esc(title_text_str)));
let legend_height = LEGEND_RECT_SIZE + LEGEND_SPACING;
let legend_offset = (legend_height * all_sections.len() as f64) / 2.0;
for (i, (label, value)) in all_sections.iter().enumerate() {
let color = slice_color(i);
let vertical = (i as f64) * legend_height - legend_offset;
let legend_text = if diag.show_data {
format!("{} [{}]", label, fmt_value(**value))
} else {
(*label).to_string()
};
svg_parts.push(templates::legend_item(
&fmt(LEGEND_HORIZONTAL_OFFSET),
&fmt(vertical),
color,
&esc(&legend_text),
));
}
svg_parts.push("</g>".to_string());
svg_parts.push("</svg>".to_string());
svg_parts.join("")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagrams::pie::parser;
#[test]
fn basic_render_produces_svg() {
let input = "pie\n \"Dogs\" : 386\n \"Cats\" : 85\n \"Rats\" : 15";
let diag = parser::parse(input).diagram;
let svg = render(&diag, Theme::Default, false);
assert!(svg.contains("<svg"));
assert!(svg.contains("pieCircle"));
assert!(svg.contains("pieOuterCircle"));
assert!(svg.contains("79%"));
assert!(svg.contains("Dogs"));
}
#[test]
fn arc_path_full_circle_large_arc() {
let path = arc_path(-PI / 2.0, PI / 2.0 * 3.0, 185.0);
assert!(
path.contains(",0,1,1,"),
"Expected large-arc-flag=1 in: {}",
path
);
}
#[test]
fn fmt_value_integer() {
assert_eq!(fmt_value(386.0), "386");
assert_eq!(fmt_value(42.96), "42.96");
}
#[test]
fn pie_color_0_is_valid() {
assert!(
PIE_COLORS[0].starts_with('#')
|| PIE_COLORS[0].starts_with("hsl(")
|| PIE_COLORS[0].starts_with("rgb("),
"Expected a valid color string, got: {}",
PIE_COLORS[0]
);
}
#[test]
fn pie_color_1_is_valid() {
assert!(
PIE_COLORS[1].starts_with('#')
|| PIE_COLORS[1].starts_with("hsl(")
|| PIE_COLORS[1].starts_with("rgb("),
"Expected a valid color string, got: {}",
PIE_COLORS[1]
);
}
#[test]
fn snapshot_default_theme() {
let input = "pie title Pets\n \"Dogs\" : 386\n \"Cats\" : 85\n \"Rats\" : 15";
let diag = parser::parse(input).diagram;
let svg = render(&diag, crate::theme::Theme::Default, false);
insta::assert_snapshot!(crate::svg::normalize_floats(&svg));
}
}