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