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
842fn base64(bytes: &[u8]) -> String {
860 const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
861 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
862 for c in bytes.chunks(3) {
863 let n = (c[0] as u32) << 16
864 | (*c.get(1).unwrap_or(&0) as u32) << 8
865 | *c.get(2).unwrap_or(&0) as u32;
866 for i in 0..4 {
867 if i <= c.len() {
868 out.push(T[(n >> (18 - 6 * i) & 63) as usize] as char);
869 } else {
870 out.push('=');
871 }
872 }
873 }
874 out
875}
876
877pub fn root_data_attrs(built: &crate::build::BuiltPlot) -> Vec<(String, String)> {
878 use crate::aes::Aesthetic;
879 let mut out = Vec::new();
880 if !built.panel_scales.is_empty() {
881 return out;
882 }
883 let axis = |aes: &Aesthetic| -> (Option<(f64, f64)>, Option<String>) {
884 match built.scales.get(aes) {
885 Some(s) if s.is_discrete() => {
886 let labels: Vec<String> = s
887 .breaks()
888 .into_iter()
889 .map(|(_, l)| json_string(&l))
890 .collect();
891 (None, Some(format!("[{}]", labels.join(","))))
892 }
893 Some(s) => (s.expanded_domain(), None),
894 None => (None, None),
895 }
896 };
897 if let Some(at) = &built.legend_at {
898 out.push(("data-legend-at".into(), at.clone()));
899 }
900 let (xd, xl) = axis(&Aesthetic::X);
901 let (yd, yl) = axis(&Aesthetic::Y);
902 if let (Some((x0, x1)), Some((y0, y1))) = (xd, yd) {
903 out.push((
904 "data-domain".into(),
905 format!("{} {} {} {}", g(x0), g(x1), g(y0), g(y1)),
906 ));
907 }
908 if let Some((a, b)) = xd {
909 out.push(("data-xdomain".into(), format!("{} {}", g(a), g(b))));
910 }
911 if let Some((a, b)) = yd {
912 out.push(("data-ydomain".into(), format!("{} {}", g(a), g(b))));
913 }
914 if let Some(l) = xl {
915 out.push(("data-xlevels".into(), l));
916 }
917 if let Some(l) = yl {
918 out.push(("data-ylevels".into(), l));
919 }
920 if built.coord.is_flipped() {
921 out.push(("data-flip".into(), "true".into()));
922 }
923 out
924}
925
926fn g(v: f64) -> String {
929 format!("{v}")
930}
931
932fn json_string(s: &str) -> String {
935 let mut out = String::with_capacity(s.len() + 2);
936 out.push('"');
937 for c in s.chars() {
938 match c {
939 '"' => out.push_str("\\\""),
940 '\\' => out.push_str("\\\\"),
941 '\n' => out.push_str("\\n"),
942 '\r' => out.push_str("\\r"),
943 '\t' => out.push_str("\\t"),
944 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
945 c => out.push(c),
946 }
947 }
948 out.push('"');
949 out
950}
951
952fn is_allowed_root_attr_name(name: &str) -> bool {
959 matches!(name, "role" | "id" | "class" | "lang")
960 || name
961 .strip_prefix("data-")
962 .is_some_and(crate::aes::is_valid_data_attr_name)
963 || name
964 .strip_prefix("aria-")
965 .is_some_and(crate::aes::is_valid_data_attr_name)
966}
967
968pub(crate) fn validate_root_attrs(
978 requested: &[(String, String)],
979 existing: &[(String, String)],
980 label: &str,
981 warnings: &mut Vec<String>,
982) -> Vec<(String, String)> {
983 let built_in: Vec<&str> = existing.iter().map(|(k, _)| k.as_str()).collect();
984 let mut used: Vec<&str> = built_in.clone();
985 let mut out = Vec::new();
986 for (name, value) in requested {
987 if !is_allowed_root_attr_name(name) {
988 warnings.push(format!(
989 "{label}: svg_attr(\"{name}\") is not on the allow-list (data-*, aria-*, role, id, class, lang) — ignored"
990 ));
991 continue;
992 }
993 if used.contains(&name.as_str()) {
994 if built_in.contains(&name.as_str()) {
999 warnings.push(format!(
1000 "{label}: svg_attr(\"{name}\") collides with an attribute ggplot-rs already writes to the root — ignored"
1001 ));
1002 } else {
1003 warnings.push(format!(
1004 "{label}: svg_attr(\"{name}\") was already set by an earlier svg_attr call — ignored"
1005 ));
1006 }
1007 continue;
1008 }
1009 used.push(name.as_str());
1010 out.push((name.clone(), value.clone()));
1011 }
1012 out
1013}
1014
1015pub(crate) fn warnings_json(warnings: &[String]) -> String {
1019 let items: Vec<String> = warnings.iter().map(|w| json_string(w)).collect();
1020 format!("[{}]", items.join(","))
1021}
1022
1023fn is_valid_mark_attr_name(name: &str) -> bool {
1031 match name.strip_prefix("data-") {
1032 Some(suffix) => crate::aes::is_valid_data_attr_name(suffix),
1033 None => false,
1034 }
1035}
1036
1037const POINTER_EVENTS_VALUES: &[&str] = &[
1040 "auto",
1041 "none",
1042 "visiblePainted",
1043 "visibleFill",
1044 "visibleStroke",
1045 "visible",
1046 "painted",
1047 "fill",
1048 "stroke",
1049 "all",
1050 "bounding-box",
1051];
1052
1053pub(crate) fn validate_layer_svg_attr(
1062 name: &str,
1063 value: &str,
1064 warnings: &mut Vec<String>,
1065) -> Option<(String, String)> {
1066 let keep = |v: &str| Some((name.to_string(), v.to_string()));
1067 match name {
1068 "class" => {
1069 let (ok, bad) = crate::aes::split_class_tokens(value);
1070 if !bad.is_empty() {
1071 warnings.push(format!(
1072 "layer_svg_attr(\"class\"): invalid class token(s) {bad:?} (must match [A-Za-z][A-Za-z0-9_-]*) — dropped"
1073 ));
1074 }
1075 if ok.is_empty() {
1076 None
1077 } else {
1078 keep(&ok.join(" "))
1079 }
1080 }
1081 "aria-hidden" if matches!(value, "true" | "false") => keep(value),
1082 "pointer-events" if POINTER_EVENTS_VALUES.contains(&value) => keep(value),
1083 "aria-hidden" | "pointer-events" => {
1084 warnings.push(format!(
1085 "layer_svg_attr(\"{name}\"): value {value:?} is not allowed — ignored"
1086 ));
1087 None
1088 }
1089 _ if is_valid_mark_attr_name(name) => keep(value),
1090 _ => {
1091 warnings.push(format!(
1092 "layer_svg_attr(\"{name}\") is not on the allow-list (class, aria-hidden, pointer-events, data-*) — ignored"
1093 ));
1094 None
1095 }
1096 }
1097}
1098
1099fn rgb((r, g, b): (u8, u8, u8)) -> String {
1100 format!("#{r:02X}{g:02X}{b:02X}")
1101}
1102
1103fn escape(s: &str) -> String {
1114 escape_impl(s, false)
1115}
1116
1117fn escape_attr(s: &str) -> String {
1128 escape_impl(s, true)
1129}
1130
1131fn escape_impl(s: &str, in_attr: bool) -> String {
1132 let mut out = String::with_capacity(s.len());
1133 for c in s.chars() {
1134 match c {
1135 '&' => out.push_str("&"),
1136 '<' => out.push_str("<"),
1137 '>' => out.push_str(">"),
1138 '"' => out.push_str("""),
1139 '\'' => out.push_str("'"),
1140 '\t' if in_attr => out.push_str("	"),
1141 '\n' if in_attr => out.push_str(" "),
1142 '\r' if in_attr => out.push_str(" "),
1143 '\t' | '\n' | '\r' => out.push(c),
1144 c if (c as u32) < 0x20 || c == '\u{FFFE}' || c == '\u{FFFF}' => {}
1145 c => out.push(c),
1146 }
1147 }
1148 out
1149}
1150
1151fn points(pts: &[(f64, f64)]) -> String {
1152 pts.iter()
1153 .map(|(x, y)| format!("{x:.2},{y:.2}"))
1154 .collect::<Vec<_>>()
1155 .join(" ")
1156}
1157
1158fn finite_pts(pts: &[(f64, f64)]) -> Vec<(f64, f64)> {
1159 pts.iter()
1160 .copied()
1161 .filter(|(x, y)| x.is_finite() && y.is_finite())
1162 .collect()
1163}
1164
1165fn finite_runs(pts: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
1174 let mut runs = Vec::new();
1175 let mut cur: Vec<(f64, f64)> = Vec::new();
1176 for &(x, y) in pts {
1177 if x.is_finite() && y.is_finite() {
1178 cur.push((x, y));
1179 } else if !cur.is_empty() {
1180 if cur.len() >= 2 {
1181 runs.push(std::mem::take(&mut cur));
1182 } else {
1183 cur.clear();
1184 }
1185 }
1186 }
1187 if cur.len() >= 2 {
1188 runs.push(cur);
1189 }
1190 runs
1191}
1192
1193impl DrawBackend for SvgBackend {
1194 fn begin_panel(&mut self, index: usize, label: &str, clip: &Rect, data: &Rect) {
1195 self.flush_groups();
1197 self.body.push_str(&format!(
1198 "<g data-facet-panel=\"{index}\" clip-path=\"url(#{}-panel-{index})\">",
1199 self.id_prefix
1200 ));
1201 self.panels
1202 .push((index, label.to_string(), clip.clone(), data.clone()));
1203 }
1204
1205 fn end_panel(&mut self) {
1206 self.flush_groups();
1207 self.body.push_str("</g>");
1208 }
1209
1210 fn set_viewport(&mut self, rect: &Rect) {
1211 self.viewport = Some(rect.clone());
1212 }
1213
1214 fn plot_area(&self) -> Rect {
1215 self.plot_area.clone()
1216 }
1217 fn total_area(&self) -> Rect {
1218 self.total_area.clone()
1219 }
1220
1221 fn corner_claims(&self) -> Vec<Rect> {
1222 self.corner_claims.clone()
1223 }
1224 fn claim_corner_box(&mut self, rect: Rect) {
1225 self.corner_claims.push(rect);
1226 }
1227 fn clear_corner_claims(&mut self) {
1228 self.corner_claims.clear();
1229 }
1230
1231 fn set_tooltip(&mut self, tooltip: Option<String>) {
1232 self.tooltip = tooltip;
1233 }
1234
1235 fn set_mark_axis(&mut self, key: Option<String>) {
1236 self.axis_key = key;
1237 }
1238
1239 fn set_mark_series(&mut self, series: Option<String>) {
1240 self.series_key = series;
1241 }
1242
1243 fn set_mark_value(&mut self, value: Option<String>) {
1244 self.value_key = value;
1245 }
1246
1247 fn set_mark_geom(&mut self, geom: Option<&str>) {
1248 self.mark_geom = geom.map(str::to_string);
1249 }
1250
1251 fn warn(&mut self, message: String) {
1252 self.warnings.push(message);
1253 }
1254
1255 fn set_mark_part(&mut self, part: Option<&str>) {
1256 self.mark_part = part.map(|s| s.to_string());
1257 }
1258
1259 fn set_mark_attrs(&mut self, attrs: Vec<(String, String)>) {
1260 self.mark_attrs = attrs;
1261 }
1262
1263 fn wants_formatted_values(&self) -> bool {
1264 self.formatted_values
1265 }
1266
1267 fn set_mark_fvalue(&mut self, value: Option<String>) {
1268 self.mark_fvalue = value;
1269 }
1270
1271 fn set_mark_fx(&mut self, value: Option<String>) {
1272 self.mark_fx = value;
1273 }
1274
1275 fn set_mark_class(&mut self, class: Option<String>) {
1276 self.mark_user_class = class;
1277 }
1278
1279 fn set_mark_group(&mut self, group: Option<(String, String)>) {
1280 self.mark_group = group;
1281 }
1282
1283 fn set_layer_attrs(&mut self, attrs: Vec<(String, String)>) {
1284 self.layer_attrs = attrs;
1285 }
1286
1287 fn draw_circle(
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 attrs = format!(
1297 "cx=\"{cx:.2}\" cy=\"{cy:.2}\" r=\"{:.2}\" fill=\"{}\"{}",
1298 radius.max(0.0),
1299 rgb(style.color),
1300 opacity_attr("fill", style.alpha)
1301 );
1302 self.push_mark("circle", "point", &attrs);
1303 Ok(())
1304 }
1305
1306 fn draw_shape(
1310 &mut self,
1311 (cx, cy): (f64, f64),
1312 radius: f64,
1313 style: &PointStyle,
1314 ) -> Result<(), RenderError> {
1315 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
1316 return Ok(());
1317 }
1318 let r = radius.max(0.0);
1319 let color = rgb(style.color);
1320 let fill_opacity = opacity_attr("fill", style.alpha);
1321 let stroke_opacity = opacity_attr("stroke", style.alpha);
1322 let poly = |pts: &[(f64, f64)]| {
1323 if style.filled {
1324 format!("points=\"{}\" fill=\"{color}\"{fill_opacity}", points(pts))
1325 } else {
1326 format!(
1327 "points=\"{}\" fill=\"none\" stroke=\"{color}\"{stroke_opacity}",
1328 points(pts)
1329 )
1330 }
1331 };
1332 match style.shape {
1333 PointShape::Circle => return self.draw_circle((cx, cy), radius, style),
1334 PointShape::Square => {
1335 let a = poly(&[
1336 (cx - r, cy - r),
1337 (cx + r, cy - r),
1338 (cx + r, cy + r),
1339 (cx - r, cy + r),
1340 ]);
1341 self.push_mark("polygon", "point", &a);
1342 }
1343 PointShape::Triangle => {
1344 let a = poly(&[(cx, cy - r), (cx + r, cy + r), (cx - r, cy + r)]);
1345 self.push_mark("polygon", "point", &a);
1346 }
1347 PointShape::Diamond => {
1348 let a = poly(&[(cx, cy - r), (cx + r, cy), (cx, cy + r), (cx - r, cy)]);
1349 self.push_mark("polygon", "point", &a);
1350 }
1351 PointShape::Plus | PointShape::Cross => {
1352 let d = if style.shape == PointShape::Plus {
1353 format!(
1354 "M{:.2} {cy:.2}H{:.2}M{cx:.2} {:.2}V{:.2}",
1355 cx - r,
1356 cx + r,
1357 cy - r,
1358 cy + r
1359 )
1360 } else {
1361 format!(
1362 "M{:.2} {:.2}L{:.2} {:.2}M{:.2} {:.2}L{:.2} {:.2}",
1363 cx - r,
1364 cy - r,
1365 cx + r,
1366 cy + r,
1367 cx - r,
1368 cy + r,
1369 cx + r,
1370 cy - r
1371 )
1372 };
1373 let a = format!(
1374 "d=\"{d}\" fill=\"none\" stroke=\"{color}\" stroke-width=\"{:.2}\"{stroke_opacity}",
1375 (r / 2.5).clamp(1.0, 3.0)
1376 );
1377 self.push_mark("path", "point", &a);
1378 }
1379 }
1380 Ok(())
1381 }
1382
1383 fn draw_line(&mut self, pts: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
1384 for run in finite_runs(pts) {
1390 self.draw_line_run(&run, style);
1391 }
1392 Ok(())
1393 }
1394
1395 fn draw_quadratic(
1398 &mut self,
1399 p0: (f64, f64),
1400 ctrl: (f64, f64),
1401 p1: (f64, f64),
1402 style: &LineStyle,
1403 ) -> Result<(), RenderError> {
1404 if !(p0.0.is_finite()
1405 && p0.1.is_finite()
1406 && ctrl.0.is_finite()
1407 && ctrl.1.is_finite()
1408 && p1.0.is_finite()
1409 && p1.1.is_finite())
1410 {
1411 return Ok(());
1412 }
1413 let dash = dasharray_attr(&style.linetype);
1414 let linecap = match style.lineend {
1415 LineEnd::Butt => "",
1416 LineEnd::Round => " stroke-linecap=\"round\"",
1417 LineEnd::Square => " stroke-linecap=\"square\"",
1418 };
1419 let d = format!(
1420 "M{:.2} {:.2} Q{:.2} {:.2} {:.2} {:.2}",
1421 p0.0, p0.1, ctrl.0, ctrl.1, p1.0, p1.1
1422 );
1423 let attrs = format!(
1424 "d=\"{d}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\"{}{}{}",
1425 rgb(style.color),
1426 finite_or(style.width, 1.0),
1427 opacity_attr("stroke", style.alpha),
1428 dash,
1429 linecap
1430 );
1431 self.push_mark("path", "line", &attrs);
1432 Ok(())
1433 }
1434
1435 fn draw_rect(
1436 &mut self,
1437 top_left: (f64, f64),
1438 bottom_right: (f64, f64),
1439 style: &RectStyle,
1440 ) -> Result<(), RenderError> {
1441 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
1442 self.rect_markup(top_left, bottom_right, &fill, style)
1443 }
1444
1445 fn draw_rect_pattern(
1448 &mut self,
1449 top_left: (f64, f64),
1450 bottom_right: (f64, f64),
1451 pattern: &FillPattern,
1452 style: &RectStyle,
1453 ) -> Result<(), RenderError> {
1454 let fill = self.pattern_url(pattern);
1455 self.rect_markup(top_left, bottom_right, &fill, style)
1456 }
1457
1458 fn draw_polygon(&mut self, pts: &[(f64, f64)], style: &RectStyle) -> Result<(), RenderError> {
1459 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
1460 self.polygon_markup(pts, &fill, style)
1461 }
1462
1463 fn draw_image(
1466 &mut self,
1467 bytes: &[u8],
1468 rect: &Rect,
1469 clip: &Rect,
1470 alpha: f64,
1471 ) -> Result<(), RenderError> {
1472 let finite = [rect.x, rect.y, rect.width, rect.height, clip.x, clip.y]
1473 .iter()
1474 .chain([&clip.width, &clip.height])
1475 .all(|v| v.is_finite());
1476 if !finite || rect.width <= 0.0 || rect.height <= 0.0 {
1477 return Ok(());
1478 }
1479 let mime = match bytes {
1480 [0xFF, 0xD8, ..] => "image/jpeg",
1481 _ => "image/png",
1482 };
1483 self.emit(&format!(
1484 "<svg x=\"{:.2}\" y=\"{:.2}\" width=\"{:.2}\" height=\"{:.2}\" overflow=\"hidden\">",
1485 clip.x, clip.y, clip.width, clip.height
1486 ));
1487 let attrs = format!(
1488 "x=\"{:.2}\" y=\"{:.2}\" width=\"{:.2}\" height=\"{:.2}\" preserveAspectRatio=\"none\"{} href=\"data:{mime};base64,{}\"",
1489 rect.x - clip.x,
1490 rect.y - clip.y,
1491 rect.width,
1492 rect.height,
1493 if alpha < 1.0 {
1494 format!(" opacity=\"{:.3}\"", alpha.clamp(0.0, 1.0))
1495 } else {
1496 String::new()
1497 },
1498 base64(bytes)
1499 );
1500 self.push_mark("image", "image", &attrs);
1501 self.emit("</svg>");
1502 Ok(())
1503 }
1504
1505 fn draw_polygon_pattern(
1508 &mut self,
1509 pts: &[(f64, f64)],
1510 pattern: &FillPattern,
1511 style: &RectStyle,
1512 ) -> Result<(), RenderError> {
1513 let fill = self.pattern_url(pattern);
1514 self.polygon_markup(pts, &fill, style)
1515 }
1516
1517 fn draw_polygon_rings(
1523 &mut self,
1524 rings: &[Vec<(f64, f64)>],
1525 style: &RectStyle,
1526 ) -> Result<(), RenderError> {
1527 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
1528 self.polygon_rings_markup(rings, &fill, style)
1529 }
1530
1531 fn draw_polygon_rings_pattern(
1535 &mut self,
1536 rings: &[Vec<(f64, f64)>],
1537 pattern: &FillPattern,
1538 style: &RectStyle,
1539 ) -> Result<(), RenderError> {
1540 let fill = self.pattern_url(pattern);
1541 self.polygon_rings_markup(rings, &fill, style)
1542 }
1543
1544 fn draw_polygon_rings_precise(
1550 &mut self,
1551 rings: &[Vec<(f64, f64)>],
1552 pattern: Option<&FillPattern>,
1553 precision: Option<u8>,
1554 style: &RectStyle,
1555 ) -> Result<(), RenderError> {
1556 let Some(precision) = precision else {
1557 return match pattern {
1558 Some(p) => self.draw_polygon_rings_pattern(rings, p, style),
1559 None => self.draw_polygon_rings(rings, style),
1560 };
1561 };
1562 let d = compact_rings_d(rings, precision);
1563 if d.is_empty() {
1564 return Ok(());
1565 }
1566 let fill = match pattern {
1567 Some(p) => self.pattern_url(p),
1568 None => style.fill.map(rgb).unwrap_or_else(|| "none".into()),
1569 };
1570 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
1571 let attrs = format!(
1572 "d=\"{d}\" fill-rule=\"evenodd\" fill=\"{fill}\"{} stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
1573 opacity_attr("fill", style.alpha),
1574 finite_or(style.stroke_width, 0.0)
1575 );
1576 self.push_mark("path", "band", &attrs);
1577 Ok(())
1578 }
1579
1580 fn draw_text(
1581 &mut self,
1582 text: &str,
1583 pos: (f64, f64),
1584 style: &TextStyle,
1585 ) -> Result<(), RenderError> {
1586 self.draw_text_with_title(text, pos, style, None)
1587 }
1588
1589 fn draw_text_with_title(
1593 &mut self,
1594 text: &str,
1595 (x, y): (f64, f64),
1596 style: &TextStyle,
1597 title: Option<&str>,
1598 ) -> Result<(), RenderError> {
1599 if !(x.is_finite() && y.is_finite()) {
1600 return Ok(());
1601 }
1602 let anchor = match style.anchor {
1603 TextAnchor::Start => "start",
1604 TextAnchor::Middle => "middle",
1605 TextAnchor::End => "end",
1606 };
1607 let family = escape_attr(style.family.as_deref().unwrap_or("sans-serif"));
1608 let weight = if style.face == FontFace::Bold {
1609 " font-weight=\"bold\""
1610 } else {
1611 ""
1612 };
1613 let fstyle = if style.face == FontFace::Italic {
1614 " font-style=\"italic\""
1615 } else {
1616 ""
1617 };
1618 let halo = text_halo_attrs(style);
1619 let angle = finite_or(style.angle, 0.0);
1620 let transform = if angle.abs() > 0.01 {
1621 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
1622 } else {
1623 String::new()
1624 };
1625 let class = format!(
1626 "{}{}{}",
1627 self.text_data_attrs(),
1628 self.layer_flag_attrs(false),
1629 self.mark_class("text")
1630 );
1631 let title_tag = match title {
1632 Some(t) => format!("<title>{}</title>", escape(t)),
1633 None => String::new(),
1634 };
1635 let markup = format!(
1636 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
1637 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle}{halo} fill=\"{}\"{transform}{class}>{title_tag}{}</text>",
1638 finite_or(style.size, 12.0),
1639 rgb(style.color),
1640 escape(text)
1641 );
1642 self.emit(&markup);
1643 Ok(())
1644 }
1645
1646 fn draw_text_runs(
1653 &mut self,
1654 runs: &[(&str, Option<&str>)],
1655 (x, y): (f64, f64),
1656 style: &TextStyle,
1657 ) -> Result<(), RenderError> {
1658 if !(x.is_finite() && y.is_finite()) {
1659 return Ok(());
1660 }
1661 let anchor = match style.anchor {
1662 TextAnchor::Start => "start",
1663 TextAnchor::Middle => "middle",
1664 TextAnchor::End => "end",
1665 };
1666 let family = escape_attr(style.family.as_deref().unwrap_or("sans-serif"));
1667 let weight = if style.face == FontFace::Bold {
1668 " font-weight=\"bold\""
1669 } else {
1670 ""
1671 };
1672 let fstyle = if style.face == FontFace::Italic {
1673 " font-style=\"italic\""
1674 } else {
1675 ""
1676 };
1677 let halo = text_halo_attrs(style);
1678 let angle = finite_or(style.angle, 0.0);
1679 let transform = if angle.abs() > 0.01 {
1680 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
1681 } else {
1682 String::new()
1683 };
1684 let class = format!(
1685 "{}{}{}",
1686 self.text_data_attrs(),
1687 self.layer_flag_attrs(false),
1688 self.mark_class("text")
1689 );
1690 let mut inner = String::new();
1691 for (text, href) in runs {
1692 match href {
1693 Some(u) => inner.push_str(&format!(
1694 "<a href=\"{}\" target=\"_blank\" rel=\"noopener\"><tspan text-decoration=\"underline\">{}</tspan></a>",
1695 escape_attr(u),
1696 escape(text)
1697 )),
1698 None => inner.push_str(&format!("<tspan>{}</tspan>", escape(text))),
1699 }
1700 }
1701 let markup = format!(
1702 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
1703 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle}{halo} fill=\"{}\"{transform}{class}>{inner}</text>",
1704 finite_or(style.size, 12.0),
1705 rgb(style.color),
1706 );
1707 self.emit(&markup);
1708 Ok(())
1709 }
1710}
1711
1712fn text_halo_attrs(style: &TextStyle) -> String {
1722 let Some(bg) = style.bg_colour else {
1723 return String::new();
1724 };
1725 let r = finite_or(style.bg_r, 0.0);
1726 if r <= 0.0 {
1727 return String::new();
1728 }
1729 format!(
1730 " stroke=\"{}\" stroke-width=\"{:.2}\" stroke-linejoin=\"round\" paint-order=\"stroke\"",
1731 rgb(bg),
1732 2.0 * r
1733 )
1734}
1735
1736#[cfg(test)]
1737mod tests {
1738 use super::*;
1739 use crate::render::Rect;
1740
1741 #[test]
1744 fn issue221_backend_rechecks_class_group_and_layer_attrs() {
1745 let mut b = backend();
1746 b.set_mark_geom(Some("point"));
1747 b.set_mark_class(Some("ok \"bad 1x ok".into()));
1748 b.set_layer_attrs(vec![
1749 ("aria-hidden".into(), "maybe".into()),
1750 ("pointer-events".into(), "none\" onload=\"x".into()),
1751 ("style".into(), "fill:red".into()),
1752 ("class".into(), "lay".into()),
1753 ]);
1754 b.set_mark_group(Some(("onclick".into(), "x".into())));
1755 b.draw_circle((1.0, 1.0), 1.0, &PointStyle::default())
1756 .unwrap();
1757 b.set_mark_group(Some(("data-of".into(), "n".into())));
1758 let runs = [("a", None), ("b", Some("https://e.x"))];
1759 b.draw_text_runs(&runs, (5.0, 5.0), &TextStyle::default())
1760 .unwrap();
1761 b.set_mark_group(None);
1762 b.set_mark_geom(None);
1763 let body = b.body().to_string();
1764 assert!(
1765 body.contains(r#"class="gg-mark gg-point lay ok""#),
1766 "{body}"
1767 );
1768 assert!(!body.contains("bad") && !body.contains("1x"), "{body}");
1769 assert!(
1770 !body.contains("aria-hidden") && !body.contains("onload"),
1771 "{body}"
1772 );
1773 assert!(
1774 !body.contains("style=") && !body.contains("onclick"),
1775 "{body}"
1776 );
1777 assert!(body.contains(r#"<g data-of="n"><text"#), "{body}");
1778 assert!(body.contains("gg-text lay ok"), "{body}");
1779 assert!(b.finish().contains(r#"<g data-of="n">"#));
1781 }
1782
1783 fn backend() -> SvgBackend {
1784 SvgBackend::new(
1785 100,
1786 100,
1787 Rect {
1788 x: 0.0,
1789 y: 0.0,
1790 width: 100.0,
1791 height: 100.0,
1792 },
1793 )
1794 }
1795
1796 #[test]
1797 fn draw_quadratic_emits_a_path_with_m_and_q() {
1798 let mut b = backend();
1799 b.draw_quadratic((0.0, 0.0), (10.0, 20.0), (20.0, 0.0), &LineStyle::default())
1800 .unwrap();
1801 let svg = b.finish();
1802 assert!(svg.contains("<path"));
1803 assert!(svg.contains("d=\"M0.00 0.00 Q10.00 20.00 20.00 0.00\""));
1804 assert!(!svg.contains("NaN"));
1805 assert!(!svg.contains("inf"));
1806 }
1807
1808 #[test]
1809 fn draw_quadratic_drops_non_finite_input() {
1810 let mut b = backend();
1811 b.draw_quadratic(
1812 (f64::NAN, 0.0),
1813 (10.0, 20.0),
1814 (20.0, 0.0),
1815 &LineStyle::default(),
1816 )
1817 .unwrap();
1818 b.draw_quadratic(
1819 (0.0, 0.0),
1820 (f64::INFINITY, 20.0),
1821 (20.0, 0.0),
1822 &LineStyle::default(),
1823 )
1824 .unwrap();
1825 let svg = b.finish();
1826 assert!(!svg.contains("<path"));
1827 }
1828
1829 #[test]
1830 fn draw_line_omits_linecap_by_default_but_writes_it_when_set() {
1831 let mut b = backend();
1832 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &LineStyle::default())
1833 .unwrap();
1834 let svg = b.finish();
1835 assert!(!svg.contains("stroke-linecap"));
1836
1837 let mut b2 = backend();
1838 let style = LineStyle {
1839 lineend: LineEnd::Round,
1840 ..Default::default()
1841 };
1842 b2.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
1843 let svg2 = b2.finish();
1844 assert!(svg2.contains("stroke-linecap=\"round\""));
1845 }
1846
1847 #[test]
1848 fn square_linecap_is_written_on_both_draw_line_and_draw_quadratic() {
1849 let style = LineStyle {
1850 lineend: LineEnd::Square,
1851 ..Default::default()
1852 };
1853 let mut b = backend();
1854 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
1855 assert!(b.finish().contains("stroke-linecap=\"square\""));
1856
1857 let mut b2 = backend();
1858 b2.draw_quadratic((0.0, 0.0), (5.0, 10.0), (10.0, 0.0), &style)
1859 .unwrap();
1860 assert!(b2.finish().contains("stroke-linecap=\"square\""));
1861 }
1862
1863 #[test]
1868 fn set_mark_attrs_defence_in_depth_drops_invalid_names() {
1869 let mut b = backend();
1870 b.set_mark_attrs(vec![
1871 ("not-data-prefixed".to_string(), "x".to_string()),
1872 ("data-".to_string(), "x".to_string()), ("data-Upper".to_string(), "x".to_string()), ("data-ok".to_string(), "valid".to_string()), ]);
1876 b.draw_circle((1.0, 1.0), 1.0, &PointStyle::default())
1877 .unwrap();
1878 let svg = b.finish();
1879 assert!(!svg.contains("not-data-prefixed"));
1880 assert!(!svg.contains("data-Upper"));
1881 assert!(!svg.contains("data-=\""));
1882 assert!(svg.contains("data-ok=\"valid\""));
1883 }
1884
1885 fn stripe() -> FillPattern {
1888 FillPattern::Stripe {
1889 fg: (10, 20, 30),
1890 bg: (200, 210, 220),
1891 size_px: 4.0,
1892 angle_deg: 45.0,
1893 }
1894 }
1895
1896 fn pattern_id(svg: &str) -> String {
1897 let i = svg.find("<pattern id=\"").unwrap() + "<pattern id=\"".len();
1898 svg[i..i + svg[i..].find('"').unwrap()].to_string()
1899 }
1900
1901 #[test]
1902 fn a_pattern_fill_registers_one_defs_pattern_and_references_it() {
1903 let mut b = backend();
1904 b.draw_rect_pattern((0.0, 0.0), (10.0, 10.0), &stripe(), &RectStyle::default())
1905 .unwrap();
1906 let svg = b.finish();
1907 assert_eq!(svg.matches("<pattern ").count(), 1, "{svg}");
1908 let id = pattern_id(&svg);
1909 assert!(
1910 svg.contains(&format!("fill=\"url(#{id})\"")),
1911 "rect references the registered pattern id {id}: {svg}"
1912 );
1913 }
1914
1915 #[test]
1916 fn a_rounded_rect_pattern_still_rounds_its_corners() {
1917 let mut b = backend();
1918 b.draw_rect_pattern(
1919 (0.0, 0.0),
1920 (10.0, 10.0),
1921 &stripe(),
1922 &RectStyle {
1923 corner_radius: 3.0,
1924 ..Default::default()
1925 },
1926 )
1927 .unwrap();
1928 let svg = b.finish();
1929 assert!(svg.contains("rx=\"3.00\""), "{svg}");
1930 assert!(svg.contains("fill=\"url(#"), "{svg}");
1931 }
1932
1933 #[test]
1934 fn an_equal_pattern_reuses_the_same_defs_entry() {
1935 let mut b = backend();
1936 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1937 .unwrap();
1938 b.draw_polygon_pattern(
1939 &[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)],
1940 &stripe(),
1941 &RectStyle::default(),
1942 )
1943 .unwrap();
1944 let svg = b.finish();
1945 assert_eq!(
1946 svg.matches("<pattern ").count(),
1947 1,
1948 "two equal patterns share one <defs> entry: {svg}"
1949 );
1950 }
1951
1952 #[test]
1953 fn different_patterns_get_their_own_defs_entries() {
1954 let mut b = backend();
1955 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1956 .unwrap();
1957 let other = FillPattern::Stripe {
1958 fg: (1, 1, 1),
1959 bg: (2, 2, 2),
1960 size_px: 8.0,
1961 angle_deg: 0.0,
1962 };
1963 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &other, &RectStyle::default())
1964 .unwrap();
1965 let svg = b.finish();
1966 assert_eq!(svg.matches("<pattern ").count(), 2, "{svg}");
1967 }
1968
1969 #[test]
1970 fn no_pattern_used_means_no_defs_block_at_all() {
1971 let mut b = backend();
1972 b.draw_rect((0.0, 0.0), (5.0, 5.0), &RectStyle::default())
1973 .unwrap();
1974 let svg = b.finish();
1975 assert!(!svg.contains("<defs>"), "{svg}");
1976 }
1977
1978 #[test]
1979 fn pattern_fill_cuts_out_holes_like_a_flat_fill_does() {
1980 let mut b = backend();
1981 let outer = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1982 let hole = vec![(2.0, 2.0), (8.0, 2.0), (8.0, 8.0), (2.0, 8.0)];
1983 b.draw_polygon_rings_pattern(&[outer, hole], &stripe(), &RectStyle::default())
1984 .unwrap();
1985 let svg = b.finish();
1986 assert!(svg.contains("fill-rule=\"evenodd\""), "{svg}");
1987 assert_eq!(
1988 svg.matches("<path").count(),
1989 1,
1990 "one path, both rings: {svg}"
1991 );
1992 }
1993
1994 #[test]
1995 fn two_backends_on_one_page_never_collide_on_a_pattern_id() {
1996 let mut a = backend();
1997 a.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
1998 .unwrap();
1999 let mut b = backend();
2000 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
2001 .unwrap();
2002 let (sa, sb) = (a.finish(), b.finish());
2003 assert_ne!(pattern_id(&sa), pattern_id(&sb), "{sa}\n{sb}");
2004 }
2005
2006 fn decode_compact_d(d: &str) -> Vec<Vec<(f64, f64)>> {
2013 let b = d.as_bytes();
2014 let mut i = 0;
2015 let read_num = |b: &[u8], i: &mut usize| -> f64 {
2016 let start = *i;
2017 if *i < b.len() && b[*i] == b'-' {
2018 *i += 1;
2019 }
2020 while *i < b.len() && b[*i].is_ascii_digit() {
2021 *i += 1;
2022 }
2023 if *i < b.len() && b[*i] == b'.' {
2024 *i += 1;
2025 while *i < b.len() && b[*i].is_ascii_digit() {
2026 *i += 1;
2027 }
2028 }
2029 std::str::from_utf8(&b[start..*i]).unwrap().parse().unwrap()
2030 };
2031 let is_num_start = |c: u8| c.is_ascii_digit() || c == b'-' || c == b'.';
2032 let mut rings = Vec::new();
2033 let mut current = Vec::new();
2034 let mut cursor = (0.0, 0.0);
2035 while i < b.len() {
2036 match b[i] {
2037 b'M' => {
2038 if !current.is_empty() {
2039 rings.push(std::mem::take(&mut current));
2040 }
2041 i += 1;
2042 let x = read_num(b, &mut i);
2043 if i < b.len() && b[i] == b',' {
2044 i += 1;
2045 }
2046 let y = read_num(b, &mut i);
2047 cursor = (x, y);
2048 current.push(cursor);
2049 }
2050 b'l' => {
2051 i += 1;
2052 while i < b.len() && is_num_start(b[i]) {
2053 let dx = read_num(b, &mut i);
2054 if i < b.len() && b[i] == b',' {
2055 i += 1;
2056 }
2057 let dy = read_num(b, &mut i);
2058 if i < b.len() && b[i] == b',' {
2059 i += 1;
2060 }
2061 cursor = (cursor.0 + dx, cursor.1 + dy);
2062 current.push(cursor);
2063 }
2064 }
2065 b'z' => {
2066 i += 1;
2067 if !current.is_empty() {
2068 rings.push(std::mem::take(&mut current));
2069 }
2070 }
2071 _ => i += 1,
2072 }
2073 }
2074 if !current.is_empty() {
2075 rings.push(current);
2076 }
2077 rings
2078 }
2079
2080 fn noisy_ring(n: usize, cx: f64, cy: f64, r: f64) -> Vec<(f64, f64)> {
2083 (0..n)
2084 .map(|i| {
2085 let t = i as f64 / n as f64 * std::f64::consts::TAU;
2086 let wobble = 1.0 + 0.03 * ((i * 7) as f64).sin();
2087 (cx + r * wobble * t.cos(), cy + r * wobble * t.sin())
2088 })
2089 .collect()
2090 }
2091
2092 #[test]
2093 fn compact_encoding_is_well_under_the_absolute_encodings_size() {
2094 let ring = noisy_ring(2000, 400.0, 300.0, 280.0);
2098 let mut b = backend();
2099 b.draw_polygon_rings(std::slice::from_ref(&ring), &RectStyle::default())
2100 .unwrap();
2101 let absolute_svg = b.finish();
2102 let absolute_d = absolute_svg
2103 .split("d=\"")
2104 .nth(1)
2105 .and_then(|s| s.split('"').next())
2106 .expect("absolute path d");
2107
2108 let compact_d = compact_rings_d(std::slice::from_ref(&ring), 1);
2109
2110 assert!(
2111 compact_d.len() * 3 < absolute_d.len() * 2,
2112 "compact {} bytes should be well under 2/3 of absolute {} bytes",
2113 compact_d.len(),
2114 absolute_d.len()
2115 );
2116 }
2117
2118 #[test]
2119 fn compact_encoding_round_trips_within_precision() {
2120 let ring = noisy_ring(40, 100.0, 80.0, 60.0);
2121 let precision: u8 = 1;
2122 let d = compact_rings_d(std::slice::from_ref(&ring), precision);
2123 let decoded = decode_compact_d(&d);
2124 assert_eq!(decoded.len(), 1, "{d}");
2125 assert_eq!(decoded[0].len(), ring.len(), "{d}");
2126 let step_tol = 0.5 * 10f64.powi(-(precision as i32));
2131 for (k, (orig, got)) in ring.iter().zip(decoded[0].iter()).enumerate() {
2132 let tol = step_tol * (k as f64 + 1.0) + 1e-9;
2133 assert!(
2134 (orig.0 - got.0).abs() <= tol && (orig.1 - got.1).abs() <= tol,
2135 "point {k}: {orig:?} vs {got:?} (tol {tol}): {d}"
2136 );
2137 }
2138 }
2139
2140 #[test]
2141 fn compact_encoding_round_trips_a_hole_and_a_multipolygons_parts() {
2142 let part1 = vec![(0.0, 0.0), (20.0, 0.0), (20.0, 20.0), (0.0, 20.0)];
2146 let part2_outer = vec![(50.0, 0.0), (90.0, 0.0), (90.0, 40.0), (50.0, 40.0)];
2147 let part2_hole = vec![(60.0, 10.0), (80.0, 10.0), (80.0, 30.0), (60.0, 30.0)];
2148 let rings = vec![part1.clone(), part2_outer.clone(), part2_hole.clone()];
2149 let d = compact_rings_d(&rings, 1);
2150 let decoded = decode_compact_d(&d);
2151 assert_eq!(decoded.len(), 3, "{d}");
2152 for (orig, got) in [part1, part2_outer, part2_hole].iter().zip(decoded.iter()) {
2153 assert_eq!(orig.len(), got.len(), "{d}");
2154 for (o, g) in orig.iter().zip(got.iter()) {
2155 assert!((o.0 - g.0).abs() <= 0.1 && (o.1 - g.1).abs() <= 0.1, "{d}");
2156 }
2157 }
2158 assert!(d.contains("M0,0l20,0,0,20-20,0z"), "{d}");
2159 }
2160}