pub mod packet;
pub mod pie;
pub mod quadrant;
pub mod radar;
pub mod sankey;
pub mod treemap;
pub mod xychart;
#[cfg(test)]
mod tests;
use crate::preview::mermaid::chart::Preamble;
use crate::preview::mermaid::layout::Point;
use super::labels::line_height;
use super::shapes::Glyph;
use super::{Diagram, Label, PlacedEdge, PlacedNode, Size, Tip};
use crate::preview::mermaid::flowchart::Stroke;
pub const TITLE_GAP: f64 = 12.0;
pub const TICK_GAP: f64 = 5.0;
pub const TICK_LEN: f64 = 4.0;
pub const AXIS_TITLE_GAP: f64 = 6.0;
pub const SWATCH: f64 = 11.0;
pub const SWATCH_GAP: f64 = 6.0;
pub const LEGEND_ROW_GAP: f64 = 4.0;
pub const LEGEND_GAP: f64 = 18.0;
pub const POINT_RADIUS: f64 = 3.0;
pub fn title_label(preamble: &Preamble) -> Option<Label> {
let text = preamble.title.as_deref()?.trim();
if text.is_empty() {
return None;
}
let label = Label::measure(text);
(!label.is_blank()).then_some(label)
}
pub fn label_node(
id: impl Into<String>,
label: Label,
center: Point,
series: Option<usize>,
) -> Option<PlacedNode> {
if label.is_blank() {
return None;
}
Some(PlacedNode {
id: id.into(),
shape: Glyph::ChartLabel,
center,
size: Size::new(label.width, label.height),
label,
panel: None,
series,
mark: None,
})
}
pub fn text_node(
id: impl Into<String>,
text: &str,
center: Point,
series: Option<usize>,
) -> Option<PlacedNode> {
label_node(id, Label::measure(text), center, series)
}
pub fn rule(from: Point, to: Point, stroke: Stroke, series: Option<usize>) -> PlacedEdge {
PlacedEdge {
from: String::new(),
to: String::new(),
points: vec![from, to],
tip_start: Tip::None,
tip_end: Tip::None,
stroke,
label: None,
start_label: None,
end_label: None,
badge: None,
series,
straight: false,
}
}
pub fn bar_node(
id: impl Into<String>,
center: Point,
size: Size,
series: Option<usize>,
) -> PlacedNode {
PlacedNode {
id: id.into(),
shape: Glyph::ChartBar,
center,
size,
label: Label::measure(""),
panel: None,
series,
mark: None,
}
}
pub struct LegendEntry {
pub label: Label,
pub series: usize,
}
pub fn legend_size(entries: &[LegendEntry]) -> Size {
if entries.is_empty() {
return Size::new(0.0, 0.0);
}
let w = entries
.iter()
.map(|e| SWATCH + SWATCH_GAP + e.label.width)
.fold(0.0_f64, f64::max);
let row = line_height().max(SWATCH);
let h = entries.len() as f64 * row + (entries.len() - 1) as f64 * LEGEND_ROW_GAP;
Size::new(w, h)
}
pub fn legend_nodes(entries: &[LegendEntry], left: f64, top: f64) -> Vec<PlacedNode> {
let row = line_height().max(SWATCH);
let mut out = Vec::with_capacity(entries.len() * 2);
for (i, e) in entries.iter().enumerate() {
let cy = top + i as f64 * (row + LEGEND_ROW_GAP) + row / 2.0;
out.push(bar_node(
format!("legend#{i}#swatch"),
Point::new(left + SWATCH / 2.0, cy),
Size::new(SWATCH, SWATCH),
Some(e.series),
));
if let Some(n) = label_node(
format!("legend#{i}#label"),
e.label.clone(),
Point::new(left + SWATCH + SWATCH_GAP + e.label.width / 2.0, cy),
None,
) {
out.push(n);
}
}
out
}
pub fn ticks(min: f64, max: f64, target: usize) -> Vec<f64> {
if !min.is_finite() || !max.is_finite() || max <= min || target == 0 {
return vec![min, max]
.into_iter()
.filter(|v| v.is_finite())
.collect();
}
let raw = (max - min) / target as f64;
let magnitude = 10f64.powf(raw.log10().floor());
let normalised = raw / magnitude;
let step = magnitude
* if normalised <= 1.0 {
1.0
} else if normalised <= 2.0 {
2.0
} else if normalised <= 5.0 {
5.0
} else {
10.0
};
if step <= 0.0 || !step.is_finite() {
return vec![min, max];
}
let mut out = Vec::new();
let first = (min / step).ceil() * step;
let mut v = first;
while v <= max + step * 1e-9 && out.len() <= target * 4 {
out.push(if v.abs() < step * 1e-9 { 0.0 } else { v });
v += step;
}
out.retain(|t| *t >= min - step * 1e-9 && *t <= max + step * 1e-9);
if out.is_empty() {
out.push(min);
out.push(max);
}
out
}
pub fn axis_tick_texts(values: &[f64]) -> Vec<String> {
for decimals in 0..=15usize {
let out: Vec<String> = values.iter().map(|v| fixed(*v, decimals)).collect();
let mut seen = std::collections::HashSet::with_capacity(out.len());
if out.iter().all(|t| seen.insert(t.clone())) {
return out;
}
}
values.iter().map(|v| fixed(*v, 15)).collect()
}
fn fixed(v: f64, decimals: usize) -> String {
let s = format!("{v:.decimals$}");
let s = if s.contains('.') {
s.trim_end_matches('0').trim_end_matches('.').to_string()
} else {
s
};
if s.is_empty() || s == "-0" {
"0".to_string()
} else {
s
}
}
pub fn tick_text(v: f64) -> String {
if v == 0.0 {
return "0".to_string();
}
let a = v.abs();
let decimals = if a >= 100.0 {
0
} else if a >= 10.0 {
1
} else if a >= 1.0 {
2
} else {
3
};
let s = format!("{v:.decimals$}");
let s = if s.contains('.') {
s.trim_end_matches('0').trim_end_matches('.').to_string()
} else {
s
};
if s.is_empty() || s == "-0" {
"0".to_string()
} else {
s
}
}
pub fn add_title(diagram: &mut Diagram, preamble: &Preamble) {
let Some(label) = title_label(preamble) else {
return;
};
let (mut min_x, mut min_y) = (f64::INFINITY, f64::INFINITY);
let mut max_x = f64::NEG_INFINITY;
for n in &diagram.nodes {
let (l, t, r, _) = n.bounds();
min_x = min_x.min(l);
min_y = min_y.min(t);
max_x = max_x.max(r);
}
for e in &diagram.edges {
for p in &e.points {
min_x = min_x.min(p.x);
min_y = min_y.min(p.y);
max_x = max_x.max(p.x);
}
}
if !min_x.is_finite() {
return;
}
let center = Point::new(
(min_x + max_x) / 2.0,
min_y - TITLE_GAP - label.height / 2.0,
);
if let Some(n) = label_node("chart#title", label, center, None) {
diagram.nodes.push(n);
}
}