1use super::backend::{
8 DrawBackend, FontFace, LineStyle, PointShape, 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 warnings: Vec<String>,
28 plot_attr: bool,
31}
32
33impl SvgBackend {
34 pub fn new(width: u32, height: u32, plot_area: Rect) -> Self {
35 SvgBackend {
36 plot_area,
37 total_area: Rect {
38 x: 0.0,
39 y: 0.0,
40 width: width as f64,
41 height: height as f64,
42 },
43 body: String::new(),
44 tooltip: None,
45 axis_key: None,
46 series_key: None,
47 value_key: None,
48 root_attrs: Vec::new(),
49 warnings: Vec::new(),
50 plot_attr: true,
51 }
52 }
53
54 pub fn set_root_attrs(&mut self, attrs: Vec<(String, String)>) {
58 self.root_attrs = attrs;
59 }
60
61 pub(crate) fn without_plot_attr(&mut self) {
63 self.plot_attr = false;
64 }
65
66 pub(crate) fn push_raw(&mut self, markup: &str) {
69 self.body.push_str(markup);
70 }
71
72 pub(crate) fn body(&self) -> &str {
74 &self.body
75 }
76
77 pub fn take_warnings(&mut self) -> Vec<String> {
79 std::mem::take(&mut self.warnings)
80 }
81
82 fn push_mark(&mut self, tag: &str, attrs: &str) {
85 let mut data = String::new();
86 for (name, val) in [
87 ("data-x", &self.axis_key),
88 ("data-series", &self.series_key),
89 ("data-value", &self.value_key),
90 ] {
91 if let Some(v) = val {
92 data.push_str(&format!(" {name}=\"{}\"", escape(v)));
93 }
94 }
95 let mark = match &self.tooltip {
96 Some(t) => format!("<{tag} {attrs}{data}><title>{}</title></{tag}>", escape(t)),
97 None => format!("<{tag} {attrs}{data}/>"),
98 };
99 self.body.push_str(&mark);
100 }
101
102 fn root_open(&self, prefix: &str) -> String {
103 let p = &self.plot_area;
104 let (w, h) = (self.total_area.width as i64, self.total_area.height as i64);
105 let mut extra = String::new();
106 for (k, v) in &self.root_attrs {
107 extra.push_str(&format!(" {k}=\"{}\"", escape(v)));
108 }
109 let plot = if self.plot_attr {
110 format!(
111 " data-plot=\"{} {} {} {}\"",
112 num(p.x),
113 num(p.y),
114 num(p.width),
115 num(p.height),
116 )
117 } else {
118 String::new()
119 };
120 format!("<svg {prefix}width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\"{plot}{extra}>")
121 }
122
123 pub fn finish(self) -> String {
127 let open = self.root_open("xmlns=\"http://www.w3.org/2000/svg\" ");
128 format!("{open}{}</svg>", self.body)
129 }
130
131 pub fn finish_fragment(self, x: f64, y: f64) -> String {
136 let open = self.root_open(&format!("x=\"{}\" y=\"{}\" ", num(x), num(y)));
137 format!("{open}{}</svg>", self.body)
138 }
139}
140
141fn num(v: f64) -> String {
144 if v.is_finite() {
145 format!("{v:.2}")
146 } else {
147 "0".to_string()
148 }
149}
150
151fn finite_or(v: f64, fallback: f64) -> f64 {
152 if v.is_finite() {
153 v
154 } else {
155 fallback
156 }
157}
158
159pub fn root_data_attrs(built: &crate::build::BuiltPlot) -> Vec<(String, String)> {
176 use crate::aes::Aesthetic;
177 let mut out = Vec::new();
178 if !built.panel_scales.is_empty() {
179 return out;
180 }
181 let axis = |aes: &Aesthetic| -> (Option<(f64, f64)>, Option<String>) {
182 match built.scales.get(aes) {
183 Some(s) if s.is_discrete() => {
184 let labels: Vec<String> = s
185 .breaks()
186 .into_iter()
187 .map(|(_, l)| json_string(&l))
188 .collect();
189 (None, Some(format!("[{}]", labels.join(","))))
190 }
191 Some(s) => (s.expanded_domain(), None),
192 None => (None, None),
193 }
194 };
195 let (xd, xl) = axis(&Aesthetic::X);
196 let (yd, yl) = axis(&Aesthetic::Y);
197 if let (Some((x0, x1)), Some((y0, y1))) = (xd, yd) {
198 out.push((
199 "data-domain".into(),
200 format!("{} {} {} {}", g(x0), g(x1), g(y0), g(y1)),
201 ));
202 }
203 if let Some((a, b)) = xd {
204 out.push(("data-xdomain".into(), format!("{} {}", g(a), g(b))));
205 }
206 if let Some((a, b)) = yd {
207 out.push(("data-ydomain".into(), format!("{} {}", g(a), g(b))));
208 }
209 if let Some(l) = xl {
210 out.push(("data-xlevels".into(), l));
211 }
212 if let Some(l) = yl {
213 out.push(("data-ylevels".into(), l));
214 }
215 if built.coord.is_flipped() {
216 out.push(("data-flip".into(), "true".into()));
217 }
218 out
219}
220
221fn g(v: f64) -> String {
224 format!("{v}")
225}
226
227fn json_string(s: &str) -> String {
230 let mut out = String::with_capacity(s.len() + 2);
231 out.push('"');
232 for c in s.chars() {
233 match c {
234 '"' => out.push_str("\\\""),
235 '\\' => out.push_str("\\\\"),
236 '\n' => out.push_str("\\n"),
237 '\r' => out.push_str("\\r"),
238 '\t' => out.push_str("\\t"),
239 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
240 c => out.push(c),
241 }
242 }
243 out.push('"');
244 out
245}
246
247fn rgb((r, g, b): (u8, u8, u8)) -> String {
248 format!("#{r:02X}{g:02X}{b:02X}")
249}
250
251fn escape(s: &str) -> String {
258 let mut out = String::with_capacity(s.len());
259 for c in s.chars() {
260 match c {
261 '&' => out.push_str("&"),
262 '<' => out.push_str("<"),
263 '>' => out.push_str(">"),
264 '"' => out.push_str("""),
265 '\'' => out.push_str("'"),
266 '\t' | '\n' | '\r' => out.push(c),
267 c if (c as u32) < 0x20 || c == '\u{FFFE}' || c == '\u{FFFF}' => {}
268 c => out.push(c),
269 }
270 }
271 out
272}
273
274fn points(pts: &[(f64, f64)]) -> String {
275 pts.iter()
276 .map(|(x, y)| format!("{x:.2},{y:.2}"))
277 .collect::<Vec<_>>()
278 .join(" ")
279}
280
281fn finite_pts(pts: &[(f64, f64)]) -> Vec<(f64, f64)> {
282 pts.iter()
283 .copied()
284 .filter(|(x, y)| x.is_finite() && y.is_finite())
285 .collect()
286}
287
288impl DrawBackend for SvgBackend {
289 fn plot_area(&self) -> Rect {
290 self.plot_area.clone()
291 }
292 fn total_area(&self) -> Rect {
293 self.total_area.clone()
294 }
295
296 fn set_tooltip(&mut self, tooltip: Option<String>) {
297 self.tooltip = tooltip;
298 }
299
300 fn set_mark_axis(&mut self, key: Option<String>) {
301 self.axis_key = key;
302 }
303
304 fn set_mark_series(&mut self, series: Option<String>) {
305 self.series_key = series;
306 }
307
308 fn set_mark_value(&mut self, value: Option<String>) {
309 self.value_key = value;
310 }
311
312 fn warn(&mut self, message: String) {
313 self.warnings.push(message);
314 }
315
316 fn draw_circle(
317 &mut self,
318 (cx, cy): (f64, f64),
319 radius: f64,
320 style: &PointStyle,
321 ) -> Result<(), RenderError> {
322 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
323 return Ok(());
324 }
325 let attrs = format!(
326 "cx=\"{cx:.2}\" cy=\"{cy:.2}\" r=\"{:.2}\" fill=\"{}\" fill-opacity=\"{:.3}\"",
327 radius.max(0.0),
328 rgb(style.color),
329 finite_or(style.alpha, 1.0)
330 );
331 self.push_mark("circle", &attrs);
332 Ok(())
333 }
334
335 fn draw_shape(
339 &mut self,
340 (cx, cy): (f64, f64),
341 radius: f64,
342 style: &PointStyle,
343 ) -> Result<(), RenderError> {
344 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
345 return Ok(());
346 }
347 let r = radius.max(0.0);
348 let color = rgb(style.color);
349 let alpha = finite_or(style.alpha, 1.0);
350 let poly = |pts: &[(f64, f64)]| {
351 if style.filled {
352 format!(
353 "points=\"{}\" fill=\"{color}\" fill-opacity=\"{alpha:.3}\"",
354 points(pts)
355 )
356 } else {
357 format!(
358 "points=\"{}\" fill=\"none\" stroke=\"{color}\" stroke-opacity=\"{alpha:.3}\"",
359 points(pts)
360 )
361 }
362 };
363 match style.shape {
364 PointShape::Circle => return self.draw_circle((cx, cy), radius, style),
365 PointShape::Square => {
366 let a = poly(&[
367 (cx - r, cy - r),
368 (cx + r, cy - r),
369 (cx + r, cy + r),
370 (cx - r, cy + r),
371 ]);
372 self.push_mark("polygon", &a);
373 }
374 PointShape::Triangle => {
375 let a = poly(&[(cx, cy - r), (cx + r, cy + r), (cx - r, cy + r)]);
376 self.push_mark("polygon", &a);
377 }
378 PointShape::Diamond => {
379 let a = poly(&[(cx, cy - r), (cx + r, cy), (cx, cy + r), (cx - r, cy)]);
380 self.push_mark("polygon", &a);
381 }
382 PointShape::Plus | PointShape::Cross => {
383 let d = if style.shape == PointShape::Plus {
384 format!(
385 "M{:.2} {cy:.2}H{:.2}M{cx:.2} {:.2}V{:.2}",
386 cx - r,
387 cx + r,
388 cy - r,
389 cy + r
390 )
391 } else {
392 format!(
393 "M{:.2} {:.2}L{:.2} {:.2}M{:.2} {:.2}L{:.2} {:.2}",
394 cx - r,
395 cy - r,
396 cx + r,
397 cy + r,
398 cx - r,
399 cy + r,
400 cx + r,
401 cy - r
402 )
403 };
404 let a = format!(
405 "d=\"{d}\" fill=\"none\" stroke=\"{color}\" stroke-width=\"{:.2}\" \
406 stroke-opacity=\"{alpha:.3}\"",
407 (r / 2.5).clamp(1.0, 3.0)
408 );
409 self.push_mark("path", &a);
410 }
411 }
412 Ok(())
413 }
414
415 fn draw_line(&mut self, pts: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
416 let pts = finite_pts(pts);
417 if pts.len() < 2 {
418 return Ok(());
419 }
420 let dash = match style
421 .linetype
422 .pattern()
423 .iter()
424 .flat_map(|(d, g)| [*d, *g])
425 .map(|v| format!("{v}"))
426 .collect::<Vec<_>>()
427 .join(",")
428 {
429 s if s.is_empty() => String::new(),
430 s => format!(" stroke-dasharray=\"{s}\""),
431 };
432 let attrs = format!(
433 "points=\"{}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\" stroke-opacity=\"{:.3}\"{}",
434 points(&pts),
435 rgb(style.color),
436 finite_or(style.width, 1.0),
437 finite_or(style.alpha, 1.0),
438 dash
439 );
440 self.push_mark("polyline", &attrs);
441 Ok(())
442 }
443
444 fn draw_rect(
445 &mut self,
446 (x0, y0): (f64, f64),
447 (x1, y1): (f64, f64),
448 style: &RectStyle,
449 ) -> Result<(), RenderError> {
450 if !(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite()) {
451 return Ok(());
452 }
453 let (x, y) = (x0.min(x1), y0.min(y1));
454 let (w, h) = ((x1 - x0).abs(), (y1 - y0).abs());
455 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
456 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
457 let attrs = format!(
458 "x=\"{x:.2}\" y=\"{y:.2}\" width=\"{w:.2}\" height=\"{h:.2}\" fill=\"{fill}\" \
459 fill-opacity=\"{:.3}\" stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
460 finite_or(style.alpha, 1.0),
461 finite_or(style.stroke_width, 0.0)
462 );
463 self.push_mark("rect", &attrs);
464 Ok(())
465 }
466
467 fn draw_polygon(&mut self, pts: &[(f64, f64)], style: &RectStyle) -> Result<(), RenderError> {
468 let pts = finite_pts(pts);
469 if pts.len() < 3 {
470 return Ok(());
471 }
472 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
473 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
474 let attrs = format!(
475 "points=\"{}\" fill=\"{fill}\" fill-opacity=\"{:.3}\" stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
476 points(&pts),
477 finite_or(style.alpha, 1.0),
478 finite_or(style.stroke_width, 0.0)
479 );
480 self.push_mark("polygon", &attrs);
481 Ok(())
482 }
483
484 fn draw_text(
485 &mut self,
486 text: &str,
487 (x, y): (f64, f64),
488 style: &TextStyle,
489 ) -> Result<(), RenderError> {
490 if !(x.is_finite() && y.is_finite()) {
491 return Ok(());
492 }
493 let anchor = match style.anchor {
494 TextAnchor::Start => "start",
495 TextAnchor::Middle => "middle",
496 TextAnchor::End => "end",
497 };
498 let family = escape(style.family.as_deref().unwrap_or("sans-serif"));
499 let weight = if style.face == FontFace::Bold {
500 " font-weight=\"bold\""
501 } else {
502 ""
503 };
504 let fstyle = if style.face == FontFace::Italic {
505 " font-style=\"italic\""
506 } else {
507 ""
508 };
509 let angle = finite_or(style.angle, 0.0);
510 let transform = if angle.abs() > 0.01 {
511 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
512 } else {
513 String::new()
514 };
515 self.body.push_str(&format!(
516 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
517 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle} fill=\"{}\"{transform}>{}</text>",
518 finite_or(style.size, 12.0),
519 rgb(style.color),
520 escape(text)
521 ));
522 Ok(())
523 }
524}