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 mark_geom: Option<String>,
31 root_attrs: Vec<(String, String)>,
32 warnings: Vec<String>,
33 plot_attr: bool,
36 formatted_values: bool,
39 mark_fvalue: Option<String>,
42 mark_fx: Option<String>,
45}
46
47impl SvgBackend {
48 pub fn new(width: u32, height: u32, plot_area: Rect) -> Self {
49 SvgBackend {
50 plot_area,
51 total_area: Rect {
52 x: 0.0,
53 y: 0.0,
54 width: width as f64,
55 height: height as f64,
56 },
57 body: String::new(),
58 tooltip: None,
59 axis_key: None,
60 series_key: None,
61 value_key: None,
62 mark_part: None,
63 mark_attrs: Vec::new(),
64 mark_geom: None,
65 root_attrs: Vec::new(),
66 warnings: Vec::new(),
67 plot_attr: true,
68 formatted_values: false,
69 mark_fvalue: None,
70 mark_fx: None,
71 }
72 }
73
74 pub(crate) fn set_formatted_values(&mut self, on: bool) {
78 self.formatted_values = on;
79 }
80
81 pub fn set_root_attrs(&mut self, attrs: Vec<(String, String)>) {
85 self.root_attrs = attrs;
86 }
87
88 pub fn extend_root_attrs(&mut self, attrs: Vec<(String, String)>) {
96 self.root_attrs.extend(attrs);
97 }
98
99 pub(crate) fn without_plot_attr(&mut self) {
101 self.plot_attr = false;
102 }
103
104 pub(crate) fn push_raw(&mut self, markup: &str) {
107 self.body.push_str(markup);
108 }
109
110 pub(crate) fn body(&self) -> &str {
112 &self.body
113 }
114
115 pub fn take_warnings(&mut self) -> Vec<String> {
117 std::mem::take(&mut self.warnings)
118 }
119
120 fn mark_class(&self, kind: &str) -> String {
123 match &self.mark_geom {
124 Some(g) => format!(" class=\"gg-mark gg-{kind}\" data-geom=\"{}\"", escape(g)),
125 None => String::new(),
126 }
127 }
128
129 fn push_mark(&mut self, tag: &str, kind: &str, attrs: &str) {
137 let mut pairs: Vec<(String, String)> = Vec::new();
147 for (name, val) in [
148 ("data-x", &self.axis_key),
149 ("data-series", &self.series_key),
150 ("data-value", &self.value_key),
151 ] {
152 if let Some(v) = val {
153 pairs.push((name.to_string(), v.clone()));
154 }
155 }
156 if let Some(fx) = &self.mark_fx {
160 if self.axis_key.as_deref() != Some(fx.as_str()) {
161 pairs.push(("data-fx".to_string(), fx.clone()));
162 }
163 }
164 if let Some(fvalue) = &self.mark_fvalue {
165 pairs.push(("data-fvalue".to_string(), fvalue.clone()));
166 }
167 for (name, val) in &self.mark_attrs {
168 if !is_valid_mark_attr_name(name) {
169 continue;
170 }
171 match pairs.iter_mut().find(|(n, _)| n == name) {
172 Some(existing) => existing.1 = val.clone(),
173 None => pairs.push((name.clone(), val.clone())),
174 }
175 }
176 if let Some(part) = &self.mark_part {
181 match pairs.iter_mut().find(|(n, _)| n == "data-part") {
182 Some(existing) => existing.1 = part.clone(),
183 None => pairs.push(("data-part".to_string(), part.clone())),
184 }
185 }
186 let mut data = String::new();
187 for (name, val) in &pairs {
188 data.push_str(&format!(" {name}=\"{}\"", escape(val)));
189 }
190 if self.mark_part.is_some() {
191 data.push_str(" aria-hidden=\"true\"");
192 }
193 let effective_kind: &str = self.mark_part.as_deref().unwrap_or(kind);
198 data.push_str(&self.mark_class(effective_kind));
199 let mark = if self.mark_part.is_some() {
200 format!("<{tag} {attrs}{data}/>")
201 } else {
202 match &self.tooltip {
203 Some(t) => format!("<{tag} {attrs}{data}><title>{}</title></{tag}>", escape(t)),
204 None => format!("<{tag} {attrs}{data}/>"),
205 }
206 };
207 self.body.push_str(&mark);
208 }
209
210 fn root_open(&self, prefix: &str) -> String {
211 let p = &self.plot_area;
212 let (w, h) = (self.total_area.width as i64, self.total_area.height as i64);
213 let mut extra = String::new();
214 for (k, v) in &self.root_attrs {
215 extra.push_str(&format!(" {k}=\"{}\"", escape(v)));
216 }
217 let plot = if self.plot_attr {
218 format!(
219 " data-plot=\"{} {} {} {}\"",
220 num(p.x),
221 num(p.y),
222 num(p.width),
223 num(p.height),
224 )
225 } else {
226 String::new()
227 };
228 format!("<svg {prefix}width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\"{plot}{extra}>")
229 }
230
231 pub fn finish(self) -> String {
235 let open = self.root_open("xmlns=\"http://www.w3.org/2000/svg\" ");
236 format!("{open}{}</svg>", self.body)
237 }
238
239 pub fn finish_fragment(self, x: f64, y: f64) -> String {
244 let open = self.root_open(&format!("x=\"{}\" y=\"{}\" ", num(x), num(y)));
245 format!("{open}{}</svg>", self.body)
246 }
247
248 fn draw_line_run(&mut self, pts: &[(f64, f64)], style: &LineStyle) {
251 let dash = match style
252 .linetype
253 .pattern()
254 .iter()
255 .flat_map(|(d, g)| [*d, *g])
256 .map(|v| format!("{v}"))
257 .collect::<Vec<_>>()
258 .join(",")
259 {
260 s if s.is_empty() => String::new(),
261 s => format!(" stroke-dasharray=\"{s}\""),
262 };
263 let linecap = match style.lineend {
266 LineEnd::Butt => "",
267 LineEnd::Round => " stroke-linecap=\"round\"",
268 LineEnd::Square => " stroke-linecap=\"square\"",
269 };
270 let attrs = format!(
271 "points=\"{}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\"{}{}{}",
272 points(pts),
273 rgb(style.color),
274 finite_or(style.width, 1.0),
275 opacity_attr("stroke", style.alpha),
276 dash,
277 linecap
278 );
279 self.push_mark("polyline", "line", &attrs);
280 }
281}
282
283fn num(v: f64) -> String {
286 if v.is_finite() {
287 format!("{v:.2}")
288 } else {
289 "0".to_string()
290 }
291}
292
293fn finite_or(v: f64, fallback: f64) -> f64 {
294 if v.is_finite() {
295 v
296 } else {
297 fallback
298 }
299}
300
301fn opacity_attr(prefix: &str, alpha: f64) -> String {
307 let a = finite_or(alpha, 1.0);
308 if format!("{a:.3}") == "1.000" {
309 String::new()
310 } else {
311 format!(" {prefix}-opacity=\"{a:.3}\"")
312 }
313}
314
315pub fn root_data_attrs(built: &crate::build::BuiltPlot) -> Vec<(String, String)> {
332 use crate::aes::Aesthetic;
333 let mut out = Vec::new();
334 if !built.panel_scales.is_empty() {
335 return out;
336 }
337 let axis = |aes: &Aesthetic| -> (Option<(f64, f64)>, Option<String>) {
338 match built.scales.get(aes) {
339 Some(s) if s.is_discrete() => {
340 let labels: Vec<String> = s
341 .breaks()
342 .into_iter()
343 .map(|(_, l)| json_string(&l))
344 .collect();
345 (None, Some(format!("[{}]", labels.join(","))))
346 }
347 Some(s) => (s.expanded_domain(), None),
348 None => (None, None),
349 }
350 };
351 let (xd, xl) = axis(&Aesthetic::X);
352 let (yd, yl) = axis(&Aesthetic::Y);
353 if let (Some((x0, x1)), Some((y0, y1))) = (xd, yd) {
354 out.push((
355 "data-domain".into(),
356 format!("{} {} {} {}", g(x0), g(x1), g(y0), g(y1)),
357 ));
358 }
359 if let Some((a, b)) = xd {
360 out.push(("data-xdomain".into(), format!("{} {}", g(a), g(b))));
361 }
362 if let Some((a, b)) = yd {
363 out.push(("data-ydomain".into(), format!("{} {}", g(a), g(b))));
364 }
365 if let Some(l) = xl {
366 out.push(("data-xlevels".into(), l));
367 }
368 if let Some(l) = yl {
369 out.push(("data-ylevels".into(), l));
370 }
371 if built.coord.is_flipped() {
372 out.push(("data-flip".into(), "true".into()));
373 }
374 out
375}
376
377fn g(v: f64) -> String {
380 format!("{v}")
381}
382
383fn json_string(s: &str) -> String {
386 let mut out = String::with_capacity(s.len() + 2);
387 out.push('"');
388 for c in s.chars() {
389 match c {
390 '"' => out.push_str("\\\""),
391 '\\' => out.push_str("\\\\"),
392 '\n' => out.push_str("\\n"),
393 '\r' => out.push_str("\\r"),
394 '\t' => out.push_str("\\t"),
395 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
396 c => out.push(c),
397 }
398 }
399 out.push('"');
400 out
401}
402
403fn is_allowed_root_attr_name(name: &str) -> bool {
410 matches!(name, "role" | "id" | "class" | "lang")
411 || name
412 .strip_prefix("data-")
413 .is_some_and(crate::aes::is_valid_data_attr_name)
414 || name
415 .strip_prefix("aria-")
416 .is_some_and(crate::aes::is_valid_data_attr_name)
417}
418
419pub(crate) fn validate_root_attrs(
429 requested: &[(String, String)],
430 existing: &[(String, String)],
431 label: &str,
432 warnings: &mut Vec<String>,
433) -> Vec<(String, String)> {
434 let built_in: Vec<&str> = existing.iter().map(|(k, _)| k.as_str()).collect();
435 let mut used: Vec<&str> = built_in.clone();
436 let mut out = Vec::new();
437 for (name, value) in requested {
438 if !is_allowed_root_attr_name(name) {
439 warnings.push(format!(
440 "{label}: svg_attr(\"{name}\") is not on the allow-list (data-*, aria-*, role, id, class, lang) — ignored"
441 ));
442 continue;
443 }
444 if used.contains(&name.as_str()) {
445 if built_in.contains(&name.as_str()) {
450 warnings.push(format!(
451 "{label}: svg_attr(\"{name}\") collides with an attribute ggplot-rs already writes to the root — ignored"
452 ));
453 } else {
454 warnings.push(format!(
455 "{label}: svg_attr(\"{name}\") was already set by an earlier svg_attr call — ignored"
456 ));
457 }
458 continue;
459 }
460 used.push(name.as_str());
461 out.push((name.clone(), value.clone()));
462 }
463 out
464}
465
466pub(crate) fn warnings_json(warnings: &[String]) -> String {
470 let items: Vec<String> = warnings.iter().map(|w| json_string(w)).collect();
471 format!("[{}]", items.join(","))
472}
473
474fn is_valid_mark_attr_name(name: &str) -> bool {
482 match name.strip_prefix("data-") {
483 Some(suffix) => crate::aes::is_valid_data_attr_name(suffix),
484 None => false,
485 }
486}
487
488fn rgb((r, g, b): (u8, u8, u8)) -> String {
489 format!("#{r:02X}{g:02X}{b:02X}")
490}
491
492fn escape(s: &str) -> String {
499 let mut out = String::with_capacity(s.len());
500 for c in s.chars() {
501 match c {
502 '&' => out.push_str("&"),
503 '<' => out.push_str("<"),
504 '>' => out.push_str(">"),
505 '"' => out.push_str("""),
506 '\'' => out.push_str("'"),
507 '\t' | '\n' | '\r' => out.push(c),
508 c if (c as u32) < 0x20 || c == '\u{FFFE}' || c == '\u{FFFF}' => {}
509 c => out.push(c),
510 }
511 }
512 out
513}
514
515fn points(pts: &[(f64, f64)]) -> String {
516 pts.iter()
517 .map(|(x, y)| format!("{x:.2},{y:.2}"))
518 .collect::<Vec<_>>()
519 .join(" ")
520}
521
522fn finite_pts(pts: &[(f64, f64)]) -> Vec<(f64, f64)> {
523 pts.iter()
524 .copied()
525 .filter(|(x, y)| x.is_finite() && y.is_finite())
526 .collect()
527}
528
529fn finite_runs(pts: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
538 let mut runs = Vec::new();
539 let mut cur: Vec<(f64, f64)> = Vec::new();
540 for &(x, y) in pts {
541 if x.is_finite() && y.is_finite() {
542 cur.push((x, y));
543 } else if !cur.is_empty() {
544 if cur.len() >= 2 {
545 runs.push(std::mem::take(&mut cur));
546 } else {
547 cur.clear();
548 }
549 }
550 }
551 if cur.len() >= 2 {
552 runs.push(cur);
553 }
554 runs
555}
556
557impl DrawBackend for SvgBackend {
558 fn plot_area(&self) -> Rect {
559 self.plot_area.clone()
560 }
561 fn total_area(&self) -> Rect {
562 self.total_area.clone()
563 }
564
565 fn set_tooltip(&mut self, tooltip: Option<String>) {
566 self.tooltip = tooltip;
567 }
568
569 fn set_mark_axis(&mut self, key: Option<String>) {
570 self.axis_key = key;
571 }
572
573 fn set_mark_series(&mut self, series: Option<String>) {
574 self.series_key = series;
575 }
576
577 fn set_mark_value(&mut self, value: Option<String>) {
578 self.value_key = value;
579 }
580
581 fn set_mark_geom(&mut self, geom: Option<&str>) {
582 self.mark_geom = geom.map(str::to_string);
583 }
584
585 fn warn(&mut self, message: String) {
586 self.warnings.push(message);
587 }
588
589 fn set_mark_part(&mut self, part: Option<&str>) {
590 self.mark_part = part.map(|s| s.to_string());
591 }
592
593 fn set_mark_attrs(&mut self, attrs: Vec<(String, String)>) {
594 self.mark_attrs = attrs;
595 }
596
597 fn wants_formatted_values(&self) -> bool {
598 self.formatted_values
599 }
600
601 fn set_mark_fvalue(&mut self, value: Option<String>) {
602 self.mark_fvalue = value;
603 }
604
605 fn set_mark_fx(&mut self, value: Option<String>) {
606 self.mark_fx = value;
607 }
608
609 fn draw_circle(
610 &mut self,
611 (cx, cy): (f64, f64),
612 radius: f64,
613 style: &PointStyle,
614 ) -> Result<(), RenderError> {
615 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
616 return Ok(());
617 }
618 let attrs = format!(
619 "cx=\"{cx:.2}\" cy=\"{cy:.2}\" r=\"{:.2}\" fill=\"{}\"{}",
620 radius.max(0.0),
621 rgb(style.color),
622 opacity_attr("fill", style.alpha)
623 );
624 self.push_mark("circle", "point", &attrs);
625 Ok(())
626 }
627
628 fn draw_shape(
632 &mut self,
633 (cx, cy): (f64, f64),
634 radius: f64,
635 style: &PointStyle,
636 ) -> Result<(), RenderError> {
637 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
638 return Ok(());
639 }
640 let r = radius.max(0.0);
641 let color = rgb(style.color);
642 let fill_opacity = opacity_attr("fill", style.alpha);
643 let stroke_opacity = opacity_attr("stroke", style.alpha);
644 let poly = |pts: &[(f64, f64)]| {
645 if style.filled {
646 format!("points=\"{}\" fill=\"{color}\"{fill_opacity}", points(pts))
647 } else {
648 format!(
649 "points=\"{}\" fill=\"none\" stroke=\"{color}\"{stroke_opacity}",
650 points(pts)
651 )
652 }
653 };
654 match style.shape {
655 PointShape::Circle => return self.draw_circle((cx, cy), radius, style),
656 PointShape::Square => {
657 let a = poly(&[
658 (cx - r, cy - r),
659 (cx + r, cy - r),
660 (cx + r, cy + r),
661 (cx - r, cy + r),
662 ]);
663 self.push_mark("polygon", "point", &a);
664 }
665 PointShape::Triangle => {
666 let a = poly(&[(cx, cy - r), (cx + r, cy + r), (cx - r, cy + r)]);
667 self.push_mark("polygon", "point", &a);
668 }
669 PointShape::Diamond => {
670 let a = poly(&[(cx, cy - r), (cx + r, cy), (cx, cy + r), (cx - r, cy)]);
671 self.push_mark("polygon", "point", &a);
672 }
673 PointShape::Plus | PointShape::Cross => {
674 let d = if style.shape == PointShape::Plus {
675 format!(
676 "M{:.2} {cy:.2}H{:.2}M{cx:.2} {:.2}V{:.2}",
677 cx - r,
678 cx + r,
679 cy - r,
680 cy + r
681 )
682 } else {
683 format!(
684 "M{:.2} {:.2}L{:.2} {:.2}M{:.2} {:.2}L{:.2} {:.2}",
685 cx - r,
686 cy - r,
687 cx + r,
688 cy + r,
689 cx - r,
690 cy + r,
691 cx + r,
692 cy - r
693 )
694 };
695 let a = format!(
696 "d=\"{d}\" fill=\"none\" stroke=\"{color}\" stroke-width=\"{:.2}\"{stroke_opacity}",
697 (r / 2.5).clamp(1.0, 3.0)
698 );
699 self.push_mark("path", "point", &a);
700 }
701 }
702 Ok(())
703 }
704
705 fn draw_line(&mut self, pts: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
706 for run in finite_runs(pts) {
712 self.draw_line_run(&run, style);
713 }
714 Ok(())
715 }
716
717 fn draw_quadratic(
720 &mut self,
721 p0: (f64, f64),
722 ctrl: (f64, f64),
723 p1: (f64, f64),
724 style: &LineStyle,
725 ) -> Result<(), RenderError> {
726 if !(p0.0.is_finite()
727 && p0.1.is_finite()
728 && ctrl.0.is_finite()
729 && ctrl.1.is_finite()
730 && p1.0.is_finite()
731 && p1.1.is_finite())
732 {
733 return Ok(());
734 }
735 let dash = match style
736 .linetype
737 .pattern()
738 .iter()
739 .flat_map(|(d, g)| [*d, *g])
740 .map(|v| format!("{v}"))
741 .collect::<Vec<_>>()
742 .join(",")
743 {
744 s if s.is_empty() => String::new(),
745 s => format!(" stroke-dasharray=\"{s}\""),
746 };
747 let linecap = match style.lineend {
748 LineEnd::Butt => "",
749 LineEnd::Round => " stroke-linecap=\"round\"",
750 LineEnd::Square => " stroke-linecap=\"square\"",
751 };
752 let d = format!(
753 "M{:.2} {:.2} Q{:.2} {:.2} {:.2} {:.2}",
754 p0.0, p0.1, ctrl.0, ctrl.1, p1.0, p1.1
755 );
756 let attrs = format!(
757 "d=\"{d}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\"{}{}{}",
758 rgb(style.color),
759 finite_or(style.width, 1.0),
760 opacity_attr("stroke", style.alpha),
761 dash,
762 linecap
763 );
764 self.push_mark("path", "line", &attrs);
765 Ok(())
766 }
767
768 fn draw_rect(
769 &mut self,
770 (x0, y0): (f64, f64),
771 (x1, y1): (f64, f64),
772 style: &RectStyle,
773 ) -> Result<(), RenderError> {
774 if !(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite()) {
775 return Ok(());
776 }
777 let (x, y) = (x0.min(x1), y0.min(y1));
778 let (w, h) = ((x1 - x0).abs(), (y1 - y0).abs());
779 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
780 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
781 let r = style.corner_radius.max(0.0).min(w / 2.0).min(h / 2.0);
784 let radius = if r > 0.0 {
785 format!(" rx=\"{r:.2}\" ry=\"{r:.2}\"")
786 } else {
787 String::new()
788 };
789 let attrs = format!(
790 "x=\"{x:.2}\" y=\"{y:.2}\" width=\"{w:.2}\" height=\"{h:.2}\"{radius} fill=\"{fill}\"{} \
791 stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
792 opacity_attr("fill", style.alpha),
793 finite_or(style.stroke_width, 0.0)
794 );
795 self.push_mark("rect", "bar", &attrs);
796 Ok(())
797 }
798
799 fn draw_polygon(&mut self, pts: &[(f64, f64)], style: &RectStyle) -> Result<(), RenderError> {
800 let pts = finite_pts(pts);
801 if pts.len() < 3 {
802 return Ok(());
803 }
804 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
805 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
806 let attrs = format!(
807 "points=\"{}\" fill=\"{fill}\"{} stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
808 points(&pts),
809 opacity_attr("fill", style.alpha),
810 finite_or(style.stroke_width, 0.0)
811 );
812 self.push_mark("polygon", "band", &attrs);
813 Ok(())
814 }
815
816 fn draw_polygon_rings(
822 &mut self,
823 rings: &[Vec<(f64, f64)>],
824 style: &RectStyle,
825 ) -> Result<(), RenderError> {
826 let mut d = String::new();
827 for ring in rings {
828 let pts = finite_pts(ring);
829 if pts.len() < 3 {
830 continue;
831 }
832 d.push('M');
833 d.push_str(&format!("{},{}", num(pts[0].0), num(pts[0].1)));
834 for p in &pts[1..] {
835 d.push('L');
836 d.push_str(&format!("{},{}", num(p.0), num(p.1)));
837 }
838 d.push('Z');
839 }
840 if d.is_empty() {
841 return Ok(());
842 }
843 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
844 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
845 let attrs = format!(
846 "d=\"{d}\" fill-rule=\"evenodd\" fill=\"{fill}\"{} stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
847 opacity_attr("fill", style.alpha),
848 finite_or(style.stroke_width, 0.0)
849 );
850 self.push_mark("path", "band", &attrs);
851 Ok(())
852 }
853
854 fn draw_text(
855 &mut self,
856 text: &str,
857 pos: (f64, f64),
858 style: &TextStyle,
859 ) -> Result<(), RenderError> {
860 self.draw_text_with_title(text, pos, style, None)
861 }
862
863 fn draw_text_with_title(
867 &mut self,
868 text: &str,
869 (x, y): (f64, f64),
870 style: &TextStyle,
871 title: Option<&str>,
872 ) -> Result<(), RenderError> {
873 if !(x.is_finite() && y.is_finite()) {
874 return Ok(());
875 }
876 let anchor = match style.anchor {
877 TextAnchor::Start => "start",
878 TextAnchor::Middle => "middle",
879 TextAnchor::End => "end",
880 };
881 let family = escape(style.family.as_deref().unwrap_or("sans-serif"));
882 let weight = if style.face == FontFace::Bold {
883 " font-weight=\"bold\""
884 } else {
885 ""
886 };
887 let fstyle = if style.face == FontFace::Italic {
888 " font-style=\"italic\""
889 } else {
890 ""
891 };
892 let angle = finite_or(style.angle, 0.0);
893 let transform = if angle.abs() > 0.01 {
894 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
895 } else {
896 String::new()
897 };
898 let class = self.mark_class("text");
899 let title_tag = match title {
900 Some(t) => format!("<title>{}</title>", escape(t)),
901 None => String::new(),
902 };
903 self.body.push_str(&format!(
904 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
905 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle} fill=\"{}\"{transform}{class}>{title_tag}{}</text>",
906 finite_or(style.size, 12.0),
907 rgb(style.color),
908 escape(text)
909 ));
910 Ok(())
911 }
912}
913
914#[cfg(test)]
915mod tests {
916 use super::*;
917 use crate::render::Rect;
918
919 fn backend() -> SvgBackend {
920 SvgBackend::new(
921 100,
922 100,
923 Rect {
924 x: 0.0,
925 y: 0.0,
926 width: 100.0,
927 height: 100.0,
928 },
929 )
930 }
931
932 #[test]
933 fn draw_quadratic_emits_a_path_with_m_and_q() {
934 let mut b = backend();
935 b.draw_quadratic((0.0, 0.0), (10.0, 20.0), (20.0, 0.0), &LineStyle::default())
936 .unwrap();
937 let svg = b.finish();
938 assert!(svg.contains("<path"));
939 assert!(svg.contains("d=\"M0.00 0.00 Q10.00 20.00 20.00 0.00\""));
940 assert!(!svg.contains("NaN"));
941 assert!(!svg.contains("inf"));
942 }
943
944 #[test]
945 fn draw_quadratic_drops_non_finite_input() {
946 let mut b = backend();
947 b.draw_quadratic(
948 (f64::NAN, 0.0),
949 (10.0, 20.0),
950 (20.0, 0.0),
951 &LineStyle::default(),
952 )
953 .unwrap();
954 b.draw_quadratic(
955 (0.0, 0.0),
956 (f64::INFINITY, 20.0),
957 (20.0, 0.0),
958 &LineStyle::default(),
959 )
960 .unwrap();
961 let svg = b.finish();
962 assert!(!svg.contains("<path"));
963 }
964
965 #[test]
966 fn draw_line_omits_linecap_by_default_but_writes_it_when_set() {
967 let mut b = backend();
968 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &LineStyle::default())
969 .unwrap();
970 let svg = b.finish();
971 assert!(!svg.contains("stroke-linecap"));
972
973 let mut b2 = backend();
974 let style = LineStyle {
975 lineend: LineEnd::Round,
976 ..Default::default()
977 };
978 b2.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
979 let svg2 = b2.finish();
980 assert!(svg2.contains("stroke-linecap=\"round\""));
981 }
982
983 #[test]
984 fn square_linecap_is_written_on_both_draw_line_and_draw_quadratic() {
985 let style = LineStyle {
986 lineend: LineEnd::Square,
987 ..Default::default()
988 };
989 let mut b = backend();
990 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
991 assert!(b.finish().contains("stroke-linecap=\"square\""));
992
993 let mut b2 = backend();
994 b2.draw_quadratic((0.0, 0.0), (5.0, 10.0), (10.0, 0.0), &style)
995 .unwrap();
996 assert!(b2.finish().contains("stroke-linecap=\"square\""));
997 }
998
999 #[test]
1004 fn set_mark_attrs_defence_in_depth_drops_invalid_names() {
1005 let mut b = backend();
1006 b.set_mark_attrs(vec![
1007 ("not-data-prefixed".to_string(), "x".to_string()),
1008 ("data-".to_string(), "x".to_string()), ("data-Upper".to_string(), "x".to_string()), ("data-ok".to_string(), "valid".to_string()), ]);
1012 b.draw_circle((1.0, 1.0), 1.0, &PointStyle::default())
1013 .unwrap();
1014 let svg = b.finish();
1015 assert!(!svg.contains("not-data-prefixed"));
1016 assert!(!svg.contains("data-Upper"));
1017 assert!(!svg.contains("data-=\""));
1018 assert!(svg.contains("data-ok=\"valid\""));
1019 }
1020}