diff --git a/plotters/src/element/basic_shapes.rs b/plotters/src/element/basic_shapes.rs index ad1e1344..3c2564ec 100644 --- a/plotters/src/element/basic_shapes.rs +++ b/plotters/src/element/basic_shapes.rs @@ -2,6 +2,16 @@ use super::{Drawable, PointCollection}; use crate::style::{Color, ShapeStyle, SizeDesc}; use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind}; +#[inline] +fn to_i((x, y): (f32, f32)) -> (i32, i32) { + (x.round() as i32, y.round() as i32) +} + +#[inline] +fn to_f((x, y): (i32, i32)) -> (f32, f32) { + (x as f32, y as f32) +} + /** An element representing a single pixel. @@ -181,8 +191,6 @@ impl Drawable backend: &mut DB, ps: (u32, u32), ) -> Result<(), DrawingErrorKind> { - let to_i = |(x, y): (f32, f32)| (x.round() as i32, y.round() as i32); - let to_f = |(x, y): (i32, i32)| (x as f32, y as f32); let mut start = match points.next() { Some(c) => to_f(c), None => return Ok(()), @@ -264,6 +272,123 @@ fn test_dashed_path_element() { .expect("Drawing Failure"); } +/// An element of a series of connected lines in dot style for any markers. +/// +/// It's similar to [`PathElement`] but use a marker function to draw markers with spacing. +pub struct DottedPathElement { + points: I, + shift: Size, + spacing: Size, + func: Box Marker>, +} + +impl DottedPathElement { + /// Create a new path + /// - `points`: The iterator of the points + /// - `shift`: The shift of the first marker + /// - `spacing`: The spacing between markers + /// - `func`: The marker function + /// - returns the created element + pub fn new(points: I0, shift: Size, spacing: Size, func: F) -> Self + where + I0: IntoIterator, + F: Fn(BackendCoord) -> Marker + 'static, + { + Self { + points: points.into_iter(), + shift, + spacing, + func: Box::new(func), + } + } +} + +impl<'a, I: Iterator + Clone, Size: SizeDesc, Marker> PointCollection<'a, I::Item> + for &'a DottedPathElement +{ + type Point = I::Item; + type IntoIter = I; + fn point_iter(self) -> Self::IntoIter { + self.points.clone() + } +} + +impl Drawable for DottedPathElement +where + I0: Iterator + Clone, + Size: SizeDesc, + DB: DrawingBackend, + Marker: crate::element::IntoDynElement<'static, DB, BackendCoord>, +{ + fn draw>( + &self, + mut points: I, + backend: &mut DB, + ps: (u32, u32), + ) -> Result<(), DrawingErrorKind> { + let mut shift = self.shift.in_pixels(&ps).max(0) as f32; + let mut start = match points.next() { + Some(start_i) => { + // Draw the first marker if no shift + if shift == 0. { + let mk = (self.func)(start_i).into_dyn(); + mk.draw(mk.point_iter().iter().copied(), backend, ps)?; + } + to_f(start_i) + } + None => return Ok(()), + }; + let spacing = self.spacing.in_pixels(&ps).max(0) as f32; + let mut dist = 0.; + for curr in points { + let end = to_f(curr); + // Loop for spacing + while start != end { + let (dx, dy) = (end.0 - start.0, end.1 - start.1); + let d = dx.hypot(dy); + let spacing = if shift == 0. { spacing } else { shift }; + let left = spacing - dist; + // Set next point to `start` + if left < d { + let t = left / d; + start = (start.0 + dx * t, start.1 + dy * t); + dist += left; + } else { + start = end; + dist += d; + } + // Draw if needed + if spacing <= dist { + let mk = (self.func)(to_i(start)).into_dyn(); + mk.draw(mk.point_iter().iter().copied(), backend, ps)?; + shift = 0.; + dist = 0.; + } + } + } + Ok(()) + } +} + +#[cfg(test)] +#[test] +fn test_dotted_path_element() { + use crate::prelude::*; + let da = crate::create_mocked_drawing_area(300, 300, |m| { + m.drop_check(|b| { + assert_eq!(b.num_draw_path_call, 0); + assert_eq!(b.draw_count, 7); + }); + }); + da.draw(&DottedPathElement::new( + vec![(100, 100), (105, 105), (150, 150)], + 5, + 10, + |c| Circle::new(c, 5, Into::::into(RED).filled()), + )) + .expect("Drawing Failure"); +} + /// A rectangle element pub struct Rectangle { points: [Coord; 2], diff --git a/plotters/src/lib.rs b/plotters/src/lib.rs index dff81c90..923c79c1 100644 --- a/plotters/src/lib.rs +++ b/plotters/src/lib.rs @@ -844,7 +844,7 @@ pub mod prelude { #[cfg(feature = "surface_series")] pub use crate::series::SurfaceSeries; #[cfg(feature = "line_series")] - pub use crate::series::{DashedLineSeries, LineSeries}; + pub use crate::series::{DashedLineSeries, DottedLineSeries, LineSeries}; // Styles pub use crate::style::{BLACK, BLUE, CYAN, GREEN, MAGENTA, RED, TRANSPARENT, WHITE, YELLOW}; diff --git a/plotters/src/series/line_series.rs b/plotters/src/series/line_series.rs index fbaaba34..2e67503b 100644 --- a/plotters/src/series/line_series.rs +++ b/plotters/src/series/line_series.rs @@ -1,6 +1,8 @@ -use crate::element::{Circle, DashedPathElement, DynElement, IntoDynElement, PathElement}; +use crate::element::{ + Circle, DashedPathElement, DottedPathElement, DynElement, IntoDynElement, PathElement, +}; use crate::style::{ShapeStyle, SizeDesc}; -use plotters_backend::DrawingBackend; +use plotters_backend::{BackendCoord, DrawingBackend}; use std::marker::PhantomData; /** @@ -126,6 +128,51 @@ impl IntoIterator for DashedLineSeries { + points: I, + shift: Size, + spacing: Size, + func: Box Marker>, +} + +impl DottedLineSeries { + /// Create a new line series from + /// - `points`: The iterator of the points + /// - `shift`: The shift of the first marker + /// - `spacing`: The spacing between markers + /// - `func`: The marker function + /// - returns the created element + pub fn new(points: I0, shift: Size, spacing: Size, func: F) -> Self + where + I0: IntoIterator, + F: Fn(BackendCoord) -> Marker + 'static, + { + Self { + points: points.into_iter(), + shift, + spacing, + func: Box::new(func), + } + } +} + +impl IntoIterator + for DottedLineSeries +{ + type Item = DottedPathElement; + type IntoIter = std::iter::Once; + + fn into_iter(self) -> Self::IntoIter { + std::iter::once(DottedPathElement::new( + self.points, + self.shift, + self.spacing, + self.func, + )) + } +} + #[cfg(test)] mod test { use crate::prelude::*; @@ -145,7 +192,7 @@ mod test { m.drop_check(|b| { assert_eq!(b.num_draw_path_call, 8); - assert_eq!(b.draw_count, 8); + assert_eq!(b.draw_count, 27); }); }); @@ -167,5 +214,9 @@ mod test { Into::::into(RED).stroke_width(3), )) .expect("Drawing Error"); + let mk_f = |c| Circle::new(c, 3, Into::::into(RED).filled()); + chart + .draw_series(DottedLineSeries::new((0..=50).map(|x| (x, 0)), 5, 5, mk_f)) + .expect("Drawing Error"); } } diff --git a/plotters/src/series/mod.rs b/plotters/src/series/mod.rs index 33677a45..f3ffdfea 100644 --- a/plotters/src/series/mod.rs +++ b/plotters/src/series/mod.rs @@ -26,7 +26,7 @@ pub use area_series::AreaSeries; #[cfg(feature = "histogram")] pub use histogram::Histogram; #[cfg(feature = "line_series")] -pub use line_series::{DashedLineSeries, LineSeries}; +pub use line_series::{DashedLineSeries, DottedLineSeries, LineSeries}; #[cfg(feature = "point_series")] pub use point_series::PointSeries; #[cfg(feature = "surface_series")]