plotters_svg/
svg.rs

1/*!
2The SVG image drawing backend
3*/
4
5use plotters_backend::{
6    text_anchor::{HPos, VPos},
7    BackendColor, BackendCoord, BackendStyle, BackendTextStyle, DrawingBackend, DrawingErrorKind,
8    FontStyle, FontTransform,
9};
10
11use std::fmt::Write as _;
12use std::fs::File;
13#[allow(unused_imports)]
14use std::io::Cursor;
15use std::io::{BufWriter, Error, Write};
16use std::path::Path;
17
18fn make_svg_color(color: BackendColor) -> String {
19    let (r, g, b) = color.rgb;
20    format!("#{:02X}{:02X}{:02X}", r, g, b)
21}
22
23fn make_svg_opacity(color: BackendColor) -> String {
24    format!("{}", color.alpha)
25}
26
27enum Target<'a> {
28    File(String, &'a Path),
29    Buffer(&'a mut String),
30}
31
32impl Target<'_> {
33    fn get_mut(&mut self) -> &mut String {
34        match self {
35            Target::File(ref mut buf, _) => buf,
36            Target::Buffer(buf) => buf,
37        }
38    }
39}
40
41enum SVGTag {
42    Svg,
43    Circle,
44    Line,
45    Polygon,
46    Polyline,
47    Rectangle,
48    Text,
49    #[allow(dead_code)]
50    Image,
51}
52
53impl SVGTag {
54    fn to_tag_name(&self) -> &'static str {
55        match self {
56            SVGTag::Svg => "svg",
57            SVGTag::Circle => "circle",
58            SVGTag::Line => "line",
59            SVGTag::Polyline => "polyline",
60            SVGTag::Rectangle => "rect",
61            SVGTag::Text => "text",
62            SVGTag::Image => "image",
63            SVGTag::Polygon => "polygon",
64        }
65    }
66}
67
68/// The SVG image drawing backend
69pub struct SVGBackend<'a> {
70    target: Target<'a>,
71    size: (u32, u32),
72    tag_stack: Vec<SVGTag>,
73    saved: bool,
74}
75
76impl<'a> SVGBackend<'a> {
77    fn escape_and_push(buf: &mut String, value: &str) {
78        value.chars().for_each(|c| match c {
79            '<' => buf.push_str("&lt;"),
80            '>' => buf.push_str("&gt;"),
81            '&' => buf.push_str("&amp;"),
82            '"' => buf.push_str("&quot;"),
83            '\'' => buf.push_str("&apos;"),
84            other => buf.push(other),
85        });
86    }
87    fn open_tag(&mut self, tag: SVGTag, attr: &[(&str, &str)], close: bool) {
88        let buf = self.target.get_mut();
89        buf.push('<');
90        buf.push_str(tag.to_tag_name());
91        for (key, value) in attr {
92            buf.push(' ');
93            buf.push_str(key);
94            buf.push_str("=\"");
95            Self::escape_and_push(buf, value);
96            buf.push('\"');
97        }
98        if close {
99            buf.push_str("/>\n");
100        } else {
101            self.tag_stack.push(tag);
102            buf.push_str(">\n");
103        }
104    }
105
106    fn close_tag(&mut self) -> bool {
107        if let Some(tag) = self.tag_stack.pop() {
108            let buf = self.target.get_mut();
109            buf.push_str("</");
110            buf.push_str(tag.to_tag_name());
111            buf.push_str(">\n");
112            return true;
113        }
114        false
115    }
116
117    fn init_svg_file(&mut self, size: (u32, u32)) {
118        self.open_tag(
119            SVGTag::Svg,
120            &[
121                ("width", &format!("{}", size.0)),
122                ("height", &format!("{}", size.1)),
123                ("viewBox", &format!("0 0 {} {}", size.0, size.1)),
124                ("xmlns", "http://www.w3.org/2000/svg"),
125            ],
126            false,
127        );
128    }
129
130    /// Create a new SVG drawing backend
131    pub fn new<T: AsRef<Path> + ?Sized>(path: &'a T, size: (u32, u32)) -> Self {
132        let mut ret = Self {
133            target: Target::File(String::default(), path.as_ref()),
134            size,
135            tag_stack: vec![],
136            saved: false,
137        };
138
139        ret.init_svg_file(size);
140        ret
141    }
142
143    /// Create a new SVG drawing backend and store the document into a String buffer
144    pub fn with_string(buf: &'a mut String, size: (u32, u32)) -> Self {
145        let mut ret = Self {
146            target: Target::Buffer(buf),
147            size,
148            tag_stack: vec![],
149            saved: false,
150        };
151
152        ret.init_svg_file(size);
153
154        ret
155    }
156}
157
158impl<'a> DrawingBackend for SVGBackend<'a> {
159    type ErrorType = Error;
160
161    fn get_size(&self) -> (u32, u32) {
162        self.size
163    }
164
165    fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<Error>> {
166        Ok(())
167    }
168
169    fn present(&mut self) -> Result<(), DrawingErrorKind<Error>> {
170        if !self.saved {
171            while self.close_tag() {}
172            match self.target {
173                Target::File(ref buf, path) => {
174                    let outfile = File::create(path).map_err(DrawingErrorKind::DrawingError)?;
175                    let mut outfile = BufWriter::new(outfile);
176                    outfile
177                        .write_all(buf.as_ref())
178                        .map_err(DrawingErrorKind::DrawingError)?;
179                }
180                Target::Buffer(_) => {}
181            }
182            self.saved = true;
183        }
184        Ok(())
185    }
186
187    fn draw_pixel(
188        &mut self,
189        point: BackendCoord,
190        color: BackendColor,
191    ) -> Result<(), DrawingErrorKind<Error>> {
192        if color.alpha == 0.0 {
193            return Ok(());
194        }
195        self.open_tag(
196            SVGTag::Rectangle,
197            &[
198                ("x", &format!("{}", point.0)),
199                ("y", &format!("{}", point.1)),
200                ("width", "1"),
201                ("height", "1"),
202                ("stroke", "none"),
203                ("opacity", &make_svg_opacity(color)),
204                ("fill", &make_svg_color(color)),
205            ],
206            true,
207        );
208        Ok(())
209    }
210
211    fn draw_line<S: BackendStyle>(
212        &mut self,
213        from: BackendCoord,
214        to: BackendCoord,
215        style: &S,
216    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
217        if style.color().alpha == 0.0 {
218            return Ok(());
219        }
220        self.open_tag(
221            SVGTag::Line,
222            &[
223                ("opacity", &make_svg_opacity(style.color())),
224                ("stroke", &make_svg_color(style.color())),
225                ("stroke-width", &format!("{}", style.stroke_width())),
226                ("x1", &format!("{}", from.0)),
227                ("y1", &format!("{}", from.1)),
228                ("x2", &format!("{}", to.0)),
229                ("y2", &format!("{}", to.1)),
230            ],
231            true,
232        );
233        Ok(())
234    }
235
236    fn draw_rect<S: BackendStyle>(
237        &mut self,
238        upper_left: BackendCoord,
239        bottom_right: BackendCoord,
240        style: &S,
241        fill: bool,
242    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
243        if style.color().alpha == 0.0 {
244            return Ok(());
245        }
246
247        let (fill, stroke) = if !fill {
248            ("none".to_string(), make_svg_color(style.color()))
249        } else {
250            (make_svg_color(style.color()), "none".to_string())
251        };
252
253        self.open_tag(
254            SVGTag::Rectangle,
255            &[
256                ("x", &format!("{}", upper_left.0)),
257                ("y", &format!("{}", upper_left.1)),
258                ("width", &format!("{}", bottom_right.0 - upper_left.0)),
259                ("height", &format!("{}", bottom_right.1 - upper_left.1)),
260                ("opacity", &make_svg_opacity(style.color())),
261                ("fill", &fill),
262                ("stroke", &stroke),
263            ],
264            true,
265        );
266
267        Ok(())
268    }
269
270    fn draw_path<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
271        &mut self,
272        path: I,
273        style: &S,
274    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
275        if style.color().alpha == 0.0 {
276            return Ok(());
277        }
278        self.open_tag(
279            SVGTag::Polyline,
280            &[
281                ("fill", "none"),
282                ("opacity", &make_svg_opacity(style.color())),
283                ("stroke", &make_svg_color(style.color())),
284                ("stroke-width", &format!("{}", style.stroke_width())),
285                (
286                    "points",
287                    &path.into_iter().fold(String::new(), |mut s, (x, y)| {
288                        write!(s, "{},{} ", x, y).ok();
289                        s
290                    }),
291                ),
292            ],
293            true,
294        );
295        Ok(())
296    }
297
298    fn fill_polygon<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
299        &mut self,
300        path: I,
301        style: &S,
302    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
303        if style.color().alpha == 0.0 {
304            return Ok(());
305        }
306        self.open_tag(
307            SVGTag::Polygon,
308            &[
309                ("opacity", &make_svg_opacity(style.color())),
310                ("fill", &make_svg_color(style.color())),
311                (
312                    "points",
313                    &path.into_iter().fold(String::new(), |mut s, (x, y)| {
314                        write!(s, "{},{} ", x, y).ok();
315                        s
316                    }),
317                ),
318            ],
319            true,
320        );
321        Ok(())
322    }
323
324    fn draw_circle<S: BackendStyle>(
325        &mut self,
326        center: BackendCoord,
327        radius: u32,
328        style: &S,
329        fill: bool,
330    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
331        if style.color().alpha == 0.0 {
332            return Ok(());
333        }
334        let (stroke, fill) = if !fill {
335            (make_svg_color(style.color()), "none".to_string())
336        } else {
337            ("none".to_string(), make_svg_color(style.color()))
338        };
339        self.open_tag(
340            SVGTag::Circle,
341            &[
342                ("cx", &format!("{}", center.0)),
343                ("cy", &format!("{}", center.1)),
344                ("r", &format!("{}", radius)),
345                ("opacity", &make_svg_opacity(style.color())),
346                ("fill", &fill),
347                ("stroke", &stroke),
348                ("stroke-width", &format!("{}", style.stroke_width())),
349            ],
350            true,
351        );
352        Ok(())
353    }
354
355    fn draw_text<S: BackendTextStyle>(
356        &mut self,
357        text: &str,
358        style: &S,
359        pos: BackendCoord,
360    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
361        let color = style.color();
362        if color.alpha == 0.0 {
363            return Ok(());
364        }
365
366        let (x0, y0) = pos;
367        let text_anchor = match style.anchor().h_pos {
368            HPos::Left => "start",
369            HPos::Right => "end",
370            HPos::Center => "middle",
371        };
372
373        let dy = match style.anchor().v_pos {
374            VPos::Top => "0.76em",
375            VPos::Center => "0.5ex",
376            VPos::Bottom => "-0.5ex",
377        };
378
379        #[cfg(feature = "debug")]
380        {
381            let ((fx0, fy0), (fx1, fy1)) =
382                font.layout_box(text).map_err(DrawingErrorKind::FontError)?;
383            let x0 = match style.anchor().h_pos {
384                HPos::Left => x0,
385                HPos::Center => x0 - fx1 / 2 + fx0 / 2,
386                HPos::Right => x0 - fx1 + fx0,
387            };
388            let y0 = match style.anchor().v_pos {
389                VPos::Top => y0,
390                VPos::Center => y0 - fy1 / 2 + fy0 / 2,
391                VPos::Bottom => y0 - fy1 + fy0,
392            };
393            self.draw_rect(
394                (x0, y0),
395                (x0 + fx1 - fx0, y0 + fy1 - fy0),
396                &crate::prelude::RED,
397                false,
398            )
399            .unwrap();
400            self.draw_circle((x0, y0), 2, &crate::prelude::RED, false)
401                .unwrap();
402        }
403
404        let mut attrs = vec![
405            ("x", format!("{}", x0)),
406            ("y", format!("{}", y0)),
407            ("dy", dy.to_owned()),
408            ("text-anchor", text_anchor.to_string()),
409            ("font-family", style.family().as_str().to_string()),
410            ("font-size", format!("{}", style.size() / 1.24)),
411            ("opacity", make_svg_opacity(color)),
412            ("fill", make_svg_color(color)),
413        ];
414
415        match style.style() {
416            FontStyle::Normal => {}
417            FontStyle::Bold => attrs.push(("font-weight", "bold".to_string())),
418            other_style => attrs.push(("font-style", other_style.as_str().to_string())),
419        };
420
421        let trans = style.transform();
422        match trans {
423            FontTransform::Rotate90 => {
424                attrs.push(("transform", format!("rotate(90, {}, {})", x0, y0)))
425            }
426            FontTransform::Rotate180 => {
427                attrs.push(("transform", format!("rotate(180, {}, {})", x0, y0)));
428            }
429            FontTransform::Rotate270 => {
430                attrs.push(("transform", format!("rotate(270, {}, {})", x0, y0)));
431            }
432            _ => {}
433        }
434
435        self.open_tag(
436            SVGTag::Text,
437            attrs
438                .iter()
439                .map(|(a, b)| (*a, b.as_ref()))
440                .collect::<Vec<_>>()
441                .as_ref(),
442            false,
443        );
444
445        Self::escape_and_push(self.target.get_mut(), text);
446        self.target.get_mut().push('\n');
447
448        self.close_tag();
449
450        Ok(())
451    }
452
453    #[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
454    fn blit_bitmap(
455        &mut self,
456        pos: BackendCoord,
457        (w, h): (u32, u32),
458        src: &[u8],
459    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
460        use image::codecs::png::PngEncoder;
461        use image::ImageEncoder;
462
463        let mut data = vec![0; 0];
464
465        {
466            let cursor = Cursor::new(&mut data);
467
468            let encoder = PngEncoder::new(cursor);
469
470            let color = image::ColorType::Rgb8;
471
472            encoder.write_image(src, w, h, color).map_err(|e| {
473                DrawingErrorKind::DrawingError(Error::new(
474                    std::io::ErrorKind::Other,
475                    format!("Image error: {}", e),
476                ))
477            })?;
478        }
479
480        let padding = (3 - data.len() % 3) % 3;
481        data.resize(data.len() + padding, 0);
482
483        let mut rem_bits = 0;
484        let mut rem_num = 0;
485
486        fn cvt_base64(from: u8) -> char {
487            (if from < 26 {
488                b'A' + from
489            } else if from < 52 {
490                b'a' + from - 26
491            } else if from < 62 {
492                b'0' + from - 52
493            } else if from == 62 {
494                b'+'
495            } else {
496                b'/'
497            })
498            .into()
499        }
500
501        let mut buf = String::new();
502        buf.push_str("data:png;base64,");
503
504        for byte in data {
505            let value = (rem_bits << (6 - rem_num)) | (byte >> (rem_num + 2));
506            rem_bits = byte & ((1 << (2 + rem_num)) - 1);
507            rem_num += 2;
508
509            buf.push(cvt_base64(value));
510            if rem_num == 6 {
511                buf.push(cvt_base64(rem_bits));
512                rem_bits = 0;
513                rem_num = 0;
514            }
515        }
516
517        for _ in 0..padding {
518            buf.pop();
519            buf.push('=');
520        }
521
522        self.open_tag(
523            SVGTag::Image,
524            &[
525                ("x", &format!("{}", pos.0)),
526                ("y", &format!("{}", pos.1)),
527                ("width", &format!("{}", w)),
528                ("height", &format!("{}", h)),
529                ("href", buf.as_str()),
530            ],
531            true,
532        );
533
534        Ok(())
535    }
536}
537
538impl Drop for SVGBackend<'_> {
539    fn drop(&mut self) {
540        if !self.saved {
541            // drop should not panic, so we ignore a failed present
542            let _ = self.present();
543        }
544    }
545}
546
547#[cfg(test)]
548mod test {
549    use super::*;
550    use plotters::element::Circle;
551    use plotters::prelude::{
552        ChartBuilder, Color, IntoDrawingArea, IntoFont, SeriesLabelPosition, TextStyle, BLACK,
553        BLUE, RED, WHITE,
554    };
555    use plotters::style::text_anchor::{HPos, Pos, VPos};
556    use std::fs;
557    use std::path::Path;
558
559    static DST_DIR: &str = "target/test/svg";
560
561    fn checked_save_file(name: &str, content: &str) {
562        /*
563          Please use the SVG file to manually verify the results.
564        */
565        assert!(!content.is_empty());
566        fs::create_dir_all(DST_DIR).unwrap();
567        let file_name = format!("{}.svg", name);
568        let file_path = Path::new(DST_DIR).join(file_name);
569        println!("{:?} created", file_path);
570        fs::write(file_path, &content).unwrap();
571    }
572
573    fn draw_mesh_with_custom_ticks(tick_size: i32, test_name: &str) {
574        let mut content: String = Default::default();
575        {
576            let root = SVGBackend::with_string(&mut content, (500, 500)).into_drawing_area();
577
578            let mut chart = ChartBuilder::on(&root)
579                .caption("This is a test", ("sans-serif", 20u32))
580                .set_all_label_area_size(40u32)
581                .build_cartesian_2d(0..10, 0..10)
582                .unwrap();
583
584            chart
585                .configure_mesh()
586                .set_all_tick_mark_size(tick_size)
587                .draw()
588                .unwrap();
589        }
590
591        checked_save_file(test_name, &content);
592
593        assert!(content.contains("This is a test"));
594    }
595
596    #[test]
597    fn test_draw_mesh_no_ticks() {
598        draw_mesh_with_custom_ticks(0, "test_draw_mesh_no_ticks");
599    }
600
601    #[test]
602    fn test_draw_mesh_negative_ticks() {
603        draw_mesh_with_custom_ticks(-10, "test_draw_mesh_negative_ticks");
604    }
605
606    #[test]
607    fn test_text_alignments() {
608        let mut content: String = Default::default();
609        {
610            let mut root = SVGBackend::with_string(&mut content, (500, 500));
611
612            let style = TextStyle::from(("sans-serif", 20).into_font())
613                .pos(Pos::new(HPos::Right, VPos::Top));
614            root.draw_text("right-align", &style, (150, 50)).unwrap();
615
616            let style = style.pos(Pos::new(HPos::Center, VPos::Top));
617            root.draw_text("center-align", &style, (150, 150)).unwrap();
618
619            let style = style.pos(Pos::new(HPos::Left, VPos::Top));
620            root.draw_text("left-align", &style, (150, 200)).unwrap();
621        }
622
623        checked_save_file("test_text_alignments", &content);
624
625        for svg_line in content.split("</text>") {
626            if let Some(anchor_and_rest) = svg_line.split("text-anchor=\"").nth(1) {
627                if anchor_and_rest.starts_with("end") {
628                    assert!(anchor_and_rest.contains("right-align"))
629                }
630                if anchor_and_rest.starts_with("middle") {
631                    assert!(anchor_and_rest.contains("center-align"))
632                }
633                if anchor_and_rest.starts_with("start") {
634                    assert!(anchor_and_rest.contains("left-align"))
635                }
636            }
637        }
638    }
639
640    #[test]
641    fn test_text_draw() {
642        let mut content: String = Default::default();
643        {
644            let root = SVGBackend::with_string(&mut content, (1500, 800)).into_drawing_area();
645            let root = root
646                .titled("Image Title", ("sans-serif", 60).into_font())
647                .unwrap();
648
649            let mut chart = ChartBuilder::on(&root)
650                .caption("All anchor point positions", ("sans-serif", 20u32))
651                .set_all_label_area_size(40u32)
652                .build_cartesian_2d(0..100i32, 0..50i32)
653                .unwrap();
654
655            chart
656                .configure_mesh()
657                .disable_x_mesh()
658                .disable_y_mesh()
659                .x_desc("X Axis")
660                .y_desc("Y Axis")
661                .draw()
662                .unwrap();
663
664            let ((x1, y1), (x2, y2), (x3, y3)) = ((-30, 30), (0, -30), (30, 30));
665
666            for (dy, trans) in [
667                FontTransform::None,
668                FontTransform::Rotate90,
669                FontTransform::Rotate180,
670                FontTransform::Rotate270,
671            ]
672            .iter()
673            .enumerate()
674            {
675                for (dx1, h_pos) in [HPos::Left, HPos::Right, HPos::Center].iter().enumerate() {
676                    for (dx2, v_pos) in [VPos::Top, VPos::Center, VPos::Bottom].iter().enumerate() {
677                        let x = 150_i32 + (dx1 as i32 * 3 + dx2 as i32) * 150;
678                        let y = 120 + dy as i32 * 150;
679                        let draw = |x, y, text| {
680                            root.draw(&Circle::new((x, y), 3, &BLACK.mix(0.5))).unwrap();
681                            let style = TextStyle::from(("sans-serif", 20).into_font())
682                                .pos(Pos::new(*h_pos, *v_pos))
683                                .transform(trans.clone());
684                            root.draw_text(text, &style, (x, y)).unwrap();
685                        };
686                        draw(x + x1, y + y1, "dood");
687                        draw(x + x2, y + y2, "dog");
688                        draw(x + x3, y + y3, "goog");
689                    }
690                }
691            }
692        }
693
694        checked_save_file("test_text_draw", &content);
695
696        assert_eq!(content.matches("dog").count(), 36);
697        assert_eq!(content.matches("dood").count(), 36);
698        assert_eq!(content.matches("goog").count(), 36);
699    }
700
701    #[test]
702    fn test_text_clipping() {
703        let mut content: String = Default::default();
704        {
705            let (width, height) = (500_i32, 500_i32);
706            let root = SVGBackend::with_string(&mut content, (width as u32, height as u32))
707                .into_drawing_area();
708
709            let style = TextStyle::from(("sans-serif", 20).into_font())
710                .pos(Pos::new(HPos::Center, VPos::Center));
711            root.draw_text("TOP LEFT", &style, (0, 0)).unwrap();
712            root.draw_text("TOP CENTER", &style, (width / 2, 0))
713                .unwrap();
714            root.draw_text("TOP RIGHT", &style, (width, 0)).unwrap();
715
716            root.draw_text("MIDDLE LEFT", &style, (0, height / 2))
717                .unwrap();
718            root.draw_text("MIDDLE RIGHT", &style, (width, height / 2))
719                .unwrap();
720
721            root.draw_text("BOTTOM LEFT", &style, (0, height)).unwrap();
722            root.draw_text("BOTTOM CENTER", &style, (width / 2, height))
723                .unwrap();
724            root.draw_text("BOTTOM RIGHT", &style, (width, height))
725                .unwrap();
726        }
727
728        checked_save_file("test_text_clipping", &content);
729    }
730
731    #[test]
732    fn test_series_labels() {
733        let mut content = String::default();
734        {
735            let (width, height) = (500, 500);
736            let root = SVGBackend::with_string(&mut content, (width, height)).into_drawing_area();
737
738            let mut chart = ChartBuilder::on(&root)
739                .caption("All series label positions", ("sans-serif", 20u32))
740                .set_all_label_area_size(40u32)
741                .build_cartesian_2d(0..50i32, 0..50i32)
742                .unwrap();
743
744            chart
745                .configure_mesh()
746                .disable_x_mesh()
747                .disable_y_mesh()
748                .draw()
749                .unwrap();
750
751            chart
752                .draw_series(std::iter::once(Circle::new((5, 15), 5u32, &RED)))
753                .expect("Drawing error")
754                .label("Series 1")
755                .legend(|(x, y)| Circle::new((x, y), 3u32, RED.filled()));
756
757            chart
758                .draw_series(std::iter::once(Circle::new((5, 15), 10u32, &BLUE)))
759                .expect("Drawing error")
760                .label("Series 2")
761                .legend(|(x, y)| Circle::new((x, y), 3u32, BLUE.filled()));
762
763            for pos in vec![
764                SeriesLabelPosition::UpperLeft,
765                SeriesLabelPosition::MiddleLeft,
766                SeriesLabelPosition::LowerLeft,
767                SeriesLabelPosition::UpperMiddle,
768                SeriesLabelPosition::MiddleMiddle,
769                SeriesLabelPosition::LowerMiddle,
770                SeriesLabelPosition::UpperRight,
771                SeriesLabelPosition::MiddleRight,
772                SeriesLabelPosition::LowerRight,
773                SeriesLabelPosition::Coordinate(70, 70),
774            ]
775            .into_iter()
776            {
777                chart
778                    .configure_series_labels()
779                    .border_style(&BLACK.mix(0.5))
780                    .position(pos)
781                    .draw()
782                    .expect("Drawing error");
783            }
784        }
785
786        checked_save_file("test_series_labels", &content);
787    }
788
789    #[test]
790    fn test_draw_pixel_alphas() {
791        let mut content = String::default();
792        {
793            let (width, height) = (100_i32, 100_i32);
794            let root = SVGBackend::with_string(&mut content, (width as u32, height as u32))
795                .into_drawing_area();
796            root.fill(&WHITE).unwrap();
797
798            for i in -20..20 {
799                let alpha = i as f64 * 0.1;
800                root.draw_pixel((50 + i, 50 + i), &BLACK.mix(alpha))
801                    .unwrap();
802            }
803        }
804
805        checked_save_file("test_draw_pixel_alphas", &content);
806    }
807}