| /*! |
| The SVG image drawing backend |
| */ |
| |
| use plotters_backend::{ |
| text_anchor::{HPos, VPos}, |
| BackendColor, BackendCoord, BackendStyle, BackendTextStyle, DrawingBackend, DrawingErrorKind, |
| FontStyle, FontTransform, |
| }; |
| |
| use std::fmt::Write as _; |
| use std::fs::File; |
| #[allow(unused_imports)] |
| use std::io::Cursor; |
| use std::io::{BufWriter, Error, Write}; |
| use std::path::Path; |
| |
| fn make_svg_color(color: BackendColor) -> String { |
| let (r, g, b) = color.rgb; |
| return format!("#{:02X}{:02X}{:02X}", r, g, b); |
| } |
| |
| fn make_svg_opacity(color: BackendColor) -> String { |
| return format!("{}", color.alpha); |
| } |
| |
| enum Target<'a> { |
| File(String, &'a Path), |
| Buffer(&'a mut String), |
| // TODO: At this point we won't make the breaking change |
| // so the u8 buffer is still supported. But in 0.3, we definitely |
| // should get rid of this. |
| #[cfg(feature = "deprecated_items")] |
| U8Buffer(String, &'a mut Vec<u8>), |
| } |
| |
| impl Target<'_> { |
| fn get_mut(&mut self) -> &mut String { |
| match self { |
| Target::File(ref mut buf, _) => buf, |
| Target::Buffer(buf) => buf, |
| #[cfg(feature = "deprecated_items")] |
| Target::U8Buffer(ref mut buf, _) => buf, |
| } |
| } |
| } |
| |
| enum SVGTag { |
| Svg, |
| Circle, |
| Line, |
| Polygon, |
| Polyline, |
| Rectangle, |
| Text, |
| #[allow(dead_code)] |
| Image, |
| } |
| |
| impl SVGTag { |
| fn to_tag_name(&self) -> &'static str { |
| match self { |
| SVGTag::Svg => "svg", |
| SVGTag::Circle => "circle", |
| SVGTag::Line => "line", |
| SVGTag::Polyline => "polyline", |
| SVGTag::Rectangle => "rect", |
| SVGTag::Text => "text", |
| SVGTag::Image => "image", |
| SVGTag::Polygon => "polygon", |
| } |
| } |
| } |
| |
| /// The SVG image drawing backend |
| pub struct SVGBackend<'a> { |
| target: Target<'a>, |
| size: (u32, u32), |
| tag_stack: Vec<SVGTag>, |
| saved: bool, |
| } |
| |
| impl<'a> SVGBackend<'a> { |
| fn escape_and_push(buf: &mut String, value: &str) { |
| value.chars().for_each(|c| match c { |
| '<' => buf.push_str("<"), |
| '>' => buf.push_str(">"), |
| '&' => buf.push_str("&"), |
| '"' => buf.push_str("""), |
| '\'' => buf.push_str("'"), |
| other => buf.push(other), |
| }); |
| } |
| fn open_tag(&mut self, tag: SVGTag, attr: &[(&str, &str)], close: bool) { |
| let buf = self.target.get_mut(); |
| buf.push('<'); |
| buf.push_str(tag.to_tag_name()); |
| for (key, value) in attr { |
| buf.push(' '); |
| buf.push_str(key); |
| buf.push_str("=\""); |
| Self::escape_and_push(buf, value); |
| buf.push('\"'); |
| } |
| if close { |
| buf.push_str("/>\n"); |
| } else { |
| self.tag_stack.push(tag); |
| buf.push_str(">\n"); |
| } |
| } |
| |
| fn close_tag(&mut self) -> bool { |
| if let Some(tag) = self.tag_stack.pop() { |
| let buf = self.target.get_mut(); |
| buf.push_str("</"); |
| buf.push_str(tag.to_tag_name()); |
| buf.push_str(">\n"); |
| return true; |
| } |
| false |
| } |
| |
| fn init_svg_file(&mut self, size: (u32, u32)) { |
| self.open_tag( |
| SVGTag::Svg, |
| &[ |
| ("width", &format!("{}", size.0)), |
| ("height", &format!("{}", size.1)), |
| ("viewBox", &format!("0 0 {} {}", size.0, size.1)), |
| ("xmlns", "http://www.w3.org/2000/svg"), |
| ], |
| false, |
| ); |
| } |
| |
| /// Create a new SVG drawing backend |
| pub fn new<T: AsRef<Path> + ?Sized>(path: &'a T, size: (u32, u32)) -> Self { |
| let mut ret = Self { |
| target: Target::File(String::default(), path.as_ref()), |
| size, |
| tag_stack: vec![], |
| saved: false, |
| }; |
| |
| ret.init_svg_file(size); |
| ret |
| } |
| |
| /// Create a new SVG drawing backend and store the document into a u8 vector |
| #[cfg(feature = "deprecated_items")] |
| #[deprecated( |
| note = "This will be replaced by `with_string`, consider use `with_string` to avoid breaking change in the future" |
| )] |
| pub fn with_buffer(buf: &'a mut Vec<u8>, size: (u32, u32)) -> Self { |
| let mut ret = Self { |
| target: Target::U8Buffer(String::default(), buf), |
| size, |
| tag_stack: vec![], |
| saved: false, |
| }; |
| |
| ret.init_svg_file(size); |
| |
| ret |
| } |
| |
| /// Create a new SVG drawing backend and store the document into a String buffer |
| pub fn with_string(buf: &'a mut String, size: (u32, u32)) -> Self { |
| let mut ret = Self { |
| target: Target::Buffer(buf), |
| size, |
| tag_stack: vec![], |
| saved: false, |
| }; |
| |
| ret.init_svg_file(size); |
| |
| ret |
| } |
| } |
| |
| impl<'a> DrawingBackend for SVGBackend<'a> { |
| type ErrorType = Error; |
| |
| fn get_size(&self) -> (u32, u32) { |
| self.size |
| } |
| |
| fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<Error>> { |
| Ok(()) |
| } |
| |
| fn present(&mut self) -> Result<(), DrawingErrorKind<Error>> { |
| if !self.saved { |
| while self.close_tag() {} |
| match self.target { |
| Target::File(ref buf, path) => { |
| let outfile = File::create(path).map_err(DrawingErrorKind::DrawingError)?; |
| let mut outfile = BufWriter::new(outfile); |
| outfile |
| .write_all(buf.as_ref()) |
| .map_err(DrawingErrorKind::DrawingError)?; |
| } |
| Target::Buffer(_) => {} |
| #[cfg(feature = "deprecated_items")] |
| Target::U8Buffer(ref actual, ref mut target) => { |
| target.clear(); |
| target.extend_from_slice(actual.as_bytes()); |
| } |
| } |
| self.saved = true; |
| } |
| Ok(()) |
| } |
| |
| fn draw_pixel( |
| &mut self, |
| point: BackendCoord, |
| color: BackendColor, |
| ) -> Result<(), DrawingErrorKind<Error>> { |
| if color.alpha == 0.0 { |
| return Ok(()); |
| } |
| self.open_tag( |
| SVGTag::Rectangle, |
| &[ |
| ("x", &format!("{}", point.0)), |
| ("y", &format!("{}", point.1)), |
| ("width", "1"), |
| ("height", "1"), |
| ("stroke", "none"), |
| ("opacity", &make_svg_opacity(color)), |
| ("fill", &make_svg_color(color)), |
| ], |
| true, |
| ); |
| Ok(()) |
| } |
| |
| fn draw_line<S: BackendStyle>( |
| &mut self, |
| from: BackendCoord, |
| to: BackendCoord, |
| style: &S, |
| ) -> Result<(), DrawingErrorKind<Self::ErrorType>> { |
| if style.color().alpha == 0.0 { |
| return Ok(()); |
| } |
| self.open_tag( |
| SVGTag::Line, |
| &[ |
| ("opacity", &make_svg_opacity(style.color())), |
| ("stroke", &make_svg_color(style.color())), |
| ("stroke-width", &format!("{}", style.stroke_width())), |
| ("x1", &format!("{}", from.0)), |
| ("y1", &format!("{}", from.1)), |
| ("x2", &format!("{}", to.0)), |
| ("y2", &format!("{}", to.1)), |
| ], |
| true, |
| ); |
| Ok(()) |
| } |
| |
| fn draw_rect<S: BackendStyle>( |
| &mut self, |
| upper_left: BackendCoord, |
| bottom_right: BackendCoord, |
| style: &S, |
| fill: bool, |
| ) -> Result<(), DrawingErrorKind<Self::ErrorType>> { |
| if style.color().alpha == 0.0 { |
| return Ok(()); |
| } |
| |
| let (fill, stroke) = if !fill { |
| ("none".to_string(), make_svg_color(style.color())) |
| } else { |
| (make_svg_color(style.color()), "none".to_string()) |
| }; |
| |
| self.open_tag( |
| SVGTag::Rectangle, |
| &[ |
| ("x", &format!("{}", upper_left.0)), |
| ("y", &format!("{}", upper_left.1)), |
| ("width", &format!("{}", bottom_right.0 - upper_left.0)), |
| ("height", &format!("{}", bottom_right.1 - upper_left.1)), |
| ("opacity", &make_svg_opacity(style.color())), |
| ("fill", &fill), |
| ("stroke", &stroke), |
| ], |
| true, |
| ); |
| |
| Ok(()) |
| } |
| |
| fn draw_path<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>( |
| &mut self, |
| path: I, |
| style: &S, |
| ) -> Result<(), DrawingErrorKind<Self::ErrorType>> { |
| if style.color().alpha == 0.0 { |
| return Ok(()); |
| } |
| self.open_tag( |
| SVGTag::Polyline, |
| &[ |
| ("fill", "none"), |
| ("opacity", &make_svg_opacity(style.color())), |
| ("stroke", &make_svg_color(style.color())), |
| ("stroke-width", &format!("{}", style.stroke_width())), |
| ( |
| "points", |
| &path.into_iter().fold(String::new(), |mut s, (x, y)| { |
| write!(s, "{},{} ", x, y).ok(); |
| s |
| }), |
| ), |
| ], |
| true, |
| ); |
| Ok(()) |
| } |
| |
| fn fill_polygon<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>( |
| &mut self, |
| path: I, |
| style: &S, |
| ) -> Result<(), DrawingErrorKind<Self::ErrorType>> { |
| if style.color().alpha == 0.0 { |
| return Ok(()); |
| } |
| self.open_tag( |
| SVGTag::Polygon, |
| &[ |
| ("opacity", &make_svg_opacity(style.color())), |
| ("fill", &make_svg_color(style.color())), |
| ( |
| "points", |
| &path.into_iter().fold(String::new(), |mut s, (x, y)| { |
| write!(s, "{},{} ", x, y).ok(); |
| s |
| }), |
| ), |
| ], |
| true, |
| ); |
| Ok(()) |
| } |
| |
| fn draw_circle<S: BackendStyle>( |
| &mut self, |
| center: BackendCoord, |
| radius: u32, |
| style: &S, |
| fill: bool, |
| ) -> Result<(), DrawingErrorKind<Self::ErrorType>> { |
| if style.color().alpha == 0.0 { |
| return Ok(()); |
| } |
| let (stroke, fill) = if !fill { |
| (make_svg_color(style.color()), "none".to_string()) |
| } else { |
| ("none".to_string(), make_svg_color(style.color())) |
| }; |
| self.open_tag( |
| SVGTag::Circle, |
| &[ |
| ("cx", &format!("{}", center.0)), |
| ("cy", &format!("{}", center.1)), |
| ("r", &format!("{}", radius)), |
| ("opacity", &make_svg_opacity(style.color())), |
| ("fill", &fill), |
| ("stroke", &stroke), |
| ("stroke-width", &format!("{}", style.stroke_width())), |
| ], |
| true, |
| ); |
| Ok(()) |
| } |
| |
| fn draw_text<S: BackendTextStyle>( |
| &mut self, |
| text: &str, |
| style: &S, |
| pos: BackendCoord, |
| ) -> Result<(), DrawingErrorKind<Self::ErrorType>> { |
| let color = style.color(); |
| if color.alpha == 0.0 { |
| return Ok(()); |
| } |
| |
| let (x0, y0) = pos; |
| let text_anchor = match style.anchor().h_pos { |
| HPos::Left => "start", |
| HPos::Right => "end", |
| HPos::Center => "middle", |
| }; |
| |
| let dy = match style.anchor().v_pos { |
| VPos::Top => "0.76em", |
| VPos::Center => "0.5ex", |
| VPos::Bottom => "-0.5ex", |
| }; |
| |
| #[cfg(feature = "debug")] |
| { |
| let ((fx0, fy0), (fx1, fy1)) = |
| font.layout_box(text).map_err(DrawingErrorKind::FontError)?; |
| let x0 = match style.anchor().h_pos { |
| HPos::Left => x0, |
| HPos::Center => x0 - fx1 / 2 + fx0 / 2, |
| HPos::Right => x0 - fx1 + fx0, |
| }; |
| let y0 = match style.anchor().v_pos { |
| VPos::Top => y0, |
| VPos::Center => y0 - fy1 / 2 + fy0 / 2, |
| VPos::Bottom => y0 - fy1 + fy0, |
| }; |
| self.draw_rect( |
| (x0, y0), |
| (x0 + fx1 - fx0, y0 + fy1 - fy0), |
| &crate::prelude::RED, |
| false, |
| ) |
| .unwrap(); |
| self.draw_circle((x0, y0), 2, &crate::prelude::RED, false) |
| .unwrap(); |
| } |
| |
| let mut attrs = vec![ |
| ("x", format!("{}", x0)), |
| ("y", format!("{}", y0)), |
| ("dy", dy.to_owned()), |
| ("text-anchor", text_anchor.to_string()), |
| ("font-family", style.family().as_str().to_string()), |
| ("font-size", format!("{}", style.size() / 1.24)), |
| ("opacity", make_svg_opacity(color)), |
| ("fill", make_svg_color(color)), |
| ]; |
| |
| match style.style() { |
| FontStyle::Normal => {} |
| FontStyle::Bold => attrs.push(("font-weight", "bold".to_string())), |
| other_style => attrs.push(("font-style", other_style.as_str().to_string())), |
| }; |
| |
| let trans = style.transform(); |
| match trans { |
| FontTransform::Rotate90 => { |
| attrs.push(("transform", format!("rotate(90, {}, {})", x0, y0))) |
| } |
| FontTransform::Rotate180 => { |
| attrs.push(("transform", format!("rotate(180, {}, {})", x0, y0))); |
| } |
| FontTransform::Rotate270 => { |
| attrs.push(("transform", format!("rotate(270, {}, {})", x0, y0))); |
| } |
| _ => {} |
| } |
| |
| self.open_tag( |
| SVGTag::Text, |
| attrs |
| .iter() |
| .map(|(a, b)| (*a, b.as_ref())) |
| .collect::<Vec<_>>() |
| .as_ref(), |
| false, |
| ); |
| |
| Self::escape_and_push(self.target.get_mut(), text); |
| self.target.get_mut().push('\n'); |
| |
| self.close_tag(); |
| |
| Ok(()) |
| } |
| |
| #[cfg(all(not(target_arch = "wasm32"), feature = "image"))] |
| fn blit_bitmap<'b>( |
| &mut self, |
| pos: BackendCoord, |
| (w, h): (u32, u32), |
| src: &'b [u8], |
| ) -> Result<(), DrawingErrorKind<Self::ErrorType>> { |
| use image::codecs::png::PngEncoder; |
| use image::ImageEncoder; |
| |
| let mut data = vec![0; 0]; |
| |
| { |
| let cursor = Cursor::new(&mut data); |
| |
| let encoder = PngEncoder::new(cursor); |
| |
| let color = image::ColorType::Rgb8; |
| |
| encoder.write_image(src, w, h, color).map_err(|e| { |
| DrawingErrorKind::DrawingError(Error::new( |
| std::io::ErrorKind::Other, |
| format!("Image error: {}", e), |
| )) |
| })?; |
| } |
| |
| let padding = (3 - data.len() % 3) % 3; |
| for _ in 0..padding { |
| data.push(0); |
| } |
| |
| let mut rem_bits = 0; |
| let mut rem_num = 0; |
| |
| fn cvt_base64(from: u8) -> char { |
| (if from < 26 { |
| b'A' + from |
| } else if from < 52 { |
| b'a' + from - 26 |
| } else if from < 62 { |
| b'0' + from - 52 |
| } else if from == 62 { |
| b'+' |
| } else { |
| b'/' |
| }) |
| .into() |
| } |
| |
| let mut buf = String::new(); |
| buf.push_str("data:png;base64,"); |
| |
| for byte in data { |
| let value = (rem_bits << (6 - rem_num)) | (byte >> (rem_num + 2)); |
| rem_bits = byte & ((1 << (2 + rem_num)) - 1); |
| rem_num += 2; |
| |
| buf.push(cvt_base64(value)); |
| if rem_num == 6 { |
| buf.push(cvt_base64(rem_bits)); |
| rem_bits = 0; |
| rem_num = 0; |
| } |
| } |
| |
| for _ in 0..padding { |
| buf.pop(); |
| buf.push('='); |
| } |
| |
| self.open_tag( |
| SVGTag::Image, |
| &[ |
| ("x", &format!("{}", pos.0)), |
| ("y", &format!("{}", pos.1)), |
| ("width", &format!("{}", w)), |
| ("height", &format!("{}", h)), |
| ("href", buf.as_str()), |
| ], |
| true, |
| ); |
| |
| Ok(()) |
| } |
| } |
| |
| impl Drop for SVGBackend<'_> { |
| fn drop(&mut self) { |
| if !self.saved { |
| // drop should not panic, so we ignore a failed present |
| let _ = self.present(); |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| use plotters::element::Circle; |
| use plotters::prelude::{ |
| ChartBuilder, Color, IntoDrawingArea, IntoFont, SeriesLabelPosition, TextStyle, BLACK, |
| BLUE, RED, WHITE, |
| }; |
| use plotters::style::text_anchor::{HPos, Pos, VPos}; |
| use std::fs; |
| use std::path::Path; |
| |
| static DST_DIR: &str = "target/test/svg"; |
| |
| fn checked_save_file(name: &str, content: &str) { |
| /* |
| Please use the SVG file to manually verify the results. |
| */ |
| assert!(!content.is_empty()); |
| fs::create_dir_all(DST_DIR).unwrap(); |
| let file_name = format!("{}.svg", name); |
| let file_path = Path::new(DST_DIR).join(file_name); |
| println!("{:?} created", file_path); |
| fs::write(file_path, &content).unwrap(); |
| } |
| |
| fn draw_mesh_with_custom_ticks(tick_size: i32, test_name: &str) { |
| let mut content: String = Default::default(); |
| { |
| let root = SVGBackend::with_string(&mut content, (500, 500)).into_drawing_area(); |
| |
| let mut chart = ChartBuilder::on(&root) |
| .caption("This is a test", ("sans-serif", 20u32)) |
| .set_all_label_area_size(40u32) |
| .build_cartesian_2d(0..10, 0..10) |
| .unwrap(); |
| |
| chart |
| .configure_mesh() |
| .set_all_tick_mark_size(tick_size) |
| .draw() |
| .unwrap(); |
| } |
| |
| checked_save_file(test_name, &content); |
| |
| assert!(content.contains("This is a test")); |
| } |
| |
| #[test] |
| fn test_draw_mesh_no_ticks() { |
| draw_mesh_with_custom_ticks(0, "test_draw_mesh_no_ticks"); |
| } |
| |
| #[test] |
| fn test_draw_mesh_negative_ticks() { |
| draw_mesh_with_custom_ticks(-10, "test_draw_mesh_negative_ticks"); |
| } |
| |
| #[test] |
| fn test_text_alignments() { |
| let mut content: String = Default::default(); |
| { |
| let mut root = SVGBackend::with_string(&mut content, (500, 500)); |
| |
| let style = TextStyle::from(("sans-serif", 20).into_font()) |
| .pos(Pos::new(HPos::Right, VPos::Top)); |
| root.draw_text("right-align", &style, (150, 50)).unwrap(); |
| |
| let style = style.pos(Pos::new(HPos::Center, VPos::Top)); |
| root.draw_text("center-align", &style, (150, 150)).unwrap(); |
| |
| let style = style.pos(Pos::new(HPos::Left, VPos::Top)); |
| root.draw_text("left-align", &style, (150, 200)).unwrap(); |
| } |
| |
| checked_save_file("test_text_alignments", &content); |
| |
| for svg_line in content.split("</text>") { |
| if let Some(anchor_and_rest) = svg_line.split("text-anchor=\"").nth(1) { |
| if anchor_and_rest.starts_with("end") { |
| assert!(anchor_and_rest.contains("right-align")) |
| } |
| if anchor_and_rest.starts_with("middle") { |
| assert!(anchor_and_rest.contains("center-align")) |
| } |
| if anchor_and_rest.starts_with("start") { |
| assert!(anchor_and_rest.contains("left-align")) |
| } |
| } |
| } |
| } |
| |
| #[test] |
| fn test_text_draw() { |
| let mut content: String = Default::default(); |
| { |
| let root = SVGBackend::with_string(&mut content, (1500, 800)).into_drawing_area(); |
| let root = root |
| .titled("Image Title", ("sans-serif", 60).into_font()) |
| .unwrap(); |
| |
| let mut chart = ChartBuilder::on(&root) |
| .caption("All anchor point positions", ("sans-serif", 20u32)) |
| .set_all_label_area_size(40u32) |
| .build_cartesian_2d(0..100i32, 0..50i32) |
| .unwrap(); |
| |
| chart |
| .configure_mesh() |
| .disable_x_mesh() |
| .disable_y_mesh() |
| .x_desc("X Axis") |
| .y_desc("Y Axis") |
| .draw() |
| .unwrap(); |
| |
| let ((x1, y1), (x2, y2), (x3, y3)) = ((-30, 30), (0, -30), (30, 30)); |
| |
| for (dy, trans) in [ |
| FontTransform::None, |
| FontTransform::Rotate90, |
| FontTransform::Rotate180, |
| FontTransform::Rotate270, |
| ] |
| .iter() |
| .enumerate() |
| { |
| for (dx1, h_pos) in [HPos::Left, HPos::Right, HPos::Center].iter().enumerate() { |
| for (dx2, v_pos) in [VPos::Top, VPos::Center, VPos::Bottom].iter().enumerate() { |
| let x = 150_i32 + (dx1 as i32 * 3 + dx2 as i32) * 150; |
| let y = 120 + dy as i32 * 150; |
| let draw = |x, y, text| { |
| root.draw(&Circle::new((x, y), 3, &BLACK.mix(0.5))).unwrap(); |
| let style = TextStyle::from(("sans-serif", 20).into_font()) |
| .pos(Pos::new(*h_pos, *v_pos)) |
| .transform(trans.clone()); |
| root.draw_text(text, &style, (x, y)).unwrap(); |
| }; |
| draw(x + x1, y + y1, "dood"); |
| draw(x + x2, y + y2, "dog"); |
| draw(x + x3, y + y3, "goog"); |
| } |
| } |
| } |
| } |
| |
| checked_save_file("test_text_draw", &content); |
| |
| assert_eq!(content.matches("dog").count(), 36); |
| assert_eq!(content.matches("dood").count(), 36); |
| assert_eq!(content.matches("goog").count(), 36); |
| } |
| |
| #[test] |
| fn test_text_clipping() { |
| let mut content: String = Default::default(); |
| { |
| let (width, height) = (500_i32, 500_i32); |
| let root = SVGBackend::with_string(&mut content, (width as u32, height as u32)) |
| .into_drawing_area(); |
| |
| let style = TextStyle::from(("sans-serif", 20).into_font()) |
| .pos(Pos::new(HPos::Center, VPos::Center)); |
| root.draw_text("TOP LEFT", &style, (0, 0)).unwrap(); |
| root.draw_text("TOP CENTER", &style, (width / 2, 0)) |
| .unwrap(); |
| root.draw_text("TOP RIGHT", &style, (width, 0)).unwrap(); |
| |
| root.draw_text("MIDDLE LEFT", &style, (0, height / 2)) |
| .unwrap(); |
| root.draw_text("MIDDLE RIGHT", &style, (width, height / 2)) |
| .unwrap(); |
| |
| root.draw_text("BOTTOM LEFT", &style, (0, height)).unwrap(); |
| root.draw_text("BOTTOM CENTER", &style, (width / 2, height)) |
| .unwrap(); |
| root.draw_text("BOTTOM RIGHT", &style, (width, height)) |
| .unwrap(); |
| } |
| |
| checked_save_file("test_text_clipping", &content); |
| } |
| |
| #[test] |
| fn test_series_labels() { |
| let mut content = String::default(); |
| { |
| let (width, height) = (500, 500); |
| let root = SVGBackend::with_string(&mut content, (width, height)).into_drawing_area(); |
| |
| let mut chart = ChartBuilder::on(&root) |
| .caption("All series label positions", ("sans-serif", 20u32)) |
| .set_all_label_area_size(40u32) |
| .build_cartesian_2d(0..50i32, 0..50i32) |
| .unwrap(); |
| |
| chart |
| .configure_mesh() |
| .disable_x_mesh() |
| .disable_y_mesh() |
| .draw() |
| .unwrap(); |
| |
| chart |
| .draw_series(std::iter::once(Circle::new((5, 15), 5u32, &RED))) |
| .expect("Drawing error") |
| .label("Series 1") |
| .legend(|(x, y)| Circle::new((x, y), 3u32, RED.filled())); |
| |
| chart |
| .draw_series(std::iter::once(Circle::new((5, 15), 10u32, &BLUE))) |
| .expect("Drawing error") |
| .label("Series 2") |
| .legend(|(x, y)| Circle::new((x, y), 3u32, BLUE.filled())); |
| |
| for pos in vec![ |
| SeriesLabelPosition::UpperLeft, |
| SeriesLabelPosition::MiddleLeft, |
| SeriesLabelPosition::LowerLeft, |
| SeriesLabelPosition::UpperMiddle, |
| SeriesLabelPosition::MiddleMiddle, |
| SeriesLabelPosition::LowerMiddle, |
| SeriesLabelPosition::UpperRight, |
| SeriesLabelPosition::MiddleRight, |
| SeriesLabelPosition::LowerRight, |
| SeriesLabelPosition::Coordinate(70, 70), |
| ] |
| .into_iter() |
| { |
| chart |
| .configure_series_labels() |
| .border_style(&BLACK.mix(0.5)) |
| .position(pos) |
| .draw() |
| .expect("Drawing error"); |
| } |
| } |
| |
| checked_save_file("test_series_labels", &content); |
| } |
| |
| #[test] |
| fn test_draw_pixel_alphas() { |
| let mut content = String::default(); |
| { |
| let (width, height) = (100_i32, 100_i32); |
| let root = SVGBackend::with_string(&mut content, (width as u32, height as u32)) |
| .into_drawing_area(); |
| root.fill(&WHITE).unwrap(); |
| |
| for i in -20..20 { |
| let alpha = i as f64 * 0.1; |
| root.draw_pixel((50 + i, 50 + i), &BLACK.mix(alpha)) |
| .unwrap(); |
| } |
| } |
| |
| checked_save_file("test_draw_pixel_alphas", &content); |
| } |
| } |