1use super::backend::{
8 DrawBackend, FontFace, LineEnd, LineStyle, PointShape, PointStyle, RectStyle, TextAnchor,
9 TextStyle,
10};
11use super::{Rect, RenderError};
12
13pub struct SvgBackend {
20 plot_area: Rect,
21 total_area: Rect,
22 body: String,
23 tooltip: Option<String>,
24 axis_key: Option<String>,
25 series_key: Option<String>,
26 value_key: Option<String>,
27 mark_part: Option<String>,
28 mark_attrs: Vec<(String, String)>,
29 root_attrs: Vec<(String, String)>,
30 warnings: Vec<String>,
31 plot_attr: bool,
34}
35
36impl SvgBackend {
37 pub fn new(width: u32, height: u32, plot_area: Rect) -> Self {
38 SvgBackend {
39 plot_area,
40 total_area: Rect {
41 x: 0.0,
42 y: 0.0,
43 width: width as f64,
44 height: height as f64,
45 },
46 body: String::new(),
47 tooltip: None,
48 axis_key: None,
49 series_key: None,
50 value_key: None,
51 mark_part: None,
52 mark_attrs: Vec::new(),
53 root_attrs: Vec::new(),
54 warnings: Vec::new(),
55 plot_attr: true,
56 }
57 }
58
59 pub fn set_root_attrs(&mut self, attrs: Vec<(String, String)>) {
63 self.root_attrs = attrs;
64 }
65
66 pub(crate) fn without_plot_attr(&mut self) {
68 self.plot_attr = false;
69 }
70
71 pub(crate) fn push_raw(&mut self, markup: &str) {
74 self.body.push_str(markup);
75 }
76
77 pub(crate) fn body(&self) -> &str {
79 &self.body
80 }
81
82 pub fn take_warnings(&mut self) -> Vec<String> {
84 std::mem::take(&mut self.warnings)
85 }
86
87 fn push_mark(&mut self, tag: &str, attrs: &str) {
90 let mut pairs: Vec<(String, String)> = Vec::new();
100 for (name, val) in [
101 ("data-x", &self.axis_key),
102 ("data-series", &self.series_key),
103 ("data-value", &self.value_key),
104 ] {
105 if let Some(v) = val {
106 pairs.push((name.to_string(), v.clone()));
107 }
108 }
109 for (name, val) in &self.mark_attrs {
110 if !is_valid_mark_attr_name(name) {
111 continue;
112 }
113 match pairs.iter_mut().find(|(n, _)| n == name) {
114 Some(existing) => existing.1 = val.clone(),
115 None => pairs.push((name.clone(), val.clone())),
116 }
117 }
118 if let Some(part) = &self.mark_part {
123 match pairs.iter_mut().find(|(n, _)| n == "data-part") {
124 Some(existing) => existing.1 = part.clone(),
125 None => pairs.push(("data-part".to_string(), part.clone())),
126 }
127 }
128 let mut data = String::new();
129 for (name, val) in &pairs {
130 data.push_str(&format!(" {name}=\"{}\"", escape(val)));
131 }
132 if self.mark_part.is_some() {
133 data.push_str(" aria-hidden=\"true\"");
134 }
135 let mark = if self.mark_part.is_some() {
136 format!("<{tag} {attrs}{data}/>")
137 } else {
138 match &self.tooltip {
139 Some(t) => format!("<{tag} {attrs}{data}><title>{}</title></{tag}>", escape(t)),
140 None => format!("<{tag} {attrs}{data}/>"),
141 }
142 };
143 self.body.push_str(&mark);
144 }
145
146 fn root_open(&self, prefix: &str) -> String {
147 let p = &self.plot_area;
148 let (w, h) = (self.total_area.width as i64, self.total_area.height as i64);
149 let mut extra = String::new();
150 for (k, v) in &self.root_attrs {
151 extra.push_str(&format!(" {k}=\"{}\"", escape(v)));
152 }
153 let plot = if self.plot_attr {
154 format!(
155 " data-plot=\"{} {} {} {}\"",
156 num(p.x),
157 num(p.y),
158 num(p.width),
159 num(p.height),
160 )
161 } else {
162 String::new()
163 };
164 format!("<svg {prefix}width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\"{plot}{extra}>")
165 }
166
167 pub fn finish(self) -> String {
171 let open = self.root_open("xmlns=\"http://www.w3.org/2000/svg\" ");
172 format!("{open}{}</svg>", self.body)
173 }
174
175 pub fn finish_fragment(self, x: f64, y: f64) -> String {
180 let open = self.root_open(&format!("x=\"{}\" y=\"{}\" ", num(x), num(y)));
181 format!("{open}{}</svg>", self.body)
182 }
183}
184
185fn num(v: f64) -> String {
188 if v.is_finite() {
189 format!("{v:.2}")
190 } else {
191 "0".to_string()
192 }
193}
194
195fn finite_or(v: f64, fallback: f64) -> f64 {
196 if v.is_finite() {
197 v
198 } else {
199 fallback
200 }
201}
202
203pub fn root_data_attrs(built: &crate::build::BuiltPlot) -> Vec<(String, String)> {
220 use crate::aes::Aesthetic;
221 let mut out = Vec::new();
222 if !built.panel_scales.is_empty() {
223 return out;
224 }
225 let axis = |aes: &Aesthetic| -> (Option<(f64, f64)>, Option<String>) {
226 match built.scales.get(aes) {
227 Some(s) if s.is_discrete() => {
228 let labels: Vec<String> = s
229 .breaks()
230 .into_iter()
231 .map(|(_, l)| json_string(&l))
232 .collect();
233 (None, Some(format!("[{}]", labels.join(","))))
234 }
235 Some(s) => (s.expanded_domain(), None),
236 None => (None, None),
237 }
238 };
239 let (xd, xl) = axis(&Aesthetic::X);
240 let (yd, yl) = axis(&Aesthetic::Y);
241 if let (Some((x0, x1)), Some((y0, y1))) = (xd, yd) {
242 out.push((
243 "data-domain".into(),
244 format!("{} {} {} {}", g(x0), g(x1), g(y0), g(y1)),
245 ));
246 }
247 if let Some((a, b)) = xd {
248 out.push(("data-xdomain".into(), format!("{} {}", g(a), g(b))));
249 }
250 if let Some((a, b)) = yd {
251 out.push(("data-ydomain".into(), format!("{} {}", g(a), g(b))));
252 }
253 if let Some(l) = xl {
254 out.push(("data-xlevels".into(), l));
255 }
256 if let Some(l) = yl {
257 out.push(("data-ylevels".into(), l));
258 }
259 if built.coord.is_flipped() {
260 out.push(("data-flip".into(), "true".into()));
261 }
262 out
263}
264
265fn g(v: f64) -> String {
268 format!("{v}")
269}
270
271fn json_string(s: &str) -> String {
274 let mut out = String::with_capacity(s.len() + 2);
275 out.push('"');
276 for c in s.chars() {
277 match c {
278 '"' => out.push_str("\\\""),
279 '\\' => out.push_str("\\\\"),
280 '\n' => out.push_str("\\n"),
281 '\r' => out.push_str("\\r"),
282 '\t' => out.push_str("\\t"),
283 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
284 c => out.push(c),
285 }
286 }
287 out.push('"');
288 out
289}
290
291fn is_valid_mark_attr_name(name: &str) -> bool {
299 match name.strip_prefix("data-") {
300 Some(suffix) => crate::aes::is_valid_data_attr_name(suffix),
301 None => false,
302 }
303}
304
305fn rgb((r, g, b): (u8, u8, u8)) -> String {
306 format!("#{r:02X}{g:02X}{b:02X}")
307}
308
309fn escape(s: &str) -> String {
316 let mut out = String::with_capacity(s.len());
317 for c in s.chars() {
318 match c {
319 '&' => out.push_str("&"),
320 '<' => out.push_str("<"),
321 '>' => out.push_str(">"),
322 '"' => out.push_str("""),
323 '\'' => out.push_str("'"),
324 '\t' | '\n' | '\r' => out.push(c),
325 c if (c as u32) < 0x20 || c == '\u{FFFE}' || c == '\u{FFFF}' => {}
326 c => out.push(c),
327 }
328 }
329 out
330}
331
332fn points(pts: &[(f64, f64)]) -> String {
333 pts.iter()
334 .map(|(x, y)| format!("{x:.2},{y:.2}"))
335 .collect::<Vec<_>>()
336 .join(" ")
337}
338
339fn finite_pts(pts: &[(f64, f64)]) -> Vec<(f64, f64)> {
340 pts.iter()
341 .copied()
342 .filter(|(x, y)| x.is_finite() && y.is_finite())
343 .collect()
344}
345
346impl DrawBackend for SvgBackend {
347 fn plot_area(&self) -> Rect {
348 self.plot_area.clone()
349 }
350 fn total_area(&self) -> Rect {
351 self.total_area.clone()
352 }
353
354 fn set_tooltip(&mut self, tooltip: Option<String>) {
355 self.tooltip = tooltip;
356 }
357
358 fn set_mark_axis(&mut self, key: Option<String>) {
359 self.axis_key = key;
360 }
361
362 fn set_mark_series(&mut self, series: Option<String>) {
363 self.series_key = series;
364 }
365
366 fn set_mark_value(&mut self, value: Option<String>) {
367 self.value_key = value;
368 }
369
370 fn warn(&mut self, message: String) {
371 self.warnings.push(message);
372 }
373
374 fn set_mark_part(&mut self, part: Option<&str>) {
375 self.mark_part = part.map(|s| s.to_string());
376 }
377
378 fn set_mark_attrs(&mut self, attrs: Vec<(String, String)>) {
379 self.mark_attrs = attrs;
380 }
381
382 fn draw_circle(
383 &mut self,
384 (cx, cy): (f64, f64),
385 radius: f64,
386 style: &PointStyle,
387 ) -> Result<(), RenderError> {
388 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
389 return Ok(());
390 }
391 let attrs = format!(
392 "cx=\"{cx:.2}\" cy=\"{cy:.2}\" r=\"{:.2}\" fill=\"{}\" fill-opacity=\"{:.3}\"",
393 radius.max(0.0),
394 rgb(style.color),
395 finite_or(style.alpha, 1.0)
396 );
397 self.push_mark("circle", &attrs);
398 Ok(())
399 }
400
401 fn draw_shape(
405 &mut self,
406 (cx, cy): (f64, f64),
407 radius: f64,
408 style: &PointStyle,
409 ) -> Result<(), RenderError> {
410 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
411 return Ok(());
412 }
413 let r = radius.max(0.0);
414 let color = rgb(style.color);
415 let alpha = finite_or(style.alpha, 1.0);
416 let poly = |pts: &[(f64, f64)]| {
417 if style.filled {
418 format!(
419 "points=\"{}\" fill=\"{color}\" fill-opacity=\"{alpha:.3}\"",
420 points(pts)
421 )
422 } else {
423 format!(
424 "points=\"{}\" fill=\"none\" stroke=\"{color}\" stroke-opacity=\"{alpha:.3}\"",
425 points(pts)
426 )
427 }
428 };
429 match style.shape {
430 PointShape::Circle => return self.draw_circle((cx, cy), radius, style),
431 PointShape::Square => {
432 let a = poly(&[
433 (cx - r, cy - r),
434 (cx + r, cy - r),
435 (cx + r, cy + r),
436 (cx - r, cy + r),
437 ]);
438 self.push_mark("polygon", &a);
439 }
440 PointShape::Triangle => {
441 let a = poly(&[(cx, cy - r), (cx + r, cy + r), (cx - r, cy + r)]);
442 self.push_mark("polygon", &a);
443 }
444 PointShape::Diamond => {
445 let a = poly(&[(cx, cy - r), (cx + r, cy), (cx, cy + r), (cx - r, cy)]);
446 self.push_mark("polygon", &a);
447 }
448 PointShape::Plus | PointShape::Cross => {
449 let d = if style.shape == PointShape::Plus {
450 format!(
451 "M{:.2} {cy:.2}H{:.2}M{cx:.2} {:.2}V{:.2}",
452 cx - r,
453 cx + r,
454 cy - r,
455 cy + r
456 )
457 } else {
458 format!(
459 "M{:.2} {:.2}L{:.2} {:.2}M{:.2} {:.2}L{:.2} {:.2}",
460 cx - r,
461 cy - r,
462 cx + r,
463 cy + r,
464 cx - r,
465 cy + r,
466 cx + r,
467 cy - r
468 )
469 };
470 let a = format!(
471 "d=\"{d}\" fill=\"none\" stroke=\"{color}\" stroke-width=\"{:.2}\" \
472 stroke-opacity=\"{alpha:.3}\"",
473 (r / 2.5).clamp(1.0, 3.0)
474 );
475 self.push_mark("path", &a);
476 }
477 }
478 Ok(())
479 }
480
481 fn draw_line(&mut self, pts: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
482 let pts = finite_pts(pts);
483 if pts.len() < 2 {
484 return Ok(());
485 }
486 let dash = match style
487 .linetype
488 .pattern()
489 .iter()
490 .flat_map(|(d, g)| [*d, *g])
491 .map(|v| format!("{v}"))
492 .collect::<Vec<_>>()
493 .join(",")
494 {
495 s if s.is_empty() => String::new(),
496 s => format!(" stroke-dasharray=\"{s}\""),
497 };
498 let linecap = match style.lineend {
501 LineEnd::Butt => "",
502 LineEnd::Round => " stroke-linecap=\"round\"",
503 LineEnd::Square => " stroke-linecap=\"square\"",
504 };
505 let attrs = format!(
506 "points=\"{}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\" stroke-opacity=\"{:.3}\"{}{}",
507 points(&pts),
508 rgb(style.color),
509 finite_or(style.width, 1.0),
510 finite_or(style.alpha, 1.0),
511 dash,
512 linecap
513 );
514 self.push_mark("polyline", &attrs);
515 Ok(())
516 }
517
518 fn draw_quadratic(
521 &mut self,
522 p0: (f64, f64),
523 ctrl: (f64, f64),
524 p1: (f64, f64),
525 style: &LineStyle,
526 ) -> Result<(), RenderError> {
527 if !(p0.0.is_finite()
528 && p0.1.is_finite()
529 && ctrl.0.is_finite()
530 && ctrl.1.is_finite()
531 && p1.0.is_finite()
532 && p1.1.is_finite())
533 {
534 return Ok(());
535 }
536 let dash = match style
537 .linetype
538 .pattern()
539 .iter()
540 .flat_map(|(d, g)| [*d, *g])
541 .map(|v| format!("{v}"))
542 .collect::<Vec<_>>()
543 .join(",")
544 {
545 s if s.is_empty() => String::new(),
546 s => format!(" stroke-dasharray=\"{s}\""),
547 };
548 let linecap = match style.lineend {
549 LineEnd::Butt => "",
550 LineEnd::Round => " stroke-linecap=\"round\"",
551 LineEnd::Square => " stroke-linecap=\"square\"",
552 };
553 let d = format!(
554 "M{:.2} {:.2} Q{:.2} {:.2} {:.2} {:.2}",
555 p0.0, p0.1, ctrl.0, ctrl.1, p1.0, p1.1
556 );
557 let attrs = format!(
558 "d=\"{d}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\" stroke-opacity=\"{:.3}\"{}{}",
559 rgb(style.color),
560 finite_or(style.width, 1.0),
561 finite_or(style.alpha, 1.0),
562 dash,
563 linecap
564 );
565 self.push_mark("path", &attrs);
566 Ok(())
567 }
568
569 fn draw_rect(
570 &mut self,
571 (x0, y0): (f64, f64),
572 (x1, y1): (f64, f64),
573 style: &RectStyle,
574 ) -> Result<(), RenderError> {
575 if !(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite()) {
576 return Ok(());
577 }
578 let (x, y) = (x0.min(x1), y0.min(y1));
579 let (w, h) = ((x1 - x0).abs(), (y1 - y0).abs());
580 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
581 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
582 let attrs = format!(
583 "x=\"{x:.2}\" y=\"{y:.2}\" width=\"{w:.2}\" height=\"{h:.2}\" fill=\"{fill}\" \
584 fill-opacity=\"{:.3}\" stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
585 finite_or(style.alpha, 1.0),
586 finite_or(style.stroke_width, 0.0)
587 );
588 self.push_mark("rect", &attrs);
589 Ok(())
590 }
591
592 fn draw_polygon(&mut self, pts: &[(f64, f64)], style: &RectStyle) -> Result<(), RenderError> {
593 let pts = finite_pts(pts);
594 if pts.len() < 3 {
595 return Ok(());
596 }
597 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
598 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
599 let attrs = format!(
600 "points=\"{}\" fill=\"{fill}\" fill-opacity=\"{:.3}\" stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
601 points(&pts),
602 finite_or(style.alpha, 1.0),
603 finite_or(style.stroke_width, 0.0)
604 );
605 self.push_mark("polygon", &attrs);
606 Ok(())
607 }
608
609 fn draw_text(
610 &mut self,
611 text: &str,
612 (x, y): (f64, f64),
613 style: &TextStyle,
614 ) -> Result<(), RenderError> {
615 if !(x.is_finite() && y.is_finite()) {
616 return Ok(());
617 }
618 let anchor = match style.anchor {
619 TextAnchor::Start => "start",
620 TextAnchor::Middle => "middle",
621 TextAnchor::End => "end",
622 };
623 let family = escape(style.family.as_deref().unwrap_or("sans-serif"));
624 let weight = if style.face == FontFace::Bold {
625 " font-weight=\"bold\""
626 } else {
627 ""
628 };
629 let fstyle = if style.face == FontFace::Italic {
630 " font-style=\"italic\""
631 } else {
632 ""
633 };
634 let angle = finite_or(style.angle, 0.0);
635 let transform = if angle.abs() > 0.01 {
636 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
637 } else {
638 String::new()
639 };
640 self.body.push_str(&format!(
641 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
642 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle} fill=\"{}\"{transform}>{}</text>",
643 finite_or(style.size, 12.0),
644 rgb(style.color),
645 escape(text)
646 ));
647 Ok(())
648 }
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654 use crate::render::Rect;
655
656 fn backend() -> SvgBackend {
657 SvgBackend::new(
658 100,
659 100,
660 Rect {
661 x: 0.0,
662 y: 0.0,
663 width: 100.0,
664 height: 100.0,
665 },
666 )
667 }
668
669 #[test]
670 fn draw_quadratic_emits_a_path_with_m_and_q() {
671 let mut b = backend();
672 b.draw_quadratic((0.0, 0.0), (10.0, 20.0), (20.0, 0.0), &LineStyle::default())
673 .unwrap();
674 let svg = b.finish();
675 assert!(svg.contains("<path"));
676 assert!(svg.contains("d=\"M0.00 0.00 Q10.00 20.00 20.00 0.00\""));
677 assert!(!svg.contains("NaN"));
678 assert!(!svg.contains("inf"));
679 }
680
681 #[test]
682 fn draw_quadratic_drops_non_finite_input() {
683 let mut b = backend();
684 b.draw_quadratic(
685 (f64::NAN, 0.0),
686 (10.0, 20.0),
687 (20.0, 0.0),
688 &LineStyle::default(),
689 )
690 .unwrap();
691 b.draw_quadratic(
692 (0.0, 0.0),
693 (f64::INFINITY, 20.0),
694 (20.0, 0.0),
695 &LineStyle::default(),
696 )
697 .unwrap();
698 let svg = b.finish();
699 assert!(!svg.contains("<path"));
700 }
701
702 #[test]
703 fn draw_line_omits_linecap_by_default_but_writes_it_when_set() {
704 let mut b = backend();
705 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &LineStyle::default())
706 .unwrap();
707 let svg = b.finish();
708 assert!(!svg.contains("stroke-linecap"));
709
710 let mut b2 = backend();
711 let style = LineStyle {
712 lineend: LineEnd::Round,
713 ..Default::default()
714 };
715 b2.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
716 let svg2 = b2.finish();
717 assert!(svg2.contains("stroke-linecap=\"round\""));
718 }
719
720 #[test]
721 fn square_linecap_is_written_on_both_draw_line_and_draw_quadratic() {
722 let style = LineStyle {
723 lineend: LineEnd::Square,
724 ..Default::default()
725 };
726 let mut b = backend();
727 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
728 assert!(b.finish().contains("stroke-linecap=\"square\""));
729
730 let mut b2 = backend();
731 b2.draw_quadratic((0.0, 0.0), (5.0, 10.0), (10.0, 0.0), &style)
732 .unwrap();
733 assert!(b2.finish().contains("stroke-linecap=\"square\""));
734 }
735
736 #[test]
741 fn set_mark_attrs_defence_in_depth_drops_invalid_names() {
742 let mut b = backend();
743 b.set_mark_attrs(vec![
744 ("not-data-prefixed".to_string(), "x".to_string()),
745 ("data-".to_string(), "x".to_string()), ("data-Upper".to_string(), "x".to_string()), ("data-ok".to_string(), "valid".to_string()), ]);
749 b.draw_circle((1.0, 1.0), 1.0, &PointStyle::default())
750 .unwrap();
751 let svg = b.finish();
752 assert!(!svg.contains("not-data-prefixed"));
753 assert!(!svg.contains("data-Upper"));
754 assert!(!svg.contains("data-=\""));
755 assert!(svg.contains("data-ok=\"valid\""));
756 }
757}