1use plotters::prelude::*;
6
7use crate::draw::draw_on;
8use crate::spec::ChartSpec;
9
10#[must_use]
14pub fn render_svg(spec: &ChartSpec, width: u32, height: u32) -> String {
15 let mut buf = String::new();
16 let ok = {
17 let backend = SVGBackend::with_string(&mut buf, (width, height));
18 let root = backend.into_drawing_area();
19 draw_on(&root, spec).is_ok()
20 };
21
22 if !ok || buf.is_empty() {
23 return format!(
24 "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{width}\" height=\"{height}\">\
25 <title>{}</title>\
26 <text x=\"10\" y=\"20\" font-family=\"sans-serif\" font-size=\"14\">{}</text>\
27 </svg>",
28 escape_xml(&spec.title),
29 escape_xml(&spec.title),
30 );
31 }
32 buf
33}
34
35fn escape_xml(s: &str) -> String {
36 s.replace('&', "&")
37 .replace('<', "<")
38 .replace('>', ">")
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44 use crate::spec::{ChartKind, Series, SeriesPoint};
45
46 fn sample(kind: ChartKind) -> ChartSpec {
47 ChartSpec {
48 title: "Test".to_string(),
49 kind,
50 x_label: "X".to_string(),
51 y_label: "Y".to_string(),
52 series: vec![Series {
53 label: "A".to_string(),
54 commodity_symbol: "USD".to_string(),
55 points: vec![
56 SeriesPoint {
57 x: "Jan".to_string(),
58 y_num: 100,
59 y_denom: 1,
60 },
61 SeriesPoint {
62 x: "Feb".to_string(),
63 y_num: 200,
64 y_denom: 1,
65 },
66 ],
67 }],
68 notes: vec![],
69 }
70 }
71
72 #[test]
73 fn render_svg_emits_non_empty_svg_for_each_kind() {
74 for kind in [ChartKind::Bar, ChartKind::StackedBar, ChartKind::Line] {
75 let svg = render_svg(&sample(kind), 400, 300);
76 assert!(svg.contains("<svg"), "missing <svg for {kind:?}");
77 assert!(svg.contains("Test"), "title missing for {kind:?}");
78 }
79 }
80
81 #[test]
82 fn render_svg_includes_series_label() {
83 let svg = render_svg(&sample(ChartKind::Line), 400, 300);
84 assert!(svg.contains('A'), "series label missing");
87 }
88
89 #[test]
90 fn render_svg_empty_spec_is_safe() {
91 let spec = ChartSpec {
92 title: "Empty".to_string(),
93 kind: ChartKind::Bar,
94 x_label: String::new(),
95 y_label: String::new(),
96 series: vec![],
97 notes: vec![],
98 };
99 let svg = render_svg(&spec, 400, 300);
100 assert!(svg.contains("<svg"));
101 }
102}