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