use crate::preview::mermaid::chart::treemap::{Node, Treemap};
use crate::preview::mermaid::layout::Point;
use super::super::labels::line_height;
use super::super::panel::{Panel, PanelCell, PanelRow};
use super::super::{normalise, Diagram, Label, PlacedNode, RenderError, Size, Theme};
use super::{add_title, bar_node, tick_text};
pub const WIDTH: f64 = 480.0;
pub const HEIGHT: f64 = 300.0;
pub const SECTION_PAD: f64 = 3.0;
pub const TEXT_PAD: f64 = 5.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rect {
pub x: f64,
pub y: f64,
pub w: f64,
pub h: f64,
}
impl Rect {
pub fn area(&self) -> f64 {
self.w.max(0.0) * self.h.max(0.0)
}
pub fn center(&self) -> Point {
Point::new(self.x + self.w / 2.0, self.y + self.h / 2.0)
}
}
pub fn render(code: &str, theme: &str) -> Result<String, RenderError> {
let tm = crate::preview::mermaid::chart::treemap::parse(code)?;
let diagram = lay_out(&tm)?;
Ok(super::super::svg::emit(&diagram, &Theme::named(theme)))
}
pub fn lay_out(tm: &Treemap) -> Result<Diagram, RenderError> {
if !crate::preview::mermaid::text_metrics::fonts_available() {
return Err(RenderError::NoFonts);
}
let total: f64 = tm.roots.iter().map(Node::total).sum();
if total <= 0.0 {
return Err(RenderError::ChartHasNoExtent {
what: "every value is zero, so no tile has any area",
});
}
let mut nodes: Vec<PlacedNode> = Vec::new();
let rect = Rect {
x: 0.0,
y: 0.0,
w: WIDTH,
h: HEIGHT,
};
place(&tm.roots, rect, None, &mut nodes, &mut 0);
let mut diagram = Diagram {
nodes,
..Diagram::default()
};
add_title(&mut diagram, &tm.preamble);
normalise(&mut diagram);
Ok(diagram)
}
fn place(
items: &[Node],
rect: Rect,
series: Option<usize>,
out: &mut Vec<PlacedNode>,
counter: &mut usize,
) {
let values: Vec<f64> = items.iter().map(Node::total).collect();
let rects = squarify(&values, rect);
for (i, (item, r)) in items.iter().zip(rects.iter()).enumerate() {
let id = *counter;
*counter += 1;
if r.w <= 0.0 || r.h <= 0.0 {
*counter += item.children.iter().map(subtree_len).sum::<usize>();
continue;
}
let colour = series.unwrap_or(i);
let mut tile = bar_node(
format!("tile#{id}"),
r.center(),
Size::new(r.w, r.h),
Some(colour),
);
if item.children.is_empty() {
tile.panel = tile_panel(&item.name, item.value, *r, r.h);
out.push(tile);
continue;
}
let band = (line_height() + TEXT_PAD).min(r.h);
tile.panel = tile_panel(&item.name, None, *r, band);
out.push(tile);
let inner = Rect {
x: r.x + SECTION_PAD,
y: r.y + band,
w: (r.w - SECTION_PAD * 2.0).max(0.0),
h: (r.h - band - SECTION_PAD).max(0.0),
};
if inner.w > 1.0 && inner.h > 1.0 {
place(&item.children, inner, Some(colour), out, counter);
} else {
*counter += item.children.iter().map(subtree_len).sum::<usize>();
}
}
}
pub fn subtree_len(node: &Node) -> usize {
1 + node.children.iter().map(subtree_len).sum::<usize>()
}
fn tile_panel(name: &str, value: Option<f64>, r: Rect, fit_h: f64) -> Option<Panel> {
let name = Label::measure(name);
let value = value.map(|v| Label::measure(&tick_text(v)));
let rows = 1 + usize::from(value.is_some());
let needed_h = rows as f64 * line_height() + TEXT_PAD;
let needed_w = name.width.max(value.as_ref().map_or(0.0, |v| v.width)) + TEXT_PAD * 2.0;
if needed_w > r.w || needed_h > fit_h || name.is_blank() {
return None;
}
let mut panel_rows = vec![PanelRow {
cells: vec![PanelCell {
label: name,
x: TEXT_PAD,
centered: false,
}],
y: TEXT_PAD + line_height() / 2.0,
}];
if let Some(v) = value {
panel_rows.push(PanelRow {
cells: vec![PanelCell {
label: v,
x: TEXT_PAD,
centered: false,
}],
y: TEXT_PAD + line_height() * 1.5,
});
}
Some(Panel {
rows: panel_rows,
rules: Vec::new(),
columns: Vec::new(),
size: Size::new(r.w, r.h),
})
}
pub fn squarify(values: &[f64], rect: Rect) -> Vec<Rect> {
let total: f64 = values.iter().filter(|v| **v > 0.0).sum();
let mut out = vec![
Rect {
x: rect.x,
y: rect.y,
w: 0.0,
h: 0.0
};
values.len()
];
if total <= 0.0 || rect.area() <= 0.0 {
return out;
}
let live: Vec<usize> = (0..values.len()).filter(|i| values[*i] > 0.0).collect();
let areas: Vec<f64> = live
.iter()
.map(|i| values[*i] / total * rect.area())
.collect();
let mut remaining = rect;
let mut i = 0;
while i < areas.len() {
let short = remaining.w.min(remaining.h);
if short <= 0.0 {
break;
}
let mut end = i + 1;
let mut best = worst(&areas[i..end], short);
while end < areas.len() {
let cand = worst(&areas[i..end + 1], short);
if cand > best {
break;
}
best = cand;
end += 1;
}
let sum: f64 = areas[i..end].iter().sum();
if remaining.w >= remaining.h {
let strip = (sum / remaining.h).min(remaining.w);
let mut y = remaining.y;
for (k, a) in areas[i..end].iter().enumerate() {
let h = if strip > 0.0 { a / strip } else { 0.0 };
out[live[i + k]] = Rect {
x: remaining.x,
y,
w: strip,
h,
};
y += h;
}
remaining = Rect {
x: remaining.x + strip,
y: remaining.y,
w: remaining.w - strip,
h: remaining.h,
};
} else {
let strip = (sum / remaining.w).min(remaining.h);
let mut x = remaining.x;
for (k, a) in areas[i..end].iter().enumerate() {
let w = if strip > 0.0 { a / strip } else { 0.0 };
out[live[i + k]] = Rect {
x,
y: remaining.y,
w,
h: strip,
};
x += w;
}
remaining = Rect {
x: remaining.x,
y: remaining.y + strip,
w: remaining.w,
h: remaining.h - strip,
};
}
i = end;
}
out
}
fn worst(areas: &[f64], side: f64) -> f64 {
let sum: f64 = areas.iter().sum();
if sum <= 0.0 || side <= 0.0 {
return f64::INFINITY;
}
let max = areas.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let min = areas.iter().copied().fold(f64::INFINITY, f64::min);
let s2 = sum * sum;
let w2 = side * side;
(w2 * max / s2).max(s2 / (w2 * min))
}