1use super::backend::{
8 DrawBackend, FontFace, LineStyle, PointStyle, RectStyle, TextAnchor, TextStyle,
9};
10use super::{Rect, RenderError};
11
12pub struct SvgBackend {
14 plot_area: Rect,
15 total_area: Rect,
16 body: String,
17}
18
19impl SvgBackend {
20 pub fn new(width: u32, height: u32, plot_area: Rect) -> Self {
21 SvgBackend {
22 plot_area,
23 total_area: Rect {
24 x: 0.0,
25 y: 0.0,
26 width: width as f64,
27 height: height as f64,
28 },
29 body: String::new(),
30 }
31 }
32
33 pub fn finish(self) -> String {
35 format!(
36 "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"{}\" \
37 viewBox=\"0 0 {0} {1}\">{}</svg>",
38 self.total_area.width as i64, self.total_area.height as i64, self.body
39 )
40 }
41}
42
43fn rgb((r, g, b): (u8, u8, u8)) -> String {
44 format!("#{r:02X}{g:02X}{b:02X}")
45}
46
47fn escape(s: &str) -> String {
48 s.replace('&', "&")
49 .replace('<', "<")
50 .replace('>', ">")
51}
52
53fn points(pts: &[(f64, f64)]) -> String {
54 pts.iter()
55 .map(|(x, y)| format!("{x:.2},{y:.2}"))
56 .collect::<Vec<_>>()
57 .join(" ")
58}
59
60impl DrawBackend for SvgBackend {
61 fn plot_area(&self) -> Rect {
62 self.plot_area.clone()
63 }
64 fn total_area(&self) -> Rect {
65 self.total_area.clone()
66 }
67
68 fn draw_circle(
69 &mut self,
70 (cx, cy): (f64, f64),
71 radius: f64,
72 style: &PointStyle,
73 ) -> Result<(), RenderError> {
74 self.body.push_str(&format!(
75 "<circle cx=\"{cx:.2}\" cy=\"{cy:.2}\" r=\"{radius:.2}\" fill=\"{}\" fill-opacity=\"{:.3}\"/>",
76 rgb(style.color), style.alpha
77 ));
78 Ok(())
79 }
80
81 fn draw_line(&mut self, pts: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
82 let dash = match style
83 .linetype
84 .pattern()
85 .iter()
86 .flat_map(|(d, g)| [*d, *g])
87 .map(|v| format!("{v}"))
88 .collect::<Vec<_>>()
89 .join(",")
90 {
91 s if s.is_empty() => String::new(),
92 s => format!(" stroke-dasharray=\"{s}\""),
93 };
94 self.body.push_str(&format!(
95 "<polyline points=\"{}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\" stroke-opacity=\"{:.3}\"{}/>",
96 points(pts), rgb(style.color), style.width, style.alpha, dash
97 ));
98 Ok(())
99 }
100
101 fn draw_rect(
102 &mut self,
103 (x0, y0): (f64, f64),
104 (x1, y1): (f64, f64),
105 style: &RectStyle,
106 ) -> Result<(), RenderError> {
107 let (x, y) = (x0.min(x1), y0.min(y1));
108 let (w, h) = ((x1 - x0).abs(), (y1 - y0).abs());
109 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
110 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
111 self.body.push_str(&format!(
112 "<rect x=\"{x:.2}\" y=\"{y:.2}\" width=\"{w:.2}\" height=\"{h:.2}\" fill=\"{fill}\" \
113 fill-opacity=\"{:.3}\" stroke=\"{stroke}\" stroke-width=\"{:.2}\"/>",
114 style.alpha, style.stroke_width
115 ));
116 Ok(())
117 }
118
119 fn draw_polygon(&mut self, pts: &[(f64, f64)], style: &RectStyle) -> Result<(), RenderError> {
120 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
121 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
122 self.body.push_str(&format!(
123 "<polygon points=\"{}\" fill=\"{fill}\" fill-opacity=\"{:.3}\" stroke=\"{stroke}\" stroke-width=\"{:.2}\"/>",
124 points(pts), style.alpha, style.stroke_width
125 ));
126 Ok(())
127 }
128
129 fn draw_text(
130 &mut self,
131 text: &str,
132 (x, y): (f64, f64),
133 style: &TextStyle,
134 ) -> Result<(), RenderError> {
135 let anchor = match style.anchor {
136 TextAnchor::Start => "start",
137 TextAnchor::Middle => "middle",
138 TextAnchor::End => "end",
139 };
140 let family = style.family.as_deref().unwrap_or("sans-serif");
141 let weight = if style.face == FontFace::Bold {
142 " font-weight=\"bold\""
143 } else {
144 ""
145 };
146 let fstyle = if style.face == FontFace::Italic {
147 " font-style=\"italic\""
148 } else {
149 ""
150 };
151 let transform = if style.angle.abs() > 0.01 {
152 format!(" transform=\"rotate({:.1} {x:.2} {y:.2})\"", style.angle)
153 } else {
154 String::new()
155 };
156 self.body.push_str(&format!(
157 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
158 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle} fill=\"{}\"{transform}>{}</text>",
159 style.size, rgb(style.color), escape(text)
160 ));
161 Ok(())
162 }
163}