1use super::backend::{
8 DrawBackend, FontFace, LineStyle, PointStyle, RectStyle, TextAnchor, TextStyle,
9};
10use super::{Rect, RenderError};
11
12pub struct SvgBackend {
19 plot_area: Rect,
20 total_area: Rect,
21 body: String,
22 tooltip: Option<String>,
23 axis_key: Option<String>,
24 series_key: Option<String>,
25 value_key: Option<String>,
26 root_attrs: Vec<(String, String)>,
27}
28
29impl SvgBackend {
30 pub fn new(width: u32, height: u32, plot_area: Rect) -> Self {
31 SvgBackend {
32 plot_area,
33 total_area: Rect {
34 x: 0.0,
35 y: 0.0,
36 width: width as f64,
37 height: height as f64,
38 },
39 body: String::new(),
40 tooltip: None,
41 axis_key: None,
42 series_key: None,
43 value_key: None,
44 root_attrs: Vec::new(),
45 }
46 }
47
48 pub fn set_root_attrs(&mut self, attrs: Vec<(String, String)>) {
52 self.root_attrs = attrs;
53 }
54
55 fn push_mark(&mut self, tag: &str, attrs: &str) {
58 let mut data = String::new();
59 for (name, val) in [
60 ("data-x", &self.axis_key),
61 ("data-series", &self.series_key),
62 ("data-value", &self.value_key),
63 ] {
64 if let Some(v) = val {
65 data.push_str(&format!(" {name}=\"{}\"", escape(v)));
66 }
67 }
68 let mark = match &self.tooltip {
69 Some(t) => format!("<{tag} {attrs}{data}><title>{}</title></{tag}>", escape(t)),
70 None => format!("<{tag} {attrs}{data}/>"),
71 };
72 self.body.push_str(&mark);
73 }
74
75 fn root_open(&self, prefix: &str) -> String {
76 let p = &self.plot_area;
77 let (w, h) = (self.total_area.width as i64, self.total_area.height as i64);
78 let mut extra = String::new();
79 for (k, v) in &self.root_attrs {
80 extra.push_str(&format!(" {k}=\"{}\"", escape(v)));
81 }
82 format!(
83 "<svg {prefix}width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\" \
84 data-plot=\"{} {} {} {}\"{extra}>",
85 num(p.x),
86 num(p.y),
87 num(p.width),
88 num(p.height),
89 )
90 }
91
92 pub fn finish(self) -> String {
96 let open = self.root_open("xmlns=\"http://www.w3.org/2000/svg\" ");
97 format!("{open}{}</svg>", self.body)
98 }
99
100 pub fn finish_fragment(self, x: f64, y: f64) -> String {
105 let open = self.root_open(&format!("x=\"{}\" y=\"{}\" ", num(x), num(y)));
106 format!("{open}{}</svg>", self.body)
107 }
108}
109
110fn num(v: f64) -> String {
113 if v.is_finite() {
114 format!("{v:.2}")
115 } else {
116 "0".to_string()
117 }
118}
119
120fn finite_or(v: f64, fallback: f64) -> f64 {
121 if v.is_finite() {
122 v
123 } else {
124 fallback
125 }
126}
127
128pub fn root_data_attrs(built: &crate::build::BuiltPlot) -> Vec<(String, String)> {
145 use crate::aes::Aesthetic;
146 let mut out = Vec::new();
147 if !built.panel_scales.is_empty() {
148 return out;
149 }
150 let axis = |aes: &Aesthetic| -> (Option<(f64, f64)>, Option<String>) {
151 match built.scales.get(aes) {
152 Some(s) if s.is_discrete() => {
153 let labels: Vec<String> = s
154 .breaks()
155 .into_iter()
156 .map(|(_, l)| json_string(&l))
157 .collect();
158 (None, Some(format!("[{}]", labels.join(","))))
159 }
160 Some(s) => (s.expanded_domain(), None),
161 None => (None, None),
162 }
163 };
164 let (xd, xl) = axis(&Aesthetic::X);
165 let (yd, yl) = axis(&Aesthetic::Y);
166 if let (Some((x0, x1)), Some((y0, y1))) = (xd, yd) {
167 out.push((
168 "data-domain".into(),
169 format!("{} {} {} {}", g(x0), g(x1), g(y0), g(y1)),
170 ));
171 }
172 if let Some((a, b)) = xd {
173 out.push(("data-xdomain".into(), format!("{} {}", g(a), g(b))));
174 }
175 if let Some((a, b)) = yd {
176 out.push(("data-ydomain".into(), format!("{} {}", g(a), g(b))));
177 }
178 if let Some(l) = xl {
179 out.push(("data-xlevels".into(), l));
180 }
181 if let Some(l) = yl {
182 out.push(("data-ylevels".into(), l));
183 }
184 if built.coord.is_flipped() {
185 out.push(("data-flip".into(), "true".into()));
186 }
187 out
188}
189
190fn g(v: f64) -> String {
193 format!("{v}")
194}
195
196fn json_string(s: &str) -> String {
199 let mut out = String::with_capacity(s.len() + 2);
200 out.push('"');
201 for c in s.chars() {
202 match c {
203 '"' => out.push_str("\\\""),
204 '\\' => out.push_str("\\\\"),
205 '\n' => out.push_str("\\n"),
206 '\r' => out.push_str("\\r"),
207 '\t' => out.push_str("\\t"),
208 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
209 c => out.push(c),
210 }
211 }
212 out.push('"');
213 out
214}
215
216fn rgb((r, g, b): (u8, u8, u8)) -> String {
217 format!("#{r:02X}{g:02X}{b:02X}")
218}
219
220fn escape(s: &str) -> String {
227 let mut out = String::with_capacity(s.len());
228 for c in s.chars() {
229 match c {
230 '&' => out.push_str("&"),
231 '<' => out.push_str("<"),
232 '>' => out.push_str(">"),
233 '"' => out.push_str("""),
234 '\'' => out.push_str("'"),
235 '\t' | '\n' | '\r' => out.push(c),
236 c if (c as u32) < 0x20 || c == '\u{FFFE}' || c == '\u{FFFF}' => {}
237 c => out.push(c),
238 }
239 }
240 out
241}
242
243fn points(pts: &[(f64, f64)]) -> String {
244 pts.iter()
245 .map(|(x, y)| format!("{x:.2},{y:.2}"))
246 .collect::<Vec<_>>()
247 .join(" ")
248}
249
250fn finite_pts(pts: &[(f64, f64)]) -> Vec<(f64, f64)> {
251 pts.iter()
252 .copied()
253 .filter(|(x, y)| x.is_finite() && y.is_finite())
254 .collect()
255}
256
257impl DrawBackend for SvgBackend {
258 fn plot_area(&self) -> Rect {
259 self.plot_area.clone()
260 }
261 fn total_area(&self) -> Rect {
262 self.total_area.clone()
263 }
264
265 fn set_tooltip(&mut self, tooltip: Option<String>) {
266 self.tooltip = tooltip;
267 }
268
269 fn set_mark_axis(&mut self, key: Option<String>) {
270 self.axis_key = key;
271 }
272
273 fn set_mark_series(&mut self, series: Option<String>) {
274 self.series_key = series;
275 }
276
277 fn set_mark_value(&mut self, value: Option<String>) {
278 self.value_key = value;
279 }
280
281 fn draw_circle(
282 &mut self,
283 (cx, cy): (f64, f64),
284 radius: f64,
285 style: &PointStyle,
286 ) -> Result<(), RenderError> {
287 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
288 return Ok(());
289 }
290 let attrs = format!(
291 "cx=\"{cx:.2}\" cy=\"{cy:.2}\" r=\"{:.2}\" fill=\"{}\" fill-opacity=\"{:.3}\"",
292 radius.max(0.0),
293 rgb(style.color),
294 finite_or(style.alpha, 1.0)
295 );
296 self.push_mark("circle", &attrs);
297 Ok(())
298 }
299
300 fn draw_line(&mut self, pts: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
301 let pts = finite_pts(pts);
302 if pts.len() < 2 {
303 return Ok(());
304 }
305 let dash = match style
306 .linetype
307 .pattern()
308 .iter()
309 .flat_map(|(d, g)| [*d, *g])
310 .map(|v| format!("{v}"))
311 .collect::<Vec<_>>()
312 .join(",")
313 {
314 s if s.is_empty() => String::new(),
315 s => format!(" stroke-dasharray=\"{s}\""),
316 };
317 let attrs = format!(
318 "points=\"{}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\" stroke-opacity=\"{:.3}\"{}",
319 points(&pts),
320 rgb(style.color),
321 finite_or(style.width, 1.0),
322 finite_or(style.alpha, 1.0),
323 dash
324 );
325 self.push_mark("polyline", &attrs);
326 Ok(())
327 }
328
329 fn draw_rect(
330 &mut self,
331 (x0, y0): (f64, f64),
332 (x1, y1): (f64, f64),
333 style: &RectStyle,
334 ) -> Result<(), RenderError> {
335 if !(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite()) {
336 return Ok(());
337 }
338 let (x, y) = (x0.min(x1), y0.min(y1));
339 let (w, h) = ((x1 - x0).abs(), (y1 - y0).abs());
340 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
341 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
342 let attrs = format!(
343 "x=\"{x:.2}\" y=\"{y:.2}\" width=\"{w:.2}\" height=\"{h:.2}\" fill=\"{fill}\" \
344 fill-opacity=\"{:.3}\" stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
345 finite_or(style.alpha, 1.0),
346 finite_or(style.stroke_width, 0.0)
347 );
348 self.push_mark("rect", &attrs);
349 Ok(())
350 }
351
352 fn draw_polygon(&mut self, pts: &[(f64, f64)], style: &RectStyle) -> Result<(), RenderError> {
353 let pts = finite_pts(pts);
354 if pts.len() < 3 {
355 return Ok(());
356 }
357 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
358 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
359 let attrs = format!(
360 "points=\"{}\" fill=\"{fill}\" fill-opacity=\"{:.3}\" stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
361 points(&pts),
362 finite_or(style.alpha, 1.0),
363 finite_or(style.stroke_width, 0.0)
364 );
365 self.push_mark("polygon", &attrs);
366 Ok(())
367 }
368
369 fn draw_text(
370 &mut self,
371 text: &str,
372 (x, y): (f64, f64),
373 style: &TextStyle,
374 ) -> Result<(), RenderError> {
375 if !(x.is_finite() && y.is_finite()) {
376 return Ok(());
377 }
378 let anchor = match style.anchor {
379 TextAnchor::Start => "start",
380 TextAnchor::Middle => "middle",
381 TextAnchor::End => "end",
382 };
383 let family = escape(style.family.as_deref().unwrap_or("sans-serif"));
384 let weight = if style.face == FontFace::Bold {
385 " font-weight=\"bold\""
386 } else {
387 ""
388 };
389 let fstyle = if style.face == FontFace::Italic {
390 " font-style=\"italic\""
391 } else {
392 ""
393 };
394 let angle = finite_or(style.angle, 0.0);
395 let transform = if angle.abs() > 0.01 {
396 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
397 } else {
398 String::new()
399 };
400 self.body.push_str(&format!(
401 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
402 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle} fill=\"{}\"{transform}>{}</text>",
403 finite_or(style.size, 12.0),
404 rgb(style.color),
405 escape(text)
406 ));
407 Ok(())
408 }
409}