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