1use 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 #[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
74pub 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("<"),
86 '>' => buf.push_str(">"),
87 '&' => buf.push_str("&"),
88 '"' => buf.push_str("""),
89 '\'' => buf.push_str("'"),
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 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 #[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 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 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 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}