use crate::preview::mermaid::chart::quadrant::{QuadrantChart, QuadrantPoint};
use crate::preview::mermaid::flowchart::Stroke;
use crate::preview::mermaid::layout::Point;
use super::super::{
normalise, Diagram, Glyph, Label, PlacedNode, RenderError, ShapeStyle, Size, Theme,
};
use super::{add_title, label_node, rule, text_node, POINT_RADIUS, TICK_GAP};
pub const SIDE: f64 = 340.0;
pub const WORD_GAP: f64 = 8.0;
pub const CORNER_PAD: f64 = 8.0;
pub const DOT_RADIUS: f64 = 4.5;
pub fn render(code: &str, theme: &str) -> Result<String, RenderError> {
let chart = crate::preview::mermaid::chart::quadrant::parse(code)?;
let diagram = lay_out(&chart)?;
Ok(super::super::svg::emit(&diagram, &Theme::named(theme)))
}
pub fn place(left: f64, top: f64, side: f64, x: f64, y: f64) -> Point {
Point::new(left + x * side, top + (1.0 - y) * side)
}
fn square_side(chart: &QuadrantChart) -> f64 {
let w = |t: &str| Label::measure(t).width;
[
w(&chart.x_left) + w(&chart.x_right),
w(&chart.quadrant2) + w(&chart.quadrant1),
w(&chart.quadrant3) + w(&chart.quadrant4),
]
.into_iter()
.fold(SIDE, |side, pair| side.max(pair + WORD_GAP * 2.0))
}
pub fn lay_out(chart: &QuadrantChart) -> Result<Diagram, RenderError> {
if !crate::preview::mermaid::text_metrics::fonts_available() {
return Err(RenderError::NoFonts);
}
let (left, top) = (0.0, 0.0);
let side = square_side(chart);
let (right, bottom) = (side, side);
let mid_x = left + side / 2.0;
let mid_y = top + side / 2.0;
let mut nodes: Vec<PlacedNode> = Vec::new();
let mut edges = Vec::new();
nodes.push(PlacedNode {
id: "frame".to_string(),
shape: Glyph::PlotFrame,
center: Point::new(mid_x, mid_y),
size: Size::new(side, side),
label: Label::measure(""),
panel: None,
series: None,
mark: None,
style: None,
});
edges.push(rule(
Point::new(mid_x, top),
Point::new(mid_x, bottom),
Stroke::Normal,
None,
));
edges.push(rule(
Point::new(left, mid_y),
Point::new(right, mid_y),
Stroke::Normal,
None,
));
for (text, qx, qy) in [
(&chart.quadrant1, 1.0, 1.0),
(&chart.quadrant2, 0.0, 1.0),
(&chart.quadrant3, 0.0, 0.0),
(&chart.quadrant4, 1.0, 0.0),
] {
let label = Label::measure(text);
if label.is_blank() {
continue;
}
let cx = left + side * 0.25 + qx * side * 0.5;
let band_top = top + (1.0 - qy) * side * 0.5;
let cy = band_top + CORNER_PAD + label.height / 2.0;
if let Some(n) = label_node(
format!(
"quadrant#{}",
if qy > 0.5 { qx as u8 } else { 2 + qx as u8 }
),
label,
Point::new(cx, cy),
None,
) {
nodes.push(n);
}
}
for (i, p) in chart.points.iter().enumerate() {
let c = place(left, top, side, p.x, p.y);
nodes.push(PlacedNode {
id: format!("point#{i}"),
shape: Glyph::ChartPoint,
center: c.clone(),
size: Size::new(DOT_RADIUS * 2.0, DOT_RADIUS * 2.0),
label: Label::measure(""),
panel: None,
series: Some(i),
mark: None,
style: quadrant_style(&chart.class_defs, p),
});
let label = Label::measure(&p.label);
if let Some(n) = label_node(
format!("point#{i}#label"),
label.clone(),
Point::new(c.x, c.y + DOT_RADIUS + TICK_GAP + label.height / 2.0),
None,
) {
nodes.push(n);
}
}
let x_label_gap = POINT_RADIUS + TICK_GAP * 2.0;
for (text, cx, below) in [
(&chart.x_left, left + side * 0.25, true),
(&chart.x_right, left + side * 0.75, true),
] {
let label = Label::measure(text);
if let Some(n) = label_node(
format!(
"xaxis#{}",
if below && cx < mid_x { "left" } else { "right" }
),
label.clone(),
Point::new(cx, bottom + x_label_gap + label.height / 2.0),
None,
) {
nodes.push(n);
}
}
for (text, cy, name) in [
(&chart.y_bottom, top + side * 0.75, "bottom"),
(&chart.y_top, top + side * 0.25, "top"),
] {
let label = Label::measure(text);
if let Some(n) = label_node(
format!("yaxis#{name}"),
label.clone(),
Point::new(left - x_label_gap - label.width / 2.0, cy),
None,
) {
nodes.push(n);
}
}
let _ = text_node("", "", Point::new(0.0, 0.0), None);
let mut diagram = Diagram {
nodes,
edges,
..Diagram::default()
};
add_title(&mut diagram, &chart.preamble);
normalise(&mut diagram);
Ok(diagram)
}
fn quadrant_style(
class_defs: &[(String, Vec<String>)],
point: &QuadrantPoint,
) -> Option<ShapeStyle> {
let mut style = ShapeStyle::default();
let apply_class = |style: &mut ShapeStyle, name: &str| {
if let Some((_, decls)) = class_defs.iter().find(|(n, _)| n == name) {
for d in decls {
apply_quadrant_decl(style, d);
}
}
};
apply_class(&mut style, "default");
if let Some(c) = &point.class {
apply_class(&mut style, c);
}
for d in &point.styles {
apply_quadrant_decl(&mut style, d);
}
if style.is_empty() {
None
} else {
Some(style)
}
}
fn apply_quadrant_decl(style: &mut ShapeStyle, decl: &str) {
let Some((key, value)) = decl.split_once(':') else {
return;
};
match key.trim() {
"color" => style.apply(&format!("fill:{}", value.trim())),
"stroke-color" => style.apply(&format!("stroke:{}", value.trim())),
"stroke-width" => style.apply(decl),
_ => {}
}
}