use crate::preview::mermaid::chart::pie::Pie;
use crate::preview::mermaid::layout::Point;
use super::super::shapes::Mark;
use super::super::{normalise, Diagram, Glyph, Label, PlacedNode, RenderError, Size, Theme};
use super::{
add_title, label_node, legend_nodes, legend_size, LegendEntry, LEGEND_GAP, POINT_RADIUS,
};
pub const RADIUS: f64 = 118.0;
const TURN: f64 = 360.0;
pub fn render(code: &str, theme: &str) -> Result<String, RenderError> {
let pie = crate::preview::mermaid::chart::pie::parse(code)?;
let diagram = lay_out(&pie)?;
Ok(super::super::svg::emit(&diagram, &Theme::named(theme)))
}
pub fn lay_out(pie: &Pie) -> Result<Diagram, RenderError> {
if !crate::preview::mermaid::text_metrics::fonts_available() {
return Err(RenderError::NoFonts);
}
let total = pie.total();
if total <= 0.0 {
return Err(RenderError::ChartHasNoExtent {
what: "every slice is zero, so no slice has a share",
});
}
let mut nodes: Vec<PlacedNode> = Vec::with_capacity(pie.slices.len() * 2 + 1);
let center = Point::new(RADIUS, RADIUS);
let mut start = 0.0_f64;
for (i, slice) in pie.slices.iter().enumerate() {
let sweep = slice.value / total * TURN;
if sweep > 0.0 {
nodes.push(PlacedNode {
id: format!("slice#{i}"),
shape: Glyph::Wedge,
center: center.clone(),
size: Size::new(RADIUS * 2.0, RADIUS * 2.0),
label: Label::measure(""),
panel: None,
series: Some(i),
mark: Some(Mark::Wedge { start, sweep }),
});
}
start += sweep;
}
let mut start = 0.0_f64;
for (i, slice) in pie.slices.iter().enumerate() {
let sweep = slice.value / total * TURN;
if sweep > 0.0 {
let text = percent_text(slice.value / total);
let label = Label::measure(&text);
let mid = (start + sweep / 2.0 - 90.0).to_radians();
let at = RADIUS * 0.62;
let c = Point::new(center.x + at * mid.cos(), center.y + at * mid.sin());
if fits_inside_wedge(&label, at, sweep) {
if let Some(n) = label_node(format!("slice#{i}#pct"), label, c, Some(i)) {
nodes.push(n);
}
}
}
start += sweep;
}
let entries: Vec<LegendEntry> = pie
.slices
.iter()
.enumerate()
.map(|(i, s)| LegendEntry {
label: Label::measure(&if pie.show_data {
format!("{} [{}]", s.label, tidy(s.value))
} else {
s.label.clone()
}),
series: i,
})
.collect();
let size = legend_size(&entries);
let left = center.x + RADIUS + LEGEND_GAP;
let top = center.y - size.h / 2.0;
nodes.extend(legend_nodes(&entries, left, top));
let mut diagram = Diagram {
nodes,
..Diagram::default()
};
add_title(&mut diagram, &pie.preamble);
normalise(&mut diagram);
Ok(diagram)
}
fn fits_inside_wedge(label: &Label, at: f64, sweep_deg: f64) -> bool {
let half = (sweep_deg / 2.0).min(90.0).to_radians();
let chord = 2.0 * at * half.sin();
label.width + POINT_RADIUS * 2.0 <= chord && label.height <= RADIUS - at + RADIUS * 0.3
}
fn percent_text(share: f64) -> String {
let pct = share * 100.0;
if pct < 0.5 && pct > 0.0 {
return "<1%".to_string();
}
format!("{}%", pct.round() as i64)
}
fn tidy(v: f64) -> String {
if v.fract() == 0.0 && v.abs() < 1e15 {
return format!("{}", v as i64);
}
let s = format!("{v:.6}");
s.trim_end_matches('0').trim_end_matches('.').to_string()
}