1use super::backend::{
8 DrawBackend, FontFace, LineStyle, PointStyle, RectStyle, TextAnchor, TextStyle,
9};
10use super::{Rect, RenderError};
11
12pub struct SvgBackend {
14 plot_area: Rect,
15 total_area: Rect,
16 body: String,
17 tooltip: Option<String>,
18}
19
20impl SvgBackend {
21 pub fn new(width: u32, height: u32, plot_area: Rect) -> Self {
22 SvgBackend {
23 plot_area,
24 total_area: Rect {
25 x: 0.0,
26 y: 0.0,
27 width: width as f64,
28 height: height as f64,
29 },
30 body: String::new(),
31 tooltip: None,
32 }
33 }
34
35 fn push_mark(&mut self, tag: &str, attrs: &str) {
38 let mark = match &self.tooltip {
39 Some(t) => format!("<{tag} {attrs}><title>{}</title></{tag}>", escape(t)),
40 None => format!("<{tag} {attrs}/>"),
41 };
42 self.body.push_str(&mark);
43 }
44
45 pub fn finish(self) -> String {
47 format!(
48 "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"{}\" \
49 viewBox=\"0 0 {0} {1}\">{}</svg>",
50 self.total_area.width as i64, self.total_area.height as i64, self.body
51 )
52 }
53}
54
55fn rgb((r, g, b): (u8, u8, u8)) -> String {
56 format!("#{r:02X}{g:02X}{b:02X}")
57}
58
59fn escape(s: &str) -> String {
60 s.replace('&', "&")
61 .replace('<', "<")
62 .replace('>', ">")
63}
64
65fn points(pts: &[(f64, f64)]) -> String {
66 pts.iter()
67 .map(|(x, y)| format!("{x:.2},{y:.2}"))
68 .collect::<Vec<_>>()
69 .join(" ")
70}
71
72impl DrawBackend for SvgBackend {
73 fn plot_area(&self) -> Rect {
74 self.plot_area.clone()
75 }
76 fn total_area(&self) -> Rect {
77 self.total_area.clone()
78 }
79
80 fn set_tooltip(&mut self, tooltip: Option<String>) {
81 self.tooltip = tooltip;
82 }
83
84 fn draw_circle(
85 &mut self,
86 (cx, cy): (f64, f64),
87 radius: f64,
88 style: &PointStyle,
89 ) -> Result<(), RenderError> {
90 let attrs = format!(
91 "cx=\"{cx:.2}\" cy=\"{cy:.2}\" r=\"{radius:.2}\" fill=\"{}\" fill-opacity=\"{:.3}\"",
92 rgb(style.color),
93 style.alpha
94 );
95 self.push_mark("circle", &attrs);
96 Ok(())
97 }
98
99 fn draw_line(&mut self, pts: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
100 let dash = match style
101 .linetype
102 .pattern()
103 .iter()
104 .flat_map(|(d, g)| [*d, *g])
105 .map(|v| format!("{v}"))
106 .collect::<Vec<_>>()
107 .join(",")
108 {
109 s if s.is_empty() => String::new(),
110 s => format!(" stroke-dasharray=\"{s}\""),
111 };
112 let attrs = format!(
113 "points=\"{}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.2}\" stroke-opacity=\"{:.3}\"{}",
114 points(pts), rgb(style.color), style.width, style.alpha, dash
115 );
116 self.push_mark("polyline", &attrs);
117 Ok(())
118 }
119
120 fn draw_rect(
121 &mut self,
122 (x0, y0): (f64, f64),
123 (x1, y1): (f64, f64),
124 style: &RectStyle,
125 ) -> Result<(), RenderError> {
126 let (x, y) = (x0.min(x1), y0.min(y1));
127 let (w, h) = ((x1 - x0).abs(), (y1 - y0).abs());
128 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
129 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
130 let attrs = format!(
131 "x=\"{x:.2}\" y=\"{y:.2}\" width=\"{w:.2}\" height=\"{h:.2}\" fill=\"{fill}\" \
132 fill-opacity=\"{:.3}\" stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
133 style.alpha, style.stroke_width
134 );
135 self.push_mark("rect", &attrs);
136 Ok(())
137 }
138
139 fn draw_polygon(&mut self, pts: &[(f64, f64)], style: &RectStyle) -> Result<(), RenderError> {
140 let fill = style.fill.map(rgb).unwrap_or_else(|| "none".into());
141 let stroke = style.stroke.map(rgb).unwrap_or_else(|| "none".into());
142 let attrs = format!(
143 "points=\"{}\" fill=\"{fill}\" fill-opacity=\"{:.3}\" stroke=\"{stroke}\" stroke-width=\"{:.2}\"",
144 points(pts), style.alpha, style.stroke_width
145 );
146 self.push_mark("polygon", &attrs);
147 Ok(())
148 }
149
150 fn draw_text(
151 &mut self,
152 text: &str,
153 (x, y): (f64, f64),
154 style: &TextStyle,
155 ) -> Result<(), RenderError> {
156 let anchor = match style.anchor {
157 TextAnchor::Start => "start",
158 TextAnchor::Middle => "middle",
159 TextAnchor::End => "end",
160 };
161 let family = style.family.as_deref().unwrap_or("sans-serif");
162 let weight = if style.face == FontFace::Bold {
163 " font-weight=\"bold\""
164 } else {
165 ""
166 };
167 let fstyle = if style.face == FontFace::Italic {
168 " font-style=\"italic\""
169 } else {
170 ""
171 };
172 let transform = if style.angle.abs() > 0.01 {
173 format!(" transform=\"rotate({:.1} {x:.2} {y:.2})\"", style.angle)
174 } else {
175 String::new()
176 };
177 self.body.push_str(&format!(
178 "<text x=\"{x:.2}\" y=\"{y:.2}\" font-size=\"{:.2}\" text-anchor=\"{anchor}\" \
179 dominant-baseline=\"middle\" font-family=\"{family}\"{weight}{fstyle} fill=\"{}\"{transform}>{}</text>",
180 style.size, rgb(style.color), escape(text)
181 ));
182 Ok(())
183 }
184}