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 element_classes: bool,
94 element_class: Option<&'static str>,
95 clips: Vec<Rect>,
97}
98
99static NEXT_ID_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
102
103impl SvgBackend {
104 pub fn new(width: u32, height: u32, plot_area: Rect) -> Self {
105 SvgBackend {
106 plot_area,
107 total_area: Rect {
108 x: 0.0,
109 y: 0.0,
110 width: width as f64,
111 height: height as f64,
112 },
113 body: String::new(),
114 tooltip: None,
115 axis_key: None,
116 series_key: None,
117 value_key: None,
118 mark_part: None,
119 mark_attrs: Vec::new(),
120 mark_geom: None,
121 root_attrs: Vec::new(),
122 warnings: Vec::new(),
123 plot_attr: true,
124 formatted_values: false,
125 mark_fvalue: None,
126 mark_fx: None,
127 patterns: Vec::new(),
128 id_prefix: format!(
129 "gg{:x}",
130 NEXT_ID_PREFIX.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
131 ),
132 mark_user_class: None,
133 mark_group: None,
134 layer_attrs: Vec::new(),
135 groups: Vec::new(),
136 group_slots: Vec::new(),
137 svg_title: None,
138 svg_desc: None,
139 corner_claims: Vec::new(),
140 viewport: None,
141 panels: Vec::new(),
142 clips: Vec::new(),
143 element_classes: false,
144 element_class: None,
145 }
146 }
147
148 fn pattern_url(&mut self, pattern: &FillPattern) -> String {
152 if let Some((_, id)) = self.patterns.iter().find(|(p, _)| p == pattern) {
153 return format!("url(#{id})");
154 }
155 let id = format!("{}-pat-{}", self.id_prefix, self.patterns.len());
156 self.patterns.push((*pattern, id.clone()));
157 format!("url(#{id})")
158 }
159
160 fn rect_markup(
163 &mut self,
164 (x0, y0): (f64, f64),
165 (x1, y1): (f64, f64),
166 fill: &str,
167 style: &RectStyle,
168 ) -> Result<(), RenderError> {
169 if !(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite()) {
170 return Ok(());
171 }
172 let (x, y) = (x0.min(x1), y0.min(y1));
173 let (w, h) = ((x1 - x0).abs(), (y1 - y0).abs());
174 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
175 let r = style.corner_radius.max(0.0).min(w / 2.0).min(h / 2.0);
178 let radius = if r > 0.0 {
179 format!(" rx=\"{r:.2}\" ry=\"{r:.2}\"")
180 } else {
181 String::new()
182 };
183 let attrs = format!(
184 "x=\"{x:.2}\" y=\"{y:.2}\" width=\"{w:.2}\" height=\"{h:.2}\"{radius} fill=\"{fill}\"{} \
185 stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
186 opacity_attr("fill", style.alpha),
187 finite_or(style.stroke_width, 0.0)
188 );
189 self.push_mark("rect", "bar", &attrs);
190 Ok(())
191 }
192
193 fn polygon_markup(
195 &mut self,
196 pts: &[(f64, f64)],
197 fill: &str,
198 style: &RectStyle,
199 ) -> Result<(), RenderError> {
200 let pts = finite_pts(pts);
201 if pts.len() < 3 {
202 return Ok(());
203 }
204 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
205 let attrs = format!(
206 "points=\"{}\" fill=\"{fill}\"{} stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
207 points(&pts),
208 opacity_attr("fill", style.alpha),
209 finite_or(style.stroke_width, 0.0)
210 );
211 self.push_mark("polygon", "band", &attrs);
212 Ok(())
213 }
214
215 fn polygon_rings_markup(
218 &mut self,
219 rings: &[Vec<(f64, f64)>],
220 fill: &str,
221 style: &RectStyle,
222 ) -> Result<(), RenderError> {
223 let mut d = String::new();
224 for ring in rings {
225 let pts = finite_pts(ring);
226 if pts.len() < 3 {
227 continue;
228 }
229 d.push('M');
230 d.push_str(&format!("{},{}", num(pts[0].0), num(pts[0].1)));
231 for p in &pts[1..] {
232 d.push('L');
233 d.push_str(&format!("{},{}", num(p.0), num(p.1)));
234 }
235 d.push('Z');
236 }
237 if d.is_empty() {
238 return Ok(());
239 }
240 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
241 let attrs = format!(
242 "d=\"{d}\" fill-rule=\"evenodd\" fill=\"{fill}\"{} stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
243 opacity_attr("fill", style.alpha),
244 finite_or(style.stroke_width, 0.0)
245 );
246 self.push_mark("path", "band", &attrs);
247 Ok(())
248 }
249
250 fn pattern_defs(&self) -> String {
255 if self.patterns.is_empty() && self.panels.is_empty() && self.clips.is_empty() {
256 return String::new();
257 }
258 let mut defs = String::from("<defs>");
259 for (i, clip) in self.clips.iter().enumerate() {
260 defs.push_str(&format!(
261 "<clipPath id=\"{}-clip-{i}\"><rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\"/></clipPath>",
262 self.id_prefix,
263 num(clip.x),
264 num(clip.y),
265 num(clip.width),
266 num(clip.height),
267 ));
268 }
269 for (i, _, clip, _) in &self.panels {
270 defs.push_str(&format!(
271 "<clipPath id=\"{}-panel-{i}\"><rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\"/></clipPath>",
272 self.id_prefix,
273 num(clip.x),
274 num(clip.y),
275 num(clip.width),
276 num(clip.height),
277 ));
278 }
279 for (p, id) in &self.patterns {
280 let FillPattern::Stripe {
281 fg,
282 bg,
283 size_px,
284 angle_deg,
285 } = p;
286 let size = if *size_px > 0.0 { *size_px } else { 1.0 };
287 defs.push_str(&format!(
288 "<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>",
289 rgb(*bg),
290 size * 0.3,
291 rgb(*fg),
292 ));
293 }
294 defs.push_str("</defs>");
295 defs
296 }
297
298 pub(crate) fn set_formatted_values(&mut self, on: bool) {
302 self.formatted_values = on;
303 }
304
305 pub fn set_root_attrs(&mut self, attrs: Vec<(String, String)>) {
309 self.root_attrs = attrs;
310 }
311
312 pub fn extend_root_attrs(&mut self, attrs: Vec<(String, String)>) {
320 self.root_attrs.extend(attrs);
321 }
322
323 pub(crate) fn without_plot_attr(&mut self) {
325 self.plot_attr = false;
326 }
327
328 pub(crate) fn set_svg_title(&mut self, title: Option<String>) {
333 self.svg_title = title;
334 }
335
336 pub(crate) fn set_svg_desc(&mut self, desc: Option<String>) {
341 self.svg_desc = desc;
342 }
343
344 pub(crate) fn push_raw(&mut self, markup: &str) {
347 self.body.push_str(markup);
348 }
349
350 pub(crate) fn body(&mut self) -> &str {
353 self.flush_groups();
354 &self.body
355 }
356
357 fn emit(&mut self, markup: &str) {
360 let key = match (&self.mark_group, &self.mark_geom) {
361 (Some(k), Some(_)) => k,
362 _ => {
363 self.body.push_str(markup);
364 return;
365 }
366 };
367 match self
368 .groups
369 .iter_mut()
370 .find(|(n, v, _)| n == &key.0 && v == &key.1)
371 {
372 Some((_, _, content)) => content.push_str(markup),
373 None => {
374 self.group_slots.push((self.body.len(), self.groups.len()));
375 self.groups
376 .push((key.0.clone(), key.1.clone(), markup.to_string()));
377 }
378 }
379 }
380
381 fn flush_groups(&mut self) {
387 if self.group_slots.is_empty() {
388 return;
389 }
390 let groups = std::mem::take(&mut self.groups);
391 let slots = std::mem::take(&mut self.group_slots);
392 let extra: usize = groups
393 .iter()
394 .map(|(n, v, c)| n.len() + v.len() + c.len() + 12)
395 .sum();
396 let mut out = String::with_capacity(self.body.len() + extra);
397 let mut last = 0;
398 for (off, gi) in slots {
399 out.push_str(&self.body[last..off]);
400 last = off;
401 let (name, value, content) = &groups[gi];
402 if is_valid_mark_attr_name(name) {
403 out.push_str(&format!(
404 "<g {name}=\"{}\">{content}</g>",
405 escape_attr(value)
406 ));
407 } else {
408 out.push_str(content);
409 }
410 }
411 out.push_str(&self.body[last..]);
412 self.body = out;
413 }
414
415 pub(crate) fn wrap_body_in_group(&mut self, attrs: &[(String, String)]) {
422 self.flush_groups();
423 if attrs.is_empty() {
424 return;
425 }
426 let mut open = String::from("<g");
427 for (k, v) in attrs {
428 open.push_str(&format!(" {k}=\"{}\"", escape_attr(v)));
429 }
430 open.push('>');
431 self.body = format!("{open}{}</g>", self.body);
432 }
433
434 pub fn take_warnings(&mut self) -> Vec<String> {
436 std::mem::take(&mut self.warnings)
437 }
438
439 fn mark_class(&self, kind: &str) -> String {
446 match &self.mark_geom {
447 Some(g) => {
448 let mut class = format!("gg-mark gg-{kind}");
449 let layer = self
450 .layer_attrs
451 .iter()
452 .filter(|(n, _)| n == "class")
453 .map(|(_, v)| v.as_str());
454 let mut seen: Vec<&str> = Vec::new();
455 for t in layer
456 .chain(self.mark_user_class.as_deref())
457 .flat_map(str::split_whitespace)
458 {
459 if crate::aes::is_valid_class_token(t) && !seen.contains(&t) {
460 seen.push(t);
461 class.push(' ');
462 class.push_str(t);
463 }
464 }
465 format!(" class=\"{class}\" data-geom=\"{}\"", escape_attr(g))
466 }
467 None => match (self.element_classes, self.element_class) {
469 (true, Some(c)) => format!(" class=\"{c}\""),
470 _ => String::new(),
471 },
472 }
473 }
474
475 fn layer_flag_attrs(&self, aria_hidden_written: bool) -> String {
480 let mut out = String::new();
481 if self.mark_geom.is_none() {
482 return out;
483 }
484 for (name, value) in &self.layer_attrs {
485 let ok = match name.as_str() {
486 "aria-hidden" => !aria_hidden_written && matches!(value.as_str(), "true" | "false"),
487 "pointer-events" => POINTER_EVENTS_VALUES.contains(&value.as_str()),
488 _ => false,
489 };
490 if ok {
491 out.push_str(&format!(" {name}=\"{value}\""));
492 }
493 }
494 out
495 }
496
497 fn text_data_attrs(&self) -> String {
502 let mut pairs: Vec<(&str, &str)> = Vec::new();
503 if self.mark_geom.is_none() {
504 return String::new();
505 }
506 for (name, val) in self.layer_attrs.iter().chain(self.mark_attrs.iter()) {
507 if !is_valid_mark_attr_name(name) {
508 continue;
509 }
510 match pairs.iter_mut().find(|(n, _)| n == name) {
511 Some(existing) => existing.1 = val,
512 None => pairs.push((name, val)),
513 }
514 }
515 let mut out = String::new();
516 for (name, val) in pairs {
517 out.push_str(&format!(" {name}=\"{}\"", escape_attr(val)));
518 }
519 out
520 }
521
522 fn push_mark(&mut self, tag: &str, kind: &str, attrs: &str) {
530 let mut pairs: Vec<(String, String)> = Vec::new();
540 for (name, val) in [
541 ("data-x", &self.axis_key),
542 ("data-series", &self.series_key),
543 ("data-value", &self.value_key),
544 ] {
545 if let Some(v) = val {
546 pairs.push((name.to_string(), v.clone()));
547 }
548 }
549 if let Some(fx) = &self.mark_fx {
553 if self.axis_key.as_deref() != Some(fx.as_str()) {
554 pairs.push(("data-fx".to_string(), fx.clone()));
555 }
556 }
557 if let Some(fvalue) = &self.mark_fvalue {
558 pairs.push(("data-fvalue".to_string(), fvalue.clone()));
559 }
560 let layer_data = self.layer_attrs.iter().filter(|_| self.mark_geom.is_some());
563 for (name, val) in layer_data.chain(self.mark_attrs.iter()) {
564 if !is_valid_mark_attr_name(name) {
565 continue;
566 }
567 match pairs.iter_mut().find(|(n, _)| n == name) {
568 Some(existing) => existing.1 = val.clone(),
569 None => pairs.push((name.clone(), val.clone())),
570 }
571 }
572 if let Some(part) = &self.mark_part {
577 match pairs.iter_mut().find(|(n, _)| n == "data-part") {
578 Some(existing) => existing.1 = part.clone(),
579 None => pairs.push(("data-part".to_string(), part.clone())),
580 }
581 }
582 let mut data = String::new();
583 for (name, val) in &pairs {
584 data.push_str(&format!(" {name}=\"{}\"", escape_attr(val)));
585 }
586 if self.mark_part.is_some() {
587 data.push_str(" aria-hidden=\"true\"");
588 }
589 data.push_str(&self.layer_flag_attrs(self.mark_part.is_some()));
590 let effective_kind: &str = self.mark_part.as_deref().unwrap_or(kind);
595 data.push_str(&self.mark_class(effective_kind));
596 let mark = if self.mark_part.is_some() {
597 format!("<{tag} {attrs}{data}/>")
598 } else {
599 match &self.tooltip {
600 Some(t) => format!("<{tag} {attrs}{data}><title>{}</title></{tag}>", escape(t)),
601 None => format!("<{tag} {attrs}{data}/>"),
602 }
603 };
604 self.emit(&mark);
605 }
606
607 fn root_open(&self, prefix: &str) -> String {
608 let p = &self.plot_area;
609 let (w, h) = (self.total_area.width as i64, self.total_area.height as i64);
610 let mut extra = String::new();
611 for (k, v) in &self.root_attrs {
612 extra.push_str(&format!(" {k}=\"{}\"", escape_attr(v)));
613 }
614 let mut plot = if self.plot_attr {
615 format!(
616 " data-plot=\"{} {} {} {}\"",
617 num(p.x),
618 num(p.y),
619 num(p.width),
620 num(p.height),
621 )
622 } else {
623 String::new()
624 };
625 if self.plot_attr {
626 if let Some(v) = &self.viewport {
627 plot.push_str(&format!(
628 " data-viewport=\"{} {} {} {}\"",
629 num(v.x),
630 num(v.y),
631 num(v.width),
632 num(v.height),
633 ));
634 }
635 if !self.panels.is_empty() {
636 let items: Vec<String> = self
637 .panels
638 .iter()
639 .map(|(i, label, _, d)| {
640 format!(
641 "{{\"i\":{i},\"label\":{},\"x\":{},\"y\":{},\"w\":{},\"h\":{}}}",
642 json_string(label),
643 num(d.x),
644 num(d.y),
645 num(d.width),
646 num(d.height),
647 )
648 })
649 .collect();
650 plot.push_str(&format!(
651 " data-panels=\"{}\"",
652 escape_attr(&format!("[{}]", items.join(",")))
653 ));
654 }
655 }
656 let mut access = String::new();
661 if self.svg_title.is_some() || self.svg_desc.is_some() {
662 access.push_str(" role=\"img\"");
663 }
664 if self.svg_title.is_some() {
665 access.push_str(&format!(" aria-labelledby=\"{}-title\"", self.id_prefix));
666 }
667 if self.svg_desc.is_some() {
668 access.push_str(&format!(" aria-describedby=\"{}-desc\"", self.id_prefix));
669 }
670 format!(
671 "<svg {prefix}width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\"{plot}{access}{extra}>"
672 )
673 }
674
675 fn access_children(&self) -> String {
680 let mut out = String::new();
681 if let Some(t) = &self.svg_title {
682 out.push_str(&format!(
683 "<title id=\"{}-title\">{}</title>",
684 self.id_prefix,
685 escape(t)
686 ));
687 }
688 if let Some(d) = &self.svg_desc {
689 out.push_str(&format!(
690 "<desc id=\"{}-desc\">{}</desc>",
691 self.id_prefix,
692 escape(d)
693 ));
694 }
695 out
696 }
697
698 pub fn finish(mut self) -> String {
702 self.flush_groups();
703 let open = self.root_open("xmlns=\"http://www.w3.org/2000/svg\" ");
704 let access = self.access_children();
705 let defs = self.pattern_defs();
706 format!("{open}{access}{defs}{}</svg>", self.body)
707 }
708
709 pub fn finish_fragment(mut self, x: f64, y: f64) -> String {
714 self.flush_groups();
715 let open = self.root_open(&format!("x=\"{}\" y=\"{}\" ", num(x), num(y)));
716 let access = self.access_children();
717 let defs = self.pattern_defs();
718 format!("{open}{access}{defs}{}</svg>", self.body)
719 }
720
721 fn draw_line_run(&mut self, pts: &[(f64, f64)], style: &LineStyle) {
724 let dash = dasharray_attr(&style.linetype);
725 let linecap = match style.lineend {
728 LineEnd::Butt => "",
729 LineEnd::Round => " stroke-linecap=\"round\"",
730 LineEnd::Square => " stroke-linecap=\"square\"",
731 };
732 let attrs = format!(
733 "points=\"{}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\"{}{}{}",
734 points(pts),
735 rgb(style.color),
736 finite_or(style.width, 1.0),
737 opacity_attr("stroke", style.alpha),
738 dash,
739 linecap
740 );
741 self.push_mark("polyline", "line", &attrs);
742 }
743}
744
745fn num(v: f64) -> String {
748 if v.is_finite() {
749 format!("{v:.2}")
750 } else {
751 "0".to_string()
752 }
753}
754
755fn dasharray_attr(linetype: &Linetype) -> String {
759 let pattern = linetype.dash_pattern();
760 if pattern.is_empty() {
761 return String::new();
762 }
763 let s = pattern
764 .iter()
765 .flat_map(|(d, g)| [*d, *g])
766 .map(|v| format!("{v}"))
767 .collect::<Vec<_>>()
768 .join(",");
769 format!(" stroke-dasharray=\"{s}\"")
770}
771
772fn compact_rings_d(rings: &[Vec<(f64, f64)>], precision: u8) -> String {
782 let mut d = String::new();
783 for ring in rings {
784 let pts = finite_pts(ring);
785 if pts.len() < 3 {
786 continue;
787 }
788 d.push('M');
789 push_num(&mut d, &fmt_compact(pts[0].0, precision));
790 push_num(&mut d, &fmt_compact(pts[0].1, precision));
791 if pts.len() > 1 {
792 d.push('l');
793 let mut prev = pts[0];
794 for p in &pts[1..] {
795 push_num(&mut d, &fmt_compact(p.0 - prev.0, precision));
796 push_num(&mut d, &fmt_compact(p.1 - prev.1, precision));
797 prev = *p;
798 }
799 }
800 d.push('z');
801 }
802 d
803}
804
805fn push_num(d: &mut String, s: &str) {
813 let needs_sep =
814 matches!(d.chars().last(), Some(c) if c.is_ascii_digit()) && !s.starts_with('-');
815 if needs_sep {
816 d.push(',');
817 }
818 d.push_str(s);
819}
820
821fn fmt_compact(v: f64, precision: u8) -> String {
827 if !v.is_finite() {
828 return "0".to_string();
829 }
830 let s = format!("{v:.*}", precision as usize);
831 let trimmed = if s.contains('.') {
832 s.trim_end_matches('0').trim_end_matches('.')
833 } else {
834 s.as_str()
835 };
836 match trimmed {
837 "" | "-" | "-0" => "0".to_string(),
838 other => other.to_string(),
839 }
840}
841
842fn finite_or(v: f64, fallback: f64) -> f64 {
843 if v.is_finite() {
844 v
845 } else {
846 fallback
847 }
848}
849
850fn opacity_attr(prefix: &str, alpha: f64) -> String {
856 let a = finite_or(alpha, 1.0);
857 if format!("{a:.3}") == "1.000" {
858 String::new()
859 } else {
860 format!(" {prefix}-opacity=\"{a:.3}\"")
861 }
862}
863
864fn base64(bytes: &[u8]) -> String {
882 const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
883 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
884 for c in bytes.chunks(3) {
885 let n = (c[0] as u32) << 16
886 | (*c.get(1).unwrap_or(&0) as u32) << 8
887 | *c.get(2).unwrap_or(&0) as u32;
888 for i in 0..4 {
889 if i <= c.len() {
890 out.push(T[(n >> (18 - 6 * i) & 63) as usize] as char);
891 } else {
892 out.push('=');
893 }
894 }
895 }
896 out
897}
898
899pub fn root_data_attrs(built: &crate::build::BuiltPlot) -> Vec<(String, String)> {
900 use crate::aes::Aesthetic;
901 let mut out = Vec::new();
902 if !built.panel_scales.is_empty() {
903 return out;
904 }
905 let axis = |aes: &Aesthetic| -> (Option<(f64, f64)>, Option<String>) {
906 match built.scales.get(aes) {
907 Some(s) if s.is_discrete() => {
908 let labels: Vec<String> = s
909 .breaks()
910 .into_iter()
911 .map(|(_, l)| json_string(&l))
912 .collect();
913 (None, Some(format!("[{}]", labels.join(","))))
914 }
915 Some(s) => (s.expanded_domain(), None),
916 None => (None, None),
917 }
918 };
919 if let Some(at) = &built.legend_at {
920 out.push(("data-legend-at".into(), at.clone()));
921 }
922 let (xd, xl) = axis(&Aesthetic::X);
923 let (yd, yl) = axis(&Aesthetic::Y);
924 if let (Some((x0, x1)), Some((y0, y1))) = (xd, yd) {
925 out.push((
926 "data-domain".into(),
927 format!("{} {} {} {}", g(x0), g(x1), g(y0), g(y1)),
928 ));
929 }
930 if let Some((a, b)) = xd {
931 out.push(("data-xdomain".into(), format!("{} {}", g(a), g(b))));
932 }
933 if let Some((a, b)) = yd {
934 out.push(("data-ydomain".into(), format!("{} {}", g(a), g(b))));
935 }
936 if let Some(l) = xl {
937 out.push(("data-xlevels".into(), l));
938 }
939 if let Some(l) = yl {
940 out.push(("data-ylevels".into(), l));
941 }
942 if built.coord.is_flipped() {
943 out.push(("data-flip".into(), "true".into()));
944 }
945 out
946}
947
948fn g(v: f64) -> String {
951 format!("{v}")
952}
953
954fn json_string(s: &str) -> String {
957 let mut out = String::with_capacity(s.len() + 2);
958 out.push('"');
959 for c in s.chars() {
960 match c {
961 '"' => out.push_str("\\\""),
962 '\\' => out.push_str("\\\\"),
963 '\n' => out.push_str("\\n"),
964 '\r' => out.push_str("\\r"),
965 '\t' => out.push_str("\\t"),
966 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
967 c => out.push(c),
968 }
969 }
970 out.push('"');
971 out
972}
973
974fn is_allowed_root_attr_name(name: &str) -> bool {
981 matches!(name, "role" | "id" | "class" | "lang")
982 || name
983 .strip_prefix("data-")
984 .is_some_and(crate::aes::is_valid_data_attr_name)
985 || name
986 .strip_prefix("aria-")
987 .is_some_and(crate::aes::is_valid_data_attr_name)
988}
989
990pub(crate) fn validate_root_attrs(
1000 requested: &[(String, String)],
1001 existing: &[(String, String)],
1002 label: &str,
1003 warnings: &mut Vec<String>,
1004) -> Vec<(String, String)> {
1005 let built_in: Vec<&str> = existing.iter().map(|(k, _)| k.as_str()).collect();
1006 let mut used: Vec<&str> = built_in.clone();
1007 let mut out = Vec::new();
1008 for (name, value) in requested {
1009 if !is_allowed_root_attr_name(name) {
1010 warnings.push(format!(
1011 "{label}: svg_attr(\"{name}\") is not on the allow-list (data-*, aria-*, role, id, class, lang) — ignored"
1012 ));
1013 continue;
1014 }
1015 if used.contains(&name.as_str()) {
1016 if built_in.contains(&name.as_str()) {
1021 warnings.push(format!(
1022 "{label}: svg_attr(\"{name}\") collides with an attribute ggplot-rs already writes to the root — ignored"
1023 ));
1024 } else {
1025 warnings.push(format!(
1026 "{label}: svg_attr(\"{name}\") was already set by an earlier svg_attr call — ignored"
1027 ));
1028 }
1029 continue;
1030 }
1031 used.push(name.as_str());
1032 out.push((name.clone(), value.clone()));
1033 }
1034 out
1035}
1036
1037pub(crate) fn warnings_json(warnings: &[String]) -> String {
1041 let items: Vec<String> = warnings.iter().map(|w| json_string(w)).collect();
1042 format!("[{}]", items.join(","))
1043}
1044
1045fn is_valid_mark_attr_name(name: &str) -> bool {
1053 match name.strip_prefix("data-") {
1054 Some(suffix) => crate::aes::is_valid_data_attr_name(suffix),
1055 None => false,
1056 }
1057}
1058
1059const POINTER_EVENTS_VALUES: &[&str] = &[
1062 "auto",
1063 "none",
1064 "visiblePainted",
1065 "visibleFill",
1066 "visibleStroke",
1067 "visible",
1068 "painted",
1069 "fill",
1070 "stroke",
1071 "all",
1072 "bounding-box",
1073];
1074
1075pub(crate) fn validate_layer_svg_attr(
1084 name: &str,
1085 value: &str,
1086 warnings: &mut Vec<String>,
1087) -> Option<(String, String)> {
1088 let keep = |v: &str| Some((name.to_string(), v.to_string()));
1089 match name {
1090 "class" => {
1091 let (ok, bad) = crate::aes::split_class_tokens(value);
1092 if !bad.is_empty() {
1093 warnings.push(format!(
1094 "layer_svg_attr(\"class\"): invalid class token(s) {bad:?} (must match [A-Za-z][A-Za-z0-9_-]*) — dropped"
1095 ));
1096 }
1097 if ok.is_empty() {
1098 None
1099 } else {
1100 keep(&ok.join(" "))
1101 }
1102 }
1103 "aria-hidden" if matches!(value, "true" | "false") => keep(value),
1104 "pointer-events" if POINTER_EVENTS_VALUES.contains(&value) => keep(value),
1105 "aria-hidden" | "pointer-events" => {
1106 warnings.push(format!(
1107 "layer_svg_attr(\"{name}\"): value {value:?} is not allowed — ignored"
1108 ));
1109 None
1110 }
1111 _ if is_valid_mark_attr_name(name) => keep(value),
1112 _ => {
1113 warnings.push(format!(
1114 "layer_svg_attr(\"{name}\") is not on the allow-list (class, aria-hidden, pointer-events, data-*) — ignored"
1115 ));
1116 None
1117 }
1118 }
1119}
1120
1121fn rgb((r, g, b): (u8, u8, u8)) -> String {
1122 format!("#{r:02X}{g:02X}{b:02X}")
1123}
1124
1125fn escape(s: &str) -> String {
1136 escape_impl(s, false)
1137}
1138
1139fn escape_attr(s: &str) -> String {
1150 escape_impl(s, true)
1151}
1152
1153fn escape_impl(s: &str, in_attr: bool) -> String {
1154 let mut out = String::with_capacity(s.len());
1155 for c in s.chars() {
1156 match c {
1157 '&' => out.push_str("&"),
1158 '<' => out.push_str("<"),
1159 '>' => out.push_str(">"),
1160 '"' => out.push_str("""),
1161 '\'' => out.push_str("'"),
1162 '\t' if in_attr => out.push_str("	"),
1163 '\n' if in_attr => out.push_str(" "),
1164 '\r' if in_attr => out.push_str(" "),
1165 '\t' | '\n' | '\r' => out.push(c),
1166 c if (c as u32) < 0x20 || c == '\u{FFFE}' || c == '\u{FFFF}' => {}
1167 c => out.push(c),
1168 }
1169 }
1170 out
1171}
1172
1173fn points(pts: &[(f64, f64)]) -> String {
1174 pts.iter()
1175 .map(|(x, y)| format!("{x:.2},{y:.2}"))
1176 .collect::<Vec<_>>()
1177 .join(" ")
1178}
1179
1180fn finite_pts(pts: &[(f64, f64)]) -> Vec<(f64, f64)> {
1181 pts.iter()
1182 .copied()
1183 .filter(|(x, y)| x.is_finite() && y.is_finite())
1184 .collect()
1185}
1186
1187fn finite_runs(pts: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
1196 let mut runs = Vec::new();
1197 let mut cur: Vec<(f64, f64)> = Vec::new();
1198 for &(x, y) in pts {
1199 if x.is_finite() && y.is_finite() {
1200 cur.push((x, y));
1201 } else if !cur.is_empty() {
1202 if cur.len() >= 2 {
1203 runs.push(std::mem::take(&mut cur));
1204 } else {
1205 cur.clear();
1206 }
1207 }
1208 }
1209 if cur.len() >= 2 {
1210 runs.push(cur);
1211 }
1212 runs
1213}
1214
1215impl DrawBackend for SvgBackend {
1216 fn begin_panel(&mut self, index: usize, label: &str, clip: &Rect, data: &Rect) {
1217 self.flush_groups();
1219 self.body.push_str(&format!(
1220 "<g data-facet-panel=\"{index}\" clip-path=\"url(#{}-panel-{index})\">",
1221 self.id_prefix
1222 ));
1223 self.panels
1224 .push((index, label.to_string(), clip.clone(), data.clone()));
1225 }
1226
1227 fn end_panel(&mut self) {
1228 self.flush_groups();
1229 self.body.push_str("</g>");
1230 }
1231
1232 fn enable_element_classes(&mut self, on: bool) {
1233 self.element_classes = on;
1234 }
1235
1236 fn set_element_class(&mut self, class: Option<&'static str>) {
1237 self.element_class = class;
1238 }
1239
1240 fn begin_clip(&mut self, rect: &Rect) {
1241 self.flush_groups();
1242 self.body.push_str(&format!(
1243 "<g clip-path=\"url(#{}-clip-{})\">",
1244 self.id_prefix,
1245 self.clips.len()
1246 ));
1247 self.clips.push(rect.clone());
1248 }
1249
1250 fn end_clip(&mut self) {
1251 self.flush_groups();
1252 self.body.push_str("</g>");
1253 }
1254
1255 fn set_viewport(&mut self, rect: &Rect) {
1256 self.viewport = Some(rect.clone());
1257 }
1258
1259 fn plot_area(&self) -> Rect {
1260 self.plot_area.clone()
1261 }
1262 fn total_area(&self) -> Rect {
1263 self.total_area.clone()
1264 }
1265
1266 fn corner_claims(&self) -> Vec<Rect> {
1267 self.corner_claims.clone()
1268 }
1269 fn claim_corner_box(&mut self, rect: Rect) {
1270 self.corner_claims.push(rect);
1271 }
1272 fn clear_corner_claims(&mut self) {
1273 self.corner_claims.clear();
1274 }
1275
1276 fn set_tooltip(&mut self, tooltip: Option<String>) {
1277 self.tooltip = tooltip;
1278 }
1279
1280 fn set_mark_axis(&mut self, key: Option<String>) {
1281 self.axis_key = key;
1282 }
1283
1284 fn set_mark_series(&mut self, series: Option<String>) {
1285 self.series_key = series;
1286 }
1287
1288 fn set_mark_value(&mut self, value: Option<String>) {
1289 self.value_key = value;
1290 }
1291
1292 fn set_mark_geom(&mut self, geom: Option<&str>) {
1293 self.mark_geom = geom.map(str::to_string);
1294 }
1295
1296 fn warn(&mut self, message: String) {
1297 self.warnings.push(message);
1298 }
1299
1300 fn set_mark_part(&mut self, part: Option<&str>) {
1301 self.mark_part = part.map(|s| s.to_string());
1302 }
1303
1304 fn set_mark_attrs(&mut self, attrs: Vec<(String, String)>) {
1305 self.mark_attrs = attrs;
1306 }
1307
1308 fn wants_formatted_values(&self) -> bool {
1309 self.formatted_values
1310 }
1311
1312 fn set_mark_fvalue(&mut self, value: Option<String>) {
1313 self.mark_fvalue = value;
1314 }
1315
1316 fn set_mark_fx(&mut self, value: Option<String>) {
1317 self.mark_fx = value;
1318 }
1319
1320 fn set_mark_class(&mut self, class: Option<String>) {
1321 self.mark_user_class = class;
1322 }
1323
1324 fn set_mark_group(&mut self, group: Option<(String, String)>) {
1325 self.mark_group = group;
1326 }
1327
1328 fn set_layer_attrs(&mut self, attrs: Vec<(String, String)>) {
1329 self.layer_attrs = attrs;
1330 }
1331
1332 fn draw_circle(
1333 &mut self,
1334 (cx, cy): (f64, f64),
1335 radius: f64,
1336 style: &PointStyle,
1337 ) -> Result<(), RenderError> {
1338 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
1339 return Ok(());
1340 }
1341 let attrs = format!(
1342 "cx=\"{cx:.2}\" cy=\"{cy:.2}\" r=\"{:.2}\" fill=\"{}\"{}",
1343 radius.max(0.0),
1344 rgb(style.color),
1345 opacity_attr("fill", style.alpha)
1346 );
1347 self.push_mark("circle", "point", &attrs);
1348 Ok(())
1349 }
1350
1351 fn draw_shape(
1355 &mut self,
1356 (cx, cy): (f64, f64),
1357 radius: f64,
1358 style: &PointStyle,
1359 ) -> Result<(), RenderError> {
1360 if !(cx.is_finite() && cy.is_finite() && radius.is_finite()) {
1361 return Ok(());
1362 }
1363 let r = radius.max(0.0);
1364 let color = rgb(style.color);
1365 let fill_opacity = opacity_attr("fill", style.alpha);
1366 let stroke_opacity = opacity_attr("stroke", style.alpha);
1367 let poly = |pts: &[(f64, f64)]| {
1368 if style.filled {
1369 format!("points=\"{}\" fill=\"{color}\"{fill_opacity}", points(pts))
1370 } else {
1371 format!(
1372 "points=\"{}\" fill=\"none\" stroke=\"{color}\"{stroke_opacity}",
1373 points(pts)
1374 )
1375 }
1376 };
1377 match style.shape {
1378 PointShape::Circle => return self.draw_circle((cx, cy), radius, style),
1379 PointShape::Square => {
1380 let a = poly(&[
1381 (cx - r, cy - r),
1382 (cx + r, cy - r),
1383 (cx + r, cy + r),
1384 (cx - r, cy + r),
1385 ]);
1386 self.push_mark("polygon", "point", &a);
1387 }
1388 PointShape::Triangle => {
1389 let a = poly(&[(cx, cy - r), (cx + r, cy + r), (cx - r, cy + r)]);
1390 self.push_mark("polygon", "point", &a);
1391 }
1392 PointShape::Diamond => {
1393 let a = poly(&[(cx, cy - r), (cx + r, cy), (cx, cy + r), (cx - r, cy)]);
1394 self.push_mark("polygon", "point", &a);
1395 }
1396 PointShape::Plus | PointShape::Cross => {
1397 let d = if style.shape == PointShape::Plus {
1398 format!(
1399 "M{:.2} {cy:.2}H{:.2}M{cx:.2} {:.2}V{:.2}",
1400 cx - r,
1401 cx + r,
1402 cy - r,
1403 cy + r
1404 )
1405 } else {
1406 format!(
1407 "M{:.2} {:.2}L{:.2} {:.2}M{:.2} {:.2}L{:.2} {:.2}",
1408 cx - r,
1409 cy - r,
1410 cx + r,
1411 cy + r,
1412 cx - r,
1413 cy + r,
1414 cx + r,
1415 cy - r
1416 )
1417 };
1418 let a = format!(
1419 "d=\"{d}\" fill=\"none\" stroke=\"{color}\" stroke-width=\"{:.2}\"{stroke_opacity}",
1420 (r / 2.5).clamp(1.0, 3.0)
1421 );
1422 self.push_mark("path", "point", &a);
1423 }
1424 }
1425 Ok(())
1426 }
1427
1428 fn draw_line(&mut self, pts: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
1429 for run in finite_runs(pts) {
1435 self.draw_line_run(&run, style);
1436 }
1437 Ok(())
1438 }
1439
1440 fn draw_quadratic(
1443 &mut self,
1444 p0: (f64, f64),
1445 ctrl: (f64, f64),
1446 p1: (f64, f64),
1447 style: &LineStyle,
1448 ) -> Result<(), RenderError> {
1449 if !(p0.0.is_finite()
1450 && p0.1.is_finite()
1451 && ctrl.0.is_finite()
1452 && ctrl.1.is_finite()
1453 && p1.0.is_finite()
1454 && p1.1.is_finite())
1455 {
1456 return Ok(());
1457 }
1458 let dash = dasharray_attr(&style.linetype);
1459 let linecap = match style.lineend {
1460 LineEnd::Butt => "",
1461 LineEnd::Round => " stroke-linecap=\"round\"",
1462 LineEnd::Square => " stroke-linecap=\"square\"",
1463 };
1464 let d = format!(
1465 "M{:.2} {:.2} Q{:.2} {:.2} {:.2} {:.2}",
1466 p0.0, p0.1, ctrl.0, ctrl.1, p1.0, p1.1
1467 );
1468 let attrs = format!(
1469 "d=\"{d}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\"{}{}{}",
1470 rgb(style.color),
1471 finite_or(style.width, 1.0),
1472 opacity_attr("stroke", style.alpha),
1473 dash,
1474 linecap
1475 );
1476 self.push_mark("path", "line", &attrs);
1477 Ok(())
1478 }
1479
1480 fn draw_rect(
1481 &mut self,
1482 top_left: (f64, f64),
1483 bottom_right: (f64, f64),
1484 style: &RectStyle,
1485 ) -> Result<(), RenderError> {
1486 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
1487 self.rect_markup(top_left, bottom_right, &fill, style)
1488 }
1489
1490 fn draw_rect_pattern(
1493 &mut self,
1494 top_left: (f64, f64),
1495 bottom_right: (f64, f64),
1496 pattern: &FillPattern,
1497 style: &RectStyle,
1498 ) -> Result<(), RenderError> {
1499 let fill = self.pattern_url(pattern);
1500 self.rect_markup(top_left, bottom_right, &fill, style)
1501 }
1502
1503 fn draw_polygon(&mut self, pts: &[(f64, f64)], style: &RectStyle) -> Result<(), RenderError> {
1504 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
1505 self.polygon_markup(pts, &fill, style)
1506 }
1507
1508 fn draw_image(
1511 &mut self,
1512 bytes: &[u8],
1513 rect: &Rect,
1514 clip: &Rect,
1515 alpha: f64,
1516 ) -> Result<(), RenderError> {
1517 let finite = [rect.x, rect.y, rect.width, rect.height, clip.x, clip.y]
1518 .iter()
1519 .chain([&clip.width, &clip.height])
1520 .all(|v| v.is_finite());
1521 if !finite || rect.width <= 0.0 || rect.height <= 0.0 {
1522 return Ok(());
1523 }
1524 let mime = match bytes {
1525 [0xFF, 0xD8, ..] => "image/jpeg",
1526 _ => "image/png",
1527 };
1528 self.emit(&format!(
1529 "<svg x=\"{:.2}\" y=\"{:.2}\" width=\"{:.2}\" height=\"{:.2}\" overflow=\"hidden\">",
1530 clip.x, clip.y, clip.width, clip.height
1531 ));
1532 let attrs = format!(
1533 "x=\"{:.2}\" y=\"{:.2}\" width=\"{:.2}\" height=\"{:.2}\" preserveAspectRatio=\"none\"{} href=\"data:{mime};base64,{}\"",
1534 rect.x - clip.x,
1535 rect.y - clip.y,
1536 rect.width,
1537 rect.height,
1538 if alpha < 1.0 {
1539 format!(" opacity=\"{:.3}\"", alpha.clamp(0.0, 1.0))
1540 } else {
1541 String::new()
1542 },
1543 base64(bytes)
1544 );
1545 self.push_mark("image", "image", &attrs);
1546 self.emit("</svg>");
1547 Ok(())
1548 }
1549
1550 fn draw_polygon_pattern(
1553 &mut self,
1554 pts: &[(f64, f64)],
1555 pattern: &FillPattern,
1556 style: &RectStyle,
1557 ) -> Result<(), RenderError> {
1558 let fill = self.pattern_url(pattern);
1559 self.polygon_markup(pts, &fill, style)
1560 }
1561
1562 fn draw_polygon_rings(
1568 &mut self,
1569 rings: &[Vec<(f64, f64)>],
1570 style: &RectStyle,
1571 ) -> Result<(), RenderError> {
1572 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
1573 self.polygon_rings_markup(rings, &fill, style)
1574 }
1575
1576 fn draw_polygon_rings_pattern(
1580 &mut self,
1581 rings: &[Vec<(f64, f64)>],
1582 pattern: &FillPattern,
1583 style: &RectStyle,
1584 ) -> Result<(), RenderError> {
1585 let fill = self.pattern_url(pattern);
1586 self.polygon_rings_markup(rings, &fill, style)
1587 }
1588
1589 fn draw_polygon_rings_precise(
1595 &mut self,
1596 rings: &[Vec<(f64, f64)>],
1597 pattern: Option<&FillPattern>,
1598 precision: Option<u8>,
1599 style: &RectStyle,
1600 ) -> Result<(), RenderError> {
1601 let Some(precision) = precision else {
1602 return match pattern {
1603 Some(p) => self.draw_polygon_rings_pattern(rings, p, style),
1604 None => self.draw_polygon_rings(rings, style),
1605 };
1606 };
1607 let d = compact_rings_d(rings, precision);
1608 if d.is_empty() {
1609 return Ok(());
1610 }
1611 let fill = match pattern {
1612 Some(p) => self.pattern_url(p),
1613 None => style.fill.map(rgb).unwrap_or_else(|| "none".into()),
1614 };
1615 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
1616 let attrs = format!(
1617 "d=\"{d}\" fill-rule=\"evenodd\" fill=\"{fill}\"{} stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
1618 opacity_attr("fill", style.alpha),
1619 finite_or(style.stroke_width, 0.0)
1620 );
1621 self.push_mark("path", "band", &attrs);
1622 Ok(())
1623 }
1624
1625 fn draw_text(
1626 &mut self,
1627 text: &str,
1628 pos: (f64, f64),
1629 style: &TextStyle,
1630 ) -> Result<(), RenderError> {
1631 self.draw_text_with_title(text, pos, style, None)
1632 }
1633
1634 fn draw_text_with_title(
1638 &mut self,
1639 text: &str,
1640 (x, y): (f64, f64),
1641 style: &TextStyle,
1642 title: Option<&str>,
1643 ) -> Result<(), RenderError> {
1644 if !(x.is_finite() && y.is_finite()) {
1645 return Ok(());
1646 }
1647 let anchor = match style.anchor {
1648 TextAnchor::Start => "start",
1649 TextAnchor::Middle => "middle",
1650 TextAnchor::End => "end",
1651 };
1652 let family = escape_attr(style.family.as_deref().unwrap_or("sans-serif"));
1653 let weight = if style.face == FontFace::Bold {
1654 " font-weight=\"bold\""
1655 } else {
1656 ""
1657 };
1658 let fstyle = if style.face == FontFace::Italic {
1659 " font-style=\"italic\""
1660 } else {
1661 ""
1662 };
1663 let halo = text_halo_attrs(style);
1664 let angle = finite_or(style.angle, 0.0);
1665 let transform = if angle.abs() > 0.01 {
1666 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
1667 } else {
1668 String::new()
1669 };
1670 let class = format!(
1671 "{}{}{}",
1672 self.text_data_attrs(),
1673 self.layer_flag_attrs(false),
1674 self.mark_class("text")
1675 );
1676 let title_tag = match title {
1677 Some(t) => format!("<title>{}</title>", escape(t)),
1678 None => String::new(),
1679 };
1680 let markup = format!(
1681 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
1682 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle}{halo} fill=\"{}\"{transform}{class}>{title_tag}{}</text>",
1683 finite_or(style.size, 12.0),
1684 rgb(style.color),
1685 escape(text)
1686 );
1687 self.emit(&markup);
1688 Ok(())
1689 }
1690
1691 fn draw_rich_text(
1700 &mut self,
1701 runs: &[crate::render::rich::RichRun],
1702 (x, y): (f64, f64),
1703 style: &TextStyle,
1704 ) -> Result<(), RenderError> {
1705 if !(x.is_finite() && y.is_finite()) {
1706 return Ok(());
1707 }
1708 let anchor = match style.anchor {
1709 TextAnchor::Start => "start",
1710 TextAnchor::Middle => "middle",
1711 TextAnchor::End => "end",
1712 };
1713 let family = escape_attr(style.family.as_deref().unwrap_or("sans-serif"));
1714 let weight = if style.face == FontFace::Bold {
1715 " font-weight=\"bold\""
1716 } else {
1717 ""
1718 };
1719 let fstyle = if style.face == FontFace::Italic {
1720 " font-style=\"italic\""
1721 } else {
1722 ""
1723 };
1724 let halo = text_halo_attrs(style);
1725 let angle = finite_or(style.angle, 0.0);
1726 let transform = if angle.abs() > 0.01 {
1727 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
1728 } else {
1729 String::new()
1730 };
1731 let class = format!(
1732 "{}{}{}",
1733 self.text_data_attrs(),
1734 self.layer_flag_attrs(false),
1735 self.mark_class("text")
1736 );
1737 let size = finite_or(style.size, 12.0);
1738 let mut inner = String::new();
1739 for run in runs {
1740 let mut attrs = String::new();
1741 if run.bold && style.face != FontFace::Bold {
1742 attrs.push_str(" font-weight=\"bold\"");
1743 }
1744 if run.italic && style.face != FontFace::Italic {
1745 attrs.push_str(" font-style=\"italic\"");
1746 }
1747 if let Some(c) = run.color {
1748 attrs.push_str(&format!(" fill=\"{}\"", rgb(c)));
1749 }
1750 if run.shift != 0 {
1751 attrs.push_str(&format!(
1752 " baseline-shift=\"{}\" font-size=\"{:.2}\"",
1753 if run.shift > 0 { "super" } else { "sub" },
1754 crate::render::rich::run_size(size, run.shift)
1755 ));
1756 }
1757 inner.push_str(&format!("<tspan{attrs}>{}</tspan>", escape(&run.text)));
1758 }
1759 let markup = format!(
1760 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{size:.2}\" text-anchor=\"{anchor}\" \
1761 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle}{halo} fill=\"{}\"{transform}{class} xml:space=\"preserve\">{inner}</text>",
1762 rgb(style.color),
1763 );
1764 self.emit(&markup);
1765 Ok(())
1766 }
1767
1768 fn draw_text_runs(
1769 &mut self,
1770 runs: &[(&str, Option<&str>)],
1771 (x, y): (f64, f64),
1772 style: &TextStyle,
1773 ) -> Result<(), RenderError> {
1774 if !(x.is_finite() && y.is_finite()) {
1775 return Ok(());
1776 }
1777 let anchor = match style.anchor {
1778 TextAnchor::Start => "start",
1779 TextAnchor::Middle => "middle",
1780 TextAnchor::End => "end",
1781 };
1782 let family = escape_attr(style.family.as_deref().unwrap_or("sans-serif"));
1783 let weight = if style.face == FontFace::Bold {
1784 " font-weight=\"bold\""
1785 } else {
1786 ""
1787 };
1788 let fstyle = if style.face == FontFace::Italic {
1789 " font-style=\"italic\""
1790 } else {
1791 ""
1792 };
1793 let halo = text_halo_attrs(style);
1794 let angle = finite_or(style.angle, 0.0);
1795 let transform = if angle.abs() > 0.01 {
1796 format!(" transform=\"rotate({angle:.1} {x:.2} {y:.2})\"")
1797 } else {
1798 String::new()
1799 };
1800 let class = format!(
1801 "{}{}{}",
1802 self.text_data_attrs(),
1803 self.layer_flag_attrs(false),
1804 self.mark_class("text")
1805 );
1806 let mut inner = String::new();
1807 for (text, href) in runs {
1808 match href {
1809 Some(u) => inner.push_str(&format!(
1810 "<a href=\"{}\" target=\"_blank\" rel=\"noopener\"><tspan text-decoration=\"underline\">{}</tspan></a>",
1811 escape_attr(u),
1812 escape(text)
1813 )),
1814 None => inner.push_str(&format!("<tspan>{}</tspan>", escape(text))),
1815 }
1816 }
1817 let markup = format!(
1818 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
1819 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle}{halo} fill=\"{}\"{transform}{class}>{inner}</text>",
1820 finite_or(style.size, 12.0),
1821 rgb(style.color),
1822 );
1823 self.emit(&markup);
1824 Ok(())
1825 }
1826}
1827
1828fn text_halo_attrs(style: &TextStyle) -> String {
1838 let Some(bg) = style.bg_colour else {
1839 return String::new();
1840 };
1841 let r = finite_or(style.bg_r, 0.0);
1842 if r <= 0.0 {
1843 return String::new();
1844 }
1845 format!(
1846 " stroke=\"{}\" stroke-width=\"{:.2}\" stroke-linejoin=\"round\" paint-order=\"stroke\"",
1847 rgb(bg),
1848 2.0 * r
1849 )
1850}
1851
1852#[cfg(test)]
1853mod tests {
1854 use super::*;
1855 use crate::render::Rect;
1856
1857 #[test]
1860 fn issue221_backend_rechecks_class_group_and_layer_attrs() {
1861 let mut b = backend();
1862 b.set_mark_geom(Some("point"));
1863 b.set_mark_class(Some("ok \"bad 1x ok".into()));
1864 b.set_layer_attrs(vec![
1865 ("aria-hidden".into(), "maybe".into()),
1866 ("pointer-events".into(), "none\" onload=\"x".into()),
1867 ("style".into(), "fill:red".into()),
1868 ("class".into(), "lay".into()),
1869 ]);
1870 b.set_mark_group(Some(("onclick".into(), "x".into())));
1871 b.draw_circle((1.0, 1.0), 1.0, &PointStyle::default())
1872 .unwrap();
1873 b.set_mark_group(Some(("data-of".into(), "n".into())));
1874 let runs = [("a", None), ("b", Some("https://e.x"))];
1875 b.draw_text_runs(&runs, (5.0, 5.0), &TextStyle::default())
1876 .unwrap();
1877 b.set_mark_group(None);
1878 b.set_mark_geom(None);
1879 let body = b.body().to_string();
1880 assert!(
1881 body.contains(r#"class="gg-mark gg-point lay ok""#),
1882 "{body}"
1883 );
1884 assert!(!body.contains("bad") && !body.contains("1x"), "{body}");
1885 assert!(
1886 !body.contains("aria-hidden") && !body.contains("onload"),
1887 "{body}"
1888 );
1889 assert!(
1890 !body.contains("style=") && !body.contains("onclick"),
1891 "{body}"
1892 );
1893 assert!(body.contains(r#"<g data-of="n"><text"#), "{body}");
1894 assert!(body.contains("gg-text lay ok"), "{body}");
1895 assert!(b.finish().contains(r#"<g data-of="n">"#));
1897 }
1898
1899 fn backend() -> SvgBackend {
1900 SvgBackend::new(
1901 100,
1902 100,
1903 Rect {
1904 x: 0.0,
1905 y: 0.0,
1906 width: 100.0,
1907 height: 100.0,
1908 },
1909 )
1910 }
1911
1912 #[test]
1913 fn draw_quadratic_emits_a_path_with_m_and_q() {
1914 let mut b = backend();
1915 b.draw_quadratic((0.0, 0.0), (10.0, 20.0), (20.0, 0.0), &LineStyle::default())
1916 .unwrap();
1917 let svg = b.finish();
1918 assert!(svg.contains("<path"));
1919 assert!(svg.contains("d=\"M0.00 0.00 Q10.00 20.00 20.00 0.00\""));
1920 assert!(!svg.contains("NaN"));
1921 assert!(!svg.contains("inf"));
1922 }
1923
1924 #[test]
1925 fn draw_quadratic_drops_non_finite_input() {
1926 let mut b = backend();
1927 b.draw_quadratic(
1928 (f64::NAN, 0.0),
1929 (10.0, 20.0),
1930 (20.0, 0.0),
1931 &LineStyle::default(),
1932 )
1933 .unwrap();
1934 b.draw_quadratic(
1935 (0.0, 0.0),
1936 (f64::INFINITY, 20.0),
1937 (20.0, 0.0),
1938 &LineStyle::default(),
1939 )
1940 .unwrap();
1941 let svg = b.finish();
1942 assert!(!svg.contains("<path"));
1943 }
1944
1945 #[test]
1946 fn draw_line_omits_linecap_by_default_but_writes_it_when_set() {
1947 let mut b = backend();
1948 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &LineStyle::default())
1949 .unwrap();
1950 let svg = b.finish();
1951 assert!(!svg.contains("stroke-linecap"));
1952
1953 let mut b2 = backend();
1954 let style = LineStyle {
1955 lineend: LineEnd::Round,
1956 ..Default::default()
1957 };
1958 b2.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
1959 let svg2 = b2.finish();
1960 assert!(svg2.contains("stroke-linecap=\"round\""));
1961 }
1962
1963 #[test]
1964 fn square_linecap_is_written_on_both_draw_line_and_draw_quadratic() {
1965 let style = LineStyle {
1966 lineend: LineEnd::Square,
1967 ..Default::default()
1968 };
1969 let mut b = backend();
1970 b.draw_line(&[(0.0, 0.0), (10.0, 10.0)], &style).unwrap();
1971 assert!(b.finish().contains("stroke-linecap=\"square\""));
1972
1973 let mut b2 = backend();
1974 b2.draw_quadratic((0.0, 0.0), (5.0, 10.0), (10.0, 0.0), &style)
1975 .unwrap();
1976 assert!(b2.finish().contains("stroke-linecap=\"square\""));
1977 }
1978
1979 #[test]
1984 fn set_mark_attrs_defence_in_depth_drops_invalid_names() {
1985 let mut b = backend();
1986 b.set_mark_attrs(vec![
1987 ("not-data-prefixed".to_string(), "x".to_string()),
1988 ("data-".to_string(), "x".to_string()), ("data-Upper".to_string(), "x".to_string()), ("data-ok".to_string(), "valid".to_string()), ]);
1992 b.draw_circle((1.0, 1.0), 1.0, &PointStyle::default())
1993 .unwrap();
1994 let svg = b.finish();
1995 assert!(!svg.contains("not-data-prefixed"));
1996 assert!(!svg.contains("data-Upper"));
1997 assert!(!svg.contains("data-=\""));
1998 assert!(svg.contains("data-ok=\"valid\""));
1999 }
2000
2001 fn stripe() -> FillPattern {
2004 FillPattern::Stripe {
2005 fg: (10, 20, 30),
2006 bg: (200, 210, 220),
2007 size_px: 4.0,
2008 angle_deg: 45.0,
2009 }
2010 }
2011
2012 fn pattern_id(svg: &str) -> String {
2013 let i = svg.find("<pattern id=\"").unwrap() + "<pattern id=\"".len();
2014 svg[i..i + svg[i..].find('"').unwrap()].to_string()
2015 }
2016
2017 #[test]
2018 fn a_pattern_fill_registers_one_defs_pattern_and_references_it() {
2019 let mut b = backend();
2020 b.draw_rect_pattern((0.0, 0.0), (10.0, 10.0), &stripe(), &RectStyle::default())
2021 .unwrap();
2022 let svg = b.finish();
2023 assert_eq!(svg.matches("<pattern ").count(), 1, "{svg}");
2024 let id = pattern_id(&svg);
2025 assert!(
2026 svg.contains(&format!("fill=\"url(#{id})\"")),
2027 "rect references the registered pattern id {id}: {svg}"
2028 );
2029 }
2030
2031 #[test]
2032 fn a_rounded_rect_pattern_still_rounds_its_corners() {
2033 let mut b = backend();
2034 b.draw_rect_pattern(
2035 (0.0, 0.0),
2036 (10.0, 10.0),
2037 &stripe(),
2038 &RectStyle {
2039 corner_radius: 3.0,
2040 ..Default::default()
2041 },
2042 )
2043 .unwrap();
2044 let svg = b.finish();
2045 assert!(svg.contains("rx=\"3.00\""), "{svg}");
2046 assert!(svg.contains("fill=\"url(#"), "{svg}");
2047 }
2048
2049 #[test]
2050 fn an_equal_pattern_reuses_the_same_defs_entry() {
2051 let mut b = backend();
2052 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
2053 .unwrap();
2054 b.draw_polygon_pattern(
2055 &[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)],
2056 &stripe(),
2057 &RectStyle::default(),
2058 )
2059 .unwrap();
2060 let svg = b.finish();
2061 assert_eq!(
2062 svg.matches("<pattern ").count(),
2063 1,
2064 "two equal patterns share one <defs> entry: {svg}"
2065 );
2066 }
2067
2068 #[test]
2069 fn different_patterns_get_their_own_defs_entries() {
2070 let mut b = backend();
2071 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
2072 .unwrap();
2073 let other = FillPattern::Stripe {
2074 fg: (1, 1, 1),
2075 bg: (2, 2, 2),
2076 size_px: 8.0,
2077 angle_deg: 0.0,
2078 };
2079 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &other, &RectStyle::default())
2080 .unwrap();
2081 let svg = b.finish();
2082 assert_eq!(svg.matches("<pattern ").count(), 2, "{svg}");
2083 }
2084
2085 #[test]
2086 fn no_pattern_used_means_no_defs_block_at_all() {
2087 let mut b = backend();
2088 b.draw_rect((0.0, 0.0), (5.0, 5.0), &RectStyle::default())
2089 .unwrap();
2090 let svg = b.finish();
2091 assert!(!svg.contains("<defs>"), "{svg}");
2092 }
2093
2094 #[test]
2095 fn pattern_fill_cuts_out_holes_like_a_flat_fill_does() {
2096 let mut b = backend();
2097 let outer = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
2098 let hole = vec![(2.0, 2.0), (8.0, 2.0), (8.0, 8.0), (2.0, 8.0)];
2099 b.draw_polygon_rings_pattern(&[outer, hole], &stripe(), &RectStyle::default())
2100 .unwrap();
2101 let svg = b.finish();
2102 assert!(svg.contains("fill-rule=\"evenodd\""), "{svg}");
2103 assert_eq!(
2104 svg.matches("<path").count(),
2105 1,
2106 "one path, both rings: {svg}"
2107 );
2108 }
2109
2110 #[test]
2111 fn two_backends_on_one_page_never_collide_on_a_pattern_id() {
2112 let mut a = backend();
2113 a.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
2114 .unwrap();
2115 let mut b = backend();
2116 b.draw_rect_pattern((0.0, 0.0), (5.0, 5.0), &stripe(), &RectStyle::default())
2117 .unwrap();
2118 let (sa, sb) = (a.finish(), b.finish());
2119 assert_ne!(pattern_id(&sa), pattern_id(&sb), "{sa}\n{sb}");
2120 }
2121
2122 fn decode_compact_d(d: &str) -> Vec<Vec<(f64, f64)>> {
2129 let b = d.as_bytes();
2130 let mut i = 0;
2131 let read_num = |b: &[u8], i: &mut usize| -> f64 {
2132 let start = *i;
2133 if *i < b.len() && b[*i] == b'-' {
2134 *i += 1;
2135 }
2136 while *i < b.len() && b[*i].is_ascii_digit() {
2137 *i += 1;
2138 }
2139 if *i < b.len() && b[*i] == b'.' {
2140 *i += 1;
2141 while *i < b.len() && b[*i].is_ascii_digit() {
2142 *i += 1;
2143 }
2144 }
2145 std::str::from_utf8(&b[start..*i]).unwrap().parse().unwrap()
2146 };
2147 let is_num_start = |c: u8| c.is_ascii_digit() || c == b'-' || c == b'.';
2148 let mut rings = Vec::new();
2149 let mut current = Vec::new();
2150 let mut cursor = (0.0, 0.0);
2151 while i < b.len() {
2152 match b[i] {
2153 b'M' => {
2154 if !current.is_empty() {
2155 rings.push(std::mem::take(&mut current));
2156 }
2157 i += 1;
2158 let x = read_num(b, &mut i);
2159 if i < b.len() && b[i] == b',' {
2160 i += 1;
2161 }
2162 let y = read_num(b, &mut i);
2163 cursor = (x, y);
2164 current.push(cursor);
2165 }
2166 b'l' => {
2167 i += 1;
2168 while i < b.len() && is_num_start(b[i]) {
2169 let dx = read_num(b, &mut i);
2170 if i < b.len() && b[i] == b',' {
2171 i += 1;
2172 }
2173 let dy = read_num(b, &mut i);
2174 if i < b.len() && b[i] == b',' {
2175 i += 1;
2176 }
2177 cursor = (cursor.0 + dx, cursor.1 + dy);
2178 current.push(cursor);
2179 }
2180 }
2181 b'z' => {
2182 i += 1;
2183 if !current.is_empty() {
2184 rings.push(std::mem::take(&mut current));
2185 }
2186 }
2187 _ => i += 1,
2188 }
2189 }
2190 if !current.is_empty() {
2191 rings.push(current);
2192 }
2193 rings
2194 }
2195
2196 fn noisy_ring(n: usize, cx: f64, cy: f64, r: f64) -> Vec<(f64, f64)> {
2199 (0..n)
2200 .map(|i| {
2201 let t = i as f64 / n as f64 * std::f64::consts::TAU;
2202 let wobble = 1.0 + 0.03 * ((i * 7) as f64).sin();
2203 (cx + r * wobble * t.cos(), cy + r * wobble * t.sin())
2204 })
2205 .collect()
2206 }
2207
2208 #[test]
2209 fn compact_encoding_is_well_under_the_absolute_encodings_size() {
2210 let ring = noisy_ring(2000, 400.0, 300.0, 280.0);
2214 let mut b = backend();
2215 b.draw_polygon_rings(std::slice::from_ref(&ring), &RectStyle::default())
2216 .unwrap();
2217 let absolute_svg = b.finish();
2218 let absolute_d = absolute_svg
2219 .split("d=\"")
2220 .nth(1)
2221 .and_then(|s| s.split('"').next())
2222 .expect("absolute path d");
2223
2224 let compact_d = compact_rings_d(std::slice::from_ref(&ring), 1);
2225
2226 assert!(
2227 compact_d.len() * 3 < absolute_d.len() * 2,
2228 "compact {} bytes should be well under 2/3 of absolute {} bytes",
2229 compact_d.len(),
2230 absolute_d.len()
2231 );
2232 }
2233
2234 #[test]
2235 fn compact_encoding_round_trips_within_precision() {
2236 let ring = noisy_ring(40, 100.0, 80.0, 60.0);
2237 let precision: u8 = 1;
2238 let d = compact_rings_d(std::slice::from_ref(&ring), precision);
2239 let decoded = decode_compact_d(&d);
2240 assert_eq!(decoded.len(), 1, "{d}");
2241 assert_eq!(decoded[0].len(), ring.len(), "{d}");
2242 let step_tol = 0.5 * 10f64.powi(-(precision as i32));
2247 for (k, (orig, got)) in ring.iter().zip(decoded[0].iter()).enumerate() {
2248 let tol = step_tol * (k as f64 + 1.0) + 1e-9;
2249 assert!(
2250 (orig.0 - got.0).abs() <= tol && (orig.1 - got.1).abs() <= tol,
2251 "point {k}: {orig:?} vs {got:?} (tol {tol}): {d}"
2252 );
2253 }
2254 }
2255
2256 #[test]
2257 fn compact_encoding_round_trips_a_hole_and_a_multipolygons_parts() {
2258 let part1 = vec![(0.0, 0.0), (20.0, 0.0), (20.0, 20.0), (0.0, 20.0)];
2262 let part2_outer = vec![(50.0, 0.0), (90.0, 0.0), (90.0, 40.0), (50.0, 40.0)];
2263 let part2_hole = vec![(60.0, 10.0), (80.0, 10.0), (80.0, 30.0), (60.0, 30.0)];
2264 let rings = vec![part1.clone(), part2_outer.clone(), part2_hole.clone()];
2265 let d = compact_rings_d(&rings, 1);
2266 let decoded = decode_compact_d(&d);
2267 assert_eq!(decoded.len(), 3, "{d}");
2268 for (orig, got) in [part1, part2_outer, part2_hole].iter().zip(decoded.iter()) {
2269 assert_eq!(orig.len(), got.len(), "{d}");
2270 for (o, g) in orig.iter().zip(got.iter()) {
2271 assert!((o.0 - g.0).abs() <= 0.1 && (o.1 - g.1).abs() <= 0.1, "{d}");
2272 }
2273 }
2274 assert!(d.contains("M0,0l20,0,0,20-20,0z"), "{d}");
2275 }
2276}