1use super::backend::{
8 DrawBackend, FillPattern, FontFace, LineEnd, LineStyle, Linetype, PointShape, PointStyle,
9 RectStyle, TextAnchor, 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 patterns: Vec<(FillPattern, String)>,
50 id_prefix: String,
55 mark_user_class: Option<String>,
58 mark_group: Option<(String, String)>,
61 layer_attrs: Vec<(String, String)>,
64 groups: Vec<(String, String, String)>,
67 group_slots: Vec<(usize, usize)>,
72 svg_title: Option<String>,
77 svg_desc: Option<String>,
82 corner_claims: Vec<Rect>,
85 viewport: Option<Rect>,
88 panels: Vec<(usize, String, Rect, Rect)>,
92}
93
94static NEXT_ID_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
97
98impl SvgBackend {
99 pub fn new(width: u32, height: u32, plot_area: Rect) -> Self {
100 SvgBackend {
101 plot_area,
102 total_area: Rect {
103 x: 0.0,
104 y: 0.0,
105 width: width as f64,
106 height: height as f64,
107 },
108 body: String::new(),
109 tooltip: None,
110 axis_key: None,
111 series_key: None,
112 value_key: None,
113 mark_part: None,
114 mark_attrs: Vec::new(),
115 mark_geom: None,
116 root_attrs: Vec::new(),
117 warnings: Vec::new(),
118 plot_attr: true,
119 formatted_values: false,
120 mark_fvalue: None,
121 mark_fx: None,
122 patterns: Vec::new(),
123 id_prefix: format!(
124 "gg{:x}",
125 NEXT_ID_PREFIX.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
126 ),
127 mark_user_class: None,
128 mark_group: None,
129 layer_attrs: Vec::new(),
130 groups: Vec::new(),
131 group_slots: Vec::new(),
132 svg_title: None,
133 svg_desc: None,
134 corner_claims: Vec::new(),
135 viewport: None,
136 panels: Vec::new(),
137 }
138 }
139
140 fn pattern_url(&mut self, pattern: &FillPattern) -> String {
144 if let Some((_, id)) = self.patterns.iter().find(|(p, _)| p == pattern) {
145 return format!("url(#{id})");
146 }
147 let id = format!("{}-pat-{}", self.id_prefix, self.patterns.len());
148 self.patterns.push((*pattern, id.clone()));
149 format!("url(#{id})")
150 }
151
152 fn rect_markup(
155 &mut self,
156 (x0, y0): (f64, f64),
157 (x1, y1): (f64, f64),
158 fill: &str,
159 style: &RectStyle,
160 ) -> Result<(), RenderError> {
161 if !(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite()) {
162 return Ok(());
163 }
164 let (x, y) = (x0.min(x1), y0.min(y1));
165 let (w, h) = ((x1 - x0).abs(), (y1 - y0).abs());
166 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
167 let r = style.corner_radius.max(0.0).min(w / 2.0).min(h / 2.0);
170 let radius = if r > 0.0 {
171 format!(" rx=\"{r:.2}\" ry=\"{r:.2}\"")
172 } else {
173 String::new()
174 };
175 let attrs = format!(
176 "x=\"{x:.2}\" y=\"{y:.2}\" width=\"{w:.2}\" height=\"{h:.2}\"{radius} fill=\"{fill}\"{} \
177 stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
178 opacity_attr("fill", style.alpha),
179 finite_or(style.stroke_width, 0.0)
180 );
181 self.push_mark("rect", "bar", &attrs);
182 Ok(())
183 }
184
185 fn polygon_markup(
187 &mut self,
188 pts: &[(f64, f64)],
189 fill: &str,
190 style: &RectStyle,
191 ) -> Result<(), RenderError> {
192 let pts = finite_pts(pts);
193 if pts.len() < 3 {
194 return Ok(());
195 }
196 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
197 let attrs = format!(
198 "points=\"{}\" fill=\"{fill}\"{} stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
199 points(&pts),
200 opacity_attr("fill", style.alpha),
201 finite_or(style.stroke_width, 0.0)
202 );
203 self.push_mark("polygon", "band", &attrs);
204 Ok(())
205 }
206
207 fn polygon_rings_markup(
210 &mut self,
211 rings: &[Vec<(f64, f64)>],
212 fill: &str,
213 style: &RectStyle,
214 ) -> Result<(), RenderError> {
215 let mut d = String::new();
216 for ring in rings {
217 let pts = finite_pts(ring);
218 if pts.len() < 3 {
219 continue;
220 }
221 d.push('M');
222 d.push_str(&format!("{},{}", num(pts[0].0), num(pts[0].1)));
223 for p in &pts[1..] {
224 d.push('L');
225 d.push_str(&format!("{},{}", num(p.0), num(p.1)));
226 }
227 d.push('Z');
228 }
229 if d.is_empty() {
230 return Ok(());
231 }
232 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
233 let attrs = format!(
234 "d=\"{d}\" fill-rule=\"evenodd\" fill=\"{fill}\"{} stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
235 opacity_attr("fill", style.alpha),
236 finite_or(style.stroke_width, 0.0)
237 );
238 self.push_mark("path", "band", &attrs);
239 Ok(())
240 }
241
242 fn pattern_defs(&self) -> String {
247 if self.patterns.is_empty() && self.panels.is_empty() {
248 return String::new();
249 }
250 let mut defs = String::from("<defs>");
251 for (i, _, clip, _) in &self.panels {
252 defs.push_str(&format!(
253 "<clipPath id=\"{}-panel-{i}\"><rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\"/></clipPath>",
254 self.id_prefix,
255 num(clip.x),
256 num(clip.y),
257 num(clip.width),
258 num(clip.height),
259 ));
260 }
261 for (p, id) in &self.patterns {
262 let FillPattern::Stripe {
263 fg,
264 bg,
265 size_px,
266 angle_deg,
267 } = p;
268 let size = if *size_px > 0.0 { *size_px } else { 1.0 };
269 defs.push_str(&format!(
270 "<pattern id=\"{id}\" width=\"{size}\" height=\"{size}\" patternUnits=\"userSpaceOnUse\" patternTransform=\"rotate({angle_deg})\"><rect width=\"{size}\" height=\"{size}\" fill=\"{}\"/><rect width=\"{:.3}\" height=\"{size}\" fill=\"{}\"/></pattern>",
271 rgb(*bg),
272 size * 0.3,
273 rgb(*fg),
274 ));
275 }
276 defs.push_str("</defs>");
277 defs
278 }
279
280 pub(crate) fn set_formatted_values(&mut self, on: bool) {
284 self.formatted_values = on;
285 }
286
287 pub fn set_root_attrs(&mut self, attrs: Vec<(String, String)>) {
291 self.root_attrs = attrs;
292 }
293
294 pub fn extend_root_attrs(&mut self, attrs: Vec<(String, String)>) {
302 self.root_attrs.extend(attrs);
303 }
304
305 pub(crate) fn without_plot_attr(&mut self) {
307 self.plot_attr = false;
308 }
309
310 pub(crate) fn set_svg_title(&mut self, title: Option<String>) {
315 self.svg_title = title;
316 }
317
318 pub(crate) fn set_svg_desc(&mut self, desc: Option<String>) {
323 self.svg_desc = desc;
324 }
325
326 pub(crate) fn push_raw(&mut self, markup: &str) {
329 self.body.push_str(markup);
330 }
331
332 pub(crate) fn body(&mut self) -> &str {
335 self.flush_groups();
336 &self.body
337 }
338
339 fn emit(&mut self, markup: &str) {
342 let key = match (&self.mark_group, &self.mark_geom) {
343 (Some(k), Some(_)) => k,
344 _ => {
345 self.body.push_str(markup);
346 return;
347 }
348 };
349 match self
350 .groups
351 .iter_mut()
352 .find(|(n, v, _)| n == &key.0 && v == &key.1)
353 {
354 Some((_, _, content)) => content.push_str(markup),
355 None => {
356 self.group_slots.push((self.body.len(), self.groups.len()));
357 self.groups
358 .push((key.0.clone(), key.1.clone(), markup.to_string()));
359 }
360 }
361 }
362
363 fn flush_groups(&mut self) {
369 if self.group_slots.is_empty() {
370 return;
371 }
372 let groups = std::mem::take(&mut self.groups);
373 let slots = std::mem::take(&mut self.group_slots);
374 let extra: usize = groups
375 .iter()
376 .map(|(n, v, c)| n.len() + v.len() + c.len() + 12)
377 .sum();
378 let mut out = String::with_capacity(self.body.len() + extra);
379 let mut last = 0;
380 for (off, gi) in slots {
381 out.push_str(&self.body[last..off]);
382 last = off;
383 let (name, value, content) = &groups[gi];
384 if is_valid_mark_attr_name(name) {
385 out.push_str(&format!(
386 "<g {name}=\"{}\">{content}</g>",
387 escape_attr(value)
388 ));
389 } else {
390 out.push_str(content);
391 }
392 }
393 out.push_str(&self.body[last..]);
394 self.body = out;
395 }
396
397 pub(crate) fn wrap_body_in_group(&mut self, attrs: &[(String, String)]) {
404 self.flush_groups();
405 if attrs.is_empty() {
406 return;
407 }
408 let mut open = String::from("<g");
409 for (k, v) in attrs {
410 open.push_str(&format!(" {k}=\"{}\"", escape_attr(v)));
411 }
412 open.push('>');
413 self.body = format!("{open}{}</g>", self.body);
414 }
415
416 pub fn take_warnings(&mut self) -> Vec<String> {
418 std::mem::take(&mut self.warnings)
419 }
420
421 fn mark_class(&self, kind: &str) -> String {
428 match &self.mark_geom {
429 Some(g) => {
430 let mut class = format!("gg-mark gg-{kind}");
431 let layer = self
432 .layer_attrs
433 .iter()
434 .filter(|(n, _)| n == "class")
435 .map(|(_, v)| v.as_str());
436 let mut seen: Vec<&str> = Vec::new();
437 for t in layer
438 .chain(self.mark_user_class.as_deref())
439 .flat_map(str::split_whitespace)
440 {
441 if crate::aes::is_valid_class_token(t) && !seen.contains(&t) {
442 seen.push(t);
443 class.push(' ');
444 class.push_str(t);
445 }
446 }
447 format!(" class=\"{class}\" data-geom=\"{}\"", escape_attr(g))
448 }
449 None => String::new(),
450 }
451 }
452
453 fn layer_flag_attrs(&self, aria_hidden_written: bool) -> String {
458 let mut out = String::new();
459 if self.mark_geom.is_none() {
460 return out;
461 }
462 for (name, value) in &self.layer_attrs {
463 let ok = match name.as_str() {
464 "aria-hidden" => !aria_hidden_written && matches!(value.as_str(), "true" | "false"),
465 "pointer-events" => POINTER_EVENTS_VALUES.contains(&value.as_str()),
466 _ => false,
467 };
468 if ok {
469 out.push_str(&format!(" {name}=\"{value}\""));
470 }
471 }
472 out
473 }
474
475 fn text_data_attrs(&self) -> String {
480 let mut pairs: Vec<(&str, &str)> = Vec::new();
481 if self.mark_geom.is_none() {
482 return String::new();
483 }
484 for (name, val) in self.layer_attrs.iter().chain(self.mark_attrs.iter()) {
485 if !is_valid_mark_attr_name(name) {
486 continue;
487 }
488 match pairs.iter_mut().find(|(n, _)| n == name) {
489 Some(existing) => existing.1 = val,
490 None => pairs.push((name, val)),
491 }
492 }
493 let mut out = String::new();
494 for (name, val) in pairs {
495 out.push_str(&format!(" {name}=\"{}\"", escape_attr(val)));
496 }
497 out
498 }
499
500 fn push_mark(&mut self, tag: &str, kind: &str, attrs: &str) {
508 let mut pairs: Vec<(String, String)> = Vec::new();
518 for (name, val) in [
519 ("data-x", &self.axis_key),
520 ("data-series", &self.series_key),
521 ("data-value", &self.value_key),
522 ] {
523 if let Some(v) = val {
524 pairs.push((name.to_string(), v.clone()));
525 }
526 }
527 if let Some(fx) = &self.mark_fx {
531 if self.axis_key.as_deref() != Some(fx.as_str()) {
532 pairs.push(("data-fx".to_string(), fx.clone()));
533 }
534 }
535 if let Some(fvalue) = &self.mark_fvalue {
536 pairs.push(("data-fvalue".to_string(), fvalue.clone()));
537 }
538 let layer_data = self.layer_attrs.iter().filter(|_| self.mark_geom.is_some());
541 for (name, val) in layer_data.chain(self.mark_attrs.iter()) {
542 if !is_valid_mark_attr_name(name) {
543 continue;
544 }
545 match pairs.iter_mut().find(|(n, _)| n == name) {
546 Some(existing) => existing.1 = val.clone(),
547 None => pairs.push((name.clone(), val.clone())),
548 }
549 }
550 if let Some(part) = &self.mark_part {
555 match pairs.iter_mut().find(|(n, _)| n == "data-part") {
556 Some(existing) => existing.1 = part.clone(),
557 None => pairs.push(("data-part".to_string(), part.clone())),
558 }
559 }
560 let mut data = String::new();
561 for (name, val) in &pairs {
562 data.push_str(&format!(" {name}=\"{}\"", escape_attr(val)));
563 }
564 if self.mark_part.is_some() {
565 data.push_str(" aria-hidden=\"true\"");
566 }
567 data.push_str(&self.layer_flag_attrs(self.mark_part.is_some()));
568 let effective_kind: &str = self.mark_part.as_deref().unwrap_or(kind);
573 data.push_str(&self.mark_class(effective_kind));
574 let mark = if self.mark_part.is_some() {
575 format!("<{tag} {attrs}{data}/>")
576 } else {
577 match &self.tooltip {
578 Some(t) => format!("<{tag} {attrs}{data}><title>{}</title></{tag}>", escape(t)),
579 None => format!("<{tag} {attrs}{data}/>"),
580 }
581 };
582 self.emit(&mark);
583 }
584
585 fn root_open(&self, prefix: &str) -> String {
586 let p = &self.plot_area;
587 let (w, h) = (self.total_area.width as i64, self.total_area.height as i64);
588 let mut extra = String::new();
589 for (k, v) in &self.root_attrs {
590 extra.push_str(&format!(" {k}=\"{}\"", escape_attr(v)));
591 }
592 let mut plot = if self.plot_attr {
593 format!(
594 " data-plot=\"{} {} {} {}\"",
595 num(p.x),
596 num(p.y),
597 num(p.width),
598 num(p.height),
599 )
600 } else {
601 String::new()
602 };
603 if self.plot_attr {
604 if let Some(v) = &self.viewport {
605 plot.push_str(&format!(
606 " data-viewport=\"{} {} {} {}\"",
607 num(v.x),
608 num(v.y),
609 num(v.width),
610 num(v.height),
611 ));
612 }
613 if !self.panels.is_empty() {
614 let items: Vec<String> = self
615 .panels
616 .iter()
617 .map(|(i, label, _, d)| {
618 format!(
619 "{{\"i\":{i},\"label\":{},\"x\":{},\"y\":{},\"w\":{},\"h\":{}}}",
620 json_string(label),
621 num(d.x),
622 num(d.y),
623 num(d.width),
624 num(d.height),
625 )
626 })
627 .collect();
628 plot.push_str(&format!(
629 " data-panels=\"{}\"",
630 escape_attr(&format!("[{}]", items.join(",")))
631 ));
632 }
633 }
634 let mut access = String::new();
639 if self.svg_title.is_some() || self.svg_desc.is_some() {
640 access.push_str(" role=\"img\"");
641 }
642 if self.svg_title.is_some() {
643 access.push_str(&format!(" aria-labelledby=\"{}-title\"", self.id_prefix));
644 }
645 if self.svg_desc.is_some() {
646 access.push_str(&format!(" aria-describedby=\"{}-desc\"", self.id_prefix));
647 }
648 format!(
649 "<svg {prefix}width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\"{plot}{access}{extra}>"
650 )
651 }
652
653 fn access_children(&self) -> String {
658 let mut out = String::new();
659 if let Some(t) = &self.svg_title {
660 out.push_str(&format!(
661 "<title id=\"{}-title\">{}</title>",
662 self.id_prefix,
663 escape(t)
664 ));
665 }
666 if let Some(d) = &self.svg_desc {
667 out.push_str(&format!(
668 "<desc id=\"{}-desc\">{}</desc>",
669 self.id_prefix,
670 escape(d)
671 ));
672 }
673 out
674 }
675
676 pub fn finish(mut self) -> String {
680 self.flush_groups();
681 let open = self.root_open("xmlns=\"http://www.w3.org/2000/svg\" ");
682 let access = self.access_children();
683 let defs = self.pattern_defs();
684 format!("{open}{access}{defs}{}</svg>", self.body)
685 }
686
687 pub fn finish_fragment(mut self, x: f64, y: f64) -> String {
692 self.flush_groups();
693 let open = self.root_open(&format!("x=\"{}\" y=\"{}\" ", num(x), num(y)));
694 let access = self.access_children();
695 let defs = self.pattern_defs();
696 format!("{open}{access}{defs}{}</svg>", self.body)
697 }
698
699 fn draw_line_run(&mut self, pts: &[(f64, f64)], style: &LineStyle) {
702 let dash = dasharray_attr(&style.linetype);
703 let linecap = match style.lineend {
706 LineEnd::Butt => "",
707 LineEnd::Round => " stroke-linecap=\"round\"",
708 LineEnd::Square => " stroke-linecap=\"square\"",
709 };
710 let attrs = format!(
711 "points=\"{}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\"{}{}{}",
712 points(pts),
713 rgb(style.color),
714 finite_or(style.width, 1.0),
715 opacity_attr("stroke", style.alpha),
716 dash,
717 linecap
718 );
719 self.push_mark("polyline", "line", &attrs);
720 }
721}
722
723fn num(v: f64) -> String {
726 if v.is_finite() {
727 format!("{v:.2}")
728 } else {
729 "0".to_string()
730 }
731}
732
733fn dasharray_attr(linetype: &Linetype) -> String {
737 let pattern = linetype.dash_pattern();
738 if pattern.is_empty() {
739 return String::new();
740 }
741 let s = pattern
742 .iter()
743 .flat_map(|(d, g)| [*d, *g])
744 .map(|v| format!("{v}"))
745 .collect::<Vec<_>>()
746 .join(",");
747 format!(" stroke-dasharray=\"{s}\"")
748}
749
750fn compact_rings_d(rings: &[Vec<(f64, f64)>], precision: u8) -> String {
760 let mut d = String::new();
761 for ring in rings {
762 let pts = finite_pts(ring);
763 if pts.len() < 3 {
764 continue;
765 }
766 d.push('M');
767 push_num(&mut d, &fmt_compact(pts[0].0, precision));
768 push_num(&mut d, &fmt_compact(pts[0].1, precision));
769 if pts.len() > 1 {
770 d.push('l');
771 let mut prev = pts[0];
772 for p in &pts[1..] {
773 push_num(&mut d, &fmt_compact(p.0 - prev.0, precision));
774 push_num(&mut d, &fmt_compact(p.1 - prev.1, precision));
775 prev = *p;
776 }
777 }
778 d.push('z');
779 }
780 d
781}
782
783fn push_num(d: &mut String, s: &str) {
791 let needs_sep =
792 matches!(d.chars().last(), Some(c) if c.is_ascii_digit()) && !s.starts_with('-');
793 if needs_sep {
794 d.push(',');
795 }
796 d.push_str(s);
797}
798
799fn fmt_compact(v: f64, precision: u8) -> String {
805 if !v.is_finite() {
806 return "0".to_string();
807 }
808 let s = format!("{v:.*}", precision as usize);
809 let trimmed = if s.contains('.') {
810 s.trim_end_matches('0').trim_end_matches('.')
811 } else {
812 s.as_str()
813 };
814 match trimmed {
815 "" | "-" | "-0" => "0".to_string(),
816 other => other.to_string(),
817 }
818}
819
820fn finite_or(v: f64, fallback: f64) -> f64 {
821 if v.is_finite() {
822 v
823 } else {
824 fallback
825 }
826}
827
828fn opacity_attr(prefix: &str, alpha: f64) -> String {
834 let a = finite_or(alpha, 1.0);
835 if format!("{a:.3}") == "1.000" {
836 String::new()
837 } else {
838 format!(" {prefix}-opacity=\"{a:.3}\"")
839 }
840}
841
842pub fn root_data_attrs(built: &crate::build::BuiltPlot) -> Vec<(String, String)> {
859 use crate::aes::Aesthetic;
860 let mut out = Vec::new();
861 if !built.panel_scales.is_empty() {
862 return out;
863 }
864 let axis = |aes: &Aesthetic| -> (Option<(f64, f64)>, Option<String>) {
865 match built.scales.get(aes) {
866 Some(s) if s.is_discrete() => {
867 let labels: Vec<String> = s
868 .breaks()
869 .into_iter()
870 .map(|(_, l)| json_string(&l))
871 .collect();
872 (None, Some(format!("[{}]", labels.join(","))))
873 }
874 Some(s) => (s.expanded_domain(), None),
875 None => (None, None),
876 }
877 };
878 let (xd, xl) = axis(&Aesthetic::X);
879 let (yd, yl) = axis(&Aesthetic::Y);
880 if let (Some((x0, x1)), Some((y0, y1))) = (xd, yd) {
881 out.push((
882 "data-domain".into(),
883 format!("{} {} {} {}", g(x0), g(x1), g(y0), g(y1)),
884 ));
885 }
886 if let Some((a, b)) = xd {
887 out.push(("data-xdomain".into(), format!("{} {}", g(a), g(b))));
888 }
889 if let Some((a, b)) = yd {
890 out.push(("data-ydomain".into(), format!("{} {}", g(a), g(b))));
891 }
892 if let Some(l) = xl {
893 out.push(("data-xlevels".into(), l));
894 }
895 if let Some(l) = yl {
896 out.push(("data-ylevels".into(), l));
897 }
898 if built.coord.is_flipped() {
899 out.push(("data-flip".into(), "true".into()));
900 }
901 out
902}
903
904fn g(v: f64) -> String {
907 format!("{v}")
908}
909
910fn json_string(s: &str) -> String {
913 let mut out = String::with_capacity(s.len() + 2);
914 out.push('"');
915 for c in s.chars() {
916 match c {
917 '"' => out.push_str("\\\""),
918 '\\' => out.push_str("\\\\"),
919 '\n' => out.push_str("\\n"),
920 '\r' => out.push_str("\\r"),
921 '\t' => out.push_str("\\t"),
922 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
923 c => out.push(c),
924 }
925 }
926 out.push('"');
927 out
928}
929
930fn is_allowed_root_attr_name(name: &str) -> bool {
937 matches!(name, "role" | "id" | "class" | "lang")
938 || name
939 .strip_prefix("data-")
940 .is_some_and(crate::aes::is_valid_data_attr_name)
941 || name
942 .strip_prefix("aria-")
943 .is_some_and(crate::aes::is_valid_data_attr_name)
944}
945
946pub(crate) fn validate_root_attrs(
956 requested: &[(String, String)],
957 existing: &[(String, String)],
958 label: &str,
959 warnings: &mut Vec<String>,
960) -> Vec<(String, String)> {
961 let built_in: Vec<&str> = existing.iter().map(|(k, _)| k.as_str()).collect();
962 let mut used: Vec<&str> = built_in.clone();
963 let mut out = Vec::new();
964 for (name, value) in requested {
965 if !is_allowed_root_attr_name(name) {
966 warnings.push(format!(
967 "{label}: svg_attr(\"{name}\") is not on the allow-list (data-*, aria-*, role, id, class, lang) — ignored"
968 ));
969 continue;
970 }
971 if used.contains(&name.as_str()) {
972 if built_in.contains(&name.as_str()) {
977 warnings.push(format!(
978 "{label}: svg_attr(\"{name}\") collides with an attribute ggplot-rs already writes to the root — ignored"
979 ));
980 } else {
981 warnings.push(format!(
982 "{label}: svg_attr(\"{name}\") was already set by an earlier svg_attr call — ignored"
983 ));
984 }
985 continue;
986 }
987 used.push(name.as_str());
988 out.push((name.clone(), value.clone()));
989 }
990 out
991}
992
993pub(crate) fn warnings_json(warnings: &[String]) -> String {
997 let items: Vec<String> = warnings.iter().map(|w| json_string(w)).collect();
998 format!("[{}]", items.join(","))
999}
1000
1001fn is_valid_mark_attr_name(name: &str) -> bool {
1009 match name.strip_prefix("data-") {
1010 Some(suffix) => crate::aes::is_valid_data_attr_name(suffix),
1011 None => false,
1012 }
1013}
1014
1015const POINTER_EVENTS_VALUES: &[&str] = &[
1018 "auto",
1019 "none",
1020 "visiblePainted",
1021 "visibleFill",
1022 "visibleStroke",
1023 "visible",
1024 "painted",
1025 "fill",
1026 "stroke",
1027 "all",
1028 "bounding-box",
1029];
1030
1031pub(crate) fn validate_layer_svg_attr(
1040 name: &str,
1041 value: &str,
1042 warnings: &mut Vec<String>,
1043) -> Option<(String, String)> {
1044 let keep = |v: &str| Some((name.to_string(), v.to_string()));
1045 match name {
1046 "class" => {
1047 let (ok, bad) = crate::aes::split_class_tokens(value);
1048 if !bad.is_empty() {
1049 warnings.push(format!(
1050 "layer_svg_attr(\"class\"): invalid class token(s) {bad:?} (must match [A-Za-z][A-Za-z0-9_-]*) — dropped"
1051 ));
1052 }
1053 if ok.is_empty() {
1054 None
1055 } else {
1056 keep(&ok.join(" "))
1057 }
1058 }
1059 "aria-hidden" if matches!(value, "true" | "false") => keep(value),
1060 "pointer-events" if POINTER_EVENTS_VALUES.contains(&value) => keep(value),
1061 "aria-hidden" | "pointer-events" => {
1062 warnings.push(format!(
1063 "layer_svg_attr(\"{name}\"): value {value:?} is not allowed — ignored"
1064 ));
1065 None
1066 }
1067 _ if is_valid_mark_attr_name(name) => keep(value),
1068 _ => {
1069 warnings.push(format!(
1070 "layer_svg_attr(\"{name}\") is not on the allow-list (class, aria-hidden, pointer-events, data-*) — ignored"
1071 ));
1072 None
1073 }
1074 }
1075}
1076
1077fn rgb((r, g, b): (u8, u8, u8)) -> String {
1078 format!("#{r:02X}{g:02X}{b:02X}")
1079}
1080
1081fn escape(s: &str) -> String {
1092 escape_impl(s, false)
1093}
1094
1095fn escape_attr(s: &str) -> String {
1106 escape_impl(s, true)
1107}
1108
1109fn escape_impl(s: &str, in_attr: bool) -> String {
1110 let mut out = String::with_capacity(s.len());
1111 for c in s.chars() {
1112 match c {
1113 '&' => out.push_str("&"),
1114 '<' => out.push_str("<"),
1115 '>' => out.push_str(">"),
1116 '"' => out.push_str("""),
1117 '\'' => out.push_str("'"),
1118 '\t' if in_attr => out.push_str("	"),
1119 '\n' if in_attr => out.push_str(" "),
1120 '\r' if in_attr => out.push_str(" "),
1121 '\t' | '\n' | '\r' => out.push(c),
1122 c if (c as u32) < 0x20 || c == '\u{FFFE}' || c == '\u{FFFF}' => {}
1123 c => out.push(c),
1124 }
1125 }
1126 out
1127}
1128
1129fn points(pts: &[(f64, f64)]) -> String {
1130 pts.iter()
1131 .map(|(x, y)| format!("{x:.2},{y:.2}"))
1132 .collect::<Vec<_>>()
1133 .join(" ")
1134}
1135
1136fn finite_pts(pts: &[(f64, f64)]) -> Vec<(f64, f64)> {
1137 pts.iter()
1138 .copied()
1139 .filter(|(x, y)| x.is_finite() && y.is_finite())
1140 .collect()
1141}
1142
1143fn finite_runs(pts: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
1152 let mut runs = Vec::new();
1153 let mut cur: Vec<(f64, f64)> = Vec::new();
1154 for &(x, y) in pts {
1155 if x.is_finite() && y.is_finite() {
1156 cur.push((x, y));
1157 } else if !cur.is_empty() {
1158 if cur.len() >= 2 {
1159 runs.push(std::mem::take(&mut cur));
1160 } else {
1161 cur.clear();
1162 }
1163 }
1164 }
1165 if cur.len() >= 2 {
1166 runs.push(cur);
1167 }
1168 runs
1169}
1170
1171impl DrawBackend for SvgBackend {
1172 fn begin_panel(&mut self, index: usize, label: &str, clip: &Rect, data: &Rect) {
1173 self.flush_groups();
1175 self.body.push_str(&format!(
1176 "<g data-facet-panel=\"{index}\" clip-path=\"url(#{}-panel-{index})\">",
1177 self.id_prefix
1178 ));
1179 self.panels
1180 .push((index, label.to_string(), clip.clone(), data.clone()));
1181 }
1182
1183 fn end_panel(&mut self) {
1184 self.flush_groups();
1185 self.body.push_str("</g>");
1186 }
1187
1188 fn set_viewport(&mut self, rect: &Rect) {
1189 self.viewport = Some(rect.clone());
1190 }
1191
1192 fn plot_area(&self) -> Rect {
1193 self.plot_area.clone()
1194 }
1195 fn total_area(&self) -> Rect {
1196 self.total_area.clone()
1197 }
1198
1199 fn corner_claims(&self) -> Vec<Rect> {
1200 self.corner_claims.clone()
1201 }
1202 fn claim_corner_box(&mut self, rect: Rect) {
1203 self.corner_claims.push(rect);
1204 }
1205 fn clear_corner_claims(&mut self) {
1206 self.corner_claims.clear();
1207 }
1208
1209 fn set_tooltip(&mut self, tooltip: Option<String>) {
1210 self.tooltip = tooltip;
1211 }
1212
1213 fn set_mark_axis(&mut self, key: Option<String>) {
1214 self.axis_key = key;
1215 }
1216
1217 fn set_mark_series(&mut self, series: Option<String>) {
1218 self.series_key = series;
1219 }
1220
1221 fn set_mark_value(&mut self, value: Option<String>) {
1222 self.value_key = value;
1223 }
1224
1225 fn set_mark_geom(&mut self, geom: Option<&str>) {
1226 self.mark_geom = geom.map(str::to_string);
1227 }
1228
1229 fn warn(&mut self, message: String) {
1230 self.warnings.push(message);
1231 }
1232
1233 fn set_mark_part(&mut self, part: Option<&str>) {
1234 self.mark_part = part.map(|s| s.to_string());
1235 }
1236
1237 fn set_mark_attrs(&mut self, attrs: Vec<(String, String)>) {
1238 self.mark_attrs = attrs;
1239 }
1240
1241 fn wants_formatted_values(&self) -> bool {
1242 self.formatted_values
1243 }
1244
1245 fn set_mark_fvalue(&mut self, value: Option<String>) {
1246 self.mark_fvalue = value;
1247 }
1248
1249 fn set_mark_fx(&mut self, value: Option<String>) {
1250 self.mark_fx = value;
1251 }
1252
1253 fn set_mark_class(&mut self, class: Option<String>) {
1254 self.mark_user_class = class;
1255 }
1256
1257 fn set_mark_group(&mut self, group: Option<(String, String)>) {
1258 self.mark_group = group;
1259 }
1260
1261 fn set_layer_attrs(&mut self, attrs: Vec<(String, String)>) {
1262 self.layer_attrs = attrs;
1263 }
1264
1265 fn draw_circle(
1266 &mut self,
1267 (cx, cy): (f64, f64),
1268 radius: f64,
1269 style: &PointStyle,
1270 ) -> Result<(), RenderError> {
1271 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
1272 return Ok(());
1273 }
1274 let attrs = format!(
1275 "cx=\"{cx:.2}\" cy=\"{cy:.2}\" r=\"{:.2}\" fill=\"{}\"{}",
1276 radius.max(0.0),
1277 rgb(style.color),
1278 opacity_attr("fill", style.alpha)
1279 );
1280 self.push_mark("circle", "point", &attrs);
1281 Ok(())
1282 }
1283
1284 fn draw_shape(
1288 &mut self,
1289 (cx, cy): (f64, f64),
1290 radius: f64,
1291 style: &PointStyle,
1292 ) -> Result<(), RenderError> {
1293 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
1294 return Ok(());
1295 }
1296 let r = radius.max(0.0);
1297 let color = rgb(style.color);
1298 let fill_opacity = opacity_attr("fill", style.alpha);
1299 let stroke_opacity = opacity_attr("stroke", style.alpha);
1300 let poly = |pts: &[(f64, f64)]| {
1301 if style.filled {
1302 format!("points=\"{}\" fill=\"{color}\"{fill_opacity}", points(pts))
1303 } else {
1304 format!(
1305 "points=\"{}\" fill=\"none\" stroke=\"{color}\"{stroke_opacity}",
1306 points(pts)
1307 )
1308 }
1309 };
1310 match style.shape {
1311 PointShape::Circle => return self.draw_circle((cx, cy), radius, style),
1312 PointShape::Square => {
1313 let a = poly(&[
1314 (cx - r, cy - r),
1315 (cx + r, cy - r),
1316 (cx + r, cy + r),
1317 (cx - r, cy + r),
1318 ]);
1319 self.push_mark("polygon", "point", &a);
1320 }
1321 PointShape::Triangle => {
1322 let a = poly(&[(cx, cy - r), (cx + r, cy + r), (cx - r, cy + r)]);
1323 self.push_mark("polygon", "point", &a);
1324 }
1325 PointShape::Diamond => {
1326 let a = poly(&[(cx, cy - r), (cx + r, cy), (cx, cy + r), (cx - r, cy)]);
1327 self.push_mark("polygon", "point", &a);
1328 }
1329 PointShape::Plus | PointShape::Cross => {
1330 let d = if style.shape == PointShape::Plus {
1331 format!(
1332 "M{:.2} {cy:.2}H{:.2}M{cx:.2} {:.2}V{:.2}",
1333 cx - r,
1334 cx + r,
1335 cy - r,
1336 cy + r
1337 )
1338 } else {
1339 format!(
1340 "M{:.2} {:.2}L{:.2} {:.2}M{:.2} {:.2}L{:.2} {:.2}",
1341 cx - r,
1342 cy - r,
1343 cx + r,
1344 cy + r,
1345 cx - r,
1346 cy + r,
1347 cx + r,
1348 cy - r
1349 )
1350 };
1351 let a = format!(
1352 "d=\"{d}\" fill=\"none\" stroke=\"{color}\" stroke-width=\"{:.2}\"{stroke_opacity}",
1353 (r / 2.5).clamp(1.0, 3.0)
1354 );
1355 self.push_mark("path", "point", &a);
1356 }
1357 }
1358 Ok(())
1359 }
1360
1361 fn draw_line(&mut self, pts: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
1362 for run in finite_runs(pts) {
1368 self.draw_line_run(&run, style);
1369 }
1370 Ok(())
1371 }
1372
1373 fn draw_quadratic(
1376 &mut self,
1377 p0: (f64, f64),
1378 ctrl: (f64, f64),
1379 p1: (f64, f64),
1380 style: &LineStyle,
1381 ) -> Result<(), RenderError> {
1382 if !(p0.0.is_finite()
1383 && p0.1.is_finite()
1384 && ctrl.0.is_finite()
1385 && ctrl.1.is_finite()
1386 && p1.0.is_finite()
1387 && p1.1.is_finite())
1388 {
1389 return Ok(());
1390 }
1391 let dash = dasharray_attr(&style.linetype);
1392 let linecap = match style.lineend {
1393 LineEnd::Butt => "",
1394 LineEnd::Round => " stroke-linecap=\"round\"",
1395 LineEnd::Square => " stroke-linecap=\"square\"",
1396 };
1397 let d = format!(
1398 "M{:.2} {:.2} Q{:.2} {:.2} {:.2} {:.2}",
1399 p0.0, p0.1, ctrl.0, ctrl.1, p1.0, p1.1
1400 );
1401 let attrs = format!(
1402 "d=\"{d}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\"{}{}{}",
1403 rgb(style.color),
1404 finite_or(style.width, 1.0),
1405 opacity_attr("stroke", style.alpha),
1406 dash,
1407 linecap
1408 );
1409 self.push_mark("path", "line", &attrs);
1410 Ok(())
1411 }
1412
1413 fn draw_rect(
1414 &mut self,
1415 top_left: (f64, f64),
1416 bottom_right: (f64, f64),
1417 style: &RectStyle,
1418 ) -> Result<(), RenderError> {
1419 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
1420 self.rect_markup(top_left, bottom_right, &fill, style)
1421 }
1422
1423 fn draw_rect_pattern(
1426 &mut self,
1427 top_left: (f64, f64),
1428 bottom_right: (f64, f64),
1429 pattern: &FillPattern,
1430 style: &RectStyle,
1431 ) -> Result<(), RenderError> {
1432 let fill = self.pattern_url(pattern);
1433 self.rect_markup(top_left, bottom_right, &fill, style)
1434 }
1435
1436 fn draw_polygon(&mut self, pts: &[(f64, f64)], style: &RectStyle) -> Result<(), RenderError> {
1437 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
1438 self.polygon_markup(pts, &fill, style)
1439 }
1440
1441 fn draw_polygon_pattern(
1444 &mut self,
1445 pts: &[(f64, f64)],
1446 pattern: &FillPattern,
1447 style: &RectStyle,
1448 ) -> Result<(), RenderError> {
1449 let fill = self.pattern_url(pattern);
1450 self.polygon_markup(pts, &fill, style)
1451 }
1452
1453 fn draw_polygon_rings(
1459 &mut self,
1460 rings: &[Vec<(f64, f64)>],
1461 style: &RectStyle,
1462 ) -> Result<(), RenderError> {
1463 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
1464 self.polygon_rings_markup(rings, &fill, style)
1465 }
1466
1467 fn draw_polygon_rings_pattern(
1471 &mut self,
1472 rings: &[Vec<(f64, f64)>],
1473 pattern: &FillPattern,
1474 style: &RectStyle,
1475 ) -> Result<(), RenderError> {
1476 let fill = self.pattern_url(pattern);
1477 self.polygon_rings_markup(rings, &fill, style)
1478 }
1479
1480 fn draw_polygon_rings_precise(
1486 &mut self,
1487 rings: &[Vec<(f64, f64)>],
1488 pattern: Option<&FillPattern>,
1489 precision: Option<u8>,
1490 style: &RectStyle,
1491 ) -> Result<(), RenderError> {
1492 let Some(precision) = precision else {
1493 return match pattern {
1494 Some(p) => self.draw_polygon_rings_pattern(rings, p, style),
1495 None => self.draw_polygon_rings(rings, style),
1496 };
1497 };
1498 let d = compact_rings_d(rings, precision);
1499 if d.is_empty() {
1500 return Ok(());
1501 }
1502 let fill = match pattern {
1503 Some(p) => self.pattern_url(p),
1504 None => style.fill.map(rgb).unwrap_or_else(|| "none".into()),
1505 };
1506 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
1507 let attrs = format!(
1508 "d=\"{d}\" fill-rule=\"evenodd\" fill=\"{fill}\"{} stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
1509 opacity_attr("fill", style.alpha),
1510 finite_or(style.stroke_width, 0.0)
1511 );
1512 self.push_mark("path", "band", &attrs);
1513 Ok(())
1514 }
1515
1516 fn draw_text(
1517 &mut self,
1518 text: &str,
1519 pos: (f64, f64),
1520 style: &TextStyle,
1521 ) -> Result<(), RenderError> {
1522 self.draw_text_with_title(text, pos, style, None)
1523 }
1524
1525 fn draw_text_with_title(
1529 &mut self,
1530 text: &str,
1531 (x, y): (f64, f64),
1532 style: &TextStyle,
1533 title: Option<&str>,
1534 ) -> Result<(), RenderError> {
1535 if !(x.is_finite() && y.is_finite()) {
1536 return Ok(());
1537 }
1538 let anchor = match style.anchor {
1539 TextAnchor::Start => "start",
1540 TextAnchor::Middle => "middle",
1541 TextAnchor::End => "end",
1542 };
1543 let family = escape_attr(style.family.as_deref().unwrap_or("sans-serif"));
1544 let weight = if style.face == FontFace::Bold {
1545 " font-weight=\"bold\""
1546 } else {
1547 ""
1548 };
1549 let fstyle = if style.face == FontFace::Italic {
1550 " font-style=\"italic\""
1551 } else {
1552 ""
1553 };
1554 let halo = text_halo_attrs(style);
1555 let angle = finite_or(style.angle, 0.0);
1556 let transform = if angle.abs() > 0.01 {
1557 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
1558 } else {
1559 String::new()
1560 };
1561 let class = format!(
1562 "{}{}{}",
1563 self.text_data_attrs(),
1564 self.layer_flag_attrs(false),
1565 self.mark_class("text")
1566 );
1567 let title_tag = match title {
1568 Some(t) => format!("<title>{}</title>", escape(t)),
1569 None => String::new(),
1570 };
1571 let markup = format!(
1572 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
1573 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle}{halo} fill=\"{}\"{transform}{class}>{title_tag}{}</text>",
1574 finite_or(style.size, 12.0),
1575 rgb(style.color),
1576 escape(text)
1577 );
1578 self.emit(&markup);
1579 Ok(())
1580 }
1581
1582 fn draw_text_runs(
1589 &mut self,
1590 runs: &[(&str, Option<&str>)],
1591 (x, y): (f64, f64),
1592 style: &TextStyle,
1593 ) -> Result<(), RenderError> {
1594 if !(x.is_finite() && y.is_finite()) {
1595 return Ok(());
1596 }
1597 let anchor = match style.anchor {
1598 TextAnchor::Start => "start",
1599 TextAnchor::Middle => "middle",
1600 TextAnchor::End => "end",
1601 };
1602 let family = escape_attr(style.family.as_deref().unwrap_or("sans-serif"));
1603 let weight = if style.face == FontFace::Bold {
1604 " font-weight=\"bold\""
1605 } else {
1606 ""
1607 };
1608 let fstyle = if style.face == FontFace::Italic {
1609 " font-style=\"italic\""
1610 } else {
1611 ""
1612 };
1613 let halo = text_halo_attrs(style);
1614 let angle = finite_or(style.angle, 0.0);
1615 let transform = if angle.abs() > 0.01 {
1616 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
1617 } else {
1618 String::new()
1619 };
1620 let class = format!(
1621 "{}{}{}",
1622 self.text_data_attrs(),
1623 self.layer_flag_attrs(false),
1624 self.mark_class("text")
1625 );
1626 let mut inner = String::new();
1627 for (text, href) in runs {
1628 match href {
1629 Some(u) => inner.push_str(&format!(
1630 "<a href=\"{}\" target=\"_blank\" rel=\"noopener\"><tspan text-decoration=\"underline\">{}</tspan></a>",
1631 escape_attr(u),
1632 escape(text)
1633 )),
1634 None => inner.push_str(&format!("<tspan>{}</tspan>", escape(text))),
1635 }
1636 }
1637 let markup = format!(
1638 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
1639 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle}{halo} fill=\"{}\"{transform}{class}>{inner}</text>",
1640 finite_or(style.size, 12.0),
1641 rgb(style.color),
1642 );
1643 self.emit(&markup);
1644 Ok(())
1645 }
1646}
1647
1648fn text_halo_attrs(style: &TextStyle) -> String {
1658 let Some(bg) = style.bg_colour else {
1659 return String::new();
1660 };
1661 let r = finite_or(style.bg_r, 0.0);
1662 if r <= 0.0 {
1663 return String::new();
1664 }
1665 format!(
1666 " stroke=\"{}\" stroke-width=\"{:.2}\" stroke-linejoin=\"round\" paint-order=\"stroke\"",
1667 rgb(bg),
1668 2.0 * r
1669 )
1670}
1671
1672#[cfg(test)]
1673mod tests {
1674 use super::*;
1675 use crate::render::Rect;
1676
1677 #[test]
1680 fn issue221_backend_rechecks_class_group_and_layer_attrs() {
1681 let mut b = backend();
1682 b.set_mark_geom(Some("point"));
1683 b.set_mark_class(Some("ok \"bad 1x ok".into()));
1684 b.set_layer_attrs(vec![
1685 ("aria-hidden".into(), "maybe".into()),
1686 ("pointer-events".into(), "none\" onload=\"x".into()),
1687 ("style".into(), "fill:red".into()),
1688 ("class".into(), "lay".into()),
1689 ]);
1690 b.set_mark_group(Some(("onclick".into(), "x".into())));
1691 b.draw_circle((1.0, 1.0), 1.0, &PointStyle::default())
1692 .unwrap();
1693 b.set_mark_group(Some(("data-of".into(), "n".into())));
1694 let runs = [("a", None), ("b", Some("https://e.x"))];
1695 b.draw_text_runs(&runs, (5.0, 5.0), &TextStyle::default())
1696 .unwrap();
1697 b.set_mark_group(None);
1698 b.set_mark_geom(None);
1699 let body = b.body().to_string();
1700 assert!(
1701 body.contains(r#"class="gg-mark gg-point lay ok""#),
1702 "{body}"
1703 );
1704 assert!(!body.contains("bad") && !body.contains("1x"), "{body}");
1705 assert!(
1706 !body.contains("aria-hidden") && !body.contains("onload"),
1707 "{body}"
1708 );
1709 assert!(
1710 !body.contains("style=") && !body.contains("onclick"),
1711 "{body}"
1712 );
1713 assert!(body.contains(r#"<g data-of="n"><text"#), "{body}");
1714 assert!(body.contains("gg-text lay ok"), "{body}");
1715 assert!(b.finish().contains(r#"<g data-of="n">"#));
1717 }
1718
1719 fn backend() -> SvgBackend {
1720 SvgBackend::new(
1721 100,
1722 100,
1723 Rect {
1724 x: 0.0,
1725 y: 0.0,
1726 width: 100.0,
1727 height: 100.0,
1728 },
1729 )
1730 }
1731
1732 #[test]
1733 fn draw_quadratic_emits_a_path_with_m_and_q() {
1734 let mut b = backend();
1735 b.draw_quadratic((0.0, 0.0), (10.0, 20.0), (20.0, 0.0), &LineStyle::default())
1736 .unwrap();
1737 let svg = b.finish();
1738 assert!(svg.contains("<path"));
1739 assert!(svg.contains("d=\"M0.00 0.00 Q10.00 20.00 20.00 0.00\""));
1740 assert!(!svg.contains("NaN"));
1741 assert!(!svg.contains("inf"));
1742 }
1743
1744 #[test]
1745 fn draw_quadratic_drops_non_finite_input() {
1746 let mut b = backend();
1747 b.draw_quadratic(
1748 (f64::NAN, 0.0),
1749 (10.0, 20.0),
1750 (20.0, 0.0),
1751 &LineStyle::default(),
1752 )
1753 .unwrap();
1754 b.draw_quadratic(
1755 (0.0, 0.0),
1756 (f64::INFINITY, 20.0),
1757 (20.0, 0.0),
1758 &LineStyle::default(),
1759 )
1760 .unwrap();
1761 let svg = b.finish();
1762 assert!(!svg.contains("<path"));
1763 }
1764
1765 #[test]
1766 fn draw_line_omits_linecap_by_default_but_writes_it_when_set() {
1767 let mut b = backend();
1768 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &LineStyle::default())
1769 .unwrap();
1770 let svg = b.finish();
1771 assert!(!svg.contains("stroke-linecap"));
1772
1773 let mut b2 = backend();
1774 let style = LineStyle {
1775 lineend: LineEnd::Round,
1776 ..Default::default()
1777 };
1778 b2.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
1779 let svg2 = b2.finish();
1780 assert!(svg2.contains("stroke-linecap=\"round\""));
1781 }
1782
1783 #[test]
1784 fn square_linecap_is_written_on_both_draw_line_and_draw_quadratic() {
1785 let style = LineStyle {
1786 lineend: LineEnd::Square,
1787 ..Default::default()
1788 };
1789 let mut b = backend();
1790 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
1791 assert!(b.finish().contains("stroke-linecap=\"square\""));
1792
1793 let mut b2 = backend();
1794 b2.draw_quadratic((0.0, 0.0), (5.0, 10.0), (10.0, 0.0), &style)
1795 .unwrap();
1796 assert!(b2.finish().contains("stroke-linecap=\"square\""));
1797 }
1798
1799 #[test]
1804 fn set_mark_attrs_defence_in_depth_drops_invalid_names() {
1805 let mut b = backend();
1806 b.set_mark_attrs(vec![
1807 ("not-data-prefixed".to_string(), "x".to_string()),
1808 ("data-".to_string(), "x".to_string()), ("data-Upper".to_string(), "x".to_string()), ("data-ok".to_string(), "valid".to_string()), ]);
1812 b.draw_circle((1.0, 1.0), 1.0, &PointStyle::default())
1813 .unwrap();
1814 let svg = b.finish();
1815 assert!(!svg.contains("not-data-prefixed"));
1816 assert!(!svg.contains("data-Upper"));
1817 assert!(!svg.contains("data-=\""));
1818 assert!(svg.contains("data-ok=\"valid\""));
1819 }
1820
1821 fn stripe() -> FillPattern {
1824 FillPattern::Stripe {
1825 fg: (10, 20, 30),
1826 bg: (200, 210, 220),
1827 size_px: 4.0,
1828 angle_deg: 45.0,
1829 }
1830 }
1831
1832 fn pattern_id(svg: &str) -> String {
1833 let i = svg.find("<pattern id=\"").unwrap() + "<pattern id=\"".len();
1834 svg[i..i + svg[i..].find('"').unwrap()].to_string()
1835 }
1836
1837 #[test]
1838 fn a_pattern_fill_registers_one_defs_pattern_and_references_it() {
1839 let mut b = backend();
1840 b.draw_rect_pattern((0.0, 0.0), (10.0, 10.0), &stripe(), &RectStyle::default())
1841 .unwrap();
1842 let svg = b.finish();
1843 assert_eq!(svg.matches("<pattern ").count(), 1, "{svg}");
1844 let id = pattern_id(&svg);
1845 assert!(
1846 svg.contains(&format!("fill=\"url(#{id})\"")),
1847 "rect references the registered pattern id {id}: {svg}"
1848 );
1849 }
1850
1851 #[test]
1852 fn a_rounded_rect_pattern_still_rounds_its_corners() {
1853 let mut b = backend();
1854 b.draw_rect_pattern(
1855 (0.0, 0.0),
1856 (10.0, 10.0),
1857 &stripe(),
1858 &RectStyle {
1859 corner_radius: 3.0,
1860 ..Default::default()
1861 },
1862 )
1863 .unwrap();
1864 let svg = b.finish();
1865 assert!(svg.contains("rx=\"3.00\""), "{svg}");
1866 assert!(svg.contains("fill=\"url(#"), "{svg}");
1867 }
1868
1869 #[test]
1870 fn an_equal_pattern_reuses_the_same_defs_entry() {
1871 let mut b = backend();
1872 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1873 .unwrap();
1874 b.draw_polygon_pattern(
1875 &[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)],
1876 &stripe(),
1877 &RectStyle::default(),
1878 )
1879 .unwrap();
1880 let svg = b.finish();
1881 assert_eq!(
1882 svg.matches("<pattern ").count(),
1883 1,
1884 "two equal patterns share one <defs> entry: {svg}"
1885 );
1886 }
1887
1888 #[test]
1889 fn different_patterns_get_their_own_defs_entries() {
1890 let mut b = backend();
1891 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1892 .unwrap();
1893 let other = FillPattern::Stripe {
1894 fg: (1, 1, 1),
1895 bg: (2, 2, 2),
1896 size_px: 8.0,
1897 angle_deg: 0.0,
1898 };
1899 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &other, &RectStyle::default())
1900 .unwrap();
1901 let svg = b.finish();
1902 assert_eq!(svg.matches("<pattern ").count(), 2, "{svg}");
1903 }
1904
1905 #[test]
1906 fn no_pattern_used_means_no_defs_block_at_all() {
1907 let mut b = backend();
1908 b.draw_rect((0.0, 0.0), (5.0, 5.0), &RectStyle::default())
1909 .unwrap();
1910 let svg = b.finish();
1911 assert!(!svg.contains("<defs>"), "{svg}");
1912 }
1913
1914 #[test]
1915 fn pattern_fill_cuts_out_holes_like_a_flat_fill_does() {
1916 let mut b = backend();
1917 let outer = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1918 let hole = vec![(2.0, 2.0), (8.0, 2.0), (8.0, 8.0), (2.0, 8.0)];
1919 b.draw_polygon_rings_pattern(&[outer, hole], &stripe(), &RectStyle::default())
1920 .unwrap();
1921 let svg = b.finish();
1922 assert!(svg.contains("fill-rule=\"evenodd\""), "{svg}");
1923 assert_eq!(
1924 svg.matches("<path").count(),
1925 1,
1926 "one path, both rings: {svg}"
1927 );
1928 }
1929
1930 #[test]
1931 fn two_backends_on_one_page_never_collide_on_a_pattern_id() {
1932 let mut a = backend();
1933 a.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1934 .unwrap();
1935 let mut b = backend();
1936 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1937 .unwrap();
1938 let (sa, sb) = (a.finish(), b.finish());
1939 assert_ne!(pattern_id(&sa), pattern_id(&sb), "{sa}\n{sb}");
1940 }
1941
1942 fn decode_compact_d(d: &str) -> Vec<Vec<(f64, f64)>> {
1949 let b = d.as_bytes();
1950 let mut i = 0;
1951 let read_num = |b: &[u8], i: &mut usize| -> f64 {
1952 let start = *i;
1953 if *i < b.len() && b[*i] == b'-' {
1954 *i += 1;
1955 }
1956 while *i < b.len() && b[*i].is_ascii_digit() {
1957 *i += 1;
1958 }
1959 if *i < b.len() && b[*i] == b'.' {
1960 *i += 1;
1961 while *i < b.len() && b[*i].is_ascii_digit() {
1962 *i += 1;
1963 }
1964 }
1965 std::str::from_utf8(&b[start..*i]).unwrap().parse().unwrap()
1966 };
1967 let is_num_start = |c: u8| c.is_ascii_digit() || c == b'-' || c == b'.';
1968 let mut rings = Vec::new();
1969 let mut current = Vec::new();
1970 let mut cursor = (0.0, 0.0);
1971 while i < b.len() {
1972 match b[i] {
1973 b'M' => {
1974 if !current.is_empty() {
1975 rings.push(std::mem::take(&mut current));
1976 }
1977 i += 1;
1978 let x = read_num(b, &mut i);
1979 if i < b.len() && b[i] == b',' {
1980 i += 1;
1981 }
1982 let y = read_num(b, &mut i);
1983 cursor = (x, y);
1984 current.push(cursor);
1985 }
1986 b'l' => {
1987 i += 1;
1988 while i < b.len() && is_num_start(b[i]) {
1989 let dx = read_num(b, &mut i);
1990 if i < b.len() && b[i] == b',' {
1991 i += 1;
1992 }
1993 let dy = read_num(b, &mut i);
1994 if i < b.len() && b[i] == b',' {
1995 i += 1;
1996 }
1997 cursor = (cursor.0 + dx, cursor.1 + dy);
1998 current.push(cursor);
1999 }
2000 }
2001 b'z' => {
2002 i += 1;
2003 if !current.is_empty() {
2004 rings.push(std::mem::take(&mut current));
2005 }
2006 }
2007 _ => i += 1,
2008 }
2009 }
2010 if !current.is_empty() {
2011 rings.push(current);
2012 }
2013 rings
2014 }
2015
2016 fn noisy_ring(n: usize, cx: f64, cy: f64, r: f64) -> Vec<(f64, f64)> {
2019 (0..n)
2020 .map(|i| {
2021 let t = i as f64 / n as f64 * std::f64::consts::TAU;
2022 let wobble = 1.0 + 0.03 * ((i * 7) as f64).sin();
2023 (cx + r * wobble * t.cos(), cy + r * wobble * t.sin())
2024 })
2025 .collect()
2026 }
2027
2028 #[test]
2029 fn compact_encoding_is_well_under_the_absolute_encodings_size() {
2030 let ring = noisy_ring(2000, 400.0, 300.0, 280.0);
2034 let mut b = backend();
2035 b.draw_polygon_rings(std::slice::from_ref(&ring), &RectStyle::default())
2036 .unwrap();
2037 let absolute_svg = b.finish();
2038 let absolute_d = absolute_svg
2039 .split("d=\"")
2040 .nth(1)
2041 .and_then(|s| s.split('"').next())
2042 .expect("absolute path d");
2043
2044 let compact_d = compact_rings_d(std::slice::from_ref(&ring), 1);
2045
2046 assert!(
2047 compact_d.len() * 3 < absolute_d.len() * 2,
2048 "compact {} bytes should be well under 2/3 of absolute {} bytes",
2049 compact_d.len(),
2050 absolute_d.len()
2051 );
2052 }
2053
2054 #[test]
2055 fn compact_encoding_round_trips_within_precision() {
2056 let ring = noisy_ring(40, 100.0, 80.0, 60.0);
2057 let precision: u8 = 1;
2058 let d = compact_rings_d(std::slice::from_ref(&ring), precision);
2059 let decoded = decode_compact_d(&d);
2060 assert_eq!(decoded.len(), 1, "{d}");
2061 assert_eq!(decoded[0].len(), ring.len(), "{d}");
2062 let step_tol = 0.5 * 10f64.powi(-(precision as i32));
2067 for (k, (orig, got)) in ring.iter().zip(decoded[0].iter()).enumerate() {
2068 let tol = step_tol * (k as f64 + 1.0) + 1e-9;
2069 assert!(
2070 (orig.0 - got.0).abs() <= tol && (orig.1 - got.1).abs() <= tol,
2071 "point {k}: {orig:?} vs {got:?} (tol {tol}): {d}"
2072 );
2073 }
2074 }
2075
2076 #[test]
2077 fn compact_encoding_round_trips_a_hole_and_a_multipolygons_parts() {
2078 let part1 = vec![(0.0, 0.0), (20.0, 0.0), (20.0, 20.0), (0.0, 20.0)];
2082 let part2_outer = vec![(50.0, 0.0), (90.0, 0.0), (90.0, 40.0), (50.0, 40.0)];
2083 let part2_hole = vec![(60.0, 10.0), (80.0, 10.0), (80.0, 30.0), (60.0, 30.0)];
2084 let rings = vec![part1.clone(), part2_outer.clone(), part2_hole.clone()];
2085 let d = compact_rings_d(&rings, 1);
2086 let decoded = decode_compact_d(&d);
2087 assert_eq!(decoded.len(), 3, "{d}");
2088 for (orig, got) in [part1, part2_outer, part2_hole].iter().zip(decoded.iter()) {
2089 assert_eq!(orig.len(), got.len(), "{d}");
2090 for (o, g) in orig.iter().zip(got.iter()) {
2091 assert!((o.0 - g.0).abs() <= 0.1 && (o.1 - g.1).abs() <= 0.1, "{d}");
2092 }
2093 }
2094 assert!(d.contains("M0,0l20,0,0,20-20,0z"), "{d}");
2095 }
2096}