Lines
87.69 %
Functions
50 %
Branches
100 %
//! SVG renderer. Uses plotters' `SVGBackend` and the shared `draw`
//! dispatch in `draw.rs` so the canvas renderer produces identical
//! layouts.
use plotters::prelude::*;
use crate::draw::draw_on;
use crate::spec::ChartSpec;
/// Render `spec` to an SVG string of the given dimensions. Drawing
/// errors fall back to a minimal SVG with the chart title so a broken
/// chart never fails a page render.
#[must_use]
pub fn render_svg(spec: &ChartSpec, width: u32, height: u32) -> String {
let mut buf = String::new();
let ok = {
let backend = SVGBackend::with_string(&mut buf, (width, height));
let root = backend.into_drawing_area();
draw_on(&root, spec).is_ok()
};
if !ok || buf.is_empty() {
return format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{width}\" height=\"{height}\">\
<title>{}</title>\
<text x=\"10\" y=\"20\" font-family=\"sans-serif\" font-size=\"14\">{}</text>\
</svg>",
escape_xml(&spec.title),
);
}
buf
fn escape_xml(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
#[cfg(test)]
mod tests {
use super::*;
use crate::spec::{ChartKind, Series, SeriesPoint};
fn sample(kind: ChartKind) -> ChartSpec {
ChartSpec {
title: "Test".to_string(),
kind,
x_label: "X".to_string(),
y_label: "Y".to_string(),
series: vec![Series {
label: "A".to_string(),
commodity_symbol: "USD".to_string(),
points: vec![
SeriesPoint {
x: "Jan".to_string(),
y_num: 100,
y_denom: 1,
},
x: "Feb".to_string(),
y_num: 200,
],
}],
notes: vec![],
#[test]
fn render_svg_emits_non_empty_svg_for_each_kind() {
for kind in [ChartKind::Bar, ChartKind::StackedBar, ChartKind::Line] {
let svg = render_svg(&sample(kind), 400, 300);
assert!(svg.contains("<svg"), "missing <svg for {kind:?}");
assert!(svg.contains("Test"), "title missing for {kind:?}");
fn render_svg_includes_series_label() {
let svg = render_svg(&sample(ChartKind::Line), 400, 300);
// Plotters escapes text; "A" should appear at least once in
// the legend or axis.
assert!(svg.contains('A'), "series label missing");
fn render_svg_empty_spec_is_safe() {
let spec = ChartSpec {
title: "Empty".to_string(),
kind: ChartKind::Bar,
x_label: String::new(),
y_label: String::new(),
series: vec![],
let svg = render_svg(&spec, 400, 300);
assert!(svg.contains("<svg"));