Skip to content

Commit

Permalink
Merge pull request #544 from KmolYuan/dotted-line
Browse files Browse the repository at this point in the history
Add dotted line style
  • Loading branch information
AaronErhardt authored Feb 26, 2024
2 parents 98e2351 + 5ea6dac commit 62783f4
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 7 deletions.
129 changes: 127 additions & 2 deletions plotters/src/element/basic_shapes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -181,8 +191,6 @@ impl<I0: Iterator + Clone, Size: SizeDesc, DB: DrawingBackend> Drawable<DB>
backend: &mut DB,
ps: (u32, u32),
) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
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(()),
Expand Down Expand Up @@ -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<I: Iterator + Clone, Size: SizeDesc, Marker> {
points: I,
shift: Size,
spacing: Size,
func: Box<dyn Fn(BackendCoord) -> Marker>,
}

impl<I: Iterator + Clone, Size: SizeDesc, Marker> DottedPathElement<I, Size, Marker> {
/// 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<I0, F>(points: I0, shift: Size, spacing: Size, func: F) -> Self
where
I0: IntoIterator<IntoIter = I>,
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<I, Size, Marker>
{
type Point = I::Item;
type IntoIter = I;
fn point_iter(self) -> Self::IntoIter {
self.points.clone()
}
}

impl<I0, Size, DB, Marker> Drawable<DB> for DottedPathElement<I0, Size, Marker>
where
I0: Iterator + Clone,
Size: SizeDesc,
DB: DrawingBackend,
Marker: crate::element::IntoDynElement<'static, DB, BackendCoord>,
{
fn draw<I: Iterator<Item = BackendCoord>>(
&self,
mut points: I,
backend: &mut DB,
ps: (u32, u32),
) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
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::<ShapeStyle>::into(RED).filled()),
))
.expect("Drawing Failure");
}

/// A rectangle element
pub struct Rectangle<Coord> {
points: [Coord; 2],
Expand Down
2 changes: 1 addition & 1 deletion plotters/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
57 changes: 54 additions & 3 deletions plotters/src/series/line_series.rs
Original file line number Diff line number Diff line change
@@ -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;

/**
Expand Down Expand Up @@ -126,6 +128,51 @@ impl<I: Iterator + Clone, Size: SizeDesc> IntoIterator for DashedLineSeries<I, S
}
}

/// A dotted line series, map an iterable object to the dotted line element.
pub struct DottedLineSeries<I: Iterator + Clone, Size: SizeDesc, Marker> {
points: I,
shift: Size,
spacing: Size,
func: Box<dyn Fn(BackendCoord) -> Marker>,
}

impl<I: Iterator + Clone, Size: SizeDesc, Marker> DottedLineSeries<I, Size, Marker> {
/// 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<I0, F>(points: I0, shift: Size, spacing: Size, func: F) -> Self
where
I0: IntoIterator<IntoIter = I>,
F: Fn(BackendCoord) -> Marker + 'static,
{
Self {
points: points.into_iter(),
shift,
spacing,
func: Box::new(func),
}
}
}

impl<I: Iterator + Clone, Size: SizeDesc, Marker: 'static> IntoIterator
for DottedLineSeries<I, Size, Marker>
{
type Item = DottedPathElement<I, Size, Marker>;
type IntoIter = std::iter::Once<Self::Item>;

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::*;
Expand All @@ -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);
});
});

Expand All @@ -167,5 +214,9 @@ mod test {
Into::<ShapeStyle>::into(RED).stroke_width(3),
))
.expect("Drawing Error");
let mk_f = |c| Circle::new(c, 3, Into::<ShapeStyle>::into(RED).filled());
chart
.draw_series(DottedLineSeries::new((0..=50).map(|x| (x, 0)), 5, 5, mk_f))
.expect("Drawing Error");
}
}
2 changes: 1 addition & 1 deletion plotters/src/series/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down

0 comments on commit 62783f4

Please sign in to comment.