use crate::preview::mermaid::chart::xychart::{Plot, PlotKind, XAxis, XyChart};
use crate::preview::mermaid::flowchart::Stroke;
use crate::preview::mermaid::layout::Point;
use super::super::{normalise, Diagram, Glyph, Label, PlacedNode, RenderError, Size, Theme};
use super::{
add_title, axis_tick_texts, bar_node, label_node, legend_nodes, legend_size, rule, text_node,
ticks, LegendEntry, AXIS_TITLE_GAP, LEGEND_GAP, POINT_RADIUS, TICK_GAP, TICK_LEN,
};
pub const PLOT_WIDTH: f64 = 440.0;
pub const PLOT_HEIGHT: f64 = 260.0;
pub const SLOT_PAD: f64 = 6.0;
pub const BAR_GAP: f64 = 2.0;
pub const MIN_SLOT: f64 = 24.0;
pub const Y_TICKS: usize = 5;
pub fn render(code: &str, theme: &str) -> Result<String, RenderError> {
let chart = crate::preview::mermaid::chart::xychart::parse(code)?;
let diagram = lay_out(&chart)?;
Ok(super::super::svg::emit(&diagram, &Theme::named(theme)))
}
pub struct Frame {
pub left: f64,
pub right: f64,
pub top: f64,
pub bottom: f64,
pub min: f64,
pub max: f64,
}
impl Frame {
pub fn value_to_y(&self, v: f64) -> f64 {
let span = self.max - self.min;
if span <= 0.0 {
return self.bottom;
}
self.bottom - (v - self.min) / span * (self.bottom - self.top)
}
pub fn baseline_y(&self) -> f64 {
if self.min <= 0.0 && self.max >= 0.0 {
self.value_to_y(0.0)
} else {
self.bottom
}
}
pub fn slot_center(&self, i: usize, n: usize) -> f64 {
if n == 0 {
return (self.left + self.right) / 2.0;
}
self.left + (self.right - self.left) * (i as f64 + 0.5) / n as f64
}
pub fn slot_width(&self, n: usize) -> f64 {
if n == 0 {
0.0
} else {
(self.right - self.left) / n as f64
}
}
}
pub fn effective_range(chart: &XyChart) -> Option<(f64, f64)> {
let (mut min, mut max) = chart.y;
if !min.is_finite() || !max.is_finite() {
return None;
}
if max < min {
std::mem::swap(&mut min, &mut max);
}
for plot in &chart.plots {
for (_, v) in &plot.data {
if v.is_finite() {
min = min.min(*v);
max = max.max(*v);
}
}
}
if (max - min).abs() < f64::EPSILON {
min -= 0.5;
max += 0.5;
}
Some((min, max))
}
pub fn drawn_len(plot: &Plot, slots: usize) -> usize {
plot.data.len().min(slots)
}
pub fn lay_out(chart: &XyChart) -> Result<Diagram, RenderError> {
if !crate::preview::mermaid::text_metrics::fonts_available() {
return Err(RenderError::NoFonts);
}
let Some((min, max)) = effective_range(chart) else {
return Err(RenderError::ChartHasNoExtent {
what: "the value axis has no range",
});
};
let categories = category_labels(chart);
let widest = categories.iter().map(|l| l.width).fold(0.0_f64, f64::max);
let want_slot = (widest + TICK_GAP * 2.0).max(MIN_SLOT);
let plot_w = PLOT_WIDTH.max(want_slot * categories.len().max(1) as f64);
let tick_values = ticks(min, max, Y_TICKS);
let tick_texts = axis_tick_texts(&tick_values);
let tick_labels: Vec<Label> = tick_texts.iter().map(|t| Label::measure(t)).collect();
let widest_tick = tick_labels.iter().map(|l| l.width).fold(0.0_f64, f64::max);
let y_title = Label::measure(&chart.y_title);
let x_title = Label::measure(&chart.x_title);
let y_title_w = if y_title.is_blank() {
0.0
} else {
y_title.width + AXIS_TITLE_GAP
};
let left = y_title_w + widest_tick + TICK_GAP + TICK_LEN;
let frame = Frame {
left,
right: left + plot_w,
top: 0.0,
bottom: PLOT_HEIGHT,
min,
max,
};
let mut nodes: Vec<PlacedNode> = Vec::new();
let mut edges = Vec::new();
nodes.push(PlacedNode {
id: "plot".to_string(),
shape: Glyph::PlotFrame,
center: Point::new(
(frame.left + frame.right) / 2.0,
(frame.top + frame.bottom) / 2.0,
),
size: Size::new(frame.right - frame.left, frame.bottom - frame.top),
label: Label::measure(""),
panel: None,
series: None,
mark: None,
});
for ((v, text), label) in tick_values
.iter()
.zip(tick_texts.iter())
.zip(tick_labels.iter())
{
let y = frame.value_to_y(*v);
edges.push(rule(
Point::new(frame.left, y),
Point::new(frame.right, y),
Stroke::Dotted,
None,
));
edges.push(rule(
Point::new(frame.left - TICK_LEN, y),
Point::new(frame.left, y),
Stroke::Normal,
None,
));
if let Some(n) = label_node(
format!("ytick#{text}"),
label.clone(),
Point::new(frame.left - TICK_LEN - TICK_GAP - label.width / 2.0, y),
None,
) {
nodes.push(n);
}
}
let n = categories.len();
for (i, label) in categories.iter().enumerate() {
let x = frame.slot_center(i, n);
edges.push(rule(
Point::new(x, frame.bottom),
Point::new(x, frame.bottom + TICK_LEN),
Stroke::Normal,
None,
));
if let Some(node) = label_node(
format!("xtick#{i}"),
label.clone(),
Point::new(x, frame.bottom + TICK_LEN + TICK_GAP + label.height / 2.0),
None,
) {
nodes.push(node);
}
}
let bars: Vec<usize> = chart
.plots
.iter()
.enumerate()
.filter(|(_, p)| p.kind == PlotKind::Bar)
.map(|(i, _)| i)
.collect();
for (i, plot) in chart.plots.iter().enumerate() {
match plot.kind {
PlotKind::Bar => {
let which = bars.iter().position(|b| *b == i).unwrap_or(0);
draw_bars(&mut nodes, &frame, plot, i, which, bars.len(), n);
}
PlotKind::Line => draw_line(&mut nodes, &mut edges, &frame, plot, i, n),
}
}
let x_label_h = categories.iter().map(|l| l.height).fold(0.0_f64, f64::max);
if let Some(node) = label_node(
"xtitle",
x_title.clone(),
Point::new(
(frame.left + frame.right) / 2.0,
frame.bottom + TICK_LEN + TICK_GAP + x_label_h + AXIS_TITLE_GAP + x_title.height / 2.0,
),
None,
) {
nodes.push(node);
}
if let Some(node) = label_node(
"ytitle",
y_title.clone(),
Point::new(y_title.width / 2.0, (frame.top + frame.bottom) / 2.0),
None,
) {
nodes.push(node);
}
let entries: Vec<LegendEntry> = chart
.plots
.iter()
.enumerate()
.filter(|(_, p)| !p.title.trim().is_empty())
.map(|(i, p)| LegendEntry {
label: Label::measure(&p.title),
series: i,
})
.collect();
if !entries.is_empty() {
let size = legend_size(&entries);
nodes.extend(legend_nodes(
&entries,
frame.right + LEGEND_GAP,
(frame.top + frame.bottom) / 2.0 - size.h / 2.0,
));
}
let mut diagram = Diagram {
nodes,
edges,
..Diagram::default()
};
add_title(&mut diagram, &chart.preamble);
normalise(&mut diagram);
Ok(diagram)
}
fn category_labels(chart: &XyChart) -> Vec<Label> {
match &chart.x {
XAxis::Band(categories) if !categories.is_empty() => {
categories.iter().map(|c| Label::measure(c)).collect()
}
_ => chart
.plots
.first()
.map(|p| p.data.iter().map(|(k, _)| Label::measure(k)).collect())
.unwrap_or_default(),
}
}
fn draw_bars(
nodes: &mut Vec<PlacedNode>,
frame: &Frame,
plot: &Plot,
series: usize,
which: usize,
total: usize,
slots: usize,
) {
let slot = frame.slot_width(slots);
let usable = (slot - SLOT_PAD * 2.0).max(2.0);
let each =
((usable - BAR_GAP * (total.saturating_sub(1)) as f64) / total.max(1) as f64).max(1.0);
let base = frame.baseline_y();
for (i, (_, v)) in plot.data.iter().enumerate().take(drawn_len(plot, slots)) {
let cx = frame.slot_center(i, slots) - usable / 2.0
+ which as f64 * (each + BAR_GAP)
+ each / 2.0;
let y = frame.value_to_y(*v);
let (top, bottom) = if y <= base { (y, base) } else { (base, y) };
let h = (bottom - top).max(0.0);
nodes.push(bar_node(
format!("bar#{series}#{i}"),
Point::new(cx, (top + bottom) / 2.0),
Size::new(each, h),
Some(series),
));
}
}
fn draw_line(
nodes: &mut Vec<PlacedNode>,
edges: &mut Vec<super::super::PlacedEdge>,
frame: &Frame,
plot: &Plot,
series: usize,
slots: usize,
) {
let points: Vec<Point> = plot
.data
.iter()
.enumerate()
.take(drawn_len(plot, slots))
.map(|(i, (_, v))| Point::new(frame.slot_center(i, slots), frame.value_to_y(*v)))
.collect();
if points.len() >= 2 {
let mut e = rule(
points[0].clone(),
points[points.len() - 1].clone(),
Stroke::Normal,
Some(series),
);
e.points = points.clone();
edges.push(e);
}
for (i, p) in points.iter().enumerate() {
nodes.push(PlacedNode {
id: format!("point#{series}#{i}"),
shape: Glyph::ChartPoint,
center: p.clone(),
size: Size::new(POINT_RADIUS * 2.0, POINT_RADIUS * 2.0),
label: Label::measure(""),
panel: None,
series: Some(series),
mark: None,
});
if let Some(text) = plot.point_labels.get(i).filter(|t| !t.trim().is_empty()) {
if let Some(n) = text_node(
format!("point#{series}#{i}#label"),
text,
Point::new(
p.x,
p.y - POINT_RADIUS - TICK_GAP - super::super::labels::line_height() / 2.0,
),
None,
) {
nodes.push(n);
}
}
}
}