use crate::preview::mermaid::chart::radar::{Graticule, Radar};
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, data_path, label_node, legend_nodes, legend_size, LegendEntry, LEGEND_GAP, TICK_GAP,
};
pub const RADIUS: f64 = 120.0;
pub const LABEL_GAP: f64 = 8.0;
pub fn render(code: &str, theme: &str) -> Result<String, RenderError> {
let radar = crate::preview::mermaid::chart::radar::parse(code)?;
let diagram = lay_out(&radar)?;
Ok(super::super::svg::emit(&diagram, &Theme::named(theme)))
}
pub fn radius_of(v: f64, min: f64, max: f64) -> f64 {
if max <= min {
return 0.0;
}
RADIUS * (v.clamp(min, max) - min) / (max - min)
}
fn is_drawable(curve: &crate::preview::mermaid::chart::radar::Curve, axes: usize) -> bool {
curve.entries.len() == axes
}
pub fn angle_of(k: usize, n: usize) -> f64 {
if n == 0 {
return -std::f64::consts::FRAC_PI_2;
}
-std::f64::consts::FRAC_PI_2 + std::f64::consts::TAU * k as f64 / n as f64
}
pub fn lay_out(radar: &Radar) -> Result<Diagram, RenderError> {
if !crate::preview::mermaid::text_metrics::fonts_available() {
return Err(RenderError::NoFonts);
}
let n = radar.axes.len();
if n < 3 {
return Err(RenderError::ChartHasNoExtent {
what: "a radar chart needs at least three axes",
});
}
let (min, max) = (radar.options.min, radar.max());
if max <= min || !max.is_finite() {
return Err(RenderError::ChartHasNoExtent {
what: "the radar scale has no width",
});
}
let center = Point::new(RADIUS, RADIUS);
let mut nodes: Vec<PlacedNode> = Vec::new();
let mut edges = Vec::new();
nodes.push(PlacedNode {
id: "graticule".to_string(),
shape: Glyph::Graticule,
center: center.clone(),
size: Size::new(RADIUS * 2.0, RADIUS * 2.0),
label: Label::measure(""),
panel: None,
series: None,
mark: Some(Mark::Graticule {
rings: radar.options.ticks.max(1),
spokes: n,
polygon: radar.options.graticule == Graticule::Polygon,
}),
style: None,
});
for (k, axis) in radar.axes.iter().enumerate() {
let a = angle_of(k, n);
let label = Label::measure(&axis.label);
let out = RADIUS + LABEL_GAP;
let cx = center.x + out * a.cos() + a.cos() * label.width / 2.0;
let cy = center.y + out * a.sin() + a.sin() * label.height / 2.0;
if let Some(node) = label_node(
format!("axis#{}", axis.name),
label,
Point::new(cx, cy),
None,
) {
nodes.push(node);
}
}
for (i, curve) in radar.curves.iter().enumerate() {
if !is_drawable(curve, n) {
continue;
}
let mut points: Vec<Point> = (0..n)
.map(|k| {
let a = angle_of(k, n);
let r = radius_of(curve.entries[k], min, max);
Point::new(center.x + r * a.cos(), center.y + r * a.sin())
})
.collect();
points.push(points[0].clone());
edges.push(data_path(points, i));
}
if !edges.iter().any(|e| e.series.is_some()) {
return Err(RenderError::ChartHasNoExtent {
what: "no curve has an entry for every axis, so there is nothing to plot",
});
}
if radar.options.show_legend {
let entries: Vec<LegendEntry> = radar
.curves
.iter()
.enumerate()
.map(|(i, c)| LegendEntry {
label: Label::measure(&c.label),
series: i,
})
.collect();
if !entries.is_empty() {
let size = legend_size(&entries);
let right = nodes
.iter()
.map(|node| node.bounds().2)
.fold(f64::NEG_INFINITY, f64::max);
nodes.extend(legend_nodes(
&entries,
right + LEGEND_GAP,
center.y - size.h / 2.0,
));
}
}
let _ = TICK_GAP;
let mut diagram = Diagram {
nodes,
edges,
..Diagram::default()
};
add_title(&mut diagram, &radar.preamble);
normalise(&mut diagram);
Ok(diagram)
}