Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dotted line style #544

Merged
merged 6 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading