aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJoel Galenson <jgalenson@google.com>2021-04-12 23:43:24 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2021-04-12 23:43:24 +0000
commitb64df24e47441d6ec44a063c665ac39d21cfb141 (patch)
treef5309a0441cebfeb180fa9e396526b41dc0ed911 /src
parent201b37a7aa6ab96dbcd39df7f0a1072845a55fa0 (diff)
parenteecbedf4bd5479cfae7af7e1038ce8c8580c5840 (diff)
downloadplotters-b64df24e47441d6ec44a063c665ac39d21cfb141.tar.gz
Upgrade rust/crates/plotters to 0.3.0 am: e2d8e8d5a5 am: 844ca977cb am: 69d7d070b7 am: eecbedf4bd
Original change: https://android-review.googlesource.com/c/platform/external/rust/crates/plotters/+/1662803 Change-Id: Ib3b4ef6a0fde0948ee8f7cb0d968e94eeaf28e95
Diffstat (limited to 'src')
-rw-r--r--src/chart/axes3d.rs191
-rw-r--r--src/chart/builder.rs106
-rw-r--r--src/chart/context.rs696
-rw-r--r--src/chart/dual_coord.rs94
-rw-r--r--src/chart/mesh.rs115
-rw-r--r--src/chart/mod.rs11
-rw-r--r--src/chart/series.rs65
-rw-r--r--src/chart/state.rs112
-rw-r--r--src/coord/category.rs209
-rw-r--r--src/coord/mod.rs129
-rw-r--r--src/coord/ranged.rs397
-rw-r--r--src/coord/ranged1d/combinators/ckps.rs266
-rw-r--r--src/coord/ranged1d/combinators/group_by.rs120
-rw-r--r--src/coord/ranged1d/combinators/linspace.rs432
-rw-r--r--src/coord/ranged1d/combinators/logarithmic.rs (renamed from src/coord/logarithmic.rs)28
-rw-r--r--src/coord/ranged1d/combinators/mod.rs17
-rw-r--r--src/coord/ranged1d/combinators/nested.rs204
-rw-r--r--src/coord/ranged1d/combinators/partial_axis.rs113
-rw-r--r--src/coord/ranged1d/discrete.rs270
-rw-r--r--src/coord/ranged1d/mod.rs234
-rw-r--r--src/coord/ranged1d/types/datetime.rs (renamed from src/coord/datetime.rs)445
-rw-r--r--src/coord/ranged1d/types/mod.rs15
-rw-r--r--src/coord/ranged1d/types/numeric.rs (renamed from src/coord/numeric.rs)220
-rw-r--r--src/coord/ranged1d/types/slice.rs100
-rw-r--r--src/coord/ranged2d/cartesian.rs152
-rw-r--r--src/coord/ranged2d/mod.rs1
-rw-r--r--src/coord/ranged3d/cartesian3d.rs105
-rw-r--r--src/coord/ranged3d/mod.rs5
-rw-r--r--src/coord/ranged3d/projection.rs198
-rw-r--r--src/coord/translate.rs32
-rw-r--r--src/data/float.rs131
-rw-r--r--src/drawing/area.rs82
-rw-r--r--src/drawing/backend.rs284
-rw-r--r--src/drawing/backend_impl/bitmap.rs1607
-rw-r--r--src/drawing/backend_impl/cairo.rs567
-rw-r--r--src/drawing/backend_impl/canvas.rs530
-rw-r--r--src/drawing/backend_impl/mocked.rs43
-rw-r--r--src/drawing/backend_impl/mod.rs32
-rw-r--r--src/drawing/backend_impl/piston.rs206
-rw-r--r--src/drawing/backend_impl/svg.rs832
-rw-r--r--src/drawing/mod.rs29
-rw-r--r--src/drawing/rasterizer/circle.rs67
-rw-r--r--src/drawing/rasterizer/line.rs126
-rw-r--r--src/drawing/rasterizer/mod.rs24
-rw-r--r--src/drawing/rasterizer/path.rs115
-rw-r--r--src/drawing/rasterizer/polygon.rs245
-rw-r--r--src/drawing/rasterizer/rect.rs60
-rw-r--r--src/element/basic_shapes.rs16
-rw-r--r--src/element/boxplot.rs12
-rw-r--r--src/element/candlestick.rs4
-rw-r--r--src/element/composable.rs8
-rw-r--r--src/element/dynelem.rs4
-rw-r--r--src/element/errorbar.rs10
-rw-r--r--src/element/image.rs17
-rw-r--r--src/element/mod.rs51
-rw-r--r--src/element/points.rs6
-rw-r--r--src/element/text.rs6
-rw-r--r--src/evcxr.rs3
-rw-r--r--src/lib.rs59
-rw-r--r--src/series/area_series.rs2
-rw-r--r--src/series/histogram.rs129
-rw-r--r--src/series/line_series.rs4
-rw-r--r--src/series/mod.rs4
-rw-r--r--src/series/surface.rs82
-rw-r--r--src/style/color.rs91
-rw-r--r--src/style/colors.rs33
-rw-r--r--src/style/font/font_desc.rs113
-rw-r--r--src/style/mod.rs5
-rw-r--r--src/style/palette_ext.rs136
-rw-r--r--src/style/shape.rs10
-rw-r--r--src/style/size.rs8
-rw-r--r--src/style/text.rs145
72 files changed, 4326 insertions, 6694 deletions
diff --git a/src/chart/axes3d.rs b/src/chart/axes3d.rs
new file mode 100644
index 0000000..14dd29b
--- /dev/null
+++ b/src/chart/axes3d.rs
@@ -0,0 +1,191 @@
+use std::marker::PhantomData;
+
+use super::ChartContext;
+use crate::coord::cartesian::Cartesian3d;
+use crate::coord::ranged1d::{BoldPoints, LightPoints, Ranged, ValueFormatter};
+use crate::style::colors::{BLACK, TRANSPARENT};
+use crate::style::Color;
+use crate::style::{AsRelative, ShapeStyle, SizeDesc, TextStyle};
+
+use super::Coord3D;
+
+use crate::drawing::DrawingAreaErrorKind;
+
+use plotters_backend::DrawingBackend;
+
+/// The configurations about the 3D plot's axes
+pub struct Axes3dStyle<'a, 'b, X: Ranged, Y: Ranged, Z: Ranged, DB: DrawingBackend> {
+ pub(super) parent_size: (u32, u32),
+ pub(super) target: Option<&'b mut ChartContext<'a, DB, Cartesian3d<X, Y, Z>>>,
+ pub(super) tick_size: i32,
+ pub(super) n_labels: [usize; 3],
+ pub(super) bold_line_style: ShapeStyle,
+ pub(super) light_line_style: ShapeStyle,
+ pub(super) axis_panel_style: ShapeStyle,
+ pub(super) axis_style: ShapeStyle,
+ pub(super) label_style: TextStyle<'b>,
+ pub(super) format_x: &'b dyn Fn(&X::ValueType) -> String,
+ pub(super) format_y: &'b dyn Fn(&Y::ValueType) -> String,
+ pub(super) format_z: &'b dyn Fn(&Z::ValueType) -> String,
+ _phantom: PhantomData<&'a (X, Y, Z)>,
+}
+
+impl<'a, 'b, X, Y, Z, XT, YT, ZT, DB> Axes3dStyle<'a, 'b, X, Y, Z, DB>
+where
+ X: Ranged<ValueType = XT> + ValueFormatter<XT>,
+ Y: Ranged<ValueType = YT> + ValueFormatter<YT>,
+ Z: Ranged<ValueType = ZT> + ValueFormatter<ZT>,
+ DB: DrawingBackend,
+{
+ /// Set the size of the tick mark
+ pub fn tick_size<Size: SizeDesc>(&mut self, size: Size) -> &mut Self {
+ let actual_size = size.in_pixels(&self.parent_size);
+ self.tick_size = actual_size;
+ self
+ }
+
+ /// Set the number of labels on the X axes
+ pub fn x_labels(&mut self, n: usize) -> &mut Self {
+ self.n_labels[0] = n;
+ self
+ }
+
+ /// Set the number of labels on the Y axes
+ pub fn y_labels(&mut self, n: usize) -> &mut Self {
+ self.n_labels[1] = n;
+ self
+ }
+
+ /// Set the number of labels on the Z axes
+ pub fn z_labels(&mut self, n: usize) -> &mut Self {
+ self.n_labels[2] = n;
+ self
+ }
+
+ pub fn axis_panel_style<S: Into<ShapeStyle>>(&mut self, style: S) -> &mut Self {
+ self.axis_panel_style = style.into();
+ self
+ }
+
+ pub fn bold_grid_style<S: Into<ShapeStyle>>(&mut self, style: S) -> &mut Self {
+ self.bold_line_style = style.into();
+ self
+ }
+
+ pub fn light_grid_style<S: Into<ShapeStyle>>(&mut self, style: S) -> &mut Self {
+ self.light_line_style = style.into();
+ self
+ }
+
+ pub fn label_style<S: Into<TextStyle<'b>>>(&mut self, style: S) -> &mut Self {
+ self.label_style = style.into();
+ self
+ }
+
+ pub fn x_formatter<F: Fn(&X::ValueType) -> String>(&mut self, f: &'b F) -> &mut Self {
+ self.format_x = f;
+ self
+ }
+
+ pub fn y_formatter<F: Fn(&Y::ValueType) -> String>(&mut self, f: &'b F) -> &mut Self {
+ self.format_y = f;
+ self
+ }
+
+ pub fn z_formatter<F: Fn(&Z::ValueType) -> String>(&mut self, f: &'b F) -> &mut Self {
+ self.format_z = f;
+ self
+ }
+
+ pub(crate) fn new(chart: &'b mut ChartContext<'a, DB, Cartesian3d<X, Y, Z>>) -> Self {
+ let parent_size = chart.drawing_area.dim_in_pixel();
+ let base_tick_size = (5u32).percent().max(5).in_pixels(chart.plotting_area());
+ let tick_size = base_tick_size;
+ Self {
+ parent_size,
+ tick_size,
+ n_labels: [10, 10, 10],
+ bold_line_style: Into::<ShapeStyle>::into(&BLACK.mix(0.2)),
+ light_line_style: Into::<ShapeStyle>::into(&TRANSPARENT),
+ axis_panel_style: Into::<ShapeStyle>::into(&BLACK.mix(0.1)),
+ axis_style: Into::<ShapeStyle>::into(&BLACK.mix(0.8)),
+ label_style: ("sans-serf", (12).percent().max(12).in_pixels(&parent_size)).into(),
+ format_x: &X::format,
+ format_y: &Y::format,
+ format_z: &Z::format,
+ _phantom: PhantomData,
+ target: Some(chart),
+ }
+ }
+ pub fn draw(&mut self) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
+ where
+ XT: Clone,
+ YT: Clone,
+ ZT: Clone,
+ {
+ let chart = self.target.take().unwrap();
+ let kps_bold = chart.get_key_points(
+ BoldPoints(self.n_labels[0]),
+ BoldPoints(self.n_labels[1]),
+ BoldPoints(self.n_labels[2]),
+ );
+ let kps_light = chart.get_key_points(
+ LightPoints::new(self.n_labels[0], self.n_labels[0] * 10),
+ LightPoints::new(self.n_labels[1], self.n_labels[1] * 10),
+ LightPoints::new(self.n_labels[2], self.n_labels[2] * 10),
+ );
+
+ let panels = chart.draw_axis_panels(
+ &kps_bold,
+ &kps_light,
+ self.axis_panel_style.clone(),
+ self.bold_line_style.clone(),
+ self.light_line_style.clone(),
+ )?;
+
+ for i in 0..3 {
+ let axis = chart.draw_axis(i, &panels, self.axis_style.clone())?;
+ let labels: Vec<_> = match i {
+ 0 => kps_bold
+ .x_points
+ .iter()
+ .map(|x| {
+ let x_text = (self.format_x)(x);
+ let mut p = axis[0].clone();
+ p[0] = Coord3D::X(x.clone());
+ (p, x_text)
+ })
+ .collect(),
+ 1 => kps_bold
+ .y_points
+ .iter()
+ .map(|y| {
+ let y_text = (self.format_y)(y);
+ let mut p = axis[0].clone();
+ p[1] = Coord3D::Y(y.clone());
+ (p, y_text)
+ })
+ .collect(),
+ _ => kps_bold
+ .z_points
+ .iter()
+ .map(|z| {
+ let z_text = (self.format_z)(z);
+ let mut p = axis[0].clone();
+ p[2] = Coord3D::Z(z.clone());
+ (p, z_text)
+ })
+ .collect(),
+ };
+ chart.draw_axis_ticks(
+ axis,
+ &labels[..],
+ self.tick_size,
+ self.axis_style.clone(),
+ self.label_style.clone(),
+ )?;
+ }
+
+ Ok(())
+ }
+}
diff --git a/src/chart/builder.rs b/src/chart/builder.rs
index ee8ef12..b74167c 100644
--- a/src/chart/builder.rs
+++ b/src/chart/builder.rs
@@ -1,12 +1,17 @@
use super::context::ChartContext;
-use crate::coord::{AsRangedCoord, RangedCoord, Shift};
-use crate::drawing::backend::DrawingBackend;
+use crate::coord::cartesian::{Cartesian2d, Cartesian3d};
+use crate::coord::ranged1d::AsRangedCoord;
+use crate::coord::Shift;
+
use crate::drawing::{DrawingArea, DrawingAreaErrorKind};
use crate::style::{IntoTextStyle, SizeDesc, TextStyle};
+use plotters_backend::DrawingBackend;
+
/// The enum used to specify the position of label area.
-/// This is used when we configure the label area size with the API `set_label_area_size`
+/// This is used when we configure the label area size with the API
+/// [ChartBuilder::set_label_area_size](struct ChartBuilder.html#method.set_label_area_size)
#[derive(Copy, Clone)]
pub enum LabelAreaPosition {
Top = 0,
@@ -150,18 +155,33 @@ impl<'a, 'b, DB: DrawingBackend> ChartBuilder<'a, 'b, DB> {
self
}
+ #[allow(clippy::type_complexity)]
+ #[deprecated(
+ note = "`build_ranged` has been renamed to `build_cartesian_2d` and is to be removed in the future."
+ )]
+ pub fn build_ranged<X: AsRangedCoord, Y: AsRangedCoord>(
+ &mut self,
+ x_spec: X,
+ y_spec: Y,
+ ) -> Result<
+ ChartContext<'a, DB, Cartesian2d<X::CoordDescType, Y::CoordDescType>>,
+ DrawingAreaErrorKind<DB::ErrorType>,
+ > {
+ self.build_cartesian_2d(x_spec, y_spec)
+ }
+
/// Build the chart with a 2D Cartesian coordinate system. The function will returns a chart
/// context, where data series can be rendered on.
/// - `x_spec`: The specification of X axis
/// - `y_spec`: The specification of Y axis
/// - Returns: A chart context
#[allow(clippy::type_complexity)]
- pub fn build_ranged<X: AsRangedCoord, Y: AsRangedCoord>(
+ pub fn build_cartesian_2d<X: AsRangedCoord, Y: AsRangedCoord>(
&mut self,
x_spec: X,
y_spec: Y,
) -> Result<
- ChartContext<'a, DB, RangedCoord<X::CoordDescType, Y::CoordDescType>>,
+ ChartContext<'a, DB, Cartesian2d<X::CoordDescType, Y::CoordDescType>>,
DrawingAreaErrorKind<DB::ErrorType>,
> {
let mut label_areas = [None, None, None, None];
@@ -204,6 +224,18 @@ impl<'a, 'b, DB: DrawingBackend> ChartBuilder<'a, 'b, DB> {
actual_drawing_area_pos[idx] += split_point;
}
+ // Now the root drawing area is to be split into
+ //
+ // +----------+------------------------------+------+
+ // | 0 | 1 (Top Label Area) | 2 |
+ // +----------+------------------------------+------+
+ // | 3 | | 5 |
+ // | Left | 4 (Plotting Area) | Right|
+ // | Labels | | Label|
+ // +----------+------------------------------+------+
+ // | 6 | 7 (Bottom Labels) | 8 |
+ // +----------+------------------------------+------+
+
let mut split: Vec<_> = drawing_area
.split_by_breakpoints(
&actual_drawing_area_pos[2..4],
@@ -213,8 +245,11 @@ impl<'a, 'b, DB: DrawingBackend> ChartBuilder<'a, 'b, DB> {
.map(Some)
.collect();
+ // Take out the plotting area
std::mem::swap(&mut drawing_area, split[4].as_mut().unwrap());
+ // Initialize the label areas - since the label area might be overlapping
+ // with the plotting area, in this case, we need handle them differently
for (src_idx, dst_idx) in [1, 7, 3, 5].iter().zip(0..4) {
if !self.overlap_plotting_area[dst_idx] {
let (h, w) = split[*src_idx].as_ref().unwrap().dim_in_pixel();
@@ -259,7 +294,7 @@ impl<'a, 'b, DB: DrawingBackend> ChartBuilder<'a, 'b, DB> {
Ok(ChartContext {
x_label_area,
y_label_area,
- drawing_area: drawing_area.apply_coord_spec(RangedCoord::new(
+ drawing_area: drawing_area.apply_coord_spec(Cartesian2d::new(
x_spec,
y_spec,
pixel_range,
@@ -271,6 +306,60 @@ impl<'a, 'b, DB: DrawingBackend> ChartBuilder<'a, 'b, DB> {
),
})
}
+
+ /// Build a 3 dimensional cartesian chart. The function will returns a chart
+ /// context, where data series can be rendered on.
+ /// - `x_spec`: The specification of X axis
+ /// - `y_spec`: The specification of Y axis
+ /// - `z_sepc`: The specification of Z axis
+ /// - Returns: A chart context
+ pub fn build_cartesian_3d<X: AsRangedCoord, Y: AsRangedCoord, Z: AsRangedCoord>(
+ &mut self,
+ x_spec: X,
+ y_spec: Y,
+ z_spec: Z,
+ ) -> Result<
+ ChartContext<'a, DB, Cartesian3d<X::CoordDescType, Y::CoordDescType, Z::CoordDescType>>,
+ DrawingAreaErrorKind<DB::ErrorType>,
+ > {
+ let mut drawing_area = DrawingArea::clone(self.root_area);
+
+ if *self.margin.iter().max().unwrap_or(&0) > 0 {
+ drawing_area = drawing_area.margin(
+ self.margin[0] as i32,
+ self.margin[1] as i32,
+ self.margin[2] as i32,
+ self.margin[3] as i32,
+ );
+ }
+
+ let (title_dx, title_dy) = if let Some((ref title, ref style)) = self.title {
+ let (origin_dx, origin_dy) = drawing_area.get_base_pixel();
+ drawing_area = drawing_area.titled(title, style.clone())?;
+ let (current_dx, current_dy) = drawing_area.get_base_pixel();
+ (current_dx - origin_dx, current_dy - origin_dy)
+ } else {
+ (0, 0)
+ };
+
+ let pixel_range = drawing_area.get_pixel_range();
+
+ Ok(ChartContext {
+ x_label_area: [None, None],
+ y_label_area: [None, None],
+ drawing_area: drawing_area.apply_coord_spec(Cartesian3d::new(
+ x_spec,
+ y_spec,
+ z_spec,
+ pixel_range,
+ )),
+ series_anno: vec![],
+ drawing_area_pos: (
+ title_dx + self.margin[2] as i32,
+ title_dy + self.margin[0] as i32,
+ ),
+ })
+ }
}
#[cfg(test)]
@@ -334,10 +423,7 @@ mod test {
assert_eq!(chart.title.as_ref().unwrap().0, "This is a test case");
assert_eq!(chart.title.as_ref().unwrap().1.font.get_name(), "serif");
assert_eq!(chart.title.as_ref().unwrap().1.font.get_size(), 10.0);
- assert_eq!(
- chart.title.as_ref().unwrap().1.color.to_rgba(),
- BLACK.to_rgba()
- );
+ check_color(chart.title.as_ref().unwrap().1.color, BLACK.to_rgba());
chart.caption("This is a test case", ("serif", 10));
assert_eq!(chart.title.as_ref().unwrap().1.font.get_name(), "serif");
diff --git a/src/chart/context.rs b/src/chart/context.rs
index 6f7d09e..eac4d53 100644
--- a/src/chart/context.rs
+++ b/src/chart/context.rs
@@ -1,74 +1,28 @@
use std::borrow::Borrow;
-use std::fmt::Debug;
-use std::marker::PhantomData;
use std::ops::Range;
-use std::sync::Arc;
-use super::dual_coord::DualCoordChartContext;
-use super::mesh::MeshStyle;
-use super::series::SeriesLabelStyle;
+use super::axes3d::Axes3dStyle;
+use super::{DualCoordChartContext, MeshStyle, SeriesAnno, SeriesLabelStyle};
+
+use crate::coord::cartesian::{Cartesian2d, Cartesian3d, MeshLine};
+use crate::coord::ranged1d::{AsRangedCoord, KeyPointHint, Ranged, ValueFormatter};
+use crate::coord::ranged3d::{ProjectionMatrix, ProjectionMatrixBuilder};
+use crate::coord::{CoordTranslate, ReverseCoordTranslate, Shift};
-use crate::coord::{
- AsRangedCoord, CoordTranslate, MeshLine, Ranged, RangedCoord, ReverseCoordTranslate, Shift,
-};
-use crate::drawing::backend::{BackendCoord, DrawingBackend};
use crate::drawing::{DrawingArea, DrawingAreaErrorKind};
-use crate::element::{Drawable, DynElement, IntoDynElement, PathElement, PointCollection};
+use crate::element::{Drawable, EmptyElement, PathElement, PointCollection, Polygon, Text};
use crate::style::text_anchor::{HPos, Pos, VPos};
-use crate::style::{AsRelative, FontTransform, ShapeStyle, SizeDesc, TextStyle};
-
-/// The annotations (such as the label of the series, the legend element, etc)
-/// When a series is drawn onto a drawing area, an series annotation object
-/// is created and a mutable reference is returned.
-#[allow(clippy::type_complexity)]
-pub struct SeriesAnno<'a, DB: DrawingBackend> {
- label: Option<String>,
- draw_func: Option<Box<dyn Fn(BackendCoord) -> DynElement<'a, DB, BackendCoord> + 'a>>,
- phantom_data: PhantomData<DB>,
-}
-
-impl<'a, DB: DrawingBackend> SeriesAnno<'a, DB> {
- pub(crate) fn get_label(&self) -> &str {
- self.label.as_ref().map(|x| x.as_str()).unwrap_or("")
- }
-
- pub(crate) fn get_draw_func(
- &self,
- ) -> Option<&dyn Fn(BackendCoord) -> DynElement<'a, DB, BackendCoord>> {
- self.draw_func.as_ref().map(|x| x.borrow())
- }
+use crate::style::{ShapeStyle, TextStyle};
- fn new() -> Self {
- Self {
- label: None,
- draw_func: None,
- phantom_data: PhantomData,
- }
- }
-
- /// Set the series label
- /// - `label`: The string would be use as label for current series
- pub fn label<L: Into<String>>(&mut self, label: L) -> &mut Self {
- self.label = Some(label.into());
- self
- }
-
- /// Set the legend element creator function
- /// - `func`: The function use to create the element
- /// *Note*: The creation function uses a shifted pixel-based coordinate system. And place the
- /// point (0,0) to the mid-right point of the shape
- pub fn legend<E: IntoDynElement<'a, DB, BackendCoord>, T: Fn(BackendCoord) -> E + 'a>(
- &mut self,
- func: T,
- ) -> &mut Self {
- self.draw_func = Some(Box::new(move |p| func(p).into_dyn()));
- self
- }
-}
+use plotters_backend::{BackendCoord, DrawingBackend, FontTransform};
/// The context of the chart. This is the core object of Plotters.
/// Any plot/chart is abstracted as this type, and any data series can be placed to the chart
/// context.
+///
+/// - To draw a series on a chart context, use [ChartContext::draw_series](struct.ChartContext.html#method.draw_series)
+/// - To draw a single element to the chart, you may want to use [ChartContext::plotting_area](struct.ChartContext.html#method.plotting_area)
+///
pub struct ChartContext<'a, DB: DrawingBackend, CT: CoordTranslate> {
pub(super) x_label_area: [Option<DrawingArea<DB, Shift>>; 2],
pub(super) y_label_area: [Option<DrawingArea<DB, Shift>>; 2],
@@ -77,111 +31,16 @@ pub struct ChartContext<'a, DB: DrawingBackend, CT: CoordTranslate> {
pub(super) drawing_area_pos: (i32, i32),
}
-/// A chart context state - This is the data that is needed to reconstruct the chart context
-/// without actually drawing the chart. This is useful when we want to do realtime rendering and
-/// want to incrementally update the chart.
-///
-/// For each frame, instead of updating the entire backend, we are able to keep the keep the figure
-/// component like axis, labels untouched and make updates only in the plotting drawing area.
-pub struct ChartState<CT: CoordTranslate> {
- drawing_area_pos: (i32, i32),
- drawing_area_size: (u32, u32),
- coord: CT,
-}
-
-impl<'a, CT: CoordTranslate + Clone> Clone for ChartState<CT> {
- fn clone(&self) -> Self {
- Self {
- drawing_area_size: self.drawing_area_size,
- drawing_area_pos: self.drawing_area_pos,
- coord: self.coord.clone(),
- }
- }
-}
-
-impl<'a, DB: DrawingBackend, CT: CoordTranslate> From<ChartContext<'a, DB, CT>> for ChartState<CT> {
- fn from(chart: ChartContext<'a, DB, CT>) -> ChartState<CT> {
- ChartState {
- drawing_area_pos: chart.drawing_area_pos,
- drawing_area_size: chart.drawing_area.dim_in_pixel(),
- coord: chart.drawing_area.into_coord_spec(),
- }
- }
-}
-
-impl<'a, DB: DrawingBackend, CT: CoordTranslate> ChartContext<'a, DB, CT> {
- /// Convert a chart context into a chart state, by doing so, the chart context is consumed and
- /// a saved chart state is created for later use.
- pub fn into_chart_state(self) -> ChartState<CT> {
- self.into()
- }
-
- /// Convert the chart context into a sharable chart state.
- /// Normally a chart state can not be clone, since the coordinate spec may not be able to be
- /// cloned. In this case, we can use an `Arc` get the coordinate wrapped thus the state can be
- /// cloned and shared by multiple chart context
- pub fn into_shared_chart_state(self) -> ChartState<Arc<CT>> {
- ChartState {
- drawing_area_pos: self.drawing_area_pos,
- drawing_area_size: self.drawing_area.dim_in_pixel(),
- coord: Arc::new(self.drawing_area.into_coord_spec()),
- }
- }
-}
-
-impl<'a, 'b, DB, CT> From<&ChartContext<'a, DB, CT>> for ChartState<CT>
+impl<'a, DB, XT, YT, X, Y> ChartContext<'a, DB, Cartesian2d<X, Y>>
where
DB: DrawingBackend,
- CT: CoordTranslate + Clone,
-{
- fn from(chart: &ChartContext<'a, DB, CT>) -> ChartState<CT> {
- ChartState {
- drawing_area_pos: chart.drawing_area_pos,
- drawing_area_size: chart.drawing_area.dim_in_pixel(),
- coord: chart.drawing_area.as_coord_spec().clone(),
- }
- }
-}
-
-impl<'a, DB: DrawingBackend, CT: CoordTranslate + Clone> ChartContext<'a, DB, CT> {
- /// Make the chart context, do not consume the chart context and clone the coordinate spec
- pub fn to_chart_state(&self) -> ChartState<CT> {
- self.into()
- }
-}
-
-impl<CT: CoordTranslate> ChartState<CT> {
- /// Restore the chart context on the given drawing area
- ///
- /// - `area`: The given drawing area where we want to restore the chart context
- /// - **returns** The newly created chart context
- pub fn restore<'a, DB: DrawingBackend>(
- self,
- area: &DrawingArea<DB, Shift>,
- ) -> ChartContext<'a, DB, CT> {
- let area = area
- .clone()
- .shrink(self.drawing_area_pos, self.drawing_area_size);
- ChartContext {
- x_label_area: [None, None],
- y_label_area: [None, None],
- drawing_area: area.apply_coord_spec(self.coord),
- series_anno: vec![],
- drawing_area_pos: self.drawing_area_pos,
- }
- }
-}
-
-impl<
- 'a,
- DB: DrawingBackend,
- XT: Debug,
- YT: Debug,
- X: Ranged<ValueType = XT>,
- Y: Ranged<ValueType = YT>,
- > ChartContext<'a, DB, RangedCoord<X, Y>>
+ X: Ranged<ValueType = XT> + ValueFormatter<XT>,
+ Y: Ranged<ValueType = YT> + ValueFormatter<YT>,
{
- fn is_overlapping_drawing_area(&self, area: Option<&DrawingArea<DB, Shift>>) -> bool {
+ pub(crate) fn is_overlapping_drawing_area(
+ &self,
+ area: Option<&DrawingArea<DB, Shift>>,
+ ) -> bool {
if let Some(area) = area {
let (x0, y0) = area.get_base_pixel();
let (w, h) = area.dim_in_pixel();
@@ -200,53 +59,26 @@ impl<
}
/// Initialize a mesh configuration object and mesh drawing can be finalized by calling
- /// the function `MeshStyle::draw`
- pub fn configure_mesh<'b>(&'b mut self) -> MeshStyle<'a, 'b, X, Y, DB> {
- let base_tick_size = (5u32).percent().max(5).in_pixels(&self.drawing_area);
-
- let mut x_tick_size = [base_tick_size, base_tick_size];
- let mut y_tick_size = [base_tick_size, base_tick_size];
-
- for idx in 0..2 {
- if self.is_overlapping_drawing_area(self.x_label_area[idx].as_ref()) {
- x_tick_size[idx] = -x_tick_size[idx];
- }
- if self.is_overlapping_drawing_area(self.y_label_area[idx].as_ref()) {
- y_tick_size[idx] = -y_tick_size[idx];
- }
- }
+ /// the function `MeshStyle::draw`.
+ pub fn configure_mesh(&mut self) -> MeshStyle<'a, '_, X, Y, DB> {
+ MeshStyle::new(self)
+ }
+}
- MeshStyle {
- parent_size: self.drawing_area.dim_in_pixel(),
- axis_style: None,
- x_label_offset: 0,
- y_label_offset: 0,
- draw_x_mesh: true,
- draw_y_mesh: true,
- draw_x_axis: true,
- draw_y_axis: true,
- n_x_labels: 10,
- n_y_labels: 10,
- line_style_1: None,
- line_style_2: None,
- x_label_style: None,
- y_label_style: None,
- format_x: &|x| format!("{:?}", x),
- format_y: &|y| format!("{:?}", y),
- target: Some(self),
- _phantom_data: PhantomData,
- x_desc: None,
- y_desc: None,
- axis_desc_style: None,
- x_tick_size,
- y_tick_size,
- }
+impl<'a, DB: DrawingBackend, CT: ReverseCoordTranslate> ChartContext<'a, DB, CT> {
+ /// Convert the chart context into an closure that can be used for coordinate translation
+ pub fn into_coord_trans(self) -> impl Fn(BackendCoord) -> Option<CT::From> {
+ let coord_spec = self.drawing_area.into_coord_spec();
+ move |coord| coord_spec.reverse_translate(coord)
}
}
-impl<'a, DB: DrawingBackend + 'a, CT: CoordTranslate> ChartContext<'a, DB, CT> {
+impl<'a, DB: DrawingBackend, CT: CoordTranslate> ChartContext<'a, DB, CT> {
/// Configure the styles for drawing series labels in the chart
- pub fn configure_series_labels<'b>(&'b mut self) -> SeriesLabelStyle<'a, 'b, DB, CT> {
+ pub fn configure_series_labels<'b>(&'b mut self) -> SeriesLabelStyle<'a, 'b, DB, CT>
+ where
+ DB: 'a,
+ {
SeriesLabelStyle::new(self)
}
@@ -254,29 +86,23 @@ impl<'a, DB: DrawingBackend + 'a, CT: CoordTranslate> ChartContext<'a, DB, CT> {
pub fn plotting_area(&self) -> &DrawingArea<DB, CT> {
&self.drawing_area
}
-}
-impl<'a, DB: DrawingBackend, CT: CoordTranslate> ChartContext<'a, DB, CT> {
+ /// Cast the reference to a chart context to a reference to underlying coordinate specification.
pub fn as_coord_spec(&self) -> &CT {
self.drawing_area.as_coord_spec()
}
-}
-
-impl<'a, DB: DrawingBackend, CT: ReverseCoordTranslate> ChartContext<'a, DB, CT> {
- /// Convert the chart context into an closure that can be used for coordinate translation
- pub fn into_coord_trans(self) -> impl Fn(BackendCoord) -> Option<CT::From> {
- let coord_spec = self.drawing_area.into_coord_spec();
- move |coord| coord_spec.reverse_translate(coord)
- }
-}
-impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Arc<RangedCoord<X, Y>>> {
+ // TODO: All draw_series_impl is overly strict about lifetime, because we don't have stable HKT,
+ // what we can ensure is for all lifetime 'b the element reference &'b E is a iterator
+ // of points reference with the same lifetime.
+ // However, this doesn't work if the coordinate doesn't live longer than the backend,
+ // this is unnecessarily strict
pub(super) fn draw_series_impl<E, R, S>(
&mut self,
series: S,
) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
where
- for<'b> &'b E: PointCollection<'b, (X::ValueType, Y::ValueType)>,
+ for<'b> &'b E: PointCollection<'b, CT::From>,
E: Drawable<DB>,
R: Borrow<E>,
S: IntoIterator<Item = R>,
@@ -299,7 +125,7 @@ impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Arc<Rang
series: S,
) -> Result<&mut SeriesAnno<'a, DB>, DrawingAreaErrorKind<DB::ErrorType>>
where
- for<'b> &'b E: PointCollection<'b, (X::ValueType, Y::ValueType)>,
+ for<'b> &'b E: PointCollection<'b, CT::From>,
E: Drawable<DB>,
R: Borrow<E>,
S: IntoIterator<Item = R>,
@@ -309,7 +135,7 @@ impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Arc<Rang
}
}
-impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, RangedCoord<X, Y>> {
+impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesian2d<X, Y>> {
/// Get the range of X axis
pub fn x_range(&self) -> Range<X::ValueType> {
self.drawing_area.get_x_range()
@@ -326,49 +152,12 @@ impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, RangedCo
self.drawing_area.map_coordinate(coord)
}
- pub(super) fn draw_series_impl<E, R, S>(
- &mut self,
- series: S,
- ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
- where
- for<'b> &'b E: PointCollection<'b, (X::ValueType, Y::ValueType)>,
- E: Drawable<DB>,
- R: Borrow<E>,
- S: IntoIterator<Item = R>,
- {
- for element in series {
- self.drawing_area.draw(element.borrow())?;
- }
- Ok(())
- }
-
- pub(super) fn alloc_series_anno(&mut self) -> &mut SeriesAnno<'a, DB> {
- let idx = self.series_anno.len();
- self.series_anno.push(SeriesAnno::new());
- &mut self.series_anno[idx]
- }
-
- /// Draw a data series. A data series in Plotters is abstracted as an iterator of elements
- pub fn draw_series<E, R, S>(
- &mut self,
- series: S,
- ) -> Result<&mut SeriesAnno<'a, DB>, DrawingAreaErrorKind<DB::ErrorType>>
- where
- for<'b> &'b E: PointCollection<'b, (X::ValueType, Y::ValueType)>,
- E: Drawable<DB>,
- R: Borrow<E>,
- S: IntoIterator<Item = R>,
- {
- self.draw_series_impl(series)?;
- Ok(self.alloc_series_anno())
- }
-
/// The actual function that draws the mesh lines.
/// It also returns the label that suppose to be there.
#[allow(clippy::type_complexity)]
- fn draw_mesh_lines<FmtLabel>(
+ fn draw_mesh_lines<FmtLabel, YH: KeyPointHint, XH: KeyPointHint>(
&mut self,
- (r, c): (usize, usize),
+ (r, c): (YH, XH),
(x_mesh, y_mesh): (bool, bool),
mesh_line_style: &ShapeStyle,
mut fmt_label: FmtLabel,
@@ -665,9 +454,9 @@ impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, RangedCo
}
#[allow(clippy::too_many_arguments)]
- pub(super) fn draw_mesh<FmtLabel>(
+ pub(super) fn draw_mesh<FmtLabel, YH: KeyPointHint, XH: KeyPointHint>(
&mut self,
- (r, c): (usize, usize),
+ (r, c): (YH, XH),
mesh_line_style: &ShapeStyle,
x_label_style: &TextStyle,
y_label_style: &TextStyle,
@@ -718,7 +507,8 @@ impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, RangedCo
Ok(())
}
- /// Convert this chart context into a dual axis chart context
+ /// Convert this chart context into a dual axis chart context and attach a second coordinate spec
+ /// on the chart context. For more detailed information, see documentation for [struct DualCoordChartContext](struct.DualCoordChartContext.html)
///
/// - `x_coord`: The coordinate spec for the X axis
/// - `y_coord`: The coordinate spec for the Y axis
@@ -731,13 +521,360 @@ impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, RangedCo
) -> DualCoordChartContext<
'a,
DB,
- RangedCoord<X, Y>,
- RangedCoord<SX::CoordDescType, SY::CoordDescType>,
+ Cartesian2d<X, Y>,
+ Cartesian2d<SX::CoordDescType, SY::CoordDescType>,
> {
let mut pixel_range = self.drawing_area.get_pixel_range();
pixel_range.1 = pixel_range.1.end..pixel_range.1.start;
- DualCoordChartContext::new(self, RangedCoord::new(x_coord, y_coord, pixel_range))
+ DualCoordChartContext::new(self, Cartesian2d::new(x_coord, y_coord, pixel_range))
+ }
+}
+
+pub(super) struct KeyPoints3d<X: Ranged, Y: Ranged, Z: Ranged> {
+ pub(super) x_points: Vec<X::ValueType>,
+ pub(super) y_points: Vec<Y::ValueType>,
+ pub(super) z_points: Vec<Z::ValueType>,
+}
+
+#[derive(Clone, Debug)]
+pub(super) enum Coord3D<X, Y, Z> {
+ X(X),
+ Y(Y),
+ Z(Z),
+}
+
+impl<X, Y, Z> Coord3D<X, Y, Z> {
+ fn get_x(&self) -> &X {
+ match self {
+ Coord3D::X(ret) => ret,
+ _ => panic!("Invalid call!"),
+ }
+ }
+ fn get_y(&self) -> &Y {
+ match self {
+ Coord3D::Y(ret) => ret,
+ _ => panic!("Invalid call!"),
+ }
+ }
+ fn get_z(&self) -> &Z {
+ match self {
+ Coord3D::Z(ret) => ret,
+ _ => panic!("Invalid call!"),
+ }
+ }
+
+ fn build_coord([x, y, z]: [&Self; 3]) -> (X, Y, Z)
+ where
+ X: Clone,
+ Y: Clone,
+ Z: Clone,
+ {
+ (x.get_x().clone(), y.get_y().clone(), z.get_z().clone())
+ }
+}
+impl<'a, DB, X, Y, Z, XT, YT, ZT> ChartContext<'a, DB, Cartesian3d<X, Y, Z>>
+where
+ DB: DrawingBackend,
+ X: Ranged<ValueType = XT> + ValueFormatter<XT>,
+ Y: Ranged<ValueType = YT> + ValueFormatter<YT>,
+ Z: Ranged<ValueType = ZT> + ValueFormatter<ZT>,
+{
+ pub fn configure_axes(&mut self) -> Axes3dStyle<'a, '_, X, Y, Z, DB> {
+ Axes3dStyle::new(self)
+ }
+}
+impl<'a, DB, X: Ranged, Y: Ranged, Z: Ranged> ChartContext<'a, DB, Cartesian3d<X, Y, Z>>
+where
+ DB: DrawingBackend,
+{
+ /// Override the 3D projection matrix. This function allows to override the default projection
+ /// matrix.
+ /// - `pf`: A function that takes the default projection matrix configuration and returns the
+ /// projection matrix. This function will allow you to adjust the pitch, yaw angle and the
+ /// centeral point of the projection, etc. You can also build a projection matrix which is not
+ /// relies on the default configuration as well.
+ pub fn with_projection<P: FnOnce(ProjectionMatrixBuilder) -> ProjectionMatrix>(
+ &mut self,
+ pf: P,
+ ) -> &mut Self {
+ let (actual_x, actual_y) = self.drawing_area.get_pixel_range();
+ self.drawing_area
+ .as_coord_spec_mut()
+ .set_projection(actual_x, actual_y, pf);
+ self
+ }
+}
+
+impl<'a, DB, X: Ranged, Y: Ranged, Z: Ranged> ChartContext<'a, DB, Cartesian3d<X, Y, Z>>
+where
+ DB: DrawingBackend,
+ X::ValueType: Clone,
+ Y::ValueType: Clone,
+ Z::ValueType: Clone,
+{
+ pub(super) fn get_key_points<XH: KeyPointHint, YH: KeyPointHint, ZH: KeyPointHint>(
+ &self,
+ x_hint: XH,
+ y_hint: YH,
+ z_hint: ZH,
+ ) -> KeyPoints3d<X, Y, Z> {
+ let coord = self.plotting_area().as_coord_spec();
+ let x_points = coord.logic_x.key_points(x_hint);
+ let y_points = coord.logic_y.key_points(y_hint);
+ let z_points = coord.logic_z.key_points(z_hint);
+ KeyPoints3d {
+ x_points,
+ y_points,
+ z_points,
+ }
+ }
+ pub(super) fn draw_axis_ticks(
+ &mut self,
+ axis: [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2],
+ labels: &[(
+ [Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3],
+ String,
+ )],
+ tick_size: i32,
+ style: ShapeStyle,
+ font: TextStyle,
+ ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> {
+ let coord = self.plotting_area().as_coord_spec();
+ let begin = coord.translate(&Coord3D::build_coord([
+ &axis[0][0],
+ &axis[0][1],
+ &axis[0][2],
+ ]));
+ let end = coord.translate(&Coord3D::build_coord([
+ &axis[1][0],
+ &axis[1][1],
+ &axis[1][2],
+ ]));
+ let axis_dir = (end.0 - begin.0, end.1 - begin.1);
+ let (x_range, y_range) = self.plotting_area().get_pixel_range();
+ let x_mid = (x_range.start + x_range.end) / 2;
+ let y_mid = (y_range.start + y_range.end) / 2;
+
+ let x_dir = if begin.0 < x_mid {
+ (-tick_size, 0)
+ } else {
+ (tick_size, 0)
+ };
+
+ let y_dir = if begin.1 < y_mid {
+ (0, -tick_size)
+ } else {
+ (0, tick_size)
+ };
+
+ let x_score = (x_dir.0 * axis_dir.0 + x_dir.1 * axis_dir.1).abs();
+ let y_score = (y_dir.0 * axis_dir.0 + y_dir.1 * axis_dir.1).abs();
+
+ let dir = if x_score < y_score { x_dir } else { y_dir };
+
+ for (pos, text) in labels {
+ let logic_pos = Coord3D::build_coord([&pos[0], &pos[1], &pos[2]]);
+ let mut font = font.clone();
+ if dir.0 < 0 {
+ font.pos = Pos::new(HPos::Right, VPos::Center);
+ } else if dir.0 > 0 {
+ font.pos = Pos::new(HPos::Left, VPos::Center);
+ };
+ if dir.1 < 0 {
+ font.pos = Pos::new(HPos::Center, VPos::Bottom);
+ } else if dir.1 > 0 {
+ font.pos = Pos::new(HPos::Center, VPos::Top);
+ };
+ let element = EmptyElement::at(logic_pos)
+ + PathElement::new(vec![(0, 0), dir], style.clone())
+ + Text::new(text.to_string(), (dir.0 * 2, dir.1 * 2), font.clone());
+ self.plotting_area().draw(&element)?;
+ }
+ Ok(())
+ }
+ pub(super) fn draw_axis(
+ &mut self,
+ idx: usize,
+ panels: &[[[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2]; 3],
+ style: ShapeStyle,
+ ) -> Result<
+ [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2],
+ DrawingAreaErrorKind<DB::ErrorType>,
+ > {
+ let coord = self.plotting_area().as_coord_spec();
+ let x_range = coord.logic_x.range();
+ let y_range = coord.logic_y.range();
+ let z_range = coord.logic_z.range();
+
+ let ranges: [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 2]; 3] = [
+ [Coord3D::X(x_range.start), Coord3D::X(x_range.end)],
+ [Coord3D::Y(y_range.start), Coord3D::Y(y_range.end)],
+ [Coord3D::Z(z_range.start), Coord3D::Z(z_range.end)],
+ ];
+
+ let (start, end) = {
+ let mut start = [&ranges[0][0], &ranges[1][0], &ranges[2][0]];
+ let mut end = [&ranges[0][1], &ranges[1][1], &ranges[2][1]];
+
+ let mut plan = vec![];
+
+ for i in 0..3 {
+ if i == idx {
+ continue;
+ }
+ start[i] = &panels[i][0][i];
+ end[i] = &panels[i][0][i];
+ for j in 0..3 {
+ if i != idx && i != j && j != idx {
+ for k in 0..2 {
+ start[j] = &panels[i][k][j];
+ end[j] = &panels[i][k][j];
+ plan.push((start, end));
+ }
+ }
+ }
+ }
+ plan.into_iter()
+ .min_by_key(|&(s, e)| {
+ let d = coord.projected_depth(s[0].get_x(), s[1].get_y(), s[2].get_z());
+ let d = d + coord.projected_depth(e[0].get_x(), e[1].get_y(), e[2].get_z());
+ let (_, y1) = coord.translate(&Coord3D::build_coord(s));
+ let (_, y2) = coord.translate(&Coord3D::build_coord(e));
+ let y = y1 + y2;
+ (d, y)
+ })
+ .unwrap()
+ };
+
+ self.plotting_area().draw(&PathElement::new(
+ vec![Coord3D::build_coord(start), Coord3D::build_coord(end)],
+ style.clone(),
+ ))?;
+
+ Ok([
+ [start[0].clone(), start[1].clone(), start[2].clone()],
+ [end[0].clone(), end[1].clone(), end[2].clone()],
+ ])
+ }
+ pub(super) fn draw_axis_panels(
+ &mut self,
+ bold_points: &KeyPoints3d<X, Y, Z>,
+ light_points: &KeyPoints3d<X, Y, Z>,
+ panel_style: ShapeStyle,
+ bold_grid_style: ShapeStyle,
+ light_grid_style: ShapeStyle,
+ ) -> Result<
+ [[[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2]; 3],
+ DrawingAreaErrorKind<DB::ErrorType>,
+ > {
+ let mut r_iter = (0..3).map(|idx| {
+ self.draw_axis_panel(
+ idx,
+ bold_points,
+ light_points,
+ panel_style.clone(),
+ bold_grid_style.clone(),
+ light_grid_style.clone(),
+ )
+ });
+ Ok([
+ r_iter.next().unwrap()?,
+ r_iter.next().unwrap()?,
+ r_iter.next().unwrap()?,
+ ])
+ }
+ fn draw_axis_panel(
+ &mut self,
+ idx: usize,
+ bold_points: &KeyPoints3d<X, Y, Z>,
+ light_points: &KeyPoints3d<X, Y, Z>,
+ panel_style: ShapeStyle,
+ bold_grid_style: ShapeStyle,
+ light_grid_style: ShapeStyle,
+ ) -> Result<
+ [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2],
+ DrawingAreaErrorKind<DB::ErrorType>,
+ > {
+ let coord = self.plotting_area().as_coord_spec();
+ let x_range = coord.logic_x.range();
+ let y_range = coord.logic_y.range();
+ let z_range = coord.logic_z.range();
+
+ let ranges: [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 2]; 3] = [
+ [Coord3D::X(x_range.start), Coord3D::X(x_range.end)],
+ [Coord3D::Y(y_range.start), Coord3D::Y(y_range.end)],
+ [Coord3D::Z(z_range.start), Coord3D::Z(z_range.end)],
+ ];
+
+ let (mut panel, start, end) = {
+ let a = [&ranges[0][0], &ranges[1][0], &ranges[2][0]];
+ let mut b = [&ranges[0][1], &ranges[1][1], &ranges[2][1]];
+ let mut c = a;
+ let d = b;
+
+ b[idx] = &ranges[idx][0];
+ c[idx] = &ranges[idx][1];
+
+ let (a, b) = if coord.projected_depth(a[0].get_x(), a[1].get_y(), a[2].get_z())
+ >= coord.projected_depth(c[0].get_x(), c[1].get_y(), c[2].get_z())
+ {
+ (a, b)
+ } else {
+ (c, d)
+ };
+
+ let mut m = a.clone();
+ m[(idx + 1) % 3] = b[(idx + 1) % 3];
+ let mut n = a.clone();
+ n[(idx + 2) % 3] = b[(idx + 2) % 3];
+
+ (
+ vec![
+ Coord3D::build_coord(a),
+ Coord3D::build_coord(m),
+ Coord3D::build_coord(b),
+ Coord3D::build_coord(n),
+ ],
+ a,
+ b,
+ )
+ };
+ self.plotting_area()
+ .draw(&Polygon::new(panel.clone(), panel_style.clone()))?;
+ panel.push(panel[0].clone());
+ self.plotting_area()
+ .draw(&PathElement::new(panel, bold_grid_style.clone()))?;
+
+ for (kps, style) in vec![
+ (light_points, light_grid_style),
+ (bold_points, bold_grid_style),
+ ]
+ .into_iter()
+ {
+ for idx in (0..3).filter(|&i| i != idx) {
+ let kps: Vec<_> = match idx {
+ 0 => kps.x_points.iter().map(|x| Coord3D::X(x.clone())).collect(),
+ 1 => kps.y_points.iter().map(|y| Coord3D::Y(y.clone())).collect(),
+ _ => kps.z_points.iter().map(|z| Coord3D::Z(z.clone())).collect(),
+ };
+ for kp in kps.iter() {
+ let mut kp_start = start;
+ let mut kp_end = end;
+ kp_start[idx] = kp;
+ kp_end[idx] = kp;
+ self.plotting_area().draw(&PathElement::new(
+ vec![Coord3D::build_coord(kp_start), Coord3D::build_coord(kp_end)],
+ style.clone(),
+ ))?;
+ }
+ }
+ }
+
+ Ok([
+ [start[0].clone(), start[1].clone(), start[2].clone()],
+ [end[0].clone(), end[1].clone(), end[2].clone()],
+ ])
}
}
@@ -757,7 +894,7 @@ mod test {
.y_label_area_size(20)
.set_label_area_size(LabelAreaPosition::Top, 20)
.set_label_area_size(LabelAreaPosition::Right, 20)
- .build_ranged(0..10, 0..10)
+ .build_cartesian_2d(0..10, 0..10)
.expect("Create chart")
.set_secondary_coord(0.0..1.0, 0.0..1.0);
@@ -789,4 +926,33 @@ mod test {
.draw()
.expect("Drawing error");
}
+
+ #[test]
+ fn test_chart_context_3d() {
+ let drawing_area = create_mocked_drawing_area(200, 200, |_| {});
+
+ drawing_area.fill(&WHITE).expect("Fill");
+
+ let mut chart = ChartBuilder::on(&drawing_area)
+ .caption("Test Title", ("serif", 10))
+ .x_label_area_size(20)
+ .y_label_area_size(20)
+ .set_label_area_size(LabelAreaPosition::Top, 20)
+ .set_label_area_size(LabelAreaPosition::Right, 20)
+ .build_cartesian_3d(0..10, 0..10, 0..10)
+ .expect("Create chart");
+
+ chart.with_projection(|mut pb| {
+ pb.yaw = 0.5;
+ pb.pitch = 0.5;
+ pb.scale = 0.5;
+ pb.into_matrix()
+ });
+
+ chart.configure_axes().draw().expect("Drawing axes");
+
+ chart
+ .draw_series(std::iter::once(Circle::new((5, 5, 5), 5, &RED)))
+ .expect("Drawing error");
+ }
}
diff --git a/src/chart/dual_coord.rs b/src/chart/dual_coord.rs
index dcc0ce8..9138f65 100644
--- a/src/chart/dual_coord.rs
+++ b/src/chart/dual_coord.rs
@@ -1,35 +1,46 @@
/// The dual coordinate system support
use std::borrow::{Borrow, BorrowMut};
-use std::fmt::Debug;
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
-use super::context::{ChartContext, ChartState, SeriesAnno};
use super::mesh::SecondaryMeshStyle;
+use super::{ChartContext, ChartState, SeriesAnno};
+
+use crate::coord::cartesian::Cartesian2d;
+use crate::coord::ranged1d::{Ranged, ValueFormatter};
+use crate::coord::{CoordTranslate, ReverseCoordTranslate, Shift};
-use crate::coord::{CoordTranslate, Ranged, RangedCoord, ReverseCoordTranslate, Shift};
-use crate::drawing::backend::{BackendCoord, DrawingBackend};
use crate::drawing::DrawingArea;
use crate::drawing::DrawingAreaErrorKind;
use crate::element::{Drawable, PointCollection};
-/// The chart context that has two coordinate system attached
+use plotters_backend::{BackendCoord, DrawingBackend};
+
+/// The chart context that has two coordinate system attached.
+/// This situation is quite common, for example, we with two different coodinate system.
+/// For instance this example <img src="https://plotters-rs.github.io/plotters-doc-data/twoscale.png"></img>
+/// This is done by attaching a second coordinate system to ChartContext by method [ChartContext::set_secondary_coord](struct.ChartContext.html#method.set_secondary_coord).
+/// For instance of dual coordinate charts, see [this example](https://github.com/38/plotters/blob/master/examples/two-scales.rs#L15).
+/// Note: `DualCoordChartContext` is always deref to the chart context.
+/// - If you want to configure the secondary axis, method [DualCoordChartContext::configure_secondary_axes](struct.DualCoordChartContext.html#method.configure_secondary_axes)
+/// - If you want to draw a series using secondary coordinate system, use [DualCoordChartContext::draw_secondary_series](struct.DualCoordChartContext.html#method.draw_secondary_series). And method [ChartContext::draw_series](struct.ChartContext.html#method.draw_series) will always use primary coordinate spec.
pub struct DualCoordChartContext<'a, DB: DrawingBackend, CT1: CoordTranslate, CT2: CoordTranslate> {
pub(super) primary: ChartContext<'a, DB, CT1>,
pub(super) secondary: ChartContext<'a, DB, CT2>,
}
/// The chart state for a dual coord chart, see the detailed description for `ChartState` for more
-/// information about the purpose of a chart state
+/// information about the purpose of a chart state.
+/// Similar to [ChartState](struct.ChartState.html), but used for the dual coordinate charts.
pub struct DualCoordChartState<CT1: CoordTranslate, CT2: CoordTranslate> {
primary: ChartState<CT1>,
secondary: ChartState<CT2>,
}
-impl<'a, DB: DrawingBackend, CT1: CoordTranslate, CT2: CoordTranslate>
- DualCoordChartContext<'a, DB, CT1, CT2>
+impl<DB: DrawingBackend, CT1: CoordTranslate, CT2: CoordTranslate>
+ DualCoordChartContext<'_, DB, CT1, CT2>
{
- /// Convert the chart context into a chart state
+ /// Convert the chart context into a chart state, similar to [ChartContext::into_chart_state](struct.ChartContext.html#method.into_chart_state)
pub fn into_chart_state(self) -> DualCoordChartState<CT1, CT2> {
DualCoordChartState {
primary: self.primary.into(),
@@ -37,23 +48,20 @@ impl<'a, DB: DrawingBackend, CT1: CoordTranslate, CT2: CoordTranslate>
}
}
- /// Convert the chart context into a sharable chart state
+ /// Convert the chart context into a sharable chart state.
pub fn into_shared_chart_state(self) -> DualCoordChartState<Arc<CT1>, Arc<CT2>> {
DualCoordChartState {
primary: self.primary.into_shared_chart_state(),
secondary: self.secondary.into_shared_chart_state(),
}
}
-}
-impl<'a, DB: DrawingBackend, CT1: CoordTranslate, CT2: CoordTranslate>
- DualCoordChartContext<'a, DB, CT1, CT2>
-where
- CT1: Clone,
- CT2: Clone,
-{
- /// Copy the coordinate specs and make the chart state
- pub fn to_chart_state(&self) -> DualCoordChartState<CT1, CT2> {
+ /// Copy the coordinate specs and make a chart state
+ pub fn to_chart_state(&self) -> DualCoordChartState<CT1, CT2>
+ where
+ CT1: Clone,
+ CT2: Clone,
+ {
DualCoordChartState {
primary: self.primary.to_chart_state(),
secondary: self.secondary.to_chart_state(),
@@ -63,10 +71,10 @@ where
impl<CT1: CoordTranslate, CT2: CoordTranslate> DualCoordChartState<CT1, CT2> {
/// Restore the chart state on the given drawing area
- pub fn restore<'a, DB: DrawingBackend + 'a>(
+ pub fn restore<DB: DrawingBackend>(
self,
area: &DrawingArea<DB, Shift>,
- ) -> DualCoordChartContext<'a, DB, CT1, CT2> {
+ ) -> DualCoordChartContext<'_, DB, CT1, CT2> {
let primary = self.primary.restore(area);
let secondary = self
.secondary
@@ -75,18 +83,18 @@ impl<CT1: CoordTranslate, CT2: CoordTranslate> DualCoordChartState<CT1, CT2> {
}
}
-impl<'a, DB: DrawingBackend, CT1: CoordTranslate, CT2: CoordTranslate>
- From<DualCoordChartContext<'a, DB, CT1, CT2>> for DualCoordChartState<CT1, CT2>
+impl<DB: DrawingBackend, CT1: CoordTranslate, CT2: CoordTranslate>
+ From<DualCoordChartContext<'_, DB, CT1, CT2>> for DualCoordChartState<CT1, CT2>
{
- fn from(chart: DualCoordChartContext<'a, DB, CT1, CT2>) -> DualCoordChartState<CT1, CT2> {
+ fn from(chart: DualCoordChartContext<'_, DB, CT1, CT2>) -> DualCoordChartState<CT1, CT2> {
chart.into_chart_state()
}
}
-impl<'a, 'b, DB: DrawingBackend, CT1: CoordTranslate + Clone, CT2: CoordTranslate + Clone>
- From<&'b DualCoordChartContext<'a, DB, CT1, CT2>> for DualCoordChartState<CT1, CT2>
+impl<'b, DB: DrawingBackend, CT1: CoordTranslate + Clone, CT2: CoordTranslate + Clone>
+ From<&'b DualCoordChartContext<'_, DB, CT1, CT2>> for DualCoordChartState<CT1, CT2>
{
- fn from(chart: &'b DualCoordChartContext<'a, DB, CT1, CT2>) -> DualCoordChartState<CT1, CT2> {
+ fn from(chart: &'b DualCoordChartContext<'_, DB, CT1, CT2>) -> DualCoordChartState<CT1, CT2> {
chart.to_chart_state()
}
}
@@ -129,8 +137,8 @@ impl<'a, DB: DrawingBackend, CT1: CoordTranslate, CT2: CoordTranslate>
}
}
-impl<'a, DB: DrawingBackend, CT1: CoordTranslate, CT2: ReverseCoordTranslate>
- DualCoordChartContext<'a, DB, CT1, CT2>
+impl<DB: DrawingBackend, CT1: CoordTranslate, CT2: ReverseCoordTranslate>
+ DualCoordChartContext<'_, DB, CT1, CT2>
{
/// Convert the chart context into the secondary coordinate translation function
pub fn into_secondary_coord_trans(self) -> impl Fn(BackendCoord) -> Option<CT2::From> {
@@ -139,8 +147,8 @@ impl<'a, DB: DrawingBackend, CT1: CoordTranslate, CT2: ReverseCoordTranslate>
}
}
-impl<'a, DB: DrawingBackend, CT1: ReverseCoordTranslate, CT2: ReverseCoordTranslate>
- DualCoordChartContext<'a, DB, CT1, CT2>
+impl<DB: DrawingBackend, CT1: ReverseCoordTranslate, CT2: ReverseCoordTranslate>
+ DualCoordChartContext<'_, DB, CT1, CT2>
{
/// Convert the chart context into a pair of closures that maps the pixel coordinate into the
/// logical coordinate for both primary coordinate system and secondary coordinate system.
@@ -159,11 +167,18 @@ impl<'a, DB: DrawingBackend, CT1: ReverseCoordTranslate, CT2: ReverseCoordTransl
}
}
-impl<'a, DB: DrawingBackend, CT1: CoordTranslate, SX: Ranged, SY: Ranged>
- DualCoordChartContext<'a, DB, CT1, RangedCoord<SX, SY>>
+impl<
+ 'a,
+ DB: DrawingBackend,
+ CT1: CoordTranslate,
+ XT,
+ YT,
+ SX: Ranged<ValueType = XT>,
+ SY: Ranged<ValueType = YT>,
+ > DualCoordChartContext<'a, DB, CT1, Cartesian2d<SX, SY>>
where
- SX::ValueType: Debug,
- SY::ValueType: Debug,
+ SX: ValueFormatter<XT>,
+ SY: ValueFormatter<YT>,
{
/// Start configure the style for the secondary axes
pub fn configure_secondary_axes<'b>(&'b mut self) -> SecondaryMeshStyle<'a, 'b, SX, SY, DB> {
@@ -172,14 +187,9 @@ where
}
impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged, SX: Ranged, SY: Ranged>
- DualCoordChartContext<'a, DB, RangedCoord<X, Y>, RangedCoord<SX, SY>>
-where
- X::ValueType: Debug,
- Y::ValueType: Debug,
- SX::ValueType: Debug,
- SY::ValueType: Debug,
+ DualCoordChartContext<'a, DB, Cartesian2d<X, Y>, Cartesian2d<SX, SY>>
{
- /// Draw a series use the secondary coordinate system
+ /// Draw a series use the secondary coordinate system.
/// - `series`: The series to draw
/// - `Returns` the series annotation object or error code
pub fn draw_secondary_series<E, R, S>(
diff --git a/src/chart/mesh.rs b/src/chart/mesh.rs
index 8f96168..223bcbe 100644
--- a/src/chart/mesh.rs
+++ b/src/chart/mesh.rs
@@ -1,27 +1,29 @@
-use std::fmt::Debug;
use std::marker::PhantomData;
use super::builder::LabelAreaPosition;
use super::context::ChartContext;
-use crate::coord::{MeshLine, Ranged, RangedCoord};
-use crate::drawing::backend::DrawingBackend;
+use crate::coord::cartesian::{Cartesian2d, MeshLine};
+use crate::coord::ranged1d::{BoldPoints, LightPoints, Ranged, ValueFormatter};
use crate::drawing::DrawingAreaErrorKind;
use crate::style::{
AsRelative, Color, FontDesc, FontFamily, FontStyle, IntoTextStyle, RGBColor, ShapeStyle,
SizeDesc, TextStyle,
};
+use plotters_backend::DrawingBackend;
+
/// The style used to describe the mesh and axis for a secondary coordinate system.
pub struct SecondaryMeshStyle<'a, 'b, X: Ranged, Y: Ranged, DB: DrawingBackend> {
style: MeshStyle<'a, 'b, X, Y, DB>,
}
-impl<'a, 'b, X: Ranged, Y: Ranged, DB: DrawingBackend> SecondaryMeshStyle<'a, 'b, X, Y, DB>
+impl<'a, 'b, XT, YT, X: Ranged<ValueType = XT>, Y: Ranged<ValueType = YT>, DB: DrawingBackend>
+ SecondaryMeshStyle<'a, 'b, X, Y, DB>
where
- X::ValueType: Debug,
- Y::ValueType: Debug,
+ X: ValueFormatter<XT>,
+ Y: ValueFormatter<YT>,
{
- pub(super) fn new(target: &'b mut ChartContext<'a, DB, RangedCoord<X, Y>>) -> Self {
+ pub(super) fn new(target: &'b mut ChartContext<'a, DB, Cartesian2d<X, Y>>) -> Self {
let mut style = target.configure_mesh();
style.draw_x_mesh = false;
style.draw_y_mesh = false;
@@ -36,7 +38,8 @@ where
}
/// The offset of x labels. This is used when we want to place the label in the middle of
- /// the grid. This is useful if we are drawing a histogram
+ /// the grid. This is used to adjust label position for histograms, but since plotters 0.3, this
+ /// use case is deprecated, see [CentricDiscreteRanged coord decorator](../coord/trait.IntoCentric.html) for more details
/// - `value`: The offset in pixel
pub fn x_label_offset<S: SizeDesc>(&mut self, value: S) -> &mut Self {
self.style.x_label_offset(value);
@@ -44,7 +47,8 @@ where
}
/// The offset of y labels. This is used when we want to place the label in the middle of
- /// the grid. This is useful if we are drawing a histogram
+ /// the grid. This is used to adjust label position for histograms, but since plotters 0.3, this
+ /// use case is deprecated, see [CentricDiscreteRanged coord decorator](../coord/trait.IntoCentric.html) for more details
/// - `value`: The offset in pixel
pub fn y_label_offset<S: SizeDesc>(&mut self, value: S) -> &mut Self {
self.style.y_label_offset(value);
@@ -137,10 +141,7 @@ where
}
/// The struct that is used for tracking the configuration of a mesh of any chart
-pub struct MeshStyle<'a, 'b, X: Ranged, Y: Ranged, DB>
-where
- DB: DrawingBackend,
-{
+pub struct MeshStyle<'a, 'b, X: Ranged, Y: Ranged, DB: DrawingBackend> {
pub(super) parent_size: (u32, u32),
pub(super) draw_x_mesh: bool,
pub(super) draw_y_mesh: bool,
@@ -153,19 +154,68 @@ where
pub(super) axis_desc_style: Option<TextStyle<'b>>,
pub(super) x_desc: Option<String>,
pub(super) y_desc: Option<String>,
- pub(super) line_style_1: Option<ShapeStyle>,
- pub(super) line_style_2: Option<ShapeStyle>,
+ pub(super) bold_line_style: Option<ShapeStyle>,
+ pub(super) light_line_style: Option<ShapeStyle>,
pub(super) axis_style: Option<ShapeStyle>,
pub(super) x_label_style: Option<TextStyle<'b>>,
pub(super) y_label_style: Option<TextStyle<'b>>,
pub(super) format_x: &'b dyn Fn(&X::ValueType) -> String,
pub(super) format_y: &'b dyn Fn(&Y::ValueType) -> String,
- pub(super) target: Option<&'b mut ChartContext<'a, DB, RangedCoord<X, Y>>>,
+ pub(super) target: Option<&'b mut ChartContext<'a, DB, Cartesian2d<X, Y>>>,
pub(super) _phantom_data: PhantomData<(X, Y)>,
pub(super) x_tick_size: [i32; 2],
pub(super) y_tick_size: [i32; 2],
}
+impl<'a, 'b, X, Y, XT, YT, DB> MeshStyle<'a, 'b, X, Y, DB>
+where
+ X: Ranged<ValueType = XT> + ValueFormatter<XT>,
+ Y: Ranged<ValueType = YT> + ValueFormatter<YT>,
+ DB: DrawingBackend,
+{
+ pub(crate) fn new(chart: &'b mut ChartContext<'a, DB, Cartesian2d<X, Y>>) -> Self {
+ let base_tick_size = (5u32).percent().max(5).in_pixels(chart.plotting_area());
+
+ let mut x_tick_size = [base_tick_size, base_tick_size];
+ let mut y_tick_size = [base_tick_size, base_tick_size];
+
+ for idx in 0..2 {
+ if chart.is_overlapping_drawing_area(chart.x_label_area[idx].as_ref()) {
+ x_tick_size[idx] = -x_tick_size[idx];
+ }
+ if chart.is_overlapping_drawing_area(chart.y_label_area[idx].as_ref()) {
+ y_tick_size[idx] = -y_tick_size[idx];
+ }
+ }
+
+ MeshStyle {
+ parent_size: chart.drawing_area.dim_in_pixel(),
+ axis_style: None,
+ x_label_offset: 0,
+ y_label_offset: 0,
+ draw_x_mesh: true,
+ draw_y_mesh: true,
+ draw_x_axis: true,
+ draw_y_axis: true,
+ n_x_labels: 10,
+ n_y_labels: 10,
+ bold_line_style: None,
+ light_line_style: None,
+ x_label_style: None,
+ y_label_style: None,
+ format_x: &X::format,
+ format_y: &Y::format,
+ target: Some(chart),
+ _phantom_data: PhantomData,
+ x_desc: None,
+ y_desc: None,
+ axis_desc_style: None,
+ x_tick_size,
+ y_tick_size,
+ }
+ }
+}
+
impl<'a, 'b, X, Y, DB> MeshStyle<'a, 'b, X, Y, DB>
where
X: Ranged,
@@ -201,7 +251,8 @@ where
}
/// The offset of x labels. This is used when we want to place the label in the middle of
- /// the grid. This is useful if we are drawing a histogram
+ /// the grid. This is used to adjust label position for histograms, but since plotters 0.3, this
+ /// use case is deprecated, see [CentricDiscreteRanged coord decorator](../coord/trait.IntoCentric.html) for more details
/// - `value`: The offset in pixel
pub fn x_label_offset<S: SizeDesc>(&mut self, value: S) -> &mut Self {
self.x_label_offset = value.in_pixels(&self.parent_size);
@@ -209,7 +260,8 @@ where
}
/// The offset of y labels. This is used when we want to place the label in the middle of
- /// the grid. This is useful if we are drawing a histogram
+ /// the grid. This is used to adjust label position for histograms, but since plotters 0.3, this
+ /// use case is deprecated, see [CentricDiscreteRanged coord decorator](../coord/trait.IntoCentric.html) for more details
/// - `value`: The offset in pixel
pub fn y_label_offset<S: SizeDesc>(&mut self, value: S) -> &mut Self {
self.y_label_offset = value.in_pixels(&self.parent_size);
@@ -272,15 +324,15 @@ where
/// Set the style for the coarse grind grid
/// - `style`: This is the coarse grind grid style
- pub fn line_style_1<T: Into<ShapeStyle>>(&mut self, style: T) -> &mut Self {
- self.line_style_1 = Some(style.into());
+ pub fn bold_line_style<T: Into<ShapeStyle>>(&mut self, style: T) -> &mut Self {
+ self.bold_line_style = Some(style.into());
self
}
/// Set the style for the fine grind grid
/// - `style`: The fine grind grid style
- pub fn line_style_2<T: Into<ShapeStyle>>(&mut self, style: T) -> &mut Self {
- self.line_style_2 = Some(style.into());
+ pub fn light_line_style<T: Into<ShapeStyle>>(&mut self, style: T) -> &mut Self {
+ self.light_line_style = Some(style.into());
self
}
@@ -357,12 +409,12 @@ where
FontStyle::Normal,
);
- let mesh_style_1 = self
- .line_style_1
+ let bold_style = self
+ .bold_line_style
.clone()
.unwrap_or_else(|| (&default_mesh_color_1).into());
- let mesh_style_2 = self
- .line_style_2
+ let light_style = self
+ .light_line_style
.clone()
.unwrap_or_else(|| (&default_mesh_color_2).into());
let axis_style = self
@@ -386,8 +438,11 @@ where
.unwrap_or_else(|| x_label_style.clone());
target.draw_mesh(
- (self.n_y_labels * 10, self.n_x_labels * 10),
- &mesh_style_2,
+ (
+ LightPoints::new(self.n_y_labels, self.n_y_labels * 10),
+ LightPoints::new(self.n_x_labels, self.n_x_labels * 10),
+ ),
+ &light_style,
&x_label_style,
&y_label_style,
|_| None,
@@ -406,8 +461,8 @@ where
)?;
target.draw_mesh(
- (self.n_y_labels, self.n_x_labels),
- &mesh_style_1,
+ (BoldPoints(self.n_y_labels), BoldPoints(self.n_x_labels)),
+ &bold_style,
&x_label_style,
&y_label_style,
|m| match m {
diff --git a/src/chart/mod.rs b/src/chart/mod.rs
index edb0902..4a88029 100644
--- a/src/chart/mod.rs
+++ b/src/chart/mod.rs
@@ -12,14 +12,19 @@ In Plotters, a series is abstracted as an iterator of elements.
detailed description for each struct.
*/
+mod axes3d;
mod builder;
mod context;
mod dual_coord;
mod mesh;
mod series;
+mod state;
pub use builder::{ChartBuilder, LabelAreaPosition};
-pub use context::{ChartContext, ChartState, SeriesAnno};
+pub use context::ChartContext;
pub use dual_coord::{DualCoordChartContext, DualCoordChartState};
-pub use mesh::MeshStyle;
-pub use series::{SeriesLabelPosition, SeriesLabelStyle};
+pub use mesh::{MeshStyle, SecondaryMeshStyle};
+pub use series::{SeriesAnno, SeriesLabelPosition, SeriesLabelStyle};
+pub use state::ChartState;
+
+use context::Coord3D;
diff --git a/src/chart/series.rs b/src/chart/series.rs
index 51fe97d..efced77 100644
--- a/src/chart/series.rs
+++ b/src/chart/series.rs
@@ -1,10 +1,59 @@
use super::ChartContext;
use crate::coord::CoordTranslate;
-use crate::drawing::backend::{BackendCoord, DrawingErrorKind};
-use crate::drawing::{DrawingAreaErrorKind, DrawingBackend};
-use crate::element::{EmptyElement, IntoDynElement, MultiLineText, Rectangle};
+use crate::drawing::DrawingAreaErrorKind;
+use crate::element::{DynElement, EmptyElement, IntoDynElement, MultiLineText, Rectangle};
use crate::style::{IntoFont, IntoTextStyle, ShapeStyle, SizeDesc, TextStyle, TRANSPARENT};
+use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
+
+type SeriesAnnoDrawFn<'a, DB> = dyn Fn(BackendCoord) -> DynElement<'a, DB, BackendCoord> + 'a;
+
+/// The annotations (such as the label of the series, the legend element, etc)
+/// When a series is drawn onto a drawing area, an series annotation object
+/// is created and a mutable reference is returned.
+pub struct SeriesAnno<'a, DB: DrawingBackend> {
+ label: Option<String>,
+ draw_func: Option<Box<SeriesAnnoDrawFn<'a, DB>>>,
+}
+
+impl<'a, DB: DrawingBackend> SeriesAnno<'a, DB> {
+ #[allow(clippy::option_as_ref_deref)]
+ pub(crate) fn get_label(&self) -> &str {
+ // TODO: Change this when we bump the MSRV
+ self.label.as_ref().map(|x| x.as_str()).unwrap_or("")
+ }
+
+ pub(crate) fn get_draw_func(&self) -> Option<&SeriesAnnoDrawFn<'a, DB>> {
+ self.draw_func.as_ref().map(|x| x.as_ref())
+ }
+
+ pub(crate) fn new() -> Self {
+ Self {
+ label: None,
+ draw_func: None,
+ }
+ }
+
+ /// Set the series label
+ /// - `label`: The string would be use as label for current series
+ pub fn label<L: Into<String>>(&mut self, label: L) -> &mut Self {
+ self.label = Some(label.into());
+ self
+ }
+
+ /// Set the legend element creator function
+ /// - `func`: The function use to create the element
+ /// *Note*: The creation function uses a shifted pixel-based coordinate system. And place the
+ /// point (0,0) to the mid-right point of the shape
+ pub fn legend<E: IntoDynElement<'a, DB, BackendCoord>, T: Fn(BackendCoord) -> E + 'a>(
+ &mut self,
+ func: T,
+ ) -> &mut Self {
+ self.draw_func = Some(Box::new(move |p| func(p).into_dyn()));
+ self
+ }
+}
+
/// Describes where we want to put the series label
pub enum SeriesLabelPosition {
UpperLeft,
@@ -148,9 +197,9 @@ impl<'a, 'b, DB: DrawingBackend + 'a, CT: CoordTranslate> SeriesLabelStyle<'a, '
label_element.push_line(label_text);
}
- let (mut w, mut h) = label_element
- .estimate_dimension()
- .map_err(|e| DrawingAreaErrorKind::BackendError(DrawingErrorKind::FontError(e)))?;
+ let (mut w, mut h) = label_element.estimate_dimension().map_err(|e| {
+ DrawingAreaErrorKind::BackendError(DrawingErrorKind::FontError(Box::new(e)))
+ })?;
let margin = self.margin as i32;
@@ -178,7 +227,9 @@ impl<'a, 'b, DB: DrawingBackend + 'a, CT: CoordTranslate> SeriesLabelStyle<'a, '
for (((_, y0), (_, y1)), make_elem) in label_element
.compute_line_layout()
- .map_err(|e| DrawingAreaErrorKind::BackendError(DrawingErrorKind::FontError(e)))?
+ .map_err(|e| {
+ DrawingAreaErrorKind::BackendError(DrawingErrorKind::FontError(Box::new(e)))
+ })?
.into_iter()
.zip(funcs.into_iter())
{
diff --git a/src/chart/state.rs b/src/chart/state.rs
new file mode 100644
index 0000000..9383d44
--- /dev/null
+++ b/src/chart/state.rs
@@ -0,0 +1,112 @@
+use std::sync::Arc;
+
+use super::ChartContext;
+use crate::coord::{CoordTranslate, Shift};
+use crate::drawing::DrawingArea;
+use plotters_backend::DrawingBackend;
+
+/// A chart context state - This is the data that is needed to reconstruct the chart context
+/// without actually drawing the chart. This is useful when we want to do realtime rendering and
+/// want to incrementally update the chart.
+///
+/// For each frame, instead of updating the entire backend, we are able to keep the keep the figure
+/// component like axis, labels untouched and make updates only in the plotting drawing area.
+/// This is very useful for incremental render.
+/// ```rust
+/// use plotters::prelude::*;
+/// let mut buffer = vec![0u8;1024*768*3];
+/// let area = BitMapBackend::with_buffer(&mut buffer[..], (1024, 768))
+/// .into_drawing_area()
+/// .split_evenly((1,2));
+/// let chart = ChartBuilder::on(&area[0])
+/// .caption("Incremental Example", ("sans-serif", 20))
+/// .set_all_label_area_size(30)
+/// .build_ranged(0..10, 0..10)
+/// .expect("Unable to build ChartContext");
+/// // Draw the first frame at this point
+/// area[0].present().expect("Present");
+/// let state = chart.into_chart_state();
+/// // Let's draw the second frame
+/// let chart = state.restore(&area[0]);
+/// chart.plotting_area().fill(&WHITE).unwrap(); // Clear the previously drawn graph
+/// // At this point, you are able to draw next frame
+///```
+#[derive(Clone)]
+pub struct ChartState<CT: CoordTranslate> {
+ drawing_area_pos: (i32, i32),
+ drawing_area_size: (u32, u32),
+ coord: CT,
+}
+
+impl<'a, DB: DrawingBackend, CT: CoordTranslate> From<ChartContext<'a, DB, CT>> for ChartState<CT> {
+ fn from(chart: ChartContext<'a, DB, CT>) -> ChartState<CT> {
+ ChartState {
+ drawing_area_pos: chart.drawing_area_pos,
+ drawing_area_size: chart.drawing_area.dim_in_pixel(),
+ coord: chart.drawing_area.into_coord_spec(),
+ }
+ }
+}
+
+impl<'a, DB: DrawingBackend, CT: CoordTranslate> ChartContext<'a, DB, CT> {
+ /// Convert a chart context into a chart state, by doing so, the chart context is consumed and
+ /// a saved chart state is created for later use. This is typically used in incrmental rendering. See documentation of `ChartState` for more detailed example.
+ pub fn into_chart_state(self) -> ChartState<CT> {
+ self.into()
+ }
+
+ /// Convert the chart context into a sharable chart state.
+ /// Normally a chart state can not be clone, since the coordinate spec may not be able to be
+ /// cloned. In this case, we can use an `Arc` get the coordinate wrapped thus the state can be
+ /// cloned and shared by multiple chart context
+ pub fn into_shared_chart_state(self) -> ChartState<Arc<CT>> {
+ ChartState {
+ drawing_area_pos: self.drawing_area_pos,
+ drawing_area_size: self.drawing_area.dim_in_pixel(),
+ coord: Arc::new(self.drawing_area.into_coord_spec()),
+ }
+ }
+}
+
+impl<'a, 'b, DB, CT> From<&ChartContext<'a, DB, CT>> for ChartState<CT>
+where
+ DB: DrawingBackend,
+ CT: CoordTranslate + Clone,
+{
+ fn from(chart: &ChartContext<'a, DB, CT>) -> ChartState<CT> {
+ ChartState {
+ drawing_area_pos: chart.drawing_area_pos,
+ drawing_area_size: chart.drawing_area.dim_in_pixel(),
+ coord: chart.drawing_area.as_coord_spec().clone(),
+ }
+ }
+}
+
+impl<'a, DB: DrawingBackend, CT: CoordTranslate + Clone> ChartContext<'a, DB, CT> {
+ /// Make the chart context, do not consume the chart context and clone the coordinate spec
+ pub fn to_chart_state(&self) -> ChartState<CT> {
+ self.into()
+ }
+}
+
+impl<CT: CoordTranslate> ChartState<CT> {
+ /// Restore the chart context on the given drawing area
+ ///
+ /// - `area`: The given drawing area where we want to restore the chart context
+ /// - **returns** The newly created chart context
+ pub fn restore<'a, DB: DrawingBackend>(
+ self,
+ area: &DrawingArea<DB, Shift>,
+ ) -> ChartContext<'a, DB, CT> {
+ let area = area
+ .clone()
+ .shrink(self.drawing_area_pos, self.drawing_area_size);
+ ChartContext {
+ x_label_area: [None, None],
+ y_label_area: [None, None],
+ drawing_area: area.apply_coord_spec(self.coord),
+ series_anno: vec![],
+ drawing_area_pos: self.drawing_area_pos,
+ }
+ }
+}
diff --git a/src/coord/category.rs b/src/coord/category.rs
deleted file mode 100644
index 805bad2..0000000
--- a/src/coord/category.rs
+++ /dev/null
@@ -1,209 +0,0 @@
-use std::fmt;
-use std::ops::Range;
-use std::rc::Rc;
-
-use super::{AsRangedCoord, Ranged};
-
-/// The category coordinate
-pub struct Category<T: PartialEq> {
- name: String,
- elements: Rc<Vec<T>>,
- // i32 type is required for the empty ref (having -1 value)
- idx: i32,
-}
-
-impl<T: PartialEq> Clone for Category<T> {
- fn clone(&self) -> Self {
- Category {
- name: self.name.clone(),
- elements: Rc::clone(&self.elements),
- idx: self.idx,
- }
- }
-}
-
-impl<T: PartialEq + fmt::Display> fmt::Debug for Category<T> {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let element = &self.elements[self.idx as usize];
- write!(f, "{}", element)
- }
-}
-
-impl<T: PartialEq> Category<T> {
- /// Create a new category coordinate.
- ///
- /// - `name`: The name of the category
- /// - `elements`: The vector of category elements
- /// - **returns** The newly created category coordinate
- ///
- /// ```rust
- /// use plotters::prelude::*;
- ///
- /// let category = Category::new("color", vec!["red", "green", "blue"]);
- /// ```
- pub fn new<S: Into<String>>(name: S, elements: Vec<T>) -> Self {
- Self {
- name: name.into(),
- elements: Rc::new(elements),
- idx: -1,
- }
- }
-
- /// Get an element reference (tick) by its value.
- ///
- /// - `val`: The value of the element
- /// - **returns** The optional reference
- ///
- /// ```rust
- /// use plotters::prelude::*;
- ///
- /// let category = Category::new("color", vec!["red", "green", "blue"]);
- /// let red = category.get(&"red");
- /// assert!(red.is_some());
- /// let unknown = category.get(&"unknown");
- /// assert!(unknown.is_none());
- /// ```
- pub fn get(&self, val: &T) -> Option<Category<T>> {
- match self.elements.iter().position(|x| x == val) {
- Some(pos) => {
- let element_ref = Category {
- name: self.name.clone(),
- elements: Rc::clone(&self.elements),
- idx: pos as i32,
- };
- Some(element_ref)
- }
- _ => None,
- }
- }
-
- /// Create a full range over the category elements.
- ///
- /// - **returns** The range including all category elements
- ///
- /// ```rust
- /// use plotters::prelude::*;
- ///
- /// let category = Category::new("color", vec!["red", "green", "blue"]);
- /// let range = category.range();
- /// ```
- pub fn range(&self) -> Self {
- self.clone()
- }
-
- /// Get the number of elements in the category.
- ///
- /// - **returns** The number of elements
- ///
- /// ```rust
- /// use plotters::prelude::*;
- ///
- /// let category = Category::new("color", vec!["red", "green", "blue"]);
- /// assert_eq!(category.len(), 3);
- /// ```
- pub fn len(&self) -> usize {
- self.elements.len()
- }
-
- /// Returns `true` if the category contains no elements.
- ///
- /// - **returns** `true` is no elements, otherwise - `false`
- ///
- /// ```rust
- /// use plotters::prelude::*;
- ///
- /// let category = Category::new("color", vec!["red", "green", "blue"]);
- /// assert_eq!(category.is_empty(), false);
- ///
- /// let category = Category::new("empty", Vec::<&str>::new());
- /// assert_eq!(category.is_empty(), true);
- /// ```
- pub fn is_empty(&self) -> bool {
- self.elements.is_empty()
- }
-
- /// Get the category name.
- ///
- /// - **returns** The name of the category
- ///
- /// ```rust
- /// use plotters::prelude::*;
- ///
- /// let category = Category::new("color", vec!["red", "green", "blue"]);
- /// assert_eq!(category.name(), "color");
- /// ```
- pub fn name(&self) -> String {
- self.name.clone()
- }
-}
-
-impl<T: PartialEq> Ranged for Category<T> {
- type ValueType = Category<T>;
-
- fn range(&self) -> Range<Category<T>> {
- let mut left = self.clone();
- let mut right = self.clone();
- left.idx = 0;
- right.idx = right.len() as i32 - 1;
- left..right
- }
-
- fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
- // Add margins to spans as edge values are not applicable to category
- let total_span = (self.len() + 1) as f64;
- let value_span = f64::from(value.idx + 1);
- (f64::from(limit.1 - limit.0) * value_span / total_span) as i32 + limit.0
- }
-
- fn key_points(&self, max_points: usize) -> Vec<Self::ValueType> {
- let mut ret = vec![];
- let intervals = (self.len() - 1) as f64;
- let elements = &self.elements;
- let name = &self.name;
- let step = (intervals / max_points as f64 + 1.0) as usize;
- for idx in (0..self.len()).step_by(step) {
- ret.push(Category {
- name: name.clone(),
- elements: Rc::clone(&elements),
- idx: idx as i32,
- });
- }
- ret
- }
-}
-
-impl<T: PartialEq> AsRangedCoord for Category<T> {
- type CoordDescType = Self;
- type Value = Category<T>;
-}
-
-#[cfg(test)]
-mod test {
- use super::*;
-
- #[test]
- fn test_clone_trait() {
- let category = Category::new("color", vec!["red", "green", "blue"]);
- let red = category.get(&"red").unwrap();
- assert_eq!(red.idx, 0);
- let clone = red.clone();
- assert_eq!(clone.idx, 0);
- }
-
- #[test]
- fn test_debug_trait() {
- let category = Category::new("color", vec!["red", "green", "blue"]);
- let red = category.get(&"red").unwrap();
- assert_eq!(format!("{:?}", red), "red");
- }
-
- #[test]
- fn test_ranged_trait() {
- let category = Category::new("color", vec!["red", "green", "blue"]);
- assert_eq!(category.map(&category.get(&"red").unwrap(), (0, 8)), 2);
- assert_eq!(category.map(&category.get(&"green").unwrap(), (0, 8)), 4);
- assert_eq!(category.map(&category.get(&"blue").unwrap(), (0, 8)), 6);
- assert_eq!(category.key_points(3).len(), 3);
- assert_eq!(category.key_points(5).len(), 3);
- }
-}
diff --git a/src/coord/mod.rs b/src/coord/mod.rs
index 0afafa3..5cc1708 100644
--- a/src/coord/mod.rs
+++ b/src/coord/mod.rs
@@ -1,90 +1,58 @@
/*!
-Coordinate system abstractions.
-Coordinate systems can be attached to drawing areas. By doing so,
-the drawing area can draw elements in the guest coordinate system.
-`DrawingArea::apply_coord_spec` is used to attach new coordinate system
-to the drawing area.
+One of the key features of Plotters is flexible coordinate system abstraction and this module
+provides all the abstraction used for the coordinate abstarction of Plotters.
-`CoordTranslate` is the trait required by `DrawingArea::apply_coord_spec`. It provides
-the forward coordinate translation: from the logic coordinate to the pixel-based absolute
-backend coordinate system.
+Generally speaking, the coordinate system in Plotters is responsible for mapping logic data points into
+pixel based backend coordinate. This task is abstracted by a simple trait called
+[CoordTranslate](trait.CoordTranslate.html). Please note `CoordTranslate` trait doesn't assume any property
+about the coordinate values, thus we are able to extend Plotters's coordinate system to other types of coorindate
+easily.
-When the coordinate type implements `ReverseCoordTranslate`,
-the backward translation is possible, which allows mapping pixel-based coordinate into
-the logic coordinate. It's not usually used for static figure rendering, but may be useful
-for a interactive figure.
+Another important trait is [ReverseCoordTranslate](trait.ReverseCoordTranslate.html). This trait allows some coordinate
+retrieve the logic value based on the pixel-based backend coordinate. This is particularly interesting for interactive plots.
-`RangedCoord` is the 2D cartesian coordinate system that has two `Ranged` axis.
-A ranged axis can be logarithmic and by applying an logarithmic axis, the figure is logarithmic scale.
-Also, the ranged axis can be deserted, and this is required by the histogram series.
+Plotters contains a set of pre-defined coordinate specifications that fulfills the most common use. See documentation for
+module [types](types/index.html) for details about the basic 1D types.
+
+The coordinate system also can be tweaked by the coordinate combinators, such as logarithmic coordinate, nested coordinate, etc.
+See documentation for module [combinators](combinators/index.html) for details.
+
+Currently we support the following 2D coordinate system:
+
+- 2-dimensional Cartesian Coordinate: This is done by the combinator [Cartesian2d](cartesian/struct.Cartesian2d.html).
*/
-use crate::drawing::backend::BackendCoord;
-
-mod category;
-#[cfg(feature = "chrono")]
-mod datetime;
-mod logarithmic;
-mod numeric;
-mod ranged;
-
-#[cfg(feature = "chrono")]
-pub use datetime::{IntoMonthly, IntoYearly, RangedDate, RangedDateTime, RangedDuration};
-pub use numeric::{
- RangedCoordf32, RangedCoordf64, RangedCoordi128, RangedCoordi32, RangedCoordi64,
- RangedCoordu128, RangedCoordu32, RangedCoordu64,
-};
-pub use ranged::{
- AsRangedCoord, DiscreteRanged, IntoCentric, IntoPartialAxis, MeshLine, Ranged, RangedCoord,
- ReversibleRanged,
-};
-
-pub use ranged::make_partial_axis;
-
-pub use logarithmic::{LogCoord, LogRange, LogScalable};
-
-pub use numeric::group_integer_by::{GroupBy, ToGroupByRange};
-use std::rc::Rc;
-use std::sync::Arc;
-
-pub use category::Category;
-
-/// The trait that translates some customized object to the backend coordinate
-pub trait CoordTranslate {
- type From;
-
- /// Translate the guest coordinate to the guest coordinate
- fn translate(&self, from: &Self::From) -> BackendCoord;
-}
-impl<T: CoordTranslate> CoordTranslate for Rc<T> {
- type From = T::From;
+use plotters_backend::BackendCoord;
- fn translate(&self, from: &Self::From) -> BackendCoord {
- self.as_ref().translate(from)
- }
-}
+pub mod ranged1d;
-impl<T: CoordTranslate> CoordTranslate for Arc<T> {
- type From = T::From;
+/// The coordinate combinators
+///
+/// Coordinate combinators are very important part of Plotters' coordinate system.
+/// The combinator is more about the "combinator pattern", which takes one or more coordinate specification
+/// and transform them into a new coordinate specification.
+pub mod combinators {
+ pub use super::ranged1d::combinators::*;
+}
- fn translate(&self, from: &Self::From) -> BackendCoord {
- self.as_ref().translate(from)
- }
+/// The primitive types supported by Plotters coordinate system
+pub mod types {
+ pub use super::ranged1d::types::*;
}
-/// The trait indicates that the coordinate system supports reverse transform
-/// This is useful when we need an interactive plot, thus we need to map the event
-/// from the backend coordinate to the logical coordinate
-pub trait ReverseCoordTranslate: CoordTranslate {
- /// Reverse translate the coordinate from the drawing coordinate to the
- /// logic coordinate.
- /// Note: the return value is an option, because it's possible that the drawing
- /// coordinate isn't able to be represented in te guest coordinate system
- fn reverse_translate(&self, input: BackendCoord) -> Option<Self::From>;
+mod ranged2d;
+pub mod ranged3d;
+
+pub mod cartesian {
+ pub use super::ranged2d::cartesian::{Cartesian2d, MeshLine};
+ pub use super::ranged3d::Cartesian3d;
}
+mod translate;
+pub use translate::{CoordTranslate, ReverseCoordTranslate};
+
/// The coordinate translation that only impose shift
#[derive(Debug, Clone)]
pub struct Shift(pub BackendCoord);
@@ -101,20 +69,3 @@ impl ReverseCoordTranslate for Shift {
Some((input.0 - (self.0).0, input.1 - (self.0).1))
}
}
-
-/// We can compose an arbitrary transformation with a shift
-pub struct ShiftAndTrans<T: CoordTranslate>(Shift, T);
-
-impl<T: CoordTranslate> CoordTranslate for ShiftAndTrans<T> {
- type From = T::From;
- fn translate(&self, from: &Self::From) -> BackendCoord {
- let temp = self.1.translate(from);
- self.0.translate(&temp)
- }
-}
-
-impl<T: ReverseCoordTranslate> ReverseCoordTranslate for ShiftAndTrans<T> {
- fn reverse_translate(&self, input: BackendCoord) -> Option<T::From> {
- Some(self.1.reverse_translate(self.0.reverse_translate(input)?)?)
- }
-}
diff --git a/src/coord/ranged.rs b/src/coord/ranged.rs
deleted file mode 100644
index 2291854..0000000
--- a/src/coord/ranged.rs
+++ /dev/null
@@ -1,397 +0,0 @@
-use super::{CoordTranslate, ReverseCoordTranslate};
-use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
-use crate::style::ShapeStyle;
-
-use std::ops::Range;
-
-/// The trait that indicates we have a ordered and ranged value
-/// Which is used to describe the axis
-pub trait Ranged {
- /// The type of this value
- type ValueType;
-
- /// This function maps the value to i32, which is the drawing coordinate
- fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32;
-
- /// This function gives the key points that we can draw a grid based on this
- fn key_points(&self, max_points: usize) -> Vec<Self::ValueType>;
-
- /// Get the range of this value
- fn range(&self) -> Range<Self::ValueType>;
-
- /// This function provides the on-axis part of its range
- #[allow(clippy::range_plus_one)]
- fn axis_pixel_range(&self, limit: (i32, i32)) -> Range<i32> {
- if limit.0 < limit.1 {
- limit.0..limit.1
- } else {
- (limit.1 + 1)..(limit.0 + 1)
- }
- }
-}
-
-/// The trait indicates the ranged value can be map reversely, which means
-/// an pixel-based coordinate is given, it's possible to figure out the underlying
-/// logic value.
-pub trait ReversibleRanged: Ranged {
- fn unmap(&self, input: i32, limit: (i32, i32)) -> Option<Self::ValueType>;
-}
-
-/// The coordinate described by two ranged value
-pub struct RangedCoord<X: Ranged, Y: Ranged> {
- logic_x: X,
- logic_y: Y,
- back_x: (i32, i32),
- back_y: (i32, i32),
-}
-
-impl<X: Ranged + Clone, Y: Ranged + Clone> Clone for RangedCoord<X, Y> {
- fn clone(&self) -> Self {
- Self {
- logic_x: self.logic_x.clone(),
- logic_y: self.logic_y.clone(),
- back_x: self.back_x,
- back_y: self.back_y,
- }
- }
-}
-
-impl<X: Ranged, Y: Ranged> RangedCoord<X, Y> {
- /// Create a new ranged value coordinate system
- pub fn new<IntoX: Into<X>, IntoY: Into<Y>>(
- logic_x: IntoX,
- logic_y: IntoY,
- actual: (Range<i32>, Range<i32>),
- ) -> Self {
- Self {
- logic_x: logic_x.into(),
- logic_y: logic_y.into(),
- back_x: (actual.0.start, actual.0.end),
- back_y: (actual.1.start, actual.1.end),
- }
- }
-
- /// Draw the mesh for the coordinate system
- pub fn draw_mesh<E, DrawMesh: FnMut(MeshLine<X, Y>) -> Result<(), E>>(
- &self,
- h_limit: usize,
- v_limit: usize,
- mut draw_mesh: DrawMesh,
- ) -> Result<(), E> {
- let (xkp, ykp) = (
- self.logic_x.key_points(v_limit),
- self.logic_y.key_points(h_limit),
- );
-
- for logic_x in xkp {
- let x = self.logic_x.map(&logic_x, self.back_x);
- draw_mesh(MeshLine::XMesh(
- (x, self.back_y.0),
- (x, self.back_y.1),
- &logic_x,
- ))?;
- }
-
- for logic_y in ykp {
- let y = self.logic_y.map(&logic_y, self.back_y);
- draw_mesh(MeshLine::YMesh(
- (self.back_x.0, y),
- (self.back_x.1, y),
- &logic_y,
- ))?;
- }
-
- Ok(())
- }
-
- /// Get the range of X axis
- pub fn get_x_range(&self) -> Range<X::ValueType> {
- self.logic_x.range()
- }
-
- /// Get the range of Y axis
- pub fn get_y_range(&self) -> Range<Y::ValueType> {
- self.logic_y.range()
- }
-
- pub fn get_x_axis_pixel_range(&self) -> Range<i32> {
- self.logic_x.axis_pixel_range(self.back_x)
- }
-
- pub fn get_y_axis_pixel_range(&self) -> Range<i32> {
- self.logic_y.axis_pixel_range(self.back_y)
- }
-
- pub fn x_spec(&self) -> &X {
- &self.logic_x
- }
-
- pub fn y_spec(&self) -> &Y {
- &self.logic_y
- }
-}
-
-impl<X: Ranged, Y: Ranged> CoordTranslate for RangedCoord<X, Y> {
- type From = (X::ValueType, Y::ValueType);
-
- fn translate(&self, from: &Self::From) -> BackendCoord {
- (
- self.logic_x.map(&from.0, self.back_x),
- self.logic_y.map(&from.1, self.back_y),
- )
- }
-}
-
-impl<X: ReversibleRanged, Y: ReversibleRanged> ReverseCoordTranslate for RangedCoord<X, Y> {
- fn reverse_translate(&self, input: BackendCoord) -> Option<Self::From> {
- Some((
- self.logic_x.unmap(input.0, self.back_x)?,
- self.logic_y.unmap(input.1, self.back_y)?,
- ))
- }
-}
-
-/// Represent a coordinate mesh for the two ranged value coordinate system
-pub enum MeshLine<'a, X: Ranged, Y: Ranged> {
- XMesh(BackendCoord, BackendCoord, &'a X::ValueType),
- YMesh(BackendCoord, BackendCoord, &'a Y::ValueType),
-}
-
-impl<'a, X: Ranged, Y: Ranged> MeshLine<'a, X, Y> {
- /// Draw a single mesh line onto the backend
- pub fn draw<DB: DrawingBackend>(
- &self,
- backend: &mut DB,
- style: &ShapeStyle,
- ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
- let (&left, &right) = match self {
- MeshLine::XMesh(a, b, _) => (a, b),
- MeshLine::YMesh(a, b, _) => (a, b),
- };
- backend.draw_line(left, right, style)
- }
-}
-
-/// The trait indicates the coordinate is discrete, so that we can draw histogram on it
-pub trait DiscreteRanged
-where
- Self: Ranged,
-{
- type RangeParameter;
-
- fn get_range_parameter(&self) -> Self::RangeParameter;
-
- /// Get the smallest value that is larger than the `this` value
- fn next_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType;
-
- /// Get the largest value that is smaller than `this` value
- fn previous_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType;
-}
-
-/// The trait for the type that can be converted into a ranged coordinate axis
-pub trait AsRangedCoord: Sized {
- type CoordDescType: Ranged<ValueType = Self::Value> + From<Self>;
- type Value;
-}
-
-impl<T> AsRangedCoord for T
-where
- T: Ranged,
- Range<T::ValueType>: Into<T>,
-{
- type CoordDescType = T;
- type Value = T::ValueType;
-}
-
-/// The axis decorator that makes key-point in the center of the value range
-/// This is useful when we draw a histogram, since we want the axis value label
-/// to be shown in the middle of the range rather than exactly the location where
-/// the value mapped to.
-pub struct CentricDiscreteRange<D: DiscreteRanged>(D)
-where
- <D as Ranged>::ValueType: Eq;
-
-/// The trait for types that can decorated by `CentricDiscreteRange` decorator
-pub trait IntoCentric: AsRangedCoord
-where
- Self::CoordDescType: DiscreteRanged,
- <Self::CoordDescType as Ranged>::ValueType: Eq,
-{
- /// Convert current ranged value into a centric ranged value
- fn into_centric(self) -> CentricDiscreteRange<Self::CoordDescType> {
- CentricDiscreteRange(self.into())
- }
-}
-
-impl<T: AsRangedCoord> IntoCentric for T
-where
- T::CoordDescType: DiscreteRanged,
- <Self::CoordDescType as Ranged>::ValueType: Eq,
-{
-}
-
-impl<D: DiscreteRanged + Clone> Clone for CentricDiscreteRange<D>
-where
- <D as Ranged>::ValueType: Eq,
-{
- fn clone(&self) -> Self {
- Self(self.0.clone())
- }
-}
-
-impl<D: DiscreteRanged> Ranged for CentricDiscreteRange<D>
-where
- <D as Ranged>::ValueType: Eq,
-{
- type ValueType = <D as Ranged>::ValueType;
-
- fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
- let prev = <D as DiscreteRanged>::previous_value(&value, &self.0.get_range_parameter());
- (self.0.map(&prev, limit) + self.0.map(value, limit)) / 2
- }
-
- fn key_points(&self, max_points: usize) -> Vec<Self::ValueType> {
- self.0.key_points(max_points)
- }
-
- fn range(&self) -> Range<Self::ValueType> {
- self.0.range()
- }
-}
-
-impl<D: DiscreteRanged> DiscreteRanged for CentricDiscreteRange<D>
-where
- <D as Ranged>::ValueType: Eq,
-{
- type RangeParameter = <D as DiscreteRanged>::RangeParameter;
- fn get_range_parameter(&self) -> Self::RangeParameter {
- self.0.get_range_parameter()
- }
- fn next_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType {
- <D as DiscreteRanged>::next_value(this, param)
- }
-
- fn previous_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType {
- <D as DiscreteRanged>::previous_value(this, param)
- }
-}
-
-impl<D: DiscreteRanged> AsRangedCoord for CentricDiscreteRange<D>
-where
- <D as Ranged>::ValueType: Eq,
-{
- type CoordDescType = Self;
- type Value = <Self as Ranged>::ValueType;
-}
-
-/// This axis decorator will make the axis partially display on the axis.
-/// At some time, we want the axis only covers some part of the value.
-/// This decorator will have an additional display range defined.
-pub struct PartialAxis<R: Ranged>(R, Range<R::ValueType>);
-
-/// The trait for the types that can be converted into a partial axis
-pub trait IntoPartialAxis: AsRangedCoord {
- /// Make the partial axis
- ///
- /// - `axis_range`: The range of the axis to be displayed
- /// - **returns**: The converted range specification
- fn partial_axis(
- self,
- axis_range: Range<<Self::CoordDescType as Ranged>::ValueType>,
- ) -> PartialAxis<Self::CoordDescType> {
- PartialAxis(self.into(), axis_range)
- }
-}
-
-impl<R: AsRangedCoord> IntoPartialAxis for R {}
-
-impl<R: Ranged + Clone> Clone for PartialAxis<R>
-where
- <R as Ranged>::ValueType: Clone,
-{
- fn clone(&self) -> Self {
- PartialAxis(self.0.clone(), self.1.clone())
- }
-}
-
-impl<R: Ranged> Ranged for PartialAxis<R>
-where
- R::ValueType: Clone,
-{
- type ValueType = R::ValueType;
-
- fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
- self.0.map(value, limit)
- }
-
- fn key_points(&self, max_points: usize) -> Vec<Self::ValueType> {
- self.0.key_points(max_points)
- }
-
- fn range(&self) -> Range<Self::ValueType> {
- self.0.range()
- }
-
- fn axis_pixel_range(&self, limit: (i32, i32)) -> Range<i32> {
- let left = self.map(&self.1.start, limit);
- let right = self.map(&self.1.end, limit);
-
- left.min(right)..left.max(right)
- }
-}
-
-impl<R: DiscreteRanged> DiscreteRanged for PartialAxis<R>
-where
- R: Ranged,
- <R as Ranged>::ValueType: Eq + Clone,
-{
- type RangeParameter = <R as DiscreteRanged>::RangeParameter;
- fn get_range_parameter(&self) -> Self::RangeParameter {
- self.0.get_range_parameter()
- }
- fn next_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType {
- <R as DiscreteRanged>::next_value(this, param)
- }
-
- fn previous_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType {
- <R as DiscreteRanged>::previous_value(this, param)
- }
-}
-
-impl<R: Ranged> AsRangedCoord for PartialAxis<R>
-where
- <R as Ranged>::ValueType: Clone,
-{
- type CoordDescType = Self;
- type Value = <Self as Ranged>::ValueType;
-}
-
-/// Make a partial axis based on the percentage of visible portion.
-/// We can use `into_partial_axis` to create a partial axis range specification.
-/// But sometimes, we want to directly specify the percentage visible to the user.
-///
-/// - `axis_range`: The range specification
-/// - `part`: The visible part of the axis. Each value is from [0.0, 1.0]
-/// - **returns**: The partial axis created from the input, or `None` when not possible
-pub fn make_partial_axis<T>(
- axis_range: Range<T>,
- part: Range<f64>,
-) -> Option<PartialAxis<<Range<T> as AsRangedCoord>::CoordDescType>>
-where
- Range<T>: AsRangedCoord,
- T: num_traits::NumCast + Clone,
-{
- let left: f64 = num_traits::cast(axis_range.start.clone())?;
- let right: f64 = num_traits::cast(axis_range.end.clone())?;
-
- let full_range_size = (right - left) / (part.end - part.start);
-
- let full_left = left - full_range_size * part.start;
- let full_right = right + full_range_size * (1.0 - part.end);
-
- let full_range: Range<T> = num_traits::cast(full_left)?..num_traits::cast(full_right)?;
-
- let axis_range: <Range<T> as AsRangedCoord>::CoordDescType = axis_range.into();
-
- Some(PartialAxis(full_range.into(), axis_range.range()))
-}
diff --git a/src/coord/ranged1d/combinators/ckps.rs b/src/coord/ranged1d/combinators/ckps.rs
new file mode 100644
index 0000000..bca1d87
--- /dev/null
+++ b/src/coord/ranged1d/combinators/ckps.rs
@@ -0,0 +1,266 @@
+// The customized coordinate combinators.
+// This file contains a set of coorindate combinators that allows you determine the
+// keypoint by your own code.
+use std::ops::Range;
+
+use crate::coord::ranged1d::{AsRangedCoord, DiscreteRanged, KeyPointHint, Ranged};
+
+/// The coordinate decorator that binds a key point vector.
+/// Normally, all the ranged coordinate implements its own keypoint algorithm
+/// to determine how to render the tick mark and mesh grid.
+/// This decorator allows customized tick mark specifiied by vector.
+/// See [BindKeyPoints::with_key_points](trait.BindKeyPoints.html#tymethod.with_key_points)
+/// for details.
+/// Note: For any coordinate spec wrapped by this decorator, the maxium number of labels configured by
+/// MeshStyle will be ignored and the key point function will always returns the entire vector
+pub struct WithKeyPoints<Inner: Ranged> {
+ inner: Inner,
+ bold_points: Vec<Inner::ValueType>,
+ light_points: Vec<Inner::ValueType>,
+}
+
+impl<I: Ranged> WithKeyPoints<I> {
+ /// Specify the light key points, which is used to render the light mesh line
+ pub fn with_light_points<T: IntoIterator<Item = I::ValueType>>(mut self, iter: T) -> Self {
+ self.light_points.clear();
+ self.light_points.extend(iter);
+ self
+ }
+
+ /// Get a reference to the bold points
+ pub fn bold_points(&self) -> &[I::ValueType] {
+ self.bold_points.as_ref()
+ }
+
+ /// Get a mut reference to the bold points
+ pub fn bold_points_mut(&mut self) -> &mut [I::ValueType] {
+ self.bold_points.as_mut()
+ }
+
+ /// Get a reference to light key points
+ pub fn light_points(&self) -> &[I::ValueType] {
+ self.light_points.as_ref()
+ }
+
+ /// Get a mut reference to the light key points
+ pub fn light_points_mut(&mut self) -> &mut [I::ValueType] {
+ self.light_points.as_mut()
+ }
+}
+
+impl<R: Ranged> Ranged for WithKeyPoints<R>
+where
+ R::ValueType: Clone,
+{
+ type ValueType = R::ValueType;
+ type FormatOption = R::FormatOption;
+
+ fn range(&self) -> Range<Self::ValueType> {
+ self.inner.range()
+ }
+
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
+ self.inner.map(value, limit)
+ }
+
+ fn key_points<Hint: KeyPointHint>(&self, hint: Hint) -> Vec<Self::ValueType> {
+ if hint.weight().allow_light_points() {
+ self.light_points.clone()
+ } else {
+ self.bold_points.clone()
+ }
+ }
+
+ fn axis_pixel_range(&self, limit: (i32, i32)) -> Range<i32> {
+ self.inner.axis_pixel_range(limit)
+ }
+}
+
+impl<R: DiscreteRanged> DiscreteRanged for WithKeyPoints<R>
+where
+ R::ValueType: Clone,
+{
+ fn size(&self) -> usize {
+ self.inner.size()
+ }
+ fn index_of(&self, value: &Self::ValueType) -> Option<usize> {
+ self.inner.index_of(value)
+ }
+ fn from_index(&self, index: usize) -> Option<Self::ValueType> {
+ self.inner.from_index(index)
+ }
+}
+
+pub trait BindKeyPoints
+where
+ Self: AsRangedCoord,
+{
+ /// Bind a existing coordinate spec with a given key points vector. See [WithKeyPoints](struct.WithKeyPoints.html ) for more details.
+ /// Example:
+ /// ```
+ ///use plotters::prelude::*;
+ ///use plotters_bitmap::BitMapBackend;
+ ///let mut buffer = vec![0;1024*768*3];
+ /// let root = BitMapBackend::with_buffer(&mut buffer, (1024, 768)).into_drawing_area();
+ /// let mut chart = ChartBuilder::on(&root)
+ /// .build_ranged(
+ /// (0..100).with_key_points(vec![1,20,50,90]), // <= This line will make the plot shows 4 tick marks at 1, 20, 50, 90
+ /// 0..10
+ /// ).unwrap();
+ /// chart.configure_mesh().draw().unwrap();
+ ///```
+ fn with_key_points(self, points: Vec<Self::Value>) -> WithKeyPoints<Self::CoordDescType> {
+ WithKeyPoints {
+ inner: self.into(),
+ bold_points: points,
+ light_points: vec![],
+ }
+ }
+}
+
+impl<T: AsRangedCoord> BindKeyPoints for T {}
+
+/// The coordinate decorator that allows customized keypoint algorithms.
+/// Normally, all the coordinate spec implements its own key point algorith
+/// But this decorator allows you override the pre-defined key point algorithm.
+///
+/// To use this decorator, see [BindKeyPointMethod::with_key_point_func](trait.BindKeyPointMethod.html#tymethod.with_key_point_func)
+pub struct WithKeyPointMethod<R: Ranged> {
+ inner: R,
+ bold_func: Box<dyn Fn(usize) -> Vec<R::ValueType>>,
+ light_func: Box<dyn Fn(usize) -> Vec<R::ValueType>>,
+}
+
+pub trait BindKeyPointMethod
+where
+ Self: AsRangedCoord,
+{
+ /// Bind a existing coordinate spec with a given key points algorithm. See [WithKeyPointMethod](struct.WithKeyMethod.html ) for more details.
+ /// Example:
+ /// ```
+ ///use plotters::prelude::*;
+ ///use plotters_bitmap::BitMapBackend;
+ ///let mut buffer = vec![0;1024*768*3];
+ /// let root = BitMapBackend::with_buffer(&mut buffer, (1024, 768)).into_drawing_area();
+ /// let mut chart = ChartBuilder::on(&root)
+ /// .build_ranged(
+ /// (0..100).with_key_point_func(|n| (0..100 / n as i32).map(|x| x * 100 / n as i32).collect()),
+ /// 0..10
+ /// ).unwrap();
+ /// chart.configure_mesh().draw().unwrap();
+ ///```
+ fn with_key_point_func<F: Fn(usize) -> Vec<Self::Value> + 'static>(
+ self,
+ func: F,
+ ) -> WithKeyPointMethod<Self::CoordDescType> {
+ WithKeyPointMethod {
+ inner: self.into(),
+ bold_func: Box::new(func),
+ light_func: Box::new(|_| vec![]),
+ }
+ }
+}
+
+impl<T: AsRangedCoord> BindKeyPointMethod for T {}
+
+impl<R: Ranged> WithKeyPointMethod<R> {
+ /// Define the light key point algorithm, by default this returns an empty set
+ pub fn with_light_point_func<F: Fn(usize) -> Vec<R::ValueType> + 'static>(
+ mut self,
+ func: F,
+ ) -> Self {
+ self.light_func = Box::new(func);
+ self
+ }
+}
+
+impl<R: Ranged> Ranged for WithKeyPointMethod<R> {
+ type ValueType = R::ValueType;
+ type FormatOption = R::FormatOption;
+
+ fn range(&self) -> Range<Self::ValueType> {
+ self.inner.range()
+ }
+
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
+ self.inner.map(value, limit)
+ }
+
+ fn key_points<Hint: KeyPointHint>(&self, hint: Hint) -> Vec<Self::ValueType> {
+ if hint.weight().allow_light_points() {
+ (self.light_func)(hint.max_num_points())
+ } else {
+ (self.bold_func)(hint.max_num_points())
+ }
+ }
+
+ fn axis_pixel_range(&self, limit: (i32, i32)) -> Range<i32> {
+ self.inner.axis_pixel_range(limit)
+ }
+}
+
+impl<R: DiscreteRanged> DiscreteRanged for WithKeyPointMethod<R> {
+ fn size(&self) -> usize {
+ self.inner.size()
+ }
+ fn index_of(&self, value: &Self::ValueType) -> Option<usize> {
+ self.inner.index_of(value)
+ }
+ fn from_index(&self, index: usize) -> Option<Self::ValueType> {
+ self.inner.from_index(index)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::coord::ranged1d::{BoldPoints, LightPoints};
+ #[test]
+ fn test_with_key_points() {
+ let range = (0..100).with_key_points(vec![1, 2, 3]);
+ assert_eq!(range.map(&3, (0, 1000)), 30);
+ assert_eq!(range.range(), 0..100);
+ assert_eq!(range.key_points(BoldPoints(100)), vec![1, 2, 3]);
+ assert_eq!(range.key_points(LightPoints::new(100, 100)), vec![]);
+ let range = range.with_light_points(5..10);
+ assert_eq!(range.key_points(BoldPoints(10)), vec![1, 2, 3]);
+ assert_eq!(
+ range.key_points(LightPoints::new(10, 10)),
+ (5..10).collect::<Vec<_>>()
+ );
+
+ assert_eq!(range.size(), 101);
+ assert_eq!(range.index_of(&10), Some(10));
+ assert_eq!(range.from_index(10), Some(10));
+
+ assert_eq!(range.axis_pixel_range((0, 1000)), 0..1000);
+
+ let mut range = range;
+
+ assert_eq!(range.light_points().len(), 5);
+ assert_eq!(range.light_points_mut().len(), 5);
+ assert_eq!(range.bold_points().len(), 3);
+ assert_eq!(range.bold_points_mut().len(), 3);
+ }
+
+ #[test]
+ fn test_with_key_point_method() {
+ let range = (0..100).with_key_point_func(|_| vec![1, 2, 3]);
+ assert_eq!(range.map(&3, (0, 1000)), 30);
+ assert_eq!(range.range(), 0..100);
+ assert_eq!(range.key_points(BoldPoints(100)), vec![1, 2, 3]);
+ assert_eq!(range.key_points(LightPoints::new(100, 100)), vec![]);
+ let range = range.with_light_point_func(|_| (5..10).collect());
+ assert_eq!(range.key_points(BoldPoints(10)), vec![1, 2, 3]);
+ assert_eq!(
+ range.key_points(LightPoints::new(10, 10)),
+ (5..10).collect::<Vec<_>>()
+ );
+
+ assert_eq!(range.size(), 101);
+ assert_eq!(range.index_of(&10), Some(10));
+ assert_eq!(range.from_index(10), Some(10));
+
+ assert_eq!(range.axis_pixel_range((0, 1000)), 0..1000);
+ }
+}
diff --git a/src/coord/ranged1d/combinators/group_by.rs b/src/coord/ranged1d/combinators/group_by.rs
new file mode 100644
index 0000000..0b4b508
--- /dev/null
+++ b/src/coord/ranged1d/combinators/group_by.rs
@@ -0,0 +1,120 @@
+use crate::coord::ranged1d::{
+ AsRangedCoord, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, ValueFormatter,
+};
+use std::ops::Range;
+
+/// Grouping the value in the coordinate specification.
+///
+/// This combinator doesn't change the coordinate mapping behavior. But it changes how
+/// the key point is generated, this coordinate specification will enforce that only the first value in each group
+/// can be emitted as the bold key points.
+///
+/// This is useful, for example, when we have an X axis is a integer and denotes days.
+/// And we are expecting the tick mark denotes weeks, in this way we can make the range
+/// spec grouping by 7 elements.
+/// With the help of the GroupBy decorator, this can be archived quite easily:
+///```rust
+///use plotters::prelude::*;
+///let mut buf = vec![0;1024*768*3];
+///let area = BitMapBackend::with_buffer(buf.as_mut(), (1024, 768)).into_drawing_area();
+///let chart = ChartBuilder::on(&area)
+/// .build_ranged((0..100).group_by(7), 0..100)
+/// .unwrap();
+///```
+///
+/// To apply this combinator, call [ToGroupByRange::group_by](trait.ToGroupByRange.html#tymethod.group_by) method on any discrete coordinate spec.
+#[derive(Clone)]
+pub struct GroupBy<T: DiscreteRanged>(T, usize);
+
+/// The trait that provides method `Self::group_by` function which creates a
+/// `GroupBy` decorated ranged value.
+pub trait ToGroupByRange: AsRangedCoord + Sized
+where
+ Self::CoordDescType: DiscreteRanged,
+{
+ /// Make a grouping ranged value, see the documentation for `GroupBy` for details.
+ ///
+ /// - `value`: The number of values we want to group it
+ /// - **return**: The newly created grouping range specification
+ fn group_by(self, value: usize) -> GroupBy<<Self as AsRangedCoord>::CoordDescType> {
+ GroupBy(self.into(), value)
+ }
+}
+
+impl<T: AsRangedCoord + Sized> ToGroupByRange for T where T::CoordDescType: DiscreteRanged {}
+
+impl<T: DiscreteRanged> DiscreteRanged for GroupBy<T> {
+ fn size(&self) -> usize {
+ (self.0.size() + self.1 - 1) / self.1
+ }
+ fn index_of(&self, value: &Self::ValueType) -> Option<usize> {
+ self.0.index_of(value).map(|idx| idx / self.1)
+ }
+ fn from_index(&self, index: usize) -> Option<Self::ValueType> {
+ self.0.from_index(index * self.1)
+ }
+}
+
+impl<T, R: DiscreteRanged<ValueType = T> + ValueFormatter<T>> ValueFormatter<T> for GroupBy<R> {
+ fn format(value: &T) -> String {
+ R::format(value)
+ }
+}
+
+impl<T: DiscreteRanged> Ranged for GroupBy<T> {
+ type FormatOption = NoDefaultFormatting;
+ type ValueType = T::ValueType;
+ fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> i32 {
+ self.0.map(value, limit)
+ }
+ fn range(&self) -> Range<T::ValueType> {
+ self.0.range()
+ }
+ // TODO: See issue issue #88
+ fn key_points<HintType: KeyPointHint>(&self, hint: HintType) -> Vec<T::ValueType> {
+ let range = 0..(self.0.size() + self.1) / self.1;
+ //let logic_range: RangedCoordusize = range.into();
+
+ let interval =
+ ((range.end - range.start + hint.bold_points() - 1) / hint.bold_points()).max(1);
+ let count = (range.end - range.start) / interval;
+
+ let idx_iter = (0..hint.bold_points()).map(|x| x * interval);
+
+ if hint.weight().allow_light_points() && count < hint.bold_points() * 2 {
+ let outter_ticks = idx_iter;
+ let outter_tick_size = interval * self.1;
+ let inner_ticks_per_group = hint.max_num_points() / outter_ticks.len();
+ let inner_ticks =
+ (outter_tick_size + inner_ticks_per_group - 1) / inner_ticks_per_group;
+ let inner_ticks: Vec<_> = (0..(outter_tick_size / inner_ticks))
+ .map(move |x| x * inner_ticks)
+ .collect();
+ let size = self.0.size();
+ return outter_ticks
+ .map(|base| inner_ticks.iter().map(move |&ofs| base * self.1 + ofs))
+ .flatten()
+ .take_while(|&idx| idx < size)
+ .map(|x| self.0.from_index(x).unwrap())
+ .collect();
+ }
+
+ idx_iter
+ .map(|x| self.0.from_index(x * self.1).unwrap())
+ .collect()
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ #[test]
+ fn test_group_by() {
+ let coord = (0..100).group_by(10);
+ assert_eq!(coord.size(), 11);
+ for (idx, val) in (0..).zip(coord.values()) {
+ assert_eq!(val, idx * 10);
+ assert_eq!(coord.from_index(idx as usize), Some(val));
+ }
+ }
+}
diff --git a/src/coord/ranged1d/combinators/linspace.rs b/src/coord/ranged1d/combinators/linspace.rs
new file mode 100644
index 0000000..17b3d42
--- /dev/null
+++ b/src/coord/ranged1d/combinators/linspace.rs
@@ -0,0 +1,432 @@
+use crate::coord::ranged1d::types::RangedCoordusize;
+use crate::coord::ranged1d::{
+ AsRangedCoord, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, ValueFormatter,
+};
+use std::cmp::{Ordering, PartialOrd};
+use std::marker::PhantomData;
+use std::ops::{Add, Range, Sub};
+
+/// The type marker used to denote the rounding method.
+/// Since we are mapping any range to a discrete range thus not all values are
+/// perfect mapped to the grid points. In this case, this type marker gives hints
+/// for the linspace coord for how to treat the non-grid-point values.
+pub trait LinspaceRoundingMethod<V> {
+ /// Search for the value within the given values array and rounding method
+ ///
+ /// - `values`: The values we want to search
+ /// - `target`: The target value
+ /// - `returns`: The index if we found the matching item, otherwise none
+ fn search(values: &[V], target: &V) -> Option<usize>;
+}
+
+/// This type marker means linspace do the exact match for searching
+/// which means if there's no value strictly equals to the target, the coord spec
+/// reports not found result.
+#[derive(Clone)]
+pub struct Exact<V>(PhantomData<V>);
+
+impl<V: PartialOrd> LinspaceRoundingMethod<V> for Exact<V> {
+ fn search(values: &[V], target: &V) -> Option<usize> {
+ values.iter().position(|x| target == x)
+ }
+}
+
+/// This type marker means we round up the value. Which means we try to find a
+/// minimal value in the values array that is greater or equal to the target.
+#[derive(Clone)]
+pub struct Ceil<V>(PhantomData<V>);
+
+impl<V: PartialOrd> LinspaceRoundingMethod<V> for Ceil<V> {
+ fn search(values: &[V], target: &V) -> Option<usize> {
+ let ascending = if values.len() < 2 {
+ true
+ } else {
+ values[0].partial_cmp(&values[1]) == Some(Ordering::Less)
+ };
+
+ match values.binary_search_by(|probe| {
+ if ascending {
+ probe.partial_cmp(target).unwrap()
+ } else {
+ target.partial_cmp(probe).unwrap()
+ }
+ }) {
+ Ok(idx) => Some(idx),
+ Err(idx) => {
+ let offset = if ascending { 0 } else { 1 };
+
+ if idx < offset || idx >= values.len() + offset {
+ return None;
+ }
+ Some(idx - offset)
+ }
+ }
+ }
+}
+
+/// This means we use the round down. Which means we try to find a
+/// maximum value in the values array that is less or equal to the target.
+#[derive(Clone)]
+pub struct Floor<V>(PhantomData<V>);
+
+impl<V: PartialOrd> LinspaceRoundingMethod<V> for Floor<V> {
+ fn search(values: &[V], target: &V) -> Option<usize> {
+ let ascending = if values.len() < 2 {
+ true
+ } else {
+ values[0].partial_cmp(&values[1]) == Some(Ordering::Less)
+ };
+
+ match values.binary_search_by(|probe| {
+ if ascending {
+ probe.partial_cmp(target).unwrap()
+ } else {
+ target.partial_cmp(probe).unwrap()
+ }
+ }) {
+ Ok(idx) => Some(idx),
+ Err(idx) => {
+ let offset = if ascending { 1 } else { 0 };
+
+ if idx < offset || idx >= values.len() + offset {
+ return None;
+ }
+ Some(idx - offset)
+ }
+ }
+ }
+}
+
+/// This means we use the rounding. Which means we try to find the closet
+/// value in the array that matches the target
+#[derive(Clone)]
+pub struct Round<V, S>(PhantomData<(V, S)>);
+
+impl<V, S> LinspaceRoundingMethod<V> for Round<V, S>
+where
+ V: Add<S, Output = V> + PartialOrd + Sub<V, Output = S> + Clone,
+ S: PartialOrd + Clone,
+{
+ fn search(values: &[V], target: &V) -> Option<usize> {
+ let ascending = if values.len() < 2 {
+ true
+ } else {
+ values[0].partial_cmp(&values[1]) == Some(Ordering::Less)
+ };
+
+ match values.binary_search_by(|probe| {
+ if ascending {
+ probe.partial_cmp(target).unwrap()
+ } else {
+ target.partial_cmp(probe).unwrap()
+ }
+ }) {
+ Ok(idx) => Some(idx),
+ Err(idx) => {
+ if idx == 0 {
+ return Some(0);
+ }
+
+ if idx == values.len() {
+ return Some(idx - 1);
+ }
+
+ let left_delta = if ascending {
+ target.clone() - values[idx - 1].clone()
+ } else {
+ values[idx - 1].clone() - target.clone()
+ };
+ let right_delta = if ascending {
+ values[idx].clone() - target.clone()
+ } else {
+ target.clone() - values[idx].clone()
+ };
+
+ if left_delta.partial_cmp(&right_delta) == Some(Ordering::Less) {
+ Some(idx - 1)
+ } else {
+ Some(idx)
+ }
+ }
+ }
+ }
+}
+
+/// The coordinate combinator that transform a continous coordinate to a discrete coordinate
+/// to a discrete coordinate by a giving step.
+///
+/// For example, range `0f32..100f32` is a continuous coordinate, thus this prevent us having a
+/// histogram on it since Plotters doesn't know how to segment the range into buckets.
+/// In this case, to get a histogram, we need to split the original range to a
+/// set of discrete buckets (for example, 0.5 per bucket).
+///
+/// The linspace decorate abstracting this method. For example, we can have a discrete coordinate:
+/// `(0f32..100f32).step(0.5)`.
+///
+/// Linspace also supports different types of bucket matching method - This configuration alters the behavior of
+/// [DiscreteCoord::index_of](../trait.DiscreteCoord.html#tymethod.index_of) for Linspace coord spec
+/// - **Flooring**, the value falls into the nearst bucket smaller than it. See [Linspace::use_floor](struct.Linspace.html#method.use_floor)
+/// - **Round**, the value falls into the nearst bucket. See [Linearspace::use_round](struct.Linspace.html#method.use_round)
+/// - **Ceiling**, the value falls into the nearst bucket larger than itself. See [Linspace::use_ceil](struct.Linspace.html#method.use_ceil)
+/// - **Exact Matchting**, the value must be exactly same as the butcket value. See [Linspace::use_exact](struct.Linspace.html#method.use_exact)
+#[derive(Clone)]
+pub struct Linspace<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>>
+where
+ T::ValueType: Add<S, Output = T::ValueType> + PartialOrd + Clone,
+{
+ step: S,
+ inner: T,
+ grid_value: Vec<T::ValueType>,
+ _phatom: PhantomData<R>,
+}
+
+impl<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>> Linspace<T, S, R>
+where
+ T::ValueType: Add<S, Output = T::ValueType> + PartialOrd + Clone,
+{
+ fn compute_grid_values(&mut self) {
+ let range = self.inner.range();
+
+ match (
+ range.start.partial_cmp(&range.end),
+ (range.start.clone() + self.step.clone()).partial_cmp(&range.end),
+ ) {
+ (Some(a), Some(b)) if a != b || a == Ordering::Equal || b == Ordering::Equal => (),
+ (Some(a), Some(_)) => {
+ let mut current = range.start;
+ while current.partial_cmp(&range.end) == Some(a) {
+ self.grid_value.push(current.clone());
+ current = current + self.step.clone();
+ }
+ }
+ _ => (),
+ }
+ }
+
+ /// Set the linspace use the round up method for value matching
+ ///
+ /// - **returns**: The newly created linspace that uses new matching method
+ pub fn use_ceil(self) -> Linspace<T, S, Ceil<T::ValueType>> {
+ Linspace {
+ step: self.step,
+ inner: self.inner,
+ grid_value: self.grid_value,
+ _phatom: PhantomData,
+ }
+ }
+
+ /// Set the linspace use the round down method for value matching
+ ///
+ /// - **returns**: The newly created linspace that uses new matching method
+ pub fn use_floor(self) -> Linspace<T, S, Floor<T::ValueType>> {
+ Linspace {
+ step: self.step,
+ inner: self.inner,
+ grid_value: self.grid_value,
+ _phatom: PhantomData,
+ }
+ }
+
+ /// Set the linspace use the best match method for value matching
+ ///
+ /// - **returns**: The newly created linspace that uses new matching method
+ pub fn use_round(self) -> Linspace<T, S, Round<T::ValueType, S>>
+ where
+ T::ValueType: Sub<T::ValueType, Output = S>,
+ S: PartialOrd,
+ {
+ Linspace {
+ step: self.step,
+ inner: self.inner,
+ grid_value: self.grid_value,
+ _phatom: PhantomData,
+ }
+ }
+
+ /// Set the linspace use the exact match method for value matching
+ ///
+ /// - **returns**: The newly created linspace that uses new matching method
+ pub fn use_exact(self) -> Linspace<T, S, Exact<T::ValueType>>
+ where
+ T::ValueType: Sub<T::ValueType, Output = S>,
+ S: PartialOrd,
+ {
+ Linspace {
+ step: self.step,
+ inner: self.inner,
+ grid_value: self.grid_value,
+ _phatom: PhantomData,
+ }
+ }
+}
+
+impl<T, R, S, RM> ValueFormatter<T> for Linspace<R, S, RM>
+where
+ R: Ranged<ValueType = T> + ValueFormatter<T>,
+ RM: LinspaceRoundingMethod<T>,
+ T: Add<S, Output = T> + PartialOrd + Clone,
+ S: Clone,
+{
+ fn format(value: &T) -> String {
+ R::format(value)
+ }
+}
+
+impl<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>> Ranged for Linspace<T, S, R>
+where
+ T::ValueType: Add<S, Output = T::ValueType> + PartialOrd + Clone,
+{
+ type FormatOption = NoDefaultFormatting;
+ type ValueType = T::ValueType;
+
+ fn range(&self) -> Range<T::ValueType> {
+ self.inner.range()
+ }
+
+ fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> i32 {
+ self.inner.map(value, limit)
+ }
+
+ fn key_points<Hint: KeyPointHint>(&self, hint: Hint) -> Vec<T::ValueType> {
+ if self.grid_value.is_empty() {
+ return vec![];
+ }
+ let idx_range: RangedCoordusize = (0..(self.grid_value.len() - 1)).into();
+
+ idx_range
+ .key_points(hint)
+ .into_iter()
+ .map(|x| self.grid_value[x].clone())
+ .collect()
+ }
+}
+
+impl<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>> DiscreteRanged
+ for Linspace<T, S, R>
+where
+ T::ValueType: Add<S, Output = T::ValueType> + PartialOrd + Clone,
+{
+ fn size(&self) -> usize {
+ self.grid_value.len()
+ }
+
+ fn index_of(&self, value: &T::ValueType) -> Option<usize> {
+ R::search(self.grid_value.as_ref(), value)
+ }
+
+ fn from_index(&self, idx: usize) -> Option<T::ValueType> {
+ self.grid_value.get(idx).map(Clone::clone)
+ }
+}
+
+pub trait IntoLinspace: AsRangedCoord {
+ /// Set the step value, make a linspace coordinate from the given range.
+ /// By default the matching method use the exact match
+ ///
+ /// - `val`: The step value
+ /// - **returns*: The newly created linspace
+ fn step<S: Clone>(self, val: S) -> Linspace<Self::CoordDescType, S, Exact<Self::Value>>
+ where
+ Self::Value: Add<S, Output = Self::Value> + PartialOrd + Clone,
+ {
+ let mut ret = Linspace {
+ step: val,
+ inner: self.into(),
+ grid_value: vec![],
+ _phatom: PhantomData,
+ };
+
+ ret.compute_grid_values();
+
+ ret
+ }
+}
+
+impl<T: AsRangedCoord> IntoLinspace for T {}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_float_linspace() {
+ let coord = (0.0f64..100.0f64).step(0.1);
+
+ assert_eq!(coord.map(&23.12, (0, 10000)), 2312);
+ assert_eq!(coord.range(), 0.0..100.0);
+ assert_eq!(coord.key_points(100000).len(), 1001);
+ assert_eq!(coord.size(), 1001);
+ assert_eq!(coord.index_of(&coord.from_index(230).unwrap()), Some(230));
+ assert!((coord.from_index(230).unwrap() - 23.0).abs() < 1e-5);
+ }
+
+ #[test]
+ fn test_rounding_methods() {
+ let coord = (0.0f64..100.0f64).step(1.0);
+
+ assert_eq!(coord.index_of(&1.0), Some(1));
+ assert_eq!(coord.index_of(&1.2), None);
+
+ let coord = coord.use_floor();
+ assert_eq!(coord.index_of(&1.0), Some(1));
+ assert_eq!(coord.index_of(&1.2), Some(1));
+ assert_eq!(coord.index_of(&23.9), Some(23));
+ assert_eq!(coord.index_of(&10000.0), Some(99));
+ assert_eq!(coord.index_of(&-1.0), None);
+
+ let coord = coord.use_ceil();
+ assert_eq!(coord.index_of(&1.0), Some(1));
+ assert_eq!(coord.index_of(&1.2), Some(2));
+ assert_eq!(coord.index_of(&23.9), Some(24));
+ assert_eq!(coord.index_of(&10000.0), None);
+ assert_eq!(coord.index_of(&-1.0), Some(0));
+
+ let coord = coord.use_round();
+ assert_eq!(coord.index_of(&1.0), Some(1));
+ assert_eq!(coord.index_of(&1.2), Some(1));
+ assert_eq!(coord.index_of(&1.7), Some(2));
+ assert_eq!(coord.index_of(&23.9), Some(24));
+ assert_eq!(coord.index_of(&10000.0), Some(99));
+ assert_eq!(coord.index_of(&-1.0), Some(0));
+
+ let coord = (0.0f64..-100.0f64).step(-1.0);
+
+ assert_eq!(coord.index_of(&-1.0), Some(1));
+ assert_eq!(coord.index_of(&-1.2), None);
+
+ let coord = coord.use_floor();
+ assert_eq!(coord.index_of(&-1.0), Some(1));
+ assert_eq!(coord.index_of(&-1.2), Some(2));
+ assert_eq!(coord.index_of(&-23.9), Some(24));
+ assert_eq!(coord.index_of(&-10000.0), None);
+ assert_eq!(coord.index_of(&1.0), Some(0));
+
+ let coord = coord.use_ceil();
+ assert_eq!(coord.index_of(&-1.0), Some(1));
+ assert_eq!(coord.index_of(&-1.2), Some(1));
+ assert_eq!(coord.index_of(&-23.9), Some(23));
+ assert_eq!(coord.index_of(&-10000.0), Some(99));
+ assert_eq!(coord.index_of(&1.0), None);
+
+ let coord = coord.use_round();
+ assert_eq!(coord.index_of(&-1.0), Some(1));
+ assert_eq!(coord.index_of(&-1.2), Some(1));
+ assert_eq!(coord.index_of(&-1.7), Some(2));
+ assert_eq!(coord.index_of(&-23.9), Some(24));
+ assert_eq!(coord.index_of(&-10000.0), Some(99));
+ assert_eq!(coord.index_of(&1.0), Some(0));
+ }
+
+ #[cfg(feature = "chrono")]
+ #[test]
+ fn test_duration_linspace() {
+ use chrono::Duration;
+ let coord = (Duration::seconds(0)..Duration::seconds(100)).step(Duration::milliseconds(1));
+
+ assert_eq!(coord.size(), 100_000);
+ assert_eq!(coord.index_of(&coord.from_index(230).unwrap()), Some(230));
+ assert_eq!(coord.key_points(10000000).len(), 100_000);
+ assert_eq!(coord.range(), Duration::seconds(0)..Duration::seconds(100));
+ assert_eq!(coord.map(&Duration::seconds(25), (0, 100_000)), 25000);
+ }
+}
diff --git a/src/coord/logarithmic.rs b/src/coord/ranged1d/combinators/logarithmic.rs
index a651013..d29c73e 100644
--- a/src/coord/logarithmic.rs
+++ b/src/coord/ranged1d/combinators/logarithmic.rs
@@ -1,8 +1,10 @@
-use super::{AsRangedCoord, Ranged, RangedCoordf64};
+use crate::coord::ranged1d::types::RangedCoordf64;
+use crate::coord::ranged1d::{AsRangedCoord, DefaultFormatting, KeyPointHint, Ranged};
use std::marker::PhantomData;
use std::ops::Range;
-/// The trait for the type that is able to be presented in the log scale
+/// The trait for the type that is able to be presented in the log scale.
+/// This trait is primarily used by [LogRange](struct.LogRange.html).
pub trait LogScalable: Clone {
/// Make the conversion from the type to the floating point number
fn as_f64(&self) -> f64;
@@ -47,15 +49,23 @@ impl_log_scalable!(i, u64);
impl_log_scalable!(f, f32);
impl_log_scalable!(f, f64);
-/// The decorator type for a range of a log-scaled value
-pub struct LogRange<V: LogScalable>(pub Range<V>);
+pub trait IntoLogRange {
+ type ValueType: LogScalable;
+ fn log_scale(self) -> LogRange<Self::ValueType>;
+}
-impl<V: LogScalable + Clone> Clone for LogRange<V> {
- fn clone(&self) -> Self {
- Self(self.0.clone())
+impl<T: LogScalable> IntoLogRange for Range<T> {
+ type ValueType = T;
+ fn log_scale(self) -> LogRange<T> {
+ LogRange(self)
}
}
+/// The logarithmic coodinate decorator.
+/// This decorator is used to make the axis rendered as logarithmically.
+#[derive(Clone)]
+pub struct LogRange<V: LogScalable>(pub Range<V>);
+
impl<V: LogScalable> From<LogRange<V>> for LogCoord<V> {
fn from(range: LogRange<V>) -> LogCoord<V> {
LogCoord {
@@ -79,6 +89,7 @@ pub struct LogCoord<V: LogScalable> {
}
impl<V: LogScalable> Ranged for LogCoord<V> {
+ type FormatOption = DefaultFormatting;
type ValueType = V;
fn map(&self, value: &V, limit: (i32, i32)) -> i32 {
@@ -87,7 +98,8 @@ impl<V: LogScalable> Ranged for LogCoord<V> {
self.linear.map(&value, limit)
}
- fn key_points(&self, max_points: usize) -> Vec<Self::ValueType> {
+ fn key_points<Hint: KeyPointHint>(&self, hint: Hint) -> Vec<Self::ValueType> {
+ let max_points = hint.max_num_points();
let tier_1 = (self.logic.end.as_f64() / self.logic.start.as_f64())
.log10()
.abs()
diff --git a/src/coord/ranged1d/combinators/mod.rs b/src/coord/ranged1d/combinators/mod.rs
new file mode 100644
index 0000000..ea1ed5a
--- /dev/null
+++ b/src/coord/ranged1d/combinators/mod.rs
@@ -0,0 +1,17 @@
+mod ckps;
+pub use ckps::{BindKeyPointMethod, BindKeyPoints, WithKeyPointMethod, WithKeyPoints};
+
+mod group_by;
+pub use group_by::{GroupBy, ToGroupByRange};
+
+mod linspace;
+pub use linspace::{IntoLinspace, Linspace};
+
+mod logarithmic;
+pub use logarithmic::{IntoLogRange, LogCoord, LogRange, LogScalable};
+
+mod nested;
+pub use nested::{BuildNestedCoord, NestedRange, NestedValue};
+
+mod partial_axis;
+pub use partial_axis::{make_partial_axis, IntoPartialAxis};
diff --git a/src/coord/ranged1d/combinators/nested.rs b/src/coord/ranged1d/combinators/nested.rs
new file mode 100644
index 0000000..d23f34e
--- /dev/null
+++ b/src/coord/ranged1d/combinators/nested.rs
@@ -0,0 +1,204 @@
+use crate::coord::ranged1d::{
+ AsRangedCoord, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, ValueFormatter,
+};
+use std::ops::Range;
+
+/// Describe a value for a nested coordinate
+#[derive(PartialEq, Eq, Clone, Debug)]
+pub enum NestedValue<C, V> {
+ /// Category value
+ Category(C),
+ /// One exact nested value
+ Value(C, V),
+}
+
+impl<C, V> NestedValue<C, V> {
+ /// Get the category of current nest value
+ pub fn category(&self) -> &C {
+ match self {
+ NestedValue::Category(cat) => cat,
+ NestedValue::Value(cat, _) => cat,
+ }
+ }
+ /// Get the nested value from this value
+ pub fn nested_value(&self) -> Option<&V> {
+ match self {
+ NestedValue::Category(_) => None,
+ NestedValue::Value(_, val) => Some(val),
+ }
+ }
+}
+
+impl<C, V> From<(C, V)> for NestedValue<C, V> {
+ fn from((cat, val): (C, V)) -> NestedValue<C, V> {
+ NestedValue::Value(cat, val)
+ }
+}
+
+impl<C, V> From<C> for NestedValue<C, V> {
+ fn from(cat: C) -> NestedValue<C, V> {
+ NestedValue::Category(cat)
+ }
+}
+
+/// A nested coordinate spec which is a discrete coordinate on the top level and
+/// for each value in discrete value, there is a secondary coordinate system.
+/// And the value is defined as a tuple of primary coordinate value and secondary
+/// coordinate value
+pub struct NestedRange<Primary: DiscreteRanged, Secondary: Ranged> {
+ primary: Primary,
+ secondary: Vec<Secondary>,
+}
+
+impl<PT, ST, P, S> ValueFormatter<NestedValue<PT, ST>> for NestedRange<P, S>
+where
+ P: Ranged<ValueType = PT> + DiscreteRanged,
+ S: Ranged<ValueType = ST>,
+ P: ValueFormatter<PT>,
+ S: ValueFormatter<ST>,
+{
+ fn format(value: &NestedValue<PT, ST>) -> String {
+ match value {
+ NestedValue::Category(cat) => P::format(cat),
+ NestedValue::Value(_, val) => S::format(val),
+ }
+ }
+}
+
+impl<P: DiscreteRanged, S: Ranged> Ranged for NestedRange<P, S> {
+ type FormatOption = NoDefaultFormatting;
+ type ValueType = NestedValue<P::ValueType, S::ValueType>;
+
+ fn range(&self) -> Range<Self::ValueType> {
+ let primary_range = self.primary.range();
+
+ let secondary_left = self.secondary[0].range().start;
+ let secondary_right = self.secondary[self.primary.size() - 1].range().end;
+
+ NestedValue::Value(primary_range.start, secondary_left)
+ ..NestedValue::Value(primary_range.end, secondary_right)
+ }
+
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
+ let idx = self.primary.index_of(value.category()).unwrap_or(0);
+ let total = self.primary.size();
+
+ let bucket_size = (limit.1 - limit.0) / total as i32;
+ let mut residual = (limit.1 - limit.0) % total as i32;
+
+ if residual < 0 {
+ residual += total as i32;
+ }
+
+ let s_left = limit.0 + bucket_size * idx as i32 + residual.min(idx as i32);
+ let s_right = s_left + bucket_size + if (residual as usize) < idx { 1 } else { 0 };
+
+ if let Some(secondary_value) = value.nested_value() {
+ self.secondary[idx].map(secondary_value, (s_left, s_right))
+ } else {
+ (s_left + s_right) / 2
+ }
+ }
+
+ fn key_points<Hint: KeyPointHint>(&self, hint: Hint) -> Vec<Self::ValueType> {
+ if !hint.weight().allow_light_points() || hint.max_num_points() < self.primary.size() * 2 {
+ self.primary
+ .key_points(hint)
+ .into_iter()
+ .map(NestedValue::Category)
+ .collect()
+ } else {
+ let secondary_size =
+ (hint.max_num_points() - self.primary.size()) / self.primary.size();
+ self.primary
+ .values()
+ .enumerate()
+ .map(|(idx, val)| {
+ std::iter::once(NestedValue::Category(val)).chain(
+ self.secondary[idx]
+ .key_points(secondary_size)
+ .into_iter()
+ .map(move |v| {
+ NestedValue::Value(self.primary.from_index(idx).unwrap(), v)
+ }),
+ )
+ })
+ .flatten()
+ .collect()
+ }
+ }
+}
+
+impl<P: DiscreteRanged, S: DiscreteRanged> DiscreteRanged for NestedRange<P, S> {
+ fn size(&self) -> usize {
+ self.secondary.iter().map(|x| x.size()).sum::<usize>()
+ }
+
+ fn index_of(&self, value: &Self::ValueType) -> Option<usize> {
+ let p_idx = self.primary.index_of(value.category())?;
+ let s_idx = self.secondary[p_idx].index_of(value.nested_value()?)?;
+ Some(
+ s_idx
+ + self.secondary[..p_idx]
+ .iter()
+ .map(|x| x.size())
+ .sum::<usize>(),
+ )
+ }
+
+ fn from_index(&self, mut index: usize) -> Option<Self::ValueType> {
+ for (p_idx, snd) in self.secondary.iter().enumerate() {
+ if snd.size() > index {
+ return Some(NestedValue::Value(
+ self.primary.from_index(p_idx).unwrap(),
+ snd.from_index(index).unwrap(),
+ ));
+ }
+ index -= snd.size();
+ }
+ None
+ }
+}
+
+pub trait BuildNestedCoord: AsRangedCoord
+where
+ Self::CoordDescType: DiscreteRanged,
+{
+ fn nested_coord<S: AsRangedCoord>(
+ self,
+ builder: impl Fn(<Self::CoordDescType as Ranged>::ValueType) -> S,
+ ) -> NestedRange<Self::CoordDescType, S::CoordDescType> {
+ let primary: Self::CoordDescType = self.into();
+ assert!(primary.size() > 0);
+
+ let secondary = primary
+ .values()
+ .map(|value| builder(value).into())
+ .collect();
+
+ NestedRange { primary, secondary }
+ }
+}
+
+impl<T: AsRangedCoord> BuildNestedCoord for T where T::CoordDescType: DiscreteRanged {}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_nested_coord() {
+ let coord = (0..10).nested_coord(|x| 0..(x + 1));
+
+ let range = coord.range();
+
+ assert_eq!(NestedValue::Value(0, 0)..NestedValue::Value(10, 11), range);
+ assert_eq!(coord.map(&NestedValue::Category(0), (0, 1100)), 50);
+ assert_eq!(coord.map(&NestedValue::Value(0, 0), (0, 1100)), 0);
+ assert_eq!(coord.map(&NestedValue::Value(5, 4), (0, 1100)), 567);
+
+ assert_eq!(coord.size(), (2 + 12) * 11 / 2);
+ assert_eq!(coord.index_of(&NestedValue::Value(5, 4)), Some(24));
+ assert_eq!(coord.from_index(24), Some(NestedValue::Value(5, 4)));
+ }
+}
diff --git a/src/coord/ranged1d/combinators/partial_axis.rs b/src/coord/ranged1d/combinators/partial_axis.rs
new file mode 100644
index 0000000..b778ee2
--- /dev/null
+++ b/src/coord/ranged1d/combinators/partial_axis.rs
@@ -0,0 +1,113 @@
+use crate::coord::ranged1d::{
+ AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, Ranged,
+};
+use std::ops::Range;
+
+/// This axis decorator will make the axis partially display on the axis.
+/// At some time, we want the axis only covers some part of the value.
+/// This decorator will have an additional display range defined.
+#[derive(Clone)]
+pub struct PartialAxis<R: Ranged>(R, Range<R::ValueType>);
+
+/// The trait for the types that can be converted into a partial axis
+pub trait IntoPartialAxis: AsRangedCoord {
+ /// Make the partial axis
+ ///
+ /// - `axis_range`: The range of the axis to be displayed
+ /// - **returns**: The converted range specification
+ fn partial_axis(
+ self,
+ axis_range: Range<<Self::CoordDescType as Ranged>::ValueType>,
+ ) -> PartialAxis<Self::CoordDescType> {
+ PartialAxis(self.into(), axis_range)
+ }
+}
+
+impl<R: AsRangedCoord> IntoPartialAxis for R {}
+
+impl<R: Ranged> Ranged for PartialAxis<R>
+where
+ R::ValueType: Clone,
+{
+ type FormatOption = DefaultFormatting;
+ type ValueType = R::ValueType;
+
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
+ self.0.map(value, limit)
+ }
+
+ fn key_points<Hint: KeyPointHint>(&self, hint: Hint) -> Vec<Self::ValueType> {
+ self.0.key_points(hint)
+ }
+
+ fn range(&self) -> Range<Self::ValueType> {
+ self.0.range()
+ }
+
+ fn axis_pixel_range(&self, limit: (i32, i32)) -> Range<i32> {
+ let left = self.map(&self.1.start, limit);
+ let right = self.map(&self.1.end, limit);
+
+ left.min(right)..left.max(right)
+ }
+}
+
+impl<R: DiscreteRanged> DiscreteRanged for PartialAxis<R>
+where
+ R: Ranged,
+ <R as Ranged>::ValueType: Eq + Clone,
+{
+ fn size(&self) -> usize {
+ self.0.size()
+ }
+
+ fn index_of(&self, value: &R::ValueType) -> Option<usize> {
+ self.0.index_of(value)
+ }
+
+ fn from_index(&self, index: usize) -> Option<Self::ValueType> {
+ self.0.from_index(index)
+ }
+}
+
+/// Make a partial axis based on the percentage of visible portion.
+/// We can use `into_partial_axis` to create a partial axis range specification.
+/// But sometimes, we want to directly specify the percentage visible to the user.
+///
+/// - `axis_range`: The range specification
+/// - `part`: The visible part of the axis. Each value is from [0.0, 1.0]
+/// - **returns**: The partial axis created from the input, or `None` when not possible
+pub fn make_partial_axis<T>(
+ axis_range: Range<T>,
+ part: Range<f64>,
+) -> Option<PartialAxis<<Range<T> as AsRangedCoord>::CoordDescType>>
+where
+ Range<T>: AsRangedCoord,
+ T: num_traits::NumCast + Clone,
+{
+ let left: f64 = num_traits::cast(axis_range.start.clone())?;
+ let right: f64 = num_traits::cast(axis_range.end.clone())?;
+
+ let full_range_size = (right - left) / (part.end - part.start);
+
+ let full_left = left - full_range_size * part.start;
+ let full_right = right + full_range_size * (1.0 - part.end);
+
+ let full_range: Range<T> = num_traits::cast(full_left)?..num_traits::cast(full_right)?;
+
+ let axis_range: <Range<T> as AsRangedCoord>::CoordDescType = axis_range.into();
+
+ Some(PartialAxis(full_range.into(), axis_range.range()))
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ #[test]
+ fn test_make_partial_axis() {
+ let r = make_partial_axis(20..80, 0.2..0.8).unwrap();
+ assert_eq!(r.size(), 101);
+ assert_eq!(r.range(), 0..100);
+ assert_eq!(r.axis_pixel_range((0, 100)), 20..80);
+ }
+}
diff --git a/src/coord/ranged1d/discrete.rs b/src/coord/ranged1d/discrete.rs
new file mode 100644
index 0000000..5797dce
--- /dev/null
+++ b/src/coord/ranged1d/discrete.rs
@@ -0,0 +1,270 @@
+use crate::coord::ranged1d::{
+ AsRangedCoord, KeyPointHint, NoDefaultFormatting, Ranged, ReversibleRanged, ValueFormatter,
+};
+use std::ops::Range;
+
+/// The trait indicates the coordinate is discrete
+/// This means we can bidirectionally map the range value to 0 to N
+/// in which N is the number of distinct values of the range.
+///
+/// This is useful since for a histgoram, this is an abstraction of bucket.
+pub trait DiscreteRanged
+where
+ Self: Ranged,
+{
+ /// Get the number of element in the range
+ /// Note: we assume that all the ranged discrete coordinate has finite value
+ ///
+ /// - **returns** The number of values in the range
+ fn size(&self) -> usize;
+
+ /// Map a value to the index
+ ///
+ /// Note: This function doesn't guareentee return None when the value is out of range.
+ /// The only way to confirm the value is in the range is to examing the return value isn't
+ /// larger than self.size.
+ ///
+ /// - `value`: The value to map
+ /// - **returns** The index of the value
+ fn index_of(&self, value: &Self::ValueType) -> Option<usize>;
+
+ /// Reverse map the index to the value
+ ///
+ /// Note: This function doesn't guareentee returning None when the index is out of range.
+ ///
+ /// - `value`: The index to map
+ /// - **returns** The value
+ fn from_index(&self, index: usize) -> Option<Self::ValueType>;
+
+ /// Return a iterator that iterates over the all possible values
+ ///
+ /// - **returns** The value iterator
+ fn values(&self) -> DiscreteValueIter<'_, Self>
+ where
+ Self: Sized,
+ {
+ DiscreteValueIter(self, 0, self.size())
+ }
+
+ /// Returns the previous value in this range
+ ///
+ /// Normally, it's based on the `from_index` and `index_of` function. But for
+ /// some of the coord spec, it's possible that we value faster implementation.
+ /// If this is the case, we can impelemnet the type specific impl for the `previous`
+ /// and `next`.
+ ///
+ /// - `value`: The current value
+ /// - **returns**: The value piror to current value
+ fn previous(&self, value: &Self::ValueType) -> Option<Self::ValueType> {
+ if let Some(idx) = self.index_of(value) {
+ if idx > 0 {
+ return self.from_index(idx - 1);
+ }
+ }
+ None
+ }
+
+ /// Returns the next value in this range
+ ///
+ /// Normally, it's based on the `from_index` and `index_of` function. But for
+ /// some of the coord spec, it's possible that we value faster implementation.
+ /// If this is the case, we can impelemnet the type specific impl for the `previous`
+ /// and `next`.
+ ///
+ /// - `value`: The current value
+ /// - **returns**: The value next to current value
+ fn next(&self, value: &Self::ValueType) -> Option<Self::ValueType> {
+ if let Some(idx) = self.index_of(value) {
+ if idx + 1 < self.size() {
+ return self.from_index(idx + 1);
+ }
+ }
+ None
+ }
+}
+
+/// A `SegmentedCoord` is a decorator on any discrete coordinate specification.
+/// This decorator will convert the discrete coordiante in two ways:
+/// - Add an extra dummy element after all the values in origianl discrete coordinate
+/// - Logically each value `v` from original coordinate system is mapped into an segment `[v, v+1)` where `v+1` denotes the sucessor of the `v`
+/// - Introduce two types of values `SegmentValue::Exact(value)` which denotes the left end of value's segment and `SegmentValue::CenterOf(value)` which refers the center of the segment.
+/// This is used in histogram types, which uses a discrete coordinate as the buckets. The segmented coord always emits `CenterOf(value)` key points, thus it allows all the label and tick marks
+/// of the coordinate rendered in the middle of each segment.
+/// The coresponding trait [IntoSegmentedCoord](trait.IntoSegmentedCoord.html) is used to apply this decorator to coordinates.
+#[derive(Clone)]
+pub struct SegmentedCoord<D: DiscreteRanged>(D);
+
+/// The trait for types that can decorated by [SegmentedCoord](struct.SegmentedCoord.html) decorator.
+pub trait IntoSegmentedCoord: AsRangedCoord
+where
+ Self::CoordDescType: DiscreteRanged,
+{
+ /// Convert current ranged value into a segmented coordinate
+ fn into_segmented(self) -> SegmentedCoord<Self::CoordDescType> {
+ SegmentedCoord(self.into())
+ }
+}
+
+impl<R: AsRangedCoord> IntoSegmentedCoord for R where R::CoordDescType: DiscreteRanged {}
+
+/// The value that used by the segmented coordinate.
+#[derive(Clone, Debug)]
+pub enum SegmentValue<T> {
+ /// Means we are referring the exact position of value `T`
+ Exact(T),
+ /// Means we are referring the center of position `T` and the successor of `T`
+ CenterOf(T),
+ /// Referring the last dummy element
+ Last,
+}
+
+impl<T, D: DiscreteRanged + Ranged<ValueType = T>> ValueFormatter<SegmentValue<T>>
+ for SegmentedCoord<D>
+where
+ D: ValueFormatter<T>,
+{
+ fn format(value: &SegmentValue<T>) -> String {
+ match value {
+ SegmentValue::Exact(ref value) => D::format(value),
+ SegmentValue::CenterOf(ref value) => D::format(value),
+ _ => "".to_string(),
+ }
+ }
+}
+
+impl<D: DiscreteRanged> Ranged for SegmentedCoord<D> {
+ type FormatOption = NoDefaultFormatting;
+ type ValueType = SegmentValue<D::ValueType>;
+
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
+ let margin = ((limit.1 - limit.0) as f32 / self.0.size() as f32).round() as i32;
+
+ match value {
+ SegmentValue::Exact(coord) => self.0.map(coord, (limit.0, limit.1 - margin)),
+ SegmentValue::CenterOf(coord) => {
+ let left = self.0.map(coord, (limit.0, limit.1 - margin));
+ if let Some(idx) = self.0.index_of(coord) {
+ if idx + 1 < self.0.size() {
+ let right = self.0.map(
+ &self.0.from_index(idx + 1).unwrap(),
+ (limit.0, limit.1 - margin),
+ );
+ return (left + right) / 2;
+ }
+ }
+ left + margin / 2
+ }
+ SegmentValue::Last => limit.1,
+ }
+ }
+
+ fn key_points<HintType: KeyPointHint>(&self, hint: HintType) -> Vec<Self::ValueType> {
+ self.0
+ .key_points(hint)
+ .into_iter()
+ .map(SegmentValue::CenterOf)
+ .collect()
+ }
+
+ fn range(&self) -> Range<Self::ValueType> {
+ let range = self.0.range();
+ SegmentValue::Exact(range.start)..SegmentValue::Exact(range.end)
+ }
+}
+
+impl<D: DiscreteRanged> DiscreteRanged for SegmentedCoord<D> {
+ fn size(&self) -> usize {
+ self.0.size() + 1
+ }
+
+ fn index_of(&self, value: &Self::ValueType) -> Option<usize> {
+ match value {
+ SegmentValue::Exact(value) => self.0.index_of(value),
+ SegmentValue::CenterOf(value) => self.0.index_of(value),
+ SegmentValue::Last => Some(self.0.size()),
+ }
+ }
+
+ fn from_index(&self, idx: usize) -> Option<Self::ValueType> {
+ match idx {
+ idx if idx < self.0.size() => self.0.from_index(idx).map(SegmentValue::Exact),
+ idx if idx == self.0.size() => Some(SegmentValue::Last),
+ _ => None,
+ }
+ }
+}
+
+impl<T> From<T> for SegmentValue<T> {
+ fn from(this: T) -> SegmentValue<T> {
+ SegmentValue::Exact(this)
+ }
+}
+
+impl<DC: DiscreteRanged> ReversibleRanged for DC {
+ fn unmap(&self, input: i32, limit: (i32, i32)) -> Option<Self::ValueType> {
+ let idx = (f64::from(input - limit.0) * (self.size() as f64) / f64::from(limit.1 - limit.0))
+ .floor() as usize;
+ self.from_index(idx)
+ }
+}
+
+/// The iterator that can be used to iterate all the values defined by a discrete coordinate
+pub struct DiscreteValueIter<'a, T: DiscreteRanged>(&'a T, usize, usize);
+
+impl<'a, T: DiscreteRanged> Iterator for DiscreteValueIter<'a, T> {
+ type Item = T::ValueType;
+ fn next(&mut self) -> Option<T::ValueType> {
+ if self.1 >= self.2 {
+ return None;
+ }
+ let idx = self.1;
+ self.1 += 1;
+ self.0.from_index(idx)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ #[test]
+ fn test_value_iter() {
+ let range: crate::coord::ranged1d::types::RangedCoordi32 = (-10..10).into();
+
+ let values: Vec<_> = range.values().collect();
+
+ assert_eq!(21, values.len());
+
+ for (expected, value) in (-10..=10).zip(values) {
+ assert_eq!(expected, value);
+ }
+ assert_eq!(range.next(&5), Some(6));
+ assert_eq!(range.next(&10), None);
+ assert_eq!(range.previous(&-10), None);
+ assert_eq!(range.previous(&10), Some(9));
+ }
+
+ #[test]
+ fn test_centric_coord() {
+ let coord = (0..10).into_segmented();
+
+ assert_eq!(coord.size(), 12);
+ for i in 0..=11 {
+ match coord.from_index(i as usize) {
+ Some(SegmentValue::Exact(value)) => assert_eq!(i, value),
+ Some(SegmentValue::Last) => assert_eq!(i, 11),
+ _ => panic!(),
+ }
+ }
+
+ for (kps, idx) in coord.key_points(20).into_iter().zip(0..) {
+ match kps {
+ SegmentValue::CenterOf(value) if value <= 10 => assert_eq!(value, idx),
+ _ => panic!(),
+ }
+ }
+
+ assert_eq!(coord.map(&SegmentValue::CenterOf(0), (0, 24)), 1);
+ assert_eq!(coord.map(&SegmentValue::Exact(0), (0, 24)), 0);
+ assert_eq!(coord.map(&SegmentValue::Exact(1), (0, 24)), 2);
+ }
+}
diff --git a/src/coord/ranged1d/mod.rs b/src/coord/ranged1d/mod.rs
new file mode 100644
index 0000000..06de6bf
--- /dev/null
+++ b/src/coord/ranged1d/mod.rs
@@ -0,0 +1,234 @@
+/*!
+ The one-dimensional coordinate system abstraction.
+
+ Plotters build complex coordinate system with a combinator pattern and all the coordinate system is
+ built from the one dimensional coordinate system. This module defines the fundamental types used by
+ the one-dimensional coordinate system.
+
+ The key trait for a one dimensional coordinate is [Ranged](trait.Ranged.html). This trait describes a
+ set of values which served as the 1D coordinate system in Plotters. In order to extend the coordinate system,
+ the new coordinate spec must implement this trait.
+
+ The following example demonstrate how to make a customized coordinate specification
+ ```
+use plotters::coord::ranged1d::{Ranged, DefaultFormatting, KeyPointHint};
+use std::ops::Range;
+
+struct ZeroToOne;
+
+impl Ranged for ZeroToOne {
+ type ValueType = f64;
+ type FormatOption = DefaultFormatting;
+
+ fn map(&self, &v: &f64, pixel_range: (i32, i32)) -> i32 {
+ let size = pixel_range.1 - pixel_range.0;
+ let v = v.min(1.0).max(0.0);
+ ((size as f64) * v).round() as i32
+ }
+
+ fn key_points<Hint:KeyPointHint>(&self, hint: Hint) -> Vec<f64> {
+ if hint.max_num_points() < 3 {
+ vec![]
+ } else {
+ vec![0.0, 0.5, 1.0]
+ }
+ }
+
+ fn range(&self) -> Range<f64> {
+ 0.0..1.0
+ }
+}
+
+use plotters::prelude::*;
+
+let mut buffer = vec![0; 1024 * 768 * 3];
+let root = BitMapBackend::with_buffer(&mut buffer, (1024, 768)).into_drawing_area();
+
+let chart = ChartBuilder::on(&root)
+ .build_cartesian_2d(ZeroToOne, ZeroToOne)
+ .unwrap();
+
+ ```
+*/
+use std::fmt::Debug;
+use std::ops::Range;
+
+pub(super) mod combinators;
+pub(super) mod types;
+
+mod discrete;
+pub use discrete::{DiscreteRanged, IntoSegmentedCoord, SegmentValue, SegmentedCoord};
+
+/// Since stable Rust doesn't have specialization, it's very hard to make our own trait that
+/// automatically implemented the value formatter. This trait uses as a marker indicates if we
+/// should automatically implement the default value formater based on it's `Debug` trait
+pub trait DefaultValueFormatOption {}
+
+/// This makes the ranged coord uses the default `Debug` based formatting
+pub struct DefaultFormatting;
+impl DefaultValueFormatOption for DefaultFormatting {}
+
+/// This markers prevent Plotters to implement the default `Debug` based formatting
+pub struct NoDefaultFormatting;
+impl DefaultValueFormatOption for NoDefaultFormatting {}
+
+/// Determine how we can format a value in a coordinate system by default
+pub trait ValueFormatter<V> {
+ /// Format the value
+ fn format(value: &V) -> String;
+}
+
+// By default the value is formatted by the debug trait
+impl<R: Ranged<FormatOption = DefaultFormatting>> ValueFormatter<R::ValueType> for R
+where
+ R::ValueType: Debug,
+{
+ fn format(value: &R::ValueType) -> String {
+ format!("{:?}", value)
+ }
+}
+
+/// Specify the weight of key points.
+pub enum KeyPointWeight {
+ // Allows only bold key points
+ Bold,
+ // Allows any key points
+ Any,
+}
+
+impl KeyPointWeight {
+ /// Check if this key point weight setting allows light point
+ pub fn allow_light_points(&self) -> bool {
+ match self {
+ KeyPointWeight::Bold => false,
+ KeyPointWeight::Any => true,
+ }
+ }
+}
+
+/// The trait for a hint provided to the key point algorithm used by the coordinate specs.
+/// The most important constraint is the `max_num_points` which means the algorithm could emit no more than specific number of key points
+/// `weight` is used to determine if this is used as a bold grid line or light grid line
+/// `bold_points` returns the max number of coresponding bold grid lines
+pub trait KeyPointHint {
+ /// Returns the max number of key points
+ fn max_num_points(&self) -> usize;
+ /// Returns the weight for this hint
+ fn weight(&self) -> KeyPointWeight;
+ /// Returns the point number constraint for the bold points
+ fn bold_points(&self) -> usize {
+ self.max_num_points()
+ }
+}
+
+impl KeyPointHint for usize {
+ fn max_num_points(&self) -> usize {
+ *self
+ }
+
+ fn weight(&self) -> KeyPointWeight {
+ KeyPointWeight::Any
+ }
+}
+
+/// The key point hint indicates we only need key point for the bold grid lines
+pub struct BoldPoints(pub usize);
+
+impl KeyPointHint for BoldPoints {
+ fn max_num_points(&self) -> usize {
+ self.0
+ }
+
+ fn weight(&self) -> KeyPointWeight {
+ KeyPointWeight::Bold
+ }
+}
+
+/// The key point hint indicates that we are using the key points for the light grid lines
+pub struct LightPoints {
+ bold_points_num: usize,
+ light_limit: usize,
+}
+
+impl LightPoints {
+ /// Create a new light key point hind
+ pub fn new(bold_count: usize, requested: usize) -> Self {
+ Self {
+ bold_points_num: bold_count,
+ light_limit: requested,
+ }
+ }
+}
+
+impl KeyPointHint for LightPoints {
+ fn max_num_points(&self) -> usize {
+ self.light_limit
+ }
+
+ fn bold_points(&self) -> usize {
+ self.bold_points_num
+ }
+
+ fn weight(&self) -> KeyPointWeight {
+ KeyPointWeight::Any
+ }
+}
+
+/// The trait that indicates we have a ordered and ranged value
+/// Which is used to describe any 1D axis.
+pub trait Ranged {
+ /// This marker decides if Plotters default [ValueFormatter](trait.ValueFormatter.html) implementation should be used.
+ /// This assicated type can be one of follow two types:
+ /// - [DefaultFormatting](struct.DefaultFormatting.html) will allow Plotters automatically impl
+ /// the formatter based on `Debug` trait, if `Debug` trait is not impl for the `Self::Value`,
+ /// [ValueFormatter](trait.ValueFormatter.html) will not impl unless you impl it manually.
+ ///
+ /// - [NoDefaultFormatting](struct.NoDefaultFormatting.html) Disable the automatical `Debug`
+ /// based value formatting. Thus you have to impl the
+ /// [ValueFormatter](trait.ValueFormatter.html) manually.
+ ///
+ type FormatOption: DefaultValueFormatOption;
+
+ /// The type of this value in this range specification
+ type ValueType;
+
+ /// This function maps the value to i32, which is the drawing coordinate
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32;
+
+ /// This function gives the key points that we can draw a grid based on this
+ fn key_points<Hint: KeyPointHint>(&self, hint: Hint) -> Vec<Self::ValueType>;
+
+ /// Get the range of this value
+ fn range(&self) -> Range<Self::ValueType>;
+
+ /// This function provides the on-axis part of its range
+ #[allow(clippy::range_plus_one)]
+ fn axis_pixel_range(&self, limit: (i32, i32)) -> Range<i32> {
+ if limit.0 < limit.1 {
+ limit.0..limit.1
+ } else {
+ (limit.1 + 1)..(limit.0 + 1)
+ }
+ }
+}
+
+/// The trait indicates the ranged value can be map reversely, which means
+/// an pixel-based coordinate is given, it's possible to figure out the underlying
+/// logic value.
+pub trait ReversibleRanged: Ranged {
+ fn unmap(&self, input: i32, limit: (i32, i32)) -> Option<Self::ValueType>;
+}
+
+/// The trait for the type that can be converted into a ranged coordinate axis
+pub trait AsRangedCoord: Sized {
+ type CoordDescType: Ranged<ValueType = Self::Value> + From<Self>;
+ type Value;
+}
+
+impl<T> AsRangedCoord for T
+where
+ T: Ranged,
+{
+ type CoordDescType = T;
+ type Value = T::ValueType;
+}
diff --git a/src/coord/datetime.rs b/src/coord/ranged1d/types/datetime.rs
index cb96f93..f6b5717 100644
--- a/src/coord/datetime.rs
+++ b/src/coord/ranged1d/types/datetime.rs
@@ -1,24 +1,31 @@
/// The datetime coordinates
-use chrono::{Date, DateTime, Datelike, Duration, NaiveTime, TimeZone, Timelike};
-use std::ops::Range;
+use chrono::{Date, DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, TimeZone, Timelike};
+use std::ops::{Add, Range, Sub};
-use super::{AsRangedCoord, DiscreteRanged, Ranged};
+use crate::coord::ranged1d::{
+ AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged,
+ ValueFormatter,
+};
-/// The trait that describe some time value
+/// The trait that describe some time value. This is the uniformed abstraction that works
+/// for both Date, DateTime and Duration, etc.
pub trait TimeValue: Eq {
- type Tz: TimeZone;
+ type DateType: Datelike + PartialOrd;
+
/// Returns the date that is no later than the time
- fn date_floor(&self) -> Date<Self::Tz>;
+ fn date_floor(&self) -> Self::DateType;
/// Returns the date that is no earlier than the time
- fn date_ceil(&self) -> Date<Self::Tz>;
+ fn date_ceil(&self) -> Self::DateType;
/// Returns the maximum value that is earlier than the given date
- fn earliest_after_date(date: Date<Self::Tz>) -> Self;
+ fn earliest_after_date(date: Self::DateType) -> Self;
/// Returns the duration between two time value
fn subtract(&self, other: &Self) -> Duration;
- /// Get the timezone information for current value
- fn timezone(&self) -> Self::Tz;
+ /// Instantiate a date type for current time value;
+ fn ymd(&self, year: i32, month: u32, date: u32) -> Self::DateType;
+ /// Cast current date type into this type
+ fn from_date(date: Self::DateType) -> Self;
- /// Map the coord
+ /// Map the coord spec
fn map_coord(value: &Self, begin: &Self, end: &Self, limit: (i32, i32)) -> i32 {
let total_span = end.subtract(begin);
let value_span = value.subtract(begin);
@@ -31,6 +38,7 @@ pub trait TimeValue: Eq {
}
}
+ // Yes, converting them to floating point may lose precision, but this is Ok.
// If it overflows, it means we have a time span nearly 300 years, we are safe to ignore the
// portion less than 1 day.
let total_days = total_span.num_days() as f64;
@@ -40,8 +48,32 @@ pub trait TimeValue: Eq {
}
}
+impl TimeValue for NaiveDate {
+ type DateType = NaiveDate;
+ fn date_floor(&self) -> NaiveDate {
+ *self
+ }
+ fn date_ceil(&self) -> NaiveDate {
+ *self
+ }
+ fn earliest_after_date(date: NaiveDate) -> Self {
+ date
+ }
+ fn subtract(&self, other: &NaiveDate) -> Duration {
+ *self - *other
+ }
+
+ fn ymd(&self, year: i32, month: u32, date: u32) -> Self::DateType {
+ NaiveDate::from_ymd(year, month, date)
+ }
+
+ fn from_date(date: Self::DateType) -> Self {
+ date
+ }
+}
+
impl<Z: TimeZone> TimeValue for Date<Z> {
- type Tz = Z;
+ type DateType = Date<Z>;
fn date_floor(&self) -> Date<Z> {
self.clone()
}
@@ -54,13 +86,18 @@ impl<Z: TimeZone> TimeValue for Date<Z> {
fn subtract(&self, other: &Date<Z>) -> Duration {
self.clone() - other.clone()
}
- fn timezone(&self) -> Self::Tz {
- self.timezone()
+
+ fn ymd(&self, year: i32, month: u32, date: u32) -> Self::DateType {
+ self.timezone().ymd(year, month, date)
+ }
+
+ fn from_date(date: Self::DateType) -> Self {
+ date
}
}
impl<Z: TimeZone> TimeValue for DateTime<Z> {
- type Tz = Z;
+ type DateType = Date<Z>;
fn date_floor(&self) -> Date<Z> {
self.date()
}
@@ -78,25 +115,63 @@ impl<Z: TimeZone> TimeValue for DateTime<Z> {
fn subtract(&self, other: &DateTime<Z>) -> Duration {
self.clone() - other.clone()
}
- fn timezone(&self) -> Self::Tz {
- self.timezone()
+
+ fn ymd(&self, year: i32, month: u32, date: u32) -> Self::DateType {
+ self.timezone().ymd(year, month, date)
+ }
+
+ fn from_date(date: Self::DateType) -> Self {
+ date.and_hms(0, 0, 0)
+ }
+}
+
+impl TimeValue for NaiveDateTime {
+ type DateType = NaiveDate;
+ fn date_floor(&self) -> NaiveDate {
+ self.date()
+ }
+ fn date_ceil(&self) -> NaiveDate {
+ if self.time().num_seconds_from_midnight() > 0 {
+ self.date() + Duration::days(1)
+ } else {
+ self.date()
+ }
+ }
+ fn earliest_after_date(date: NaiveDate) -> NaiveDateTime {
+ date.and_hms(0, 0, 0)
+ }
+
+ fn subtract(&self, other: &NaiveDateTime) -> Duration {
+ *self - *other
+ }
+
+ fn ymd(&self, year: i32, month: u32, date: u32) -> Self::DateType {
+ NaiveDate::from_ymd(year, month, date)
+ }
+
+ fn from_date(date: Self::DateType) -> Self {
+ date.and_hms(0, 0, 0)
}
}
/// The ranged coordinate for date
#[derive(Clone)]
-pub struct RangedDate<Z: TimeZone>(Date<Z>, Date<Z>);
+pub struct RangedDate<D: Datelike>(D, D);
-impl<Z: TimeZone> From<Range<Date<Z>>> for RangedDate<Z> {
- fn from(range: Range<Date<Z>>) -> Self {
+impl<D: Datelike> From<Range<D>> for RangedDate<D> {
+ fn from(range: Range<D>) -> Self {
Self(range.start, range.end)
}
}
-impl<Z: TimeZone> Ranged for RangedDate<Z> {
- type ValueType = Date<Z>;
+impl<D> Ranged for RangedDate<D>
+where
+ D: Datelike + TimeValue + Sub<D, Output = Duration> + Add<Duration, Output = D> + Clone,
+{
+ type FormatOption = DefaultFormatting;
+ type ValueType = D;
- fn range(&self) -> Range<Date<Z>> {
+ fn range(&self) -> Range<D> {
self.0.clone()..self.1.clone()
}
@@ -104,7 +179,8 @@ impl<Z: TimeZone> Ranged for RangedDate<Z> {
TimeValue::map_coord(value, &self.0, &self.1, limit)
}
- fn key_points(&self, max_points: usize) -> Vec<Self::ValueType> {
+ fn key_points<HintType: KeyPointHint>(&self, hint: HintType) -> Vec<Self::ValueType> {
+ let max_points = hint.max_num_points();
let mut ret = vec![];
let total_days = (self.1.clone() - self.0.clone()).num_days();
@@ -134,23 +210,37 @@ impl<Z: TimeZone> Ranged for RangedDate<Z> {
}
}
-impl<Z: TimeZone> DiscreteRanged for RangedDate<Z> {
- type RangeParameter = ();
- fn get_range_parameter(&self) {}
- fn next_value(this: &Date<Z>, _: &()) -> Date<Z> {
- this.clone() + Duration::days(1)
+impl<D> DiscreteRanged for RangedDate<D>
+where
+ D: Datelike + TimeValue + Sub<D, Output = Duration> + Add<Duration, Output = D> + Clone,
+{
+ fn size(&self) -> usize {
+ ((self.1.clone() - self.0.clone()).num_days().max(-1) + 1) as usize
}
- fn previous_value(this: &Date<Z>, _: &()) -> Date<Z> {
- this.clone() - Duration::days(1)
+ fn index_of(&self, value: &D) -> Option<usize> {
+ let ret = (value.clone() - self.0.clone()).num_days();
+ if ret < 0 {
+ return None;
+ }
+ Some(ret as usize)
+ }
+
+ fn from_index(&self, index: usize) -> Option<D> {
+ Some(self.0.clone() + Duration::days(index as i64))
}
}
impl<Z: TimeZone> AsRangedCoord for Range<Date<Z>> {
- type CoordDescType = RangedDate<Z>;
+ type CoordDescType = RangedDate<Date<Z>>;
type Value = Date<Z>;
}
+impl AsRangedCoord for Range<NaiveDate> {
+ type CoordDescType = RangedDate<NaiveDate>;
+ type Value = NaiveDate;
+}
+
/// Indicates the coord has a monthly resolution
///
/// Note: since month doesn't have a constant duration.
@@ -159,23 +249,15 @@ impl<Z: TimeZone> AsRangedCoord for Range<Date<Z>> {
#[derive(Clone)]
pub struct Monthly<T: TimeValue>(Range<T>);
-impl<T: TimeValue + Clone> AsRangedCoord for Monthly<T> {
- type CoordDescType = Monthly<T>;
- type Value = T;
-}
-
-impl<T: TimeValue + Clone> Ranged for Monthly<T> {
- type ValueType = T;
-
- fn range(&self) -> Range<T> {
- self.0.start.clone()..self.0.end.clone()
- }
-
- fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
- T::map_coord(value, &self.0.start, &self.0.end, limit)
+impl<T: TimeValue + Datelike + Clone> ValueFormatter<T> for Monthly<T> {
+ fn format(value: &T) -> String {
+ format!("{}-{}", value.year(), value.month())
}
+}
- fn key_points(&self, max_points: usize) -> Vec<Self::ValueType> {
+impl<T: TimeValue + Clone> Monthly<T> {
+ fn bold_key_points<H: KeyPointHint>(&self, hint: &H) -> Vec<T> {
+ let max_points = hint.max_num_points();
let start_date = self.0.start.date_ceil();
let end_date = self.0.end.date_floor();
@@ -202,11 +284,11 @@ impl<T: TimeValue + Clone> Ranged for Monthly<T> {
end_year: i32,
end_month: i32,
step: u32,
- tz: T::Tz,
+ builder: &T,
) -> Vec<T> {
let mut ret = vec![];
while end_year > start_year || (end_year == start_year && end_month >= start_month) {
- ret.push(T::earliest_after_date(tz.ymd(
+ ret.push(T::earliest_after_date(builder.ymd(
start_year,
start_month as u32,
1,
@@ -230,7 +312,7 @@ impl<T: TimeValue + Clone> Ranged for Monthly<T> {
end_year,
end_month as i32,
1,
- self.0.start.timezone(),
+ &self.0.start,
);
} else if total_month as usize <= max_points * 3 {
// Quarterly
@@ -240,7 +322,7 @@ impl<T: TimeValue + Clone> Ranged for Monthly<T> {
end_year,
end_month as i32,
3,
- self.0.start.timezone(),
+ &self.0.start,
);
} else if total_month as usize <= max_points * 6 {
// Biyearly
@@ -250,7 +332,7 @@ impl<T: TimeValue + Clone> Ranged for Monthly<T> {
end_year,
end_month as i32,
6,
- self.0.start.timezone(),
+ &self.0.start,
);
}
@@ -261,34 +343,84 @@ impl<T: TimeValue + Clone> Ranged for Monthly<T> {
start_month,
end_year,
end_month,
- self.0.start.timezone(),
+ &self.0.start,
)
}
}
-impl<T: TimeValue + Clone> DiscreteRanged for Monthly<T> {
- type RangeParameter = ();
- fn get_range_parameter(&self) {}
- fn next_value(this: &T, _: &()) -> T {
- let mut year = this.date_ceil().year();
- let mut month = this.date_ceil().month();
- month += 1;
- if month == 13 {
- month = 1;
- year += 1;
+impl<T: TimeValue + Clone> Ranged for Monthly<T>
+where
+ Range<T>: AsRangedCoord<Value = T>,
+{
+ type FormatOption = NoDefaultFormatting;
+ type ValueType = T;
+
+ fn range(&self) -> Range<T> {
+ self.0.start.clone()..self.0.end.clone()
+ }
+
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
+ T::map_coord(value, &self.0.start, &self.0.end, limit)
+ }
+
+ fn key_points<HintType: KeyPointHint>(&self, hint: HintType) -> Vec<Self::ValueType> {
+ if hint.weight().allow_light_points() && self.size() <= hint.bold_points() * 2 {
+ let coord: <Range<T> as AsRangedCoord>::CoordDescType = self.0.clone().into();
+ let normal = coord.key_points(hint.max_num_points());
+ return normal;
}
- T::earliest_after_date(this.timezone().ymd(year, month, this.date_ceil().day()))
+ self.bold_key_points(&hint)
}
+}
- fn previous_value(this: &T, _: &()) -> T {
- let mut year = this.clone().date_floor().year();
- let mut month = this.clone().date_floor().month();
- month -= 1;
- if month == 0 {
- month = 12;
- year -= 1;
+impl<T: TimeValue + Clone> DiscreteRanged for Monthly<T>
+where
+ Range<T>: AsRangedCoord<Value = T>,
+{
+ fn size(&self) -> usize {
+ let (start_year, start_month) = {
+ let ceil = self.0.start.date_ceil();
+ (ceil.year(), ceil.month())
+ };
+ let (end_year, end_month) = {
+ let floor = self.0.end.date_floor();
+ (floor.year(), floor.month())
+ };
+ ((end_year - start_year).max(0) * 12
+ + (1 - start_month as i32)
+ + (end_month as i32 - 1)
+ + 1)
+ .max(0) as usize
+ }
+
+ fn index_of(&self, value: &T) -> Option<usize> {
+ let this_year = value.date_floor().year();
+ let this_month = value.date_floor().month();
+
+ let start_year = self.0.start.date_ceil().year();
+ let start_month = self.0.start.date_ceil().month();
+
+ let ret = (this_year - start_year).max(0) * 12
+ + (1 - start_month as i32)
+ + (this_month as i32 - 1);
+ if ret >= 0 {
+ return Some(ret as usize);
+ }
+ None
+ }
+
+ fn from_index(&self, index: usize) -> Option<T> {
+ if index == 0 {
+ return Some(T::earliest_after_date(self.0.start.date_ceil()));
}
- T::earliest_after_date(this.timezone().ymd(year, month, this.date_floor().day()))
+ let index_from_start_year = index + (self.0.start.date_ceil().month() - 1) as usize;
+ let year = self.0.start.date_ceil().year() + index_from_start_year as i32 / 12;
+ let month = index_from_start_year % 12;
+ Some(T::earliest_after_date(self.0.start.ymd(
+ year,
+ month as u32 + 1,
+ 1,
+ )))
}
}
@@ -296,18 +428,13 @@ impl<T: TimeValue + Clone> DiscreteRanged for Monthly<T> {
#[derive(Clone)]
pub struct Yearly<T: TimeValue>(Range<T>);
-impl<T: TimeValue + Clone> AsRangedCoord for Yearly<T> {
- type CoordDescType = Yearly<T>;
- type Value = T;
-}
-
fn generate_yearly_keypoints<T: TimeValue>(
max_points: usize,
mut start_year: i32,
start_month: u32,
mut end_year: i32,
end_month: u32,
- tz: T::Tz,
+ builder: &T,
) -> Vec<T> {
if start_month > end_month {
end_year -= 1;
@@ -331,14 +458,28 @@ fn generate_yearly_keypoints<T: TimeValue>(
let mut ret = vec![];
while start_year <= end_year {
- ret.push(T::earliest_after_date(tz.ymd(start_year, start_month, 1)));
+ ret.push(T::earliest_after_date(builder.ymd(
+ start_year,
+ start_month,
+ 1,
+ )));
start_year += freq as i32;
}
ret
}
-impl<T: TimeValue + Clone> Ranged for Yearly<T> {
+impl<T: TimeValue + Datelike + Clone> ValueFormatter<T> for Yearly<T> {
+ fn format(value: &T) -> String {
+ format!("{}-{}", value.year(), value.month())
+ }
+}
+
+impl<T: TimeValue + Clone> Ranged for Yearly<T>
+where
+ Range<T>: AsRangedCoord<Value = T>,
+{
+ type FormatOption = NoDefaultFormatting;
type ValueType = T;
fn range(&self) -> Range<T> {
@@ -349,7 +490,11 @@ impl<T: TimeValue + Clone> Ranged for Yearly<T> {
T::map_coord(value, &self.0.start, &self.0.end, limit)
}
- fn key_points(&self, max_points: usize) -> Vec<Self::ValueType> {
+ fn key_points<HintType: KeyPointHint>(&self, hint: HintType) -> Vec<Self::ValueType> {
+ if hint.weight().allow_light_points() && self.size() <= hint.bold_points() * 2 {
+ return Monthly(self.0.clone()).key_points(hint);
+ }
+ let max_points = hint.max_num_points();
let start_date = self.0.start.date_ceil();
let end_date = self.0.end.date_floor();
@@ -374,20 +519,38 @@ impl<T: TimeValue + Clone> Ranged for Yearly<T> {
start_month,
end_year,
end_month,
- self.0.start.timezone(),
+ &self.0.start,
)
}
}
-impl<T: TimeValue + Clone> DiscreteRanged for Yearly<T> {
- type RangeParameter = ();
- fn get_range_parameter(&self) {}
- fn next_value(this: &T, _: &()) -> T {
- T::earliest_after_date(this.timezone().ymd(this.date_floor().year() + 1, 1, 1))
+impl<T: TimeValue + Clone> DiscreteRanged for Yearly<T>
+where
+ Range<T>: AsRangedCoord<Value = T>,
+{
+ fn size(&self) -> usize {
+ let year_start = self.0.start.date_ceil().year();
+ let year_end = self.0.end.date_floor().year();
+ ((year_end - year_start).max(-1) + 1) as usize
}
- fn previous_value(this: &T, _: &()) -> T {
- T::earliest_after_date(this.timezone().ymd(this.date_ceil().year() - 1, 1, 1))
+ fn index_of(&self, value: &T) -> Option<usize> {
+ let year_start = self.0.start.date_ceil().year();
+ let year_value = value.date_floor().year();
+ let ret = year_value - year_start;
+ if ret < 0 {
+ return None;
+ }
+ Some(ret as usize)
+ }
+
+ fn from_index(&self, index: usize) -> Option<T> {
+ let year = self.0.start.date_ceil().year() + index as i32;
+ let ret = T::earliest_after_date(self.0.start.ymd(year, 1, 1));
+ if ret.date_ceil() <= self.0.start.date_floor() {
+ return Some(self.0.start.clone());
+ }
+ Some(ret)
}
}
@@ -415,23 +578,36 @@ impl<T: TimeValue> IntoYearly<T> for Range<T> {
/// The ranged coordinate for the date and time
#[derive(Clone)]
-pub struct RangedDateTime<Z: TimeZone>(DateTime<Z>, DateTime<Z>);
+pub struct RangedDateTime<DT: Datelike + Timelike + TimeValue>(DT, DT);
impl<Z: TimeZone> AsRangedCoord for Range<DateTime<Z>> {
- type CoordDescType = RangedDateTime<Z>;
+ type CoordDescType = RangedDateTime<DateTime<Z>>;
type Value = DateTime<Z>;
}
-impl<Z: TimeZone> From<Range<DateTime<Z>>> for RangedDateTime<Z> {
+impl<Z: TimeZone> From<Range<DateTime<Z>>> for RangedDateTime<DateTime<Z>> {
fn from(range: Range<DateTime<Z>>) -> Self {
Self(range.start, range.end)
}
}
-impl<Z: TimeZone> Ranged for RangedDateTime<Z> {
- type ValueType = DateTime<Z>;
+impl From<Range<NaiveDateTime>> for RangedDateTime<NaiveDateTime> {
+ fn from(range: Range<NaiveDateTime>) -> Self {
+ Self(range.start, range.end)
+ }
+}
- fn range(&self) -> Range<DateTime<Z>> {
+impl<DT> Ranged for RangedDateTime<DT>
+where
+ DT: Datelike + Timelike + TimeValue + Clone + PartialOrd,
+ DT: Add<Duration, Output = DT>,
+ DT: Sub<DT, Output = Duration>,
+ RangedDate<DT::DateType>: Ranged<ValueType = DT::DateType>,
+{
+ type FormatOption = DefaultFormatting;
+ type ValueType = DT;
+
+ fn range(&self) -> Range<DT> {
self.0.clone()..self.1.clone()
}
@@ -439,30 +615,23 @@ impl<Z: TimeZone> Ranged for RangedDateTime<Z> {
TimeValue::map_coord(value, &self.0, &self.1, limit)
}
- fn key_points(&self, max_points: usize) -> Vec<Self::ValueType> {
+ fn key_points<HintType: KeyPointHint>(&self, hint: HintType) -> Vec<Self::ValueType> {
+ let max_points = hint.max_num_points();
let total_span = self.1.clone() - self.0.clone();
if let Some(total_ns) = total_span.num_nanoseconds() {
if let Some(actual_ns_per_point) =
compute_period_per_point(total_ns as u64, max_points, true)
{
- let start_time_ns = u64::from(self.0.time().num_seconds_from_midnight())
- * 1_000_000_000
- + u64::from(self.0.time().nanosecond());
-
- let mut start_time = self
- .0
- .date_floor()
- .and_time(
- NaiveTime::from_hms(0, 0, 0)
- + Duration::nanoseconds(if start_time_ns % actual_ns_per_point > 0 {
- start_time_ns
- + (actual_ns_per_point - start_time_ns % actual_ns_per_point)
- } else {
- start_time_ns
- } as i64),
- )
- .unwrap();
+ let start_time_ns = u64::from(self.0.num_seconds_from_midnight()) * 1_000_000_000
+ + u64::from(self.0.nanosecond());
+
+ let mut start_time = DT::from_date(self.0.date_floor())
+ + Duration::nanoseconds(if start_time_ns % actual_ns_per_point > 0 {
+ start_time_ns + (actual_ns_per_point - start_time_ns % actual_ns_per_point)
+ } else {
+ start_time_ns
+ } as i64);
let mut ret = vec![];
@@ -481,7 +650,7 @@ impl<Z: TimeZone> Ranged for RangedDateTime<Z> {
date_range
.key_points(max_points)
.into_iter()
- .map(|x| x.and_hms(0, 0, 0))
+ .map(DT::from_date)
.collect()
}
}
@@ -502,6 +671,7 @@ impl From<Range<Duration>> for RangedDuration {
}
impl Ranged for RangedDuration {
+ type FormatOption = DefaultFormatting;
type ValueType = Duration;
fn range(&self) -> Range<Duration> {
@@ -528,7 +698,8 @@ impl Ranged for RangedDuration {
+ (f64::from(limit.1 - limit.0) * value_days as f64 / total_days as f64 + 1e-10) as i32
}
- fn key_points(&self, max_points: usize) -> Vec<Self::ValueType> {
+ fn key_points<HintType: KeyPointHint>(&self, hint: HintType) -> Vec<Self::ValueType> {
+ let max_points = hint.max_num_points();
let total_span = self.1 - self.0;
if let Some(total_ns) = total_span.num_nanoseconds() {
@@ -741,6 +912,7 @@ mod test {
#[test]
fn test_yearly_date_range() {
+ use crate::coord::ranged1d::BoldPoints;
let range = Utc.ymd(1000, 8, 5)..Utc.ymd(2999, 1, 1);
let ranged_coord = range.yearly();
@@ -768,7 +940,7 @@ mod test {
let range = Utc.ymd(2019, 8, 5)..Utc.ymd(2020, 1, 1);
let ranged_coord = range.yearly();
- let kps = ranged_coord.key_points(23);
+ let kps = ranged_coord.key_points(BoldPoints(23));
assert!(kps.len() == 1);
}
@@ -777,13 +949,15 @@ mod test {
let range = Utc.ymd(2019, 8, 5)..Utc.ymd(2020, 9, 1);
let ranged_coord = range.monthly();
- let kps = ranged_coord.key_points(15);
+ use crate::coord::ranged1d::BoldPoints;
+
+ let kps = ranged_coord.key_points(BoldPoints(15));
assert!(kps.len() <= 15);
assert!(kps.iter().all(|x| x.day() == 1));
assert!(kps.into_iter().any(|x| x.month() != 9));
- let kps = ranged_coord.key_points(5);
+ let kps = ranged_coord.key_points(BoldPoints(5));
assert!(kps.len() <= 5);
assert!(kps.iter().all(|x| x.day() == 1));
let kps: Vec<_> = kps.into_iter().map(|x| x.month()).collect();
@@ -951,4 +1125,39 @@ mod test {
assert!(max == min);
assert_eq!(max, 3600 * 2);
}
+
+ #[test]
+ fn test_date_discrete() {
+ let coord: RangedDate<Date<_>> = (Utc.ymd(2019, 1, 1)..Utc.ymd(2019, 12, 31)).into();
+ assert_eq!(coord.size(), 365);
+ assert_eq!(coord.index_of(&Utc.ymd(2019, 2, 28)), Some(31 + 28 - 1));
+ assert_eq!(coord.from_index(364), Some(Utc.ymd(2019, 12, 31)));
+ }
+
+ #[test]
+ fn test_monthly_discrete() {
+ let coord1 = (Utc.ymd(2019, 1, 10)..Utc.ymd(2019, 12, 31)).monthly();
+ let coord2 = (Utc.ymd(2019, 1, 10)..Utc.ymd(2020, 1, 1)).monthly();
+ assert_eq!(coord1.size(), 12);
+ assert_eq!(coord2.size(), 13);
+
+ for i in 1..=12 {
+ assert_eq!(coord1.from_index(i - 1).unwrap().month(), i as u32);
+ assert_eq!(
+ coord1.index_of(&coord1.from_index(i - 1).unwrap()).unwrap(),
+ i - 1
+ );
+ }
+ }
+
+ #[test]
+ fn test_yearly_discrete() {
+ let coord1 = (Utc.ymd(2000, 1, 10)..Utc.ymd(2019, 12, 31)).yearly();
+ assert_eq!(coord1.size(), 20);
+
+ for i in 0..20 {
+ assert_eq!(coord1.from_index(i).unwrap().year(), 2000 + i as i32);
+ assert_eq!(coord1.index_of(&coord1.from_index(i).unwrap()).unwrap(), i);
+ }
+ }
}
diff --git a/src/coord/ranged1d/types/mod.rs b/src/coord/ranged1d/types/mod.rs
new file mode 100644
index 0000000..5a5ca48
--- /dev/null
+++ b/src/coord/ranged1d/types/mod.rs
@@ -0,0 +1,15 @@
+#[cfg(feature = "chrono")]
+mod datetime;
+#[cfg(feature = "chrono")]
+pub use datetime::{
+ IntoMonthly, IntoYearly, Monthly, RangedDate, RangedDateTime, RangedDuration, Yearly,
+};
+
+mod numeric;
+pub use numeric::{
+ RangedCoordf32, RangedCoordf64, RangedCoordi128, RangedCoordi32, RangedCoordi64,
+ RangedCoordu128, RangedCoordu32, RangedCoordu64, RangedCoordusize,
+};
+
+mod slice;
+pub use slice::RangedSlice;
diff --git a/src/coord/numeric.rs b/src/coord/ranged1d/types/numeric.rs
index 6a9f72d..6de2bdf 100644
--- a/src/coord/numeric.rs
+++ b/src/coord/ranged1d/types/numeric.rs
@@ -1,17 +1,35 @@
+use std::convert::TryFrom;
use std::ops::Range;
-use super::{AsRangedCoord, DiscreteRanged, Ranged, ReversibleRanged};
+use crate::coord::ranged1d::{
+ AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged,
+ ReversibleRanged, ValueFormatter,
+};
macro_rules! impl_discrete_trait {
($name:ident) => {
impl DiscreteRanged for $name {
- type RangeParameter = ();
- fn get_range_parameter(&self) -> () {}
- fn next_value(this: &Self::ValueType, _: &()) -> Self::ValueType {
- return *this + 1;
+ fn size(&self) -> usize {
+ if &self.1 < &self.0 {
+ return 0;
+ }
+ let values = self.1 - self.0;
+ (values + 1) as usize
+ }
+
+ fn index_of(&self, value: &Self::ValueType) -> Option<usize> {
+ if value < &self.0 {
+ return None;
+ }
+ let ret = value - self.0;
+ Some(ret as usize)
}
- fn previous_value(this: &Self::ValueType, _: &()) -> Self::ValueType {
- return *this - 1;
+
+ fn from_index(&self, index: usize) -> Option<Self::ValueType> {
+ if let Ok(index) = Self::ValueType::try_from(index) {
+ return Some(self.0 + index);
+ }
+ None
}
}
};
@@ -25,9 +43,23 @@ macro_rules! impl_ranged_type_trait {
}
};
}
+macro_rules! impl_reverse_mapping_trait {
+ ($type:ty, $name: ident) => {
+ impl ReversibleRanged for $name {
+ fn unmap(&self, p: i32, (min, max): (i32, i32)) -> Option<$type> {
+ if p < min.min(max) || p > max.max(min) || min == max {
+ return None;
+ }
+ let logical_offset = f64::from(p - min) / f64::from(max - min);
+
+ return Some(((self.1 - self.0) as f64 * logical_offset + self.0 as f64) as $type);
+ }
+ }
+ };
+}
macro_rules! make_numeric_coord {
- ($type:ty, $name:ident, $key_points:ident, $doc: expr) => {
+ ($type:ty, $name:ident, $key_points:ident, $doc: expr, $fmt: ident) => {
#[doc = $doc]
#[derive(Clone)]
pub struct $name($type, $type);
@@ -37,9 +69,18 @@ macro_rules! make_numeric_coord {
}
}
impl Ranged for $name {
+ type FormatOption = $fmt;
type ValueType = $type;
+ #[allow(clippy::float_cmp)]
fn map(&self, v: &$type, limit: (i32, i32)) -> i32 {
+ // Corner case: If we have a range that have only one value,
+ // then we just assign everything to the only point
+ if self.1 == self.0 {
+ return (limit.1 - limit.0) / 2;
+ }
+
let logic_length = (*v - self.0) as f64 / (self.1 - self.0) as f64;
+
let actual_length = limit.1 - limit.0;
if actual_length == 0 {
@@ -48,26 +89,17 @@ macro_rules! make_numeric_coord {
return limit.0 + (actual_length as f64 * logic_length + 1e-3).floor() as i32;
}
- fn key_points(&self, max_points: usize) -> Vec<$type> {
- $key_points((self.0, self.1), max_points)
+ fn key_points<Hint: KeyPointHint>(&self, hint: Hint) -> Vec<$type> {
+ $key_points((self.0, self.1), hint.max_num_points())
}
fn range(&self) -> Range<$type> {
return self.0..self.1;
}
}
-
- impl ReversibleRanged for $name {
- fn unmap(&self, p:i32, (min,max): (i32, i32)) -> Option<$type> {
- if p < min.min(max) || p > max.max(min) {
- return None;
- }
-
- let logical_offset = (p - min) as f64 / (max - min) as f64;
-
- return Some(((self.1 - self.0) as f64 * logical_offset + self.0 as f64) as $type);
- }
- }
};
+ ($type:ty, $name:ident, $key_points:ident, $doc: expr) => {
+ make_numeric_coord!($type, $name, $key_points, $doc, DefaultFormatting);
+ }
}
macro_rules! gen_key_points_comp {
@@ -179,14 +211,38 @@ make_numeric_coord!(
f32,
RangedCoordf32,
compute_f32_key_points,
- "The ranged coordinate for type f32"
+ "The ranged coordinate for type f32",
+ NoDefaultFormatting
);
+impl_reverse_mapping_trait!(f32, RangedCoordf32);
+impl ValueFormatter<f32> for RangedCoordf32 {
+ fn format(value: &f32) -> String {
+ crate::data::float::FloatPrettyPrinter {
+ allow_scientific: false,
+ min_decimal: 1,
+ max_decimal: 5,
+ }
+ .print(*value as f64)
+ }
+}
make_numeric_coord!(
f64,
RangedCoordf64,
compute_f64_key_points,
- "The ranged coordinate for type f64"
+ "The ranged coordinate for type f64",
+ NoDefaultFormatting
);
+impl_reverse_mapping_trait!(f64, RangedCoordf64);
+impl ValueFormatter<f64> for RangedCoordf64 {
+ fn format(value: &f64) -> String {
+ crate::data::float::FloatPrettyPrinter {
+ allow_scientific: false,
+ min_decimal: 1,
+ max_decimal: 5,
+ }
+ .print(*value)
+ }
+}
make_numeric_coord!(
u32,
RangedCoordu32,
@@ -256,111 +312,9 @@ impl_ranged_type_trait!(u128, RangedCoordu128);
impl_ranged_type_trait!(isize, RangedCoordisize);
impl_ranged_type_trait!(usize, RangedCoordusize);
-// TODO: Think about how to re-organize this part
-pub mod group_integer_by {
- use super::Ranged;
- use super::{AsRangedCoord, DiscreteRanged};
- use num_traits::{FromPrimitive, PrimInt, ToPrimitive};
- use std::ops::{Mul, Range};
-
- /// The ranged value spec that needs to be grouped.
- /// This is useful, for example, when we have an X axis is a integer and denotes days.
- /// And we are expecting the tick mark denotes weeks, in this way we can make the range
- /// spec grouping by 7 elements.
- pub struct GroupBy<T>(T, T::ValueType)
- where
- T::ValueType: PrimInt + ToPrimitive + FromPrimitive + Mul,
- T: Ranged;
-
- /// The trait that provides method `Self::group_by` function which creates a
- /// `GroupBy` decorated ranged value.
- pub trait ToGroupByRange
- where
- Self: AsRangedCoord,
- <Self as AsRangedCoord>::Value: PrimInt + ToPrimitive + FromPrimitive + Mul,
- <<Self as AsRangedCoord>::CoordDescType as Ranged>::ValueType:
- PrimInt + ToPrimitive + FromPrimitive + Mul,
- {
- /// Make a grouping ranged value, see the documentation for `GroupBy` for details.
- ///
- /// - `value`: The number of values we want to group it
- /// - **return**: The newly created grouping range sepcification
- fn group_by(
- self,
- value: <<Self as AsRangedCoord>::CoordDescType as Ranged>::ValueType,
- ) -> GroupBy<<Self as AsRangedCoord>::CoordDescType> {
- GroupBy(self.into(), value)
- }
- }
-
- impl<T> ToGroupByRange for T
- where
- Self: AsRangedCoord,
- <Self as AsRangedCoord>::Value: PrimInt + FromPrimitive + ToPrimitive + Mul,
- <<Self as AsRangedCoord>::CoordDescType as Ranged>::ValueType:
- PrimInt + FromPrimitive + ToPrimitive + Mul,
- {
- }
-
- impl<T> AsRangedCoord for GroupBy<T>
- where
- T::ValueType: PrimInt + ToPrimitive + FromPrimitive + Mul,
- T: Ranged,
- {
- type Value = T::ValueType;
- type CoordDescType = Self;
- }
-
- impl<T> DiscreteRanged for GroupBy<T>
- where
- T::ValueType: PrimInt + ToPrimitive + FromPrimitive + Mul,
- T: Ranged + DiscreteRanged,
- {
- type RangeParameter = <T as DiscreteRanged>::RangeParameter;
- fn get_range_parameter(&self) -> Self::RangeParameter {
- self.0.get_range_parameter()
- }
- fn previous_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType {
- <T as DiscreteRanged>::previous_value(this, param)
- }
- fn next_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType {
- <T as DiscreteRanged>::next_value(this, param)
- }
- }
-
- impl<T> Ranged for GroupBy<T>
- where
- T::ValueType: PrimInt + ToPrimitive + FromPrimitive + Mul,
- T: Ranged,
- {
- type ValueType = T::ValueType;
- fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> i32 {
- self.0.map(value, limit)
- }
- fn range(&self) -> Range<T::ValueType> {
- self.0.range()
- }
- fn key_points(&self, max_points: usize) -> Vec<T::ValueType> {
- let actual_range = self.0.range();
- let from = ((actual_range.start + self.1 - T::ValueType::from_u8(1).unwrap()) / self.1)
- .to_isize()
- .unwrap();
- let to = (actual_range.end / self.1).to_isize().unwrap();
- let logic_range: super::RangedCoordisize = (from..to).into();
-
- logic_range
- .key_points(max_points)
- .into_iter()
- .map(|x| T::ValueType::from_isize(x).unwrap() * self.1)
- .collect()
- }
- }
-}
-
#[cfg(test)]
mod test {
use super::*;
- use crate::coord::*;
#[test]
fn test_key_points() {
let kp = compute_i32_key_points((0, 999), 28);
@@ -390,6 +344,18 @@ mod test {
#[test]
fn test_linear_coord_system() {
let _coord =
- RangedCoord::<RangedCoordu32, RangedCoordu32>::new(0..10, 0..10, (0..1024, 0..768));
+ crate::coord::ranged2d::cartesian::Cartesian2d::<RangedCoordu32, RangedCoordu32>::new(
+ 0..10,
+ 0..10,
+ (0..1024, 0..768),
+ );
+ }
+
+ #[test]
+ fn test_coord_unmap() {
+ let coord: RangedCoordu32 = (0..20).into();
+ let pos = coord.map(&5, (1000, 2000));
+ let value = coord.unmap(pos, (1000, 2000));
+ assert_eq!(value, Some(5));
}
}
diff --git a/src/coord/ranged1d/types/slice.rs b/src/coord/ranged1d/types/slice.rs
new file mode 100644
index 0000000..13be3d7
--- /dev/null
+++ b/src/coord/ranged1d/types/slice.rs
@@ -0,0 +1,100 @@
+use crate::coord::ranged1d::{
+ AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, Ranged,
+};
+use std::ops::Range;
+
+/// A range that is defined by a slice of values.
+///
+/// Please note: the behavior of constructing an empty range may cause panic
+#[derive(Clone)]
+pub struct RangedSlice<'a, T: PartialEq>(&'a [T]);
+
+impl<'a, T: PartialEq> Ranged for RangedSlice<'a, T> {
+ type FormatOption = DefaultFormatting;
+ type ValueType = &'a T;
+
+ fn range(&self) -> Range<&'a T> {
+ // If inner slice is empty, we should always panic
+ &self.0[0]..&self.0[self.0.len() - 1]
+ }
+
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
+ match self.0.iter().position(|x| &x == value) {
+ Some(pos) => {
+ let pixel_span = limit.1 - limit.0;
+ let value_span = self.0.len() - 1;
+ (f64::from(limit.0)
+ + f64::from(pixel_span)
+ * (f64::from(pos as u32) / f64::from(value_span as u32)))
+ .round() as i32
+ }
+ None => limit.0,
+ }
+ }
+
+ fn key_points<Hint: KeyPointHint>(&self, hint: Hint) -> Vec<Self::ValueType> {
+ let max_points = hint.max_num_points();
+ let mut ret = vec![];
+ let intervals = (self.0.len() - 1) as f64;
+ let step = (intervals / max_points as f64 + 1.0) as usize;
+ for idx in (0..self.0.len()).step_by(step) {
+ ret.push(&self.0[idx]);
+ }
+ ret
+ }
+}
+
+impl<'a, T: PartialEq> DiscreteRanged for RangedSlice<'a, T> {
+ fn size(&self) -> usize {
+ self.0.len()
+ }
+
+ fn index_of(&self, value: &&'a T) -> Option<usize> {
+ self.0.iter().position(|x| &x == value)
+ }
+
+ fn from_index(&self, index: usize) -> Option<&'a T> {
+ if self.0.len() <= index {
+ return None;
+ }
+ Some(&self.0[index])
+ }
+}
+
+impl<'a, T: PartialEq> From<&'a [T]> for RangedSlice<'a, T> {
+ fn from(range: &'a [T]) -> Self {
+ RangedSlice(range)
+ }
+}
+
+impl<'a, T: PartialEq> AsRangedCoord for &'a [T] {
+ type CoordDescType = RangedSlice<'a, T>;
+ type Value = &'a T;
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ #[test]
+ fn test_slice_range() {
+ let my_slice = [1, 2, 3, 0, -1, -2];
+ let slice_range: RangedSlice<i32> = my_slice[..].into();
+
+ assert_eq!(slice_range.range(), &1..&-2);
+ assert_eq!(
+ slice_range.key_points(6),
+ my_slice.iter().collect::<Vec<_>>()
+ );
+ assert_eq!(slice_range.map(&&0, (0, 50)), 30);
+ }
+
+ #[test]
+ fn test_slice_range_discrete() {
+ let my_slice = [1, 2, 3, 0, -1, -2];
+ let slice_range: RangedSlice<i32> = my_slice[..].into();
+
+ assert_eq!(slice_range.size(), 6);
+ assert_eq!(slice_range.index_of(&&3), Some(2));
+ assert_eq!(slice_range.from_index(2), Some(&3));
+ }
+}
diff --git a/src/coord/ranged2d/cartesian.rs b/src/coord/ranged2d/cartesian.rs
new file mode 100644
index 0000000..897e7f5
--- /dev/null
+++ b/src/coord/ranged2d/cartesian.rs
@@ -0,0 +1,152 @@
+/*!
+ The 2-dimensional cartesian coordinate system.
+
+ This module provides the 2D cartesian coordinate system, which is composed by two independent
+ ranged 1D coordinate sepcification.
+
+ This types of coordinate system is used by the chart constructed with [ChartBuilder::build_cartesian_2d](../../chart/ChartBuilder.html#method.build_cartesian_2d).
+*/
+
+use crate::coord::ranged1d::{KeyPointHint, Ranged, ReversibleRanged};
+use crate::coord::{CoordTranslate, ReverseCoordTranslate};
+
+use crate::style::ShapeStyle;
+use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
+
+use std::ops::Range;
+
+/// A 2D Cartesian coordinate system described by two 1D ranged coordinate specs.
+#[derive(Clone)]
+pub struct Cartesian2d<X: Ranged, Y: Ranged> {
+ logic_x: X,
+ logic_y: Y,
+ back_x: (i32, i32),
+ back_y: (i32, i32),
+}
+
+impl<X: Ranged, Y: Ranged> Cartesian2d<X, Y> {
+ /// Create a new 2D cartesian coordinate system
+ /// - `logic_x` and `logic_y` : The description for the 1D coordinate system
+ /// - `actual`: The pixel range on the screen for this coordinate system
+ pub fn new<IntoX: Into<X>, IntoY: Into<Y>>(
+ logic_x: IntoX,
+ logic_y: IntoY,
+ actual: (Range<i32>, Range<i32>),
+ ) -> Self {
+ Self {
+ logic_x: logic_x.into(),
+ logic_y: logic_y.into(),
+ back_x: (actual.0.start, actual.0.end),
+ back_y: (actual.1.start, actual.1.end),
+ }
+ }
+
+ /// Draw the mesh for the coordinate system
+ pub fn draw_mesh<
+ E,
+ DrawMesh: FnMut(MeshLine<X, Y>) -> Result<(), E>,
+ XH: KeyPointHint,
+ YH: KeyPointHint,
+ >(
+ &self,
+ h_limit: YH,
+ v_limit: XH,
+ mut draw_mesh: DrawMesh,
+ ) -> Result<(), E> {
+ let (xkp, ykp) = (
+ self.logic_x.key_points(v_limit),
+ self.logic_y.key_points(h_limit),
+ );
+
+ for logic_x in xkp {
+ let x = self.logic_x.map(&logic_x, self.back_x);
+ draw_mesh(MeshLine::XMesh(
+ (x, self.back_y.0),
+ (x, self.back_y.1),
+ &logic_x,
+ ))?;
+ }
+
+ for logic_y in ykp {
+ let y = self.logic_y.map(&logic_y, self.back_y);
+ draw_mesh(MeshLine::YMesh(
+ (self.back_x.0, y),
+ (self.back_x.1, y),
+ &logic_y,
+ ))?;
+ }
+
+ Ok(())
+ }
+
+ /// Get the range of X axis
+ pub fn get_x_range(&self) -> Range<X::ValueType> {
+ self.logic_x.range()
+ }
+
+ /// Get the range of Y axis
+ pub fn get_y_range(&self) -> Range<Y::ValueType> {
+ self.logic_y.range()
+ }
+
+ /// Get the horizental backend coordinate range where X axis should be drawn
+ pub fn get_x_axis_pixel_range(&self) -> Range<i32> {
+ self.logic_x.axis_pixel_range(self.back_x)
+ }
+
+ /// Get the vertical backend coordinate range where Y axis should be drawn
+ pub fn get_y_axis_pixel_range(&self) -> Range<i32> {
+ self.logic_y.axis_pixel_range(self.back_y)
+ }
+
+ /// Get the 1D coordinate spec for X axis
+ pub fn x_spec(&self) -> &X {
+ &self.logic_x
+ }
+
+ /// Get the 1D coordinate spec for Y axis
+ pub fn y_spec(&self) -> &Y {
+ &self.logic_y
+ }
+}
+
+impl<X: Ranged, Y: Ranged> CoordTranslate for Cartesian2d<X, Y> {
+ type From = (X::ValueType, Y::ValueType);
+
+ fn translate(&self, from: &Self::From) -> BackendCoord {
+ (
+ self.logic_x.map(&from.0, self.back_x),
+ self.logic_y.map(&from.1, self.back_y),
+ )
+ }
+}
+
+impl<X: ReversibleRanged, Y: ReversibleRanged> ReverseCoordTranslate for Cartesian2d<X, Y> {
+ fn reverse_translate(&self, input: BackendCoord) -> Option<Self::From> {
+ Some((
+ self.logic_x.unmap(input.0, self.back_x)?,
+ self.logic_y.unmap(input.1, self.back_y)?,
+ ))
+ }
+}
+
+/// Represent a coordinate mesh for the two ranged value coordinate system
+pub enum MeshLine<'a, X: Ranged, Y: Ranged> {
+ XMesh(BackendCoord, BackendCoord, &'a X::ValueType),
+ YMesh(BackendCoord, BackendCoord, &'a Y::ValueType),
+}
+
+impl<'a, X: Ranged, Y: Ranged> MeshLine<'a, X, Y> {
+ /// Draw a single mesh line onto the backend
+ pub fn draw<DB: DrawingBackend>(
+ &self,
+ backend: &mut DB,
+ style: &ShapeStyle,
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ let (&left, &right) = match self {
+ MeshLine::XMesh(a, b, _) => (a, b),
+ MeshLine::YMesh(a, b, _) => (a, b),
+ };
+ backend.draw_line(left, right, style)
+ }
+}
diff --git a/src/coord/ranged2d/mod.rs b/src/coord/ranged2d/mod.rs
new file mode 100644
index 0000000..eae9425
--- /dev/null
+++ b/src/coord/ranged2d/mod.rs
@@ -0,0 +1 @@
+pub mod cartesian;
diff --git a/src/coord/ranged3d/cartesian3d.rs b/src/coord/ranged3d/cartesian3d.rs
new file mode 100644
index 0000000..d7daffd
--- /dev/null
+++ b/src/coord/ranged3d/cartesian3d.rs
@@ -0,0 +1,105 @@
+use super::{ProjectionMatrix, ProjectionMatrixBuilder};
+use crate::coord::ranged1d::Ranged;
+use crate::coord::CoordTranslate;
+use plotters_backend::BackendCoord;
+
+use std::ops::Range;
+
+/// A 3D cartesian coordinate system
+pub struct Cartesian3d<X: Ranged, Y: Ranged, Z: Ranged> {
+ pub(crate) logic_x: X,
+ pub(crate) logic_y: Y,
+ pub(crate) logic_z: Z,
+ coord_size: (i32, i32, i32),
+ projection: ProjectionMatrix,
+}
+
+impl<X: Ranged, Y: Ranged, Z: Ranged> Cartesian3d<X, Y, Z> {
+ fn compute_default_size(actual_x: Range<i32>, actual_y: Range<i32>) -> i32 {
+ (actual_x.end - actual_x.start).min(actual_y.end - actual_y.start) * 4 / 5
+ }
+ fn create_projection<F: FnOnce(ProjectionMatrixBuilder) -> ProjectionMatrix>(
+ actual_x: Range<i32>,
+ actual_y: Range<i32>,
+ f: F,
+ ) -> ProjectionMatrix {
+ let default_size = Self::compute_default_size(actual_x.clone(), actual_y.clone());
+ let center_3d = (default_size / 2, default_size / 2, default_size / 2);
+ let center_2d = (
+ (actual_x.end + actual_x.start) / 2,
+ (actual_y.end + actual_y.start) / 2,
+ );
+ let mut pb = ProjectionMatrixBuilder::new();
+ pb.set_pivot(center_3d, center_2d);
+ f(pb)
+ }
+ pub fn with_projection<
+ SX: Into<X>,
+ SY: Into<Y>,
+ SZ: Into<Z>,
+ F: FnOnce(ProjectionMatrixBuilder) -> ProjectionMatrix,
+ >(
+ logic_x: SX,
+ logic_y: SY,
+ logic_z: SZ,
+ (actual_x, actual_y): (Range<i32>, Range<i32>),
+ build_projection_matrix: F,
+ ) -> Self {
+ let default_size = Self::compute_default_size(actual_x.clone(), actual_y.clone());
+ Self {
+ logic_x: logic_x.into(),
+ logic_y: logic_y.into(),
+ logic_z: logic_z.into(),
+ coord_size: (default_size, default_size, default_size),
+ projection: Self::create_projection(actual_x, actual_y, build_projection_matrix),
+ }
+ }
+ /// Set the projection matrix
+ pub fn set_projection<F: FnOnce(ProjectionMatrixBuilder) -> ProjectionMatrix>(
+ &mut self,
+ actual_x: Range<i32>,
+ actual_y: Range<i32>,
+ f: F,
+ ) -> &mut Self {
+ self.projection = Self::create_projection(actual_x, actual_y, f);
+ self
+ }
+
+ /// Create a new coordinate
+ pub fn new<SX: Into<X>, SY: Into<Y>, SZ: Into<Z>>(
+ logic_x: SX,
+ logic_y: SY,
+ logic_z: SZ,
+ (actual_x, actual_y): (Range<i32>, Range<i32>),
+ ) -> Self {
+ Self::with_projection(logic_x, logic_y, logic_z, (actual_x, actual_y), |pb| {
+ pb.into_matrix()
+ })
+ }
+ /// Get the projection matrix
+ pub fn projection(&self) -> &ProjectionMatrix {
+ &self.projection
+ }
+
+ /// Do not project, only transform the guest coordinate system
+ pub fn map_3d(&self, x: &X::ValueType, y: &Y::ValueType, z: &Z::ValueType) -> (i32, i32, i32) {
+ (
+ self.logic_x.map(x, (0, self.coord_size.0)),
+ self.logic_y.map(y, (0, self.coord_size.1)),
+ self.logic_z.map(z, (0, self.coord_size.2)),
+ )
+ }
+
+ /// Get the depth of the projection
+ pub fn projected_depth(&self, x: &X::ValueType, y: &Y::ValueType, z: &Z::ValueType) -> i32 {
+ self.projection.projected_depth(self.map_3d(x, y, z))
+ }
+}
+
+impl<X: Ranged, Y: Ranged, Z: Ranged> CoordTranslate for Cartesian3d<X, Y, Z> {
+ type From = (X::ValueType, Y::ValueType, Z::ValueType);
+ fn translate(&self, coord: &Self::From) -> BackendCoord {
+ let pixel_coord_3d = self.map_3d(&coord.0, &coord.1, &coord.2);
+ self.projection * pixel_coord_3d
+ }
+}
diff --git a/src/coord/ranged3d/mod.rs b/src/coord/ranged3d/mod.rs
new file mode 100644
index 0000000..274a70d
--- /dev/null
+++ b/src/coord/ranged3d/mod.rs
@@ -0,0 +1,5 @@
+mod projection;
+pub use projection::{ProjectionMatrix, ProjectionMatrixBuilder};
+
+mod cartesian3d;
+pub use cartesian3d::Cartesian3d;
diff --git a/src/coord/ranged3d/projection.rs b/src/coord/ranged3d/projection.rs
new file mode 100644
index 0000000..1aef9a7
--- /dev/null
+++ b/src/coord/ranged3d/projection.rs
@@ -0,0 +1,198 @@
+use std::f64::consts::PI;
+use std::ops::Mul;
+
+/// The projection matrix which is used to project the 3D space to the 2D display panel
+#[derive(Clone, Debug, Copy)]
+pub struct ProjectionMatrix([[f64; 4]; 4]);
+
+impl AsRef<[[f64; 4]; 4]> for ProjectionMatrix {
+ fn as_ref(&self) -> &[[f64; 4]; 4] {
+ &self.0
+ }
+}
+
+impl AsMut<[[f64; 4]; 4]> for ProjectionMatrix {
+ fn as_mut(&mut self) -> &mut [[f64; 4]; 4] {
+ &mut self.0
+ }
+}
+
+impl From<[[f64; 4]; 4]> for ProjectionMatrix {
+ fn from(data: [[f64; 4]; 4]) -> Self {
+ ProjectionMatrix(data)
+ }
+}
+
+impl Default for ProjectionMatrix {
+ fn default() -> Self {
+ ProjectionMatrix::rotate(PI, 0.0, 0.0)
+ }
+}
+
+impl Mul<ProjectionMatrix> for ProjectionMatrix {
+ type Output = ProjectionMatrix;
+ fn mul(self, other: ProjectionMatrix) -> ProjectionMatrix {
+ let mut ret = ProjectionMatrix::zero();
+ for r in 0..4 {
+ for c in 0..4 {
+ for k in 0..4 {
+ ret.0[r][c] += other.0[r][k] * self.0[k][c];
+ }
+ }
+ }
+ ret.normalize();
+ ret
+ }
+}
+
+impl Mul<(i32, i32, i32)> for ProjectionMatrix {
+ type Output = (i32, i32);
+ fn mul(self, (x, y, z): (i32, i32, i32)) -> (i32, i32) {
+ let (x, y, z) = (x as f64, y as f64, z as f64);
+ let m = self.0;
+ (
+ (x * m[0][0] + y * m[0][1] + z * m[0][2] + m[0][3]) as i32,
+ (x * m[1][0] + y * m[1][1] + z * m[1][2] + m[1][3]) as i32,
+ )
+ }
+}
+
+impl Mul<(f64, f64, f64)> for ProjectionMatrix {
+ type Output = (i32, i32);
+ fn mul(self, (x, y, z): (f64, f64, f64)) -> (i32, i32) {
+ let m = self.0;
+ (
+ (x * m[0][0] + y * m[0][1] + z * m[0][2] + m[0][3]) as i32,
+ (x * m[1][0] + y * m[1][1] + z * m[1][2] + m[1][3]) as i32,
+ )
+ }
+}
+
+impl ProjectionMatrix {
+ /// Returns the identity matrix
+ pub fn one() -> Self {
+ ProjectionMatrix([
+ [1.0, 0.0, 0.0, 0.0],
+ [0.0, 1.0, 0.0, 0.0],
+ [0.0, 0.0, 1.0, 0.0],
+ [0.0, 0.0, 0.0, 1.0],
+ ])
+ }
+ /// Returns the zero maxtrix
+ pub fn zero() -> Self {
+ ProjectionMatrix([[0.0; 4]; 4])
+ }
+ /// Returns the matrix which shift the coordinate
+ pub fn shift(x: f64, y: f64, z: f64) -> Self {
+ ProjectionMatrix([
+ [1.0, 0.0, 0.0, x],
+ [0.0, 1.0, 0.0, y],
+ [0.0, 0.0, 1.0, z],
+ [0.0, 0.0, 0.0, 1.0],
+ ])
+ }
+ /// Returns the matrix which rotates the coordinate
+ pub fn rotate(x: f64, y: f64, z: f64) -> Self {
+ let (c, b, a) = (x, y, z);
+ ProjectionMatrix([
+ [
+ a.cos() * b.cos(),
+ a.cos() * b.sin() * c.sin() - a.sin() * c.cos(),
+ a.cos() * b.sin() * c.cos() + a.sin() * c.sin(),
+ 0.0,
+ ],
+ [
+ a.sin() * b.cos(),
+ a.sin() * b.sin() * c.sin() + a.cos() * c.cos(),
+ a.sin() * b.sin() * c.cos() - a.cos() * c.sin(),
+ 0.0,
+ ],
+ [-b.sin(), b.cos() * c.sin(), b.cos() * c.cos(), 0.0],
+ [0.0, 0.0, 0.0, 1.0],
+ ])
+ }
+ /// Returns the matrix that applies a scale factor
+ pub fn scale(factor: f64) -> Self {
+ ProjectionMatrix([
+ [1.0, 0.0, 0.0, 0.0],
+ [0.0, 1.0, 0.0, 0.0],
+ [0.0, 0.0, 1.0, 0.0],
+ [0.0, 0.0, 0.0, 1.0 / factor],
+ ])
+ }
+ /// Normalize the matrix, this will make the metric unit to 1
+ pub fn normalize(&mut self) {
+ if self.0[3][3] > 1e-20 {
+ for r in 0..4 {
+ for c in 0..4 {
+ self.0[r][c] /= self.0[3][3];
+ }
+ }
+ }
+ }
+
+ /// Get the distance of the point in guest coordinate from the screen in pixels
+ pub fn projected_depth(&self, (x, y, z): (i32, i32, i32)) -> i32 {
+ let r = &self.0[2];
+ (r[0] * x as f64 + r[1] * y as f64 + r[2] * z as f64 + r[3]) as i32
+ }
+}
+
+/// The helper struct to build a projection matrix
+#[derive(Copy, Clone)]
+pub struct ProjectionMatrixBuilder {
+ pub yaw: f64,
+ pub pitch: f64,
+ pub scale: f64,
+ pivot_before: (i32, i32, i32),
+ pivot_after: (i32, i32),
+}
+
+impl ProjectionMatrixBuilder {
+ pub fn new() -> Self {
+ Self {
+ yaw: 0.5,
+ pitch: 0.15,
+ scale: 1.0,
+ pivot_after: (0, 0),
+ pivot_before: (0, 0, 0),
+ }
+ }
+
+ /// Set the pivot point, which means the 3D coordinate "before" should be mapped into
+ /// the 2D coordinatet "after"
+ pub fn set_pivot(&mut self, before: (i32, i32, i32), after: (i32, i32)) -> &mut Self {
+ self.pivot_before = before;
+ self.pivot_after = after;
+ self
+ }
+
+ /// Build the matrix based on the configuration
+ pub fn into_matrix(self) -> ProjectionMatrix {
+ let mut ret = if self.pivot_before == (0, 0, 0) {
+ ProjectionMatrix::default()
+ } else {
+ let (x, y, z) = self.pivot_before;
+ ProjectionMatrix::shift(-x as f64, -y as f64, -z as f64) * ProjectionMatrix::default()
+ };
+
+ if self.yaw.abs() > 1e-20 {
+ ret = ret * ProjectionMatrix::rotate(0.0, self.yaw, 0.0);
+ }
+
+ if self.pitch.abs() > 1e-20 {
+ ret = ret * ProjectionMatrix::rotate(self.pitch, 0.0, 0.0);
+ }
+
+ if (self.scale - 1.0).abs() > 1e-20 {
+ ret = ret * ProjectionMatrix::scale(self.scale);
+ }
+
+ if self.pivot_after != (0, 0) {
+ let (x, y) = self.pivot_after;
+ ret = ret * ProjectionMatrix::shift(x as f64, y as f64, 0.0);
+ }
+
+ ret
+ }
+}
diff --git a/src/coord/translate.rs b/src/coord/translate.rs
new file mode 100644
index 0000000..32888be
--- /dev/null
+++ b/src/coord/translate.rs
@@ -0,0 +1,32 @@
+use plotters_backend::BackendCoord;
+use std::ops::Deref;
+
+/// The trait that translates some customized object to the backend coordinate
+pub trait CoordTranslate {
+ type From;
+
+ /// Translate the guest coordinate to the guest coordinate
+ fn translate(&self, from: &Self::From) -> BackendCoord;
+}
+
+impl<C, T> CoordTranslate for T
+where
+ C: CoordTranslate,
+ T: Deref<Target = C>,
+{
+ type From = C::From;
+ fn translate(&self, from: &Self::From) -> BackendCoord {
+ self.deref().translate(from)
+ }
+}
+
+/// The trait indicates that the coordinate system supports reverse transform
+/// This is useful when we need an interactive plot, thus we need to map the event
+/// from the backend coordinate to the logical coordinate
+pub trait ReverseCoordTranslate: CoordTranslate {
+ /// Reverse translate the coordinate from the drawing coordinate to the
+ /// logic coordinate.
+ /// Note: the return value is an option, because it's possible that the drawing
+ /// coordinate isn't able to be represented in te guest coordinate system
+ fn reverse_translate(&self, input: BackendCoord) -> Option<Self::From>;
+}
diff --git a/src/data/float.rs b/src/data/float.rs
index 6115876..64e5226 100644
--- a/src/data/float.rs
+++ b/src/data/float.rs
@@ -1,5 +1,4 @@
// The code that is related to float number handling
-
fn find_minimal_repr(n: f64, eps: f64) -> (f64, usize) {
if eps >= 1.0 {
return (n, 0);
@@ -14,32 +13,91 @@ fn find_minimal_repr(n: f64, eps: f64) -> (f64, usize) {
}
}
-fn float_to_string(n: f64, max_precision: usize) -> String {
- let (sign, n) = if n < 0.0 { ("-", -n) } else { ("", n) };
- let int_part = n.floor();
+fn float_to_string(n: f64, max_precision: usize, min_decimal: usize) -> String {
+ let (mut result, mut count) = loop {
+ let (sign, n) = if n < 0.0 { ("-", -n) } else { ("", n) };
+ let int_part = n.floor();
- let dec_part =
- ((n.abs() - int_part.abs()) * (10.0f64).powf(max_precision as f64)).round() as u64;
+ let dec_part =
+ ((n.abs() - int_part.abs()) * (10.0f64).powi(max_precision as i32)).round() as u64;
- if dec_part == 0 || max_precision == 0 {
- return format!("{}{:.0}", sign, int_part);
- }
+ if dec_part == 0 || max_precision == 0 {
+ break (format!("{}{:.0}", sign, int_part), 0);
+ }
- let mut leading = "".to_string();
- let mut dec_result = format!("{}", dec_part);
+ let mut leading = "".to_string();
+ let mut dec_result = format!("{}", dec_part);
- for _ in 0..(max_precision - dec_result.len()) {
- leading.push('0');
- }
+ for _ in 0..(max_precision - dec_result.len()) {
+ leading.push('0');
+ }
- while let Some(c) = dec_result.pop() {
- if c != '0' {
- dec_result.push(c);
- break;
+ while let Some(c) = dec_result.pop() {
+ if c != '0' {
+ dec_result.push(c);
+ break;
+ }
}
+
+ break (
+ format!("{}{:.0}.{}{}", sign, int_part, leading, dec_result),
+ leading.len() + dec_result.len(),
+ );
+ };
+
+ if count == 0 && min_decimal > 0 {
+ result.push('.');
}
- format!("{}{:.0}.{}{}", sign, int_part, leading, dec_result)
+ while count < min_decimal {
+ result.push('0');
+ count += 1;
+ }
+ result
+}
+
+pub struct FloatPrettyPrinter {
+ pub allow_scientific: bool,
+ pub min_decimal: i32,
+ pub max_decimal: i32,
+}
+
+impl FloatPrettyPrinter {
+ pub fn print(&self, n: f64) -> String {
+ let (n, p) = find_minimal_repr(n, (10f64).powi(-self.max_decimal));
+ let d_repr = float_to_string(n, p, self.min_decimal as usize);
+ if !self.allow_scientific {
+ d_repr
+ } else {
+ if n == 0.0 {
+ return "0".to_string();
+ }
+
+ let mut idx = n.abs().log10().floor();
+ let mut exp = (10.0f64).powf(idx);
+
+ if n.abs() / exp + 1e-5 >= 10.0 {
+ idx += 1.0;
+ exp *= 10.0;
+ }
+
+ if idx.abs() < 3.0 {
+ return d_repr;
+ }
+
+ let (sn, sp) = find_minimal_repr(n / exp, 1e-5);
+ let s_repr = format!(
+ "{}e{}",
+ float_to_string(sn, sp, self.min_decimal as usize),
+ float_to_string(idx, 0, 0)
+ );
+ if s_repr.len() + 1 < d_repr.len() {
+ s_repr
+ } else {
+ d_repr
+ }
+ }
+ }
}
/// The function that pretty prints the floating number
@@ -51,35 +109,12 @@ fn float_to_string(n: f64, max_precision: usize) -> String {
/// - `allow_sn`: Should we use scientific notation when possible
/// - **returns**: The pretty printed string
pub fn pretty_print_float(n: f64, allow_sn: bool) -> String {
- let (n, p) = find_minimal_repr(n, 1e-10);
- let d_repr = float_to_string(n, p);
- if !allow_sn {
- d_repr
- } else {
- if n == 0.0 {
- return "0".to_string();
- }
-
- let mut idx = n.abs().log10().floor();
- let mut exp = (10.0f64).powf(idx);
-
- if n.abs() / exp + 1e-5 >= 10.0 {
- idx += 1.0;
- exp *= 10.0;
- }
-
- if idx.abs() < 3.0 {
- return d_repr;
- }
-
- let (sn, sp) = find_minimal_repr(n / exp, 1e-5);
- let s_repr = format!("{}e{}", float_to_string(sn, sp), float_to_string(idx, 0));
- if s_repr.len() + 1 < d_repr.len() {
- s_repr
- } else {
- d_repr
- }
- }
+ (FloatPrettyPrinter {
+ allow_scientific: allow_sn,
+ min_decimal: 0,
+ max_decimal: 10,
+ })
+ .print(n)
}
#[cfg(test)]
diff --git a/src/drawing/area.rs b/src/drawing/area.rs
index ae75087..511dc08 100644
--- a/src/drawing/area.rs
+++ b/src/drawing/area.rs
@@ -1,9 +1,12 @@
-/// The abstraction of a drawing area
-use super::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
-use crate::coord::{CoordTranslate, MeshLine, Ranged, RangedCoord, Shift};
+use crate::coord::cartesian::{Cartesian2d, MeshLine};
+use crate::coord::ranged1d::{KeyPointHint, Ranged};
+use crate::coord::{CoordTranslate, Shift};
use crate::element::{Drawable, PointCollection};
use crate::style::text_anchor::{HPos, Pos, VPos};
-use crate::style::{Color, FontDesc, SizeDesc, TextStyle};
+use crate::style::{Color, SizeDesc, TextStyle};
+
+/// The abstraction of a drawing area
+use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
use std::borrow::Borrow;
use std::cell::RefCell;
@@ -120,7 +123,7 @@ pub struct DrawingArea<DB: DrawingBackend, CT: CoordTranslate> {
impl<DB: DrawingBackend, CT: CoordTranslate + Clone> Clone for DrawingArea<DB, CT> {
fn clone(&self) -> Self {
Self {
- backend: self.copy_backend_ref(),
+ backend: self.backend.clone(),
rect: self.rect.clone(),
coord: self.coord.clone(),
}
@@ -181,13 +184,13 @@ impl<T: DrawingBackend> IntoDrawingArea for T {
}
}
-impl<DB: DrawingBackend, X: Ranged, Y: Ranged> DrawingArea<DB, RangedCoord<X, Y>> {
+impl<DB: DrawingBackend, X: Ranged, Y: Ranged> DrawingArea<DB, Cartesian2d<X, Y>> {
/// Draw the mesh on a area
- pub fn draw_mesh<DrawFunc>(
+ pub fn draw_mesh<DrawFunc, YH: KeyPointHint, XH: KeyPointHint>(
&self,
mut draw_func: DrawFunc,
- y_count_max: usize,
- x_count_max: usize,
+ y_count_max: YH,
+ x_count_max: XH,
) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
where
DrawFunc: FnMut(&mut DB, MeshLine<X, Y>) -> Result<(), DrawingErrorKind<DB::ErrorType>>,
@@ -227,11 +230,19 @@ impl<DB: DrawingBackend, CT: CoordTranslate> DrawingArea<DB, CT> {
pub fn strip_coord_spec(&self) -> DrawingArea<DB, Shift> {
DrawingArea {
rect: self.rect.clone(),
- backend: self.copy_backend_ref(),
+ backend: self.backend.clone(),
coord: Shift((self.rect.x0, self.rect.y0)),
}
}
+ pub fn use_screen_coord(&self) -> DrawingArea<DB, Shift> {
+ DrawingArea {
+ rect: self.rect.clone(),
+ backend: self.backend.clone(),
+ coord: Shift((0, 0)),
+ }
+ }
+
/// Get the area dimension in pixel
pub fn dim_in_pixel(&self) -> (u32, u32) {
(
@@ -255,11 +266,6 @@ impl<DB: DrawingBackend, CT: CoordTranslate> DrawingArea<DB, CT> {
(self.rect.x0..self.rect.x1, self.rect.y0..self.rect.y1)
}
- /// Copy the drawing context
- fn copy_backend_ref(&self) -> Rc<RefCell<DB>> {
- self.backend.clone()
- }
-
/// Perform operation on the drawing backend
fn backend_ops<R, O: FnOnce(&mut DB) -> Result<R, DrawingErrorKind<DB::ErrorType>>>(
&self,
@@ -293,7 +299,7 @@ impl<DB: DrawingBackend, CT: CoordTranslate> DrawingArea<DB, CT> {
color: &ColorType,
) -> Result<(), DrawingAreaError<DB>> {
let pos = self.coord.translate(&pos);
- self.backend_ops(|b| b.draw_pixel(pos, &color.to_rgba()))
+ self.backend_ops(|b| b.draw_pixel(pos, color.color()))
}
/// Present all the pending changes to the backend
@@ -330,9 +336,9 @@ impl<DB: DrawingBackend, CT: CoordTranslate> DrawingArea<DB, CT> {
pub fn estimate_text_size(
&self,
text: &str,
- font: &FontDesc,
+ style: &TextStyle,
) -> Result<(u32, u32), DrawingAreaError<DB>> {
- self.backend_ops(move |b| b.estimate_text_size(text, font))
+ self.backend_ops(move |b| b.estimate_text_size(text, style))
}
}
@@ -374,7 +380,7 @@ impl<DB: DrawingBackend> DrawingArea<DB, Shift> {
pub fn apply_coord_spec<CT: CoordTranslate>(&self, coord_spec: CT) -> DrawingArea<DB, CT> {
DrawingArea {
rect: self.rect.clone(),
- backend: self.copy_backend_ref(),
+ backend: self.backend.clone(),
coord: coord_spec,
}
}
@@ -398,7 +404,7 @@ impl<DB: DrawingBackend> DrawingArea<DB, Shift> {
x1: self.rect.x1 - right,
y1: self.rect.y1 - bottom,
},
- backend: self.copy_backend_ref(),
+ backend: self.backend.clone(),
coord: Shift((self.rect.x0 + left, self.rect.y0 + top)),
}
}
@@ -409,7 +415,7 @@ impl<DB: DrawingBackend> DrawingArea<DB, Shift> {
let split_point = [y + self.rect.y0];
let mut ret = self.rect.split(split_point.iter(), true).map(|rect| Self {
rect: rect.clone(),
- backend: self.copy_backend_ref(),
+ backend: self.backend.clone(),
coord: Shift((rect.x0, rect.y0)),
});
@@ -422,7 +428,7 @@ impl<DB: DrawingBackend> DrawingArea<DB, Shift> {
let split_point = [x + self.rect.x0];
let mut ret = self.rect.split(split_point.iter(), false).map(|rect| Self {
rect: rect.clone(),
- backend: self.copy_backend_ref(),
+ backend: self.backend.clone(),
coord: Shift((rect.x0, rect.y0)),
});
@@ -435,7 +441,7 @@ impl<DB: DrawingBackend> DrawingArea<DB, Shift> {
.split_evenly((row, col))
.map(|rect| Self {
rect: rect.clone(),
- backend: self.copy_backend_ref(),
+ backend: self.backend.clone(),
coord: Shift((rect.x0, rect.y0)),
})
.collect()
@@ -459,7 +465,7 @@ impl<DB: DrawingBackend> DrawingArea<DB, Shift> {
)
.map(|rect| Self {
rect: rect.clone(),
- backend: self.copy_backend_ref(),
+ backend: self.backend.clone(),
coord: Shift((rect.x0, rect.y0)),
})
.collect()
@@ -475,7 +481,7 @@ impl<DB: DrawingBackend> DrawingArea<DB, Shift> {
let x_padding = (self.rect.x1 - self.rect.x0) / 2;
- let (_, text_h) = self.estimate_text_size(text, &style.font)?;
+ let (_, text_h) = self.estimate_text_size(text, &style)?;
let y_padding = (text_h / 2).min(5) as i32;
let style = &style.pos(Pos::new(HPos::Center, VPos::Top));
@@ -483,7 +489,7 @@ impl<DB: DrawingBackend> DrawingArea<DB, Shift> {
self.backend_ops(|b| {
b.draw_text(
text,
- &style,
+ style,
(self.rect.x0 + x_padding, self.rect.y0 + y_padding),
)
})?;
@@ -495,7 +501,7 @@ impl<DB: DrawingBackend> DrawingArea<DB, Shift> {
x1: self.rect.x1,
y1: self.rect.y1,
},
- backend: self.copy_backend_ref(),
+ backend: self.backend.clone(),
coord: Shift((self.rect.x0, self.rect.y0 + y_padding * 2 + text_h as i32)),
})
}
@@ -507,9 +513,7 @@ impl<DB: DrawingBackend> DrawingArea<DB, Shift> {
style: &TextStyle,
pos: BackendCoord,
) -> Result<(), DrawingAreaError<DB>> {
- self.backend_ops(|b| {
- b.draw_text(text, &style, (pos.0 + self.rect.x0, pos.1 + self.rect.y0))
- })
+ self.backend_ops(|b| b.draw_text(text, style, (pos.0 + self.rect.x0, pos.1 + self.rect.y0)))
}
}
@@ -521,6 +525,10 @@ impl<DB: DrawingBackend, CT: CoordTranslate> DrawingArea<DB, CT> {
pub fn as_coord_spec(&self) -> &CT {
&self.coord
}
+
+ pub fn as_coord_spec_mut(&mut self) -> &mut CT {
+ &mut self.coord
+ }
}
#[cfg(test)]
@@ -748,15 +756,11 @@ mod drawing_area_tests {
#[test]
fn test_ranges() {
- let drawing_area =
- create_mocked_drawing_area(1024, 768, |_m| {}).apply_coord_spec(RangedCoord::<
- RangedCoordi32,
- RangedCoordu32,
- >::new(
- -100..100,
- 0..200,
- (0..1024, 0..768),
- ));
+ let drawing_area = create_mocked_drawing_area(1024, 768, |_m| {})
+ .apply_coord_spec(Cartesian2d::<
+ crate::coord::types::RangedCoordi32,
+ crate::coord::types::RangedCoordu32,
+ >::new(-100..100, 0..200, (0..1024, 0..768)));
let x_range = drawing_area.get_x_range();
assert_eq!(x_range, -100..100);
diff --git a/src/drawing/backend.rs b/src/drawing/backend.rs
deleted file mode 100644
index a5ba54b..0000000
--- a/src/drawing/backend.rs
+++ /dev/null
@@ -1,284 +0,0 @@
-use crate::style::text_anchor::{HPos, VPos};
-use crate::style::{Color, FontDesc, FontError, RGBAColor, ShapeStyle, TextStyle};
-use std::error::Error;
-
-/// A coordinate in the image
-pub type BackendCoord = (i32, i32);
-
-/// The error produced by a drawing backend
-#[derive(Debug)]
-pub enum DrawingErrorKind<E: Error + Send + Sync>
-where
- FontError: Send + Sync,
-{
- /// A drawing backend error
- DrawingError(E),
- /// A font rendering error
- FontError(FontError),
-}
-
-impl<E: Error + Send + Sync> std::fmt::Display for DrawingErrorKind<E> {
- fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
- match self {
- DrawingErrorKind::DrawingError(e) => write!(fmt, "Drawing backend error: {}", e),
- DrawingErrorKind::FontError(e) => write!(fmt, "Font loading error: {}", e),
- }
- }
-}
-
-impl<E: Error + Send + Sync> Error for DrawingErrorKind<E> {}
-
-/// The style data for the backend drawing API
-pub trait BackendStyle {
- /// The underlying type represents the color for this style
- type ColorType: Color;
-
- /// Convert the style into the underlying color
- fn as_color(&self) -> RGBAColor;
-
- // TODO: In the future we should support stroke width, line shape, etc....
- fn stroke_width(&self) -> u32 {
- 1
- }
-}
-
-impl<T: Color> BackendStyle for T {
- type ColorType = T;
- fn as_color(&self) -> RGBAColor {
- self.to_rgba()
- }
-}
-
-impl BackendStyle for ShapeStyle {
- type ColorType = RGBAColor;
- fn as_color(&self) -> RGBAColor {
- self.color.clone()
- }
- fn stroke_width(&self) -> u32 {
- self.stroke_width
- }
-}
-
-/// The drawing backend trait, which implements the low-level drawing APIs.
-/// This trait has a set of default implementation. And the minimal requirement of
-/// implementing a drawing backend is implementing the `draw_pixel` function.
-///
-/// If the drawing backend supports vector graphics, the other drawing APIs should be
-/// override by the backend specific implementation. Otherwise, the default implementation
-/// will use the pixel-based approach to draw other types of low-level shapes.
-pub trait DrawingBackend: Sized {
- /// The error type reported by the backend
- type ErrorType: Error + Send + Sync;
-
- /// Get the dimension of the drawing backend in pixels
- fn get_size(&self) -> (u32, u32);
-
- /// Ensure the backend is ready to draw
- fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<Self::ErrorType>>;
-
- /// Finalize the drawing step and present all the changes.
- /// This is used as the real-time rendering support.
- /// The backend may implement in the following way, when `ensure_prepared` is called
- /// it checks if it needs a fresh buffer and `present` is called rendering all the
- /// pending changes on the screen.
- fn present(&mut self) -> Result<(), DrawingErrorKind<Self::ErrorType>>;
-
- /// Draw a pixel on the drawing backend
- /// - `point`: The backend pixel-based coordinate to draw
- /// - `color`: The color of the pixel
- fn draw_pixel(
- &mut self,
- point: BackendCoord,
- color: &RGBAColor,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>>;
-
- /// Draw a line on the drawing backend
- /// - `from`: The start point of the line
- /// - `to`: The end point of the line
- /// - `style`: The style of the line
- fn draw_line<S: BackendStyle>(
- &mut self,
- from: BackendCoord,
- to: BackendCoord,
- style: &S,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- super::rasterizer::draw_line(self, from, to, style)
- }
-
- /// Draw a rectangle on the drawing backend
- /// - `upper_left`: The coordinate of the upper-left corner of the rect
- /// - `bottom_right`: The coordinate of the bottom-right corner of the rect
- /// - `style`: The style
- /// - `fill`: If the rectangle should be filled
- fn draw_rect<S: BackendStyle>(
- &mut self,
- upper_left: BackendCoord,
- bottom_right: BackendCoord,
- style: &S,
- fill: bool,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- super::rasterizer::draw_rect(self, upper_left, bottom_right, style, fill)
- }
-
- /// Draw a path on the drawing backend
- /// - `path`: The iterator of key points of the path
- /// - `style`: The style of the path
- fn draw_path<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
- &mut self,
- path: I,
- style: &S,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- if style.as_color().alpha() == 0.0 {
- return Ok(());
- }
-
- if style.stroke_width() == 1 {
- let mut begin: Option<BackendCoord> = None;
- for end in path.into_iter() {
- if let Some(begin) = begin {
- let result = self.draw_line(begin, end, style);
- if result.is_err() {
- return result;
- }
- }
- begin = Some(end);
- }
- } else {
- let p: Vec<_> = path.into_iter().collect();
- let v = super::rasterizer::polygonize(&p[..], style.stroke_width());
- return self.fill_polygon(v, &style.as_color());
- }
- Ok(())
- }
-
- /// Draw a circle on the drawing backend
- /// - `center`: The center coordinate of the circle
- /// - `radius`: The radius of the circle
- /// - `style`: The style of the shape
- /// - `fill`: If the circle should be filled
- fn draw_circle<S: BackendStyle>(
- &mut self,
- center: BackendCoord,
- radius: u32,
- style: &S,
- fill: bool,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- super::rasterizer::draw_circle(self, center, radius, style, fill)
- }
-
- fn fill_polygon<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
- &mut self,
- vert: I,
- style: &S,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- let vert_buf: Vec<_> = vert.into_iter().collect();
-
- super::rasterizer::fill_polygon(self, &vert_buf[..], style)
- }
-
- /// Draw a text on the drawing backend
- /// - `text`: The text to draw
- /// - `style`: The text style
- /// - `pos` : The text anchor point
- fn draw_text(
- &mut self,
- text: &str,
- style: &TextStyle,
- pos: BackendCoord,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- let font = &style.font;
- let color = &style.color;
- if color.alpha() == 0.0 {
- return Ok(());
- }
-
- let layout = font.layout_box(text).map_err(DrawingErrorKind::FontError)?;
- let ((min_x, min_y), (max_x, max_y)) = layout;
- let width = (max_x - min_x) as i32;
- let height = (max_y - min_y) as i32;
- let dx = match style.pos.h_pos {
- HPos::Left => 0,
- HPos::Right => -width,
- HPos::Center => -width / 2,
- };
- let dy = match style.pos.v_pos {
- VPos::Top => 0,
- VPos::Center => -height / 2,
- VPos::Bottom => -height,
- };
- let trans = font.get_transform();
- let (w, h) = self.get_size();
- match font.draw(text, (0, 0), |x, y, v| {
- let (x, y) = trans.transform(x + dx - min_x, y + dy - min_y);
- let (x, y) = (pos.0 + x, pos.1 + y);
- if x >= 0 && x < w as i32 && y >= 0 && y < h as i32 {
- self.draw_pixel((x, y), &color.mix(f64::from(v)))
- } else {
- Ok(())
- }
- }) {
- Ok(drawing_result) => drawing_result,
- Err(font_error) => Err(DrawingErrorKind::FontError(font_error)),
- }
- }
-
- /// Estimate the size of the horizontal text if rendered on this backend.
- /// This is important because some of the backend may not have font ability.
- /// Thus this allows those backend reports proper value rather than ask the
- /// font rasterizer for that.
- ///
- /// - `text`: The text to estimate
- /// - `font`: The font to estimate
- /// - *Returns* The estimated text size
- fn estimate_text_size<'a>(
- &self,
- text: &str,
- font: &FontDesc<'a>,
- ) -> Result<(u32, u32), DrawingErrorKind<Self::ErrorType>> {
- let layout = font.layout_box(text).map_err(DrawingErrorKind::FontError)?;
- Ok((
- ((layout.1).0 - (layout.0).0) as u32,
- ((layout.1).1 - (layout.0).1) as u32,
- ))
- }
-
- /// Blit a bitmap on to the backend.
- ///
- /// - `text`: pos the left upper conner of the bitmap to blit
- /// - `src`: The source of the image
- ///
- /// TODO: The default implementation of bitmap blitting assumes that the bitmap is RGB, but
- /// this may not be the case. But for bitmap backend it's actually ok if we use the bitmap
- /// element that matches the pixel format, but we need to fix this.
- fn blit_bitmap<'a>(
- &mut self,
- pos: BackendCoord,
- (iw, ih): (u32, u32),
- src: &'a [u8],
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- let (w, h) = self.get_size();
-
- for dx in 0..iw {
- if pos.0 + dx as i32 >= w as i32 {
- break;
- }
- for dy in 0..ih {
- if pos.1 + dy as i32 >= h as i32 {
- break;
- }
- // FIXME: This assume we have RGB image buffer
- let r = src[(dx + dy * iw) as usize * 3];
- let g = src[(dx + dy * iw) as usize * 3 + 1];
- let b = src[(dx + dy * iw) as usize * 3 + 2];
- let color = crate::style::RGBColor(r, g, b);
- let result =
- self.draw_pixel((pos.0 + dx as i32, pos.1 + dy as i32), &color.to_rgba());
- if result.is_err() {
- return result;
- }
- }
- }
-
- Ok(())
- }
-}
diff --git a/src/drawing/backend_impl/bitmap.rs b/src/drawing/backend_impl/bitmap.rs
deleted file mode 100644
index a4d776a..0000000
--- a/src/drawing/backend_impl/bitmap.rs
+++ /dev/null
@@ -1,1607 +0,0 @@
-use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind};
-use crate::style::{Color, RGBAColor};
-use std::marker::PhantomData;
-
-#[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
-mod image_encoding_support {
- pub(super) use image::{ImageBuffer, ImageError, Rgb};
- pub(super) use std::path::Path;
- pub(super) type BorrowedImage<'a> = ImageBuffer<Rgb<u8>, &'a mut [u8]>;
-}
-
-#[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
-use image_encoding_support::*;
-
-#[derive(Debug)]
-/// Indicates some error occurs within the bitmap backend
-pub enum BitMapBackendError {
- /// The buffer provided is invalid, for example, wrong pixel buffer size
- InvalidBuffer,
- /// Some IO error occurs while the bitmap maniuplation
- IOError(std::io::Error),
- #[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
- /// Image encoding error
- ImageError(ImageError),
-}
-
-impl std::fmt::Display for BitMapBackendError {
- fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
- write!(f, "{:?}", self)
- }
-}
-
-impl std::error::Error for BitMapBackendError {}
-
-#[inline(always)]
-fn blend(prev: &mut u8, new: u8, a: u64) {
- if new > *prev {
- *prev += (u64::from(new - *prev) * a / 256) as u8
- } else {
- *prev -= (u64::from(*prev - new) * a / 256) as u8
- }
-}
-
-#[cfg(all(feature = "gif", not(target_arch = "wasm32"), feature = "image"))]
-mod gif_support {
- use super::*;
- use gif::{Encoder as GifEncoder, Frame as GifFrame, Repeat, SetParameter};
- use std::fs::File;
-
- pub(super) struct GifFile {
- encoder: GifEncoder<File>,
- height: u32,
- width: u32,
- delay: u32,
- }
-
- impl GifFile {
- pub(super) fn new<T: AsRef<Path>>(
- path: T,
- dim: (u32, u32),
- delay: u32,
- ) -> Result<Self, BitMapBackendError> {
- let mut encoder = GifEncoder::new(
- File::create(path.as_ref()).map_err(BitMapBackendError::IOError)?,
- dim.0 as u16,
- dim.1 as u16,
- &[],
- )
- .map_err(BitMapBackendError::IOError)?;
-
- encoder
- .set(Repeat::Infinite)
- .map_err(BitMapBackendError::IOError)?;
-
- Ok(Self {
- encoder,
- width: dim.0,
- height: dim.1,
- delay: (delay + 5) / 10,
- })
- }
-
- pub(super) fn flush_frame(&mut self, buffer: &[u8]) -> Result<(), BitMapBackendError> {
- let mut frame =
- GifFrame::from_rgb_speed(self.width as u16, self.height as u16, buffer, 10);
-
- frame.delay = self.delay as u16;
-
- self.encoder
- .write_frame(&frame)
- .map_err(BitMapBackendError::IOError)?;
-
- Ok(())
- }
- }
-}
-
-enum Target<'a> {
- #[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
- File(&'a Path),
- Buffer(PhantomData<&'a u32>),
- #[cfg(all(feature = "gif", not(target_arch = "wasm32"), feature = "image"))]
- Gif(Box<gif_support::GifFile>),
-}
-
-enum Buffer<'a> {
- #[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
- Owned(Vec<u8>),
- Borrowed(&'a mut [u8]),
-}
-
-impl<'a> Buffer<'a> {
- #[inline(always)]
- fn borrow_buffer(&mut self) -> &mut [u8] {
- match self {
- #[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
- Buffer::Owned(buf) => &mut buf[..],
- Buffer::Borrowed(buf) => *buf,
- }
- }
-}
-
-/// The trait that describes some details about a particular pixel format
-pub trait PixelFormat: Sized {
- /// Number of bytes per pixel
- const PIXEL_SIZE: usize;
-
- /// Number of effective bytes per pixel, e.g. for BGRX pixel format, the size of pixel
- /// is 4 but the effective size is 3, since the 4th byte isn't used
- const EFFECTIVE_PIXEL_SIZE: usize;
-
- /// Encoding a pixel and returns the idx-th byte for the pixel
- fn byte_at(r: u8, g: u8, b: u8, a: u64, idx: usize) -> u8;
-
- /// Decode a pixel at the given location
- fn decode_pixel(data: &[u8]) -> (u8, u8, u8, u64);
-
- /// The fast alpha blending algorithm for this pixel format
- ///
- /// - `target`: The target bitmap backend
- /// - `upper_left`: The upper-left coord for the rect
- /// - `bottom_right`: The bottom-right coord for the rect
- /// - `r`, `g`, `b`, `a`: The blending color and alpha value
- fn blend_rect_fast(
- target: &mut BitMapBackend<'_, Self>,
- upper_left: (i32, i32),
- bottom_right: (i32, i32),
- r: u8,
- g: u8,
- b: u8,
- a: f64,
- );
-
- /// The fast vertical line filling algorithm
- ///
- /// - `target`: The target bitmap backend
- /// - `x`: the X coordinate for the entire line
- /// - `ys`: The range of y coord
- /// - `r`, `g`, `b`: The blending color and alpha value
- fn fill_vertical_line_fast(
- target: &mut BitMapBackend<'_, Self>,
- x: i32,
- ys: (i32, i32),
- r: u8,
- g: u8,
- b: u8,
- ) {
- let (w, h) = target.get_size();
- let w = w as i32;
- let h = h as i32;
-
- // Make sure we are in the range
- if x < 0 || x >= w {
- return;
- }
-
- let dst = target.get_raw_pixel_buffer();
- let (mut y0, mut y1) = ys;
- if y0 > y1 {
- std::mem::swap(&mut y0, &mut y1);
- }
- // And check the y axis isn't out of bound
- y0 = y0.max(0);
- y1 = y1.min(h - 1);
- // This is ok because once y0 > y1, there won't be any iteration anymore
- for y in y0..=y1 {
- for idx in 0..Self::EFFECTIVE_PIXEL_SIZE {
- dst[(y * w + x) as usize * Self::PIXEL_SIZE + idx] = Self::byte_at(r, g, b, 0, idx);
- }
- }
- }
-
- /// The fast rectangle filling algorithm
- ///
- /// - `target`: The target bitmap backend
- /// - `upper_left`: The upper-left coord for the rect
- /// - `bottom_right`: The bottom-right coord for the rect
- /// - `r`, `g`, `b`: The filling color
- fn fill_rect_fast(
- target: &mut BitMapBackend<'_, Self>,
- upper_left: (i32, i32),
- bottom_right: (i32, i32),
- r: u8,
- g: u8,
- b: u8,
- );
-
- #[inline(always)]
- /// Drawing a single pixel in this format
- ///
- /// - `target`: The target bitmap backend
- /// - `point`: The coord of the point
- /// - `r`, `g`, `b`: The filling color
- /// - `alpha`: The alpha value
- fn draw_pixel(
- target: &mut BitMapBackend<'_, Self>,
- point: (i32, i32),
- (r, g, b): (u8, u8, u8),
- alpha: f64,
- ) {
- let (x, y) = (point.0 as usize, point.1 as usize);
- let (w, _) = target.get_size();
- let buf = target.get_raw_pixel_buffer();
- let w = w as usize;
- let base = (y * w + x) * Self::PIXEL_SIZE;
-
- if base < buf.len() {
- unsafe {
- if alpha >= 1.0 - 1.0 / 256.0 {
- for idx in 0..Self::EFFECTIVE_PIXEL_SIZE {
- *buf.get_unchecked_mut(base + idx) = Self::byte_at(r, g, b, 0, idx);
- }
- } else {
- if alpha <= 0.0 {
- return;
- }
-
- let alpha = (alpha * 256.0).floor() as u64;
- for idx in 0..Self::EFFECTIVE_PIXEL_SIZE {
- blend(
- buf.get_unchecked_mut(base + idx),
- Self::byte_at(r, g, b, 0, idx),
- alpha,
- );
- }
- }
- }
- }
- }
-
- /// Indicates if this pixel format can be saved as image.
- /// Note: Currently we only using RGB pixel format in the image crate, but later we may lift
- /// this restriction
- ///
- /// - `returns`: If the image can be saved as image file
- fn can_be_saved() -> bool {
- false
- }
-}
-
-/// The marker type that indicates we are currently using a RGB888 pixel format
-pub struct RGBPixel;
-
-/// The marker type that indicates we are currently using a BGRX8888 pixel format
-pub struct BGRXPixel;
-
-impl PixelFormat for RGBPixel {
- const PIXEL_SIZE: usize = 3;
- const EFFECTIVE_PIXEL_SIZE: usize = 3;
-
- #[inline(always)]
- fn byte_at(r: u8, g: u8, b: u8, _a: u64, idx: usize) -> u8 {
- match idx {
- 0 => r,
- 1 => g,
- 2 => b,
- _ => 0xff,
- }
- }
-
- #[inline(always)]
- fn decode_pixel(data: &[u8]) -> (u8, u8, u8, u64) {
- (data[0], data[1], data[2], 0x255)
- }
-
- fn can_be_saved() -> bool {
- true
- }
-
- #[allow(clippy::many_single_char_names, clippy::cast_ptr_alignment)]
- fn blend_rect_fast(
- target: &mut BitMapBackend<'_, Self>,
- upper_left: (i32, i32),
- bottom_right: (i32, i32),
- r: u8,
- g: u8,
- b: u8,
- a: f64,
- ) {
- let (w, h) = target.get_size();
- let a = a.min(1.0).max(0.0);
- if a == 0.0 {
- return;
- }
-
- let (x0, y0) = (
- upper_left.0.min(bottom_right.0).max(0),
- upper_left.1.min(bottom_right.1).max(0),
- );
- let (x1, y1) = (
- upper_left.0.max(bottom_right.0).min(w as i32 - 1),
- upper_left.1.max(bottom_right.1).min(h as i32 - 1),
- );
-
- // This may happen when the minimal value is larger than the limit.
- // Thus we just have something that is completely out-of-range
- if x0 > x1 || y0 > y1 {
- return;
- }
-
- let dst = target.get_raw_pixel_buffer();
-
- let a = (256.0 * a).floor() as u64;
-
- // Since we should always make sure the RGB payload occupies the logic lower bits
- // thus, this type purning should work for both LE and BE CPUs
- #[rustfmt::skip]
- let (p1, p2, p3): (u64, u64, u64) = unsafe {
- std::mem::transmute([
- u16::from(r), u16::from(b), u16::from(g), u16::from(r), // QW1
- u16::from(b), u16::from(g), u16::from(r), u16::from(b), // QW2
- u16::from(g), u16::from(r), u16::from(b), u16::from(g), // QW3
- ])
- };
-
- #[rustfmt::skip]
- let (q1, q2, q3): (u64, u64, u64) = unsafe {
- std::mem::transmute([
- u16::from(g), u16::from(r), u16::from(b), u16::from(g), // QW1
- u16::from(r), u16::from(b), u16::from(g), u16::from(r), // QW2
- u16::from(b), u16::from(g), u16::from(r), u16::from(b), // QW3
- ])
- };
-
- const N: u64 = 0xff00_ff00_ff00_ff00;
- const M: u64 = 0x00ff_00ff_00ff_00ff;
-
- for y in y0..=y1 {
- let start = (y * w as i32 + x0) as usize;
- let count = (x1 - x0 + 1) as usize;
-
- let start_ptr = &mut dst[start * Self::PIXEL_SIZE] as *mut u8 as *mut [u8; 24];
- let slice = unsafe { std::slice::from_raw_parts_mut(start_ptr, (count - 1) / 8) };
- for p in slice.iter_mut() {
- let ptr = p as *mut [u8; 24] as *mut (u64, u64, u64);
- let (d1, d2, d3) = unsafe { *ptr };
- let (mut h1, mut h2, mut h3) = ((d1 >> 8) & M, (d2 >> 8) & M, (d3 >> 8) & M);
- let (mut l1, mut l2, mut l3) = (d1 & M, d2 & M, d3 & M);
-
- #[cfg(target_endian = "little")]
- {
- h1 = (h1 * (256 - a) + q1 * a) & N;
- h2 = (h2 * (256 - a) + q2 * a) & N;
- h3 = (h3 * (256 - a) + q3 * a) & N;
- l1 = ((l1 * (256 - a) + p1 * a) & N) >> 8;
- l2 = ((l2 * (256 - a) + p2 * a) & N) >> 8;
- l3 = ((l3 * (256 - a) + p3 * a) & N) >> 8;
- }
-
- #[cfg(target_endian = "big")]
- {
- h1 = (h1 * (256 - a) + p1 * a) & N;
- h2 = (h2 * (256 - a) + p2 * a) & N;
- h3 = (h3 * (256 - a) + p3 * a) & N;
- l1 = ((l1 * (256 - a) + q1 * a) & N) >> 8;
- l2 = ((l2 * (256 - a) + q2 * a) & N) >> 8;
- l3 = ((l3 * (256 - a) + q3 * a) & N) >> 8;
- }
-
- unsafe {
- *ptr = (h1 | l1, h2 | l2, h3 | l3);
- }
- }
-
- let mut iter = dst[((start + slice.len() * 8) * Self::PIXEL_SIZE)
- ..((start + count) * Self::PIXEL_SIZE)]
- .iter_mut();
- for _ in (slice.len() * 8)..count {
- blend(iter.next().unwrap(), r, a);
- blend(iter.next().unwrap(), g, a);
- blend(iter.next().unwrap(), b, a);
- }
- }
- }
-
- #[allow(clippy::many_single_char_names, clippy::cast_ptr_alignment)]
- fn fill_rect_fast(
- target: &mut BitMapBackend<'_, Self>,
- upper_left: (i32, i32),
- bottom_right: (i32, i32),
- r: u8,
- g: u8,
- b: u8,
- ) {
- let (w, h) = target.get_size();
- let (x0, y0) = (
- upper_left.0.min(bottom_right.0).max(0),
- upper_left.1.min(bottom_right.1).max(0),
- );
- let (x1, y1) = (
- upper_left.0.max(bottom_right.0).min(w as i32 - 1),
- upper_left.1.max(bottom_right.1).min(h as i32 - 1),
- );
-
- // This may happen when the minimal value is larger than the limit.
- // Thus we just have something that is completely out-of-range
- if x0 > x1 || y0 > y1 {
- return;
- }
-
- let dst = target.get_raw_pixel_buffer();
-
- if r == g && g == b {
- // If r == g == b, then we can use memset
- if x0 != 0 || x1 != w as i32 - 1 {
- // If it's not the entire row is filled, we can only do
- // memset per row
- for y in y0..=y1 {
- let start = (y * w as i32 + x0) as usize;
- let count = (x1 - x0 + 1) as usize;
- dst[(start * Self::PIXEL_SIZE)..((start + count) * Self::PIXEL_SIZE)]
- .iter_mut()
- .for_each(|e| *e = r);
- }
- } else {
- // If the entire memory block is going to be filled, just use single memset
- dst[Self::PIXEL_SIZE * (y0 * w as i32) as usize
- ..((y1 + 1) * w as i32) as usize * Self::PIXEL_SIZE]
- .iter_mut()
- .for_each(|e| *e = r);
- }
- } else {
- let count = (x1 - x0 + 1) as usize;
- if count < 8 {
- for y in y0..=y1 {
- let start = (y * w as i32 + x0) as usize;
- let mut iter = dst
- [(start * Self::PIXEL_SIZE)..((start + count) * Self::PIXEL_SIZE)]
- .iter_mut();
- for _ in 0..=(x1 - x0) {
- *iter.next().unwrap() = r;
- *iter.next().unwrap() = g;
- *iter.next().unwrap() = b;
- }
- }
- } else {
- for y in y0..=y1 {
- let start = (y * w as i32 + x0) as usize;
- let start_ptr = &mut dst[start * Self::PIXEL_SIZE] as *mut u8 as *mut [u8; 24];
- let slice =
- unsafe { std::slice::from_raw_parts_mut(start_ptr, (count - 1) / 8) };
- for p in slice.iter_mut() {
- // In this case, we can actually fill 8 pixels in one iteration with
- // only 3 movq instructions.
- // TODO: Consider using AVX instructions when possible
- let ptr = p as *mut [u8; 24] as *mut u64;
- unsafe {
- let (d1, d2, d3): (u64, u64, u64) = std::mem::transmute([
- r, g, b, r, g, b, r, g, // QW1
- b, r, g, b, r, g, b, r, // QW2
- g, b, r, g, b, r, g, b, // QW3
- ]);
- *ptr = d1;
- *ptr.offset(1) = d2;
- *ptr.offset(2) = d3;
- }
- }
-
- for idx in (slice.len() * 8)..count {
- dst[start * Self::PIXEL_SIZE + idx * Self::PIXEL_SIZE] = r;
- dst[start * Self::PIXEL_SIZE + idx * Self::PIXEL_SIZE + 1] = g;
- dst[start * Self::PIXEL_SIZE + idx * Self::PIXEL_SIZE + 2] = b;
- }
- }
- }
- }
- }
-}
-
-impl PixelFormat for BGRXPixel {
- const PIXEL_SIZE: usize = 4;
- const EFFECTIVE_PIXEL_SIZE: usize = 3;
-
- #[inline(always)]
- fn byte_at(r: u8, g: u8, b: u8, _a: u64, idx: usize) -> u8 {
- match idx {
- 0 => b,
- 1 => g,
- 2 => r,
- _ => 0xff,
- }
- }
-
- #[inline(always)]
- fn decode_pixel(data: &[u8]) -> (u8, u8, u8, u64) {
- (data[2], data[1], data[0], 0x255)
- }
-
- #[allow(clippy::many_single_char_names, clippy::cast_ptr_alignment)]
- fn blend_rect_fast(
- target: &mut BitMapBackend<'_, Self>,
- upper_left: (i32, i32),
- bottom_right: (i32, i32),
- r: u8,
- g: u8,
- b: u8,
- a: f64,
- ) {
- let (w, h) = target.get_size();
- let a = a.min(1.0).max(0.0);
- if a == 0.0 {
- return;
- }
-
- let (x0, y0) = (
- upper_left.0.min(bottom_right.0).max(0),
- upper_left.1.min(bottom_right.1).max(0),
- );
- let (x1, y1) = (
- upper_left.0.max(bottom_right.0).min(w as i32 - 1),
- upper_left.1.max(bottom_right.1).min(h as i32 - 1),
- );
-
- // This may happen when the minimal value is larger than the limit.
- // Thus we just have something that is completely out-of-range
- if x0 > x1 || y0 > y1 {
- return;
- }
-
- let dst = target.get_raw_pixel_buffer();
-
- let a = (256.0 * a).floor() as u64;
-
- // Since we should always make sure the RGB payload occupies the logic lower bits
- // thus, this type purning should work for both LE and BE CPUs
- #[rustfmt::skip]
- let p: u64 = unsafe {
- std::mem::transmute([
- u16::from(b), u16::from(r), u16::from(b), u16::from(r), // QW1
- ])
- };
-
- #[rustfmt::skip]
- let q: u64 = unsafe {
- std::mem::transmute([
- u16::from(g), 0u16, u16::from(g), 0u16, // QW1
- ])
- };
-
- const N: u64 = 0xff00_ff00_ff00_ff00;
- const M: u64 = 0x00ff_00ff_00ff_00ff;
-
- for y in y0..=y1 {
- let start = (y * w as i32 + x0) as usize;
- let count = (x1 - x0 + 1) as usize;
-
- let start_ptr = &mut dst[start * Self::PIXEL_SIZE] as *mut u8 as *mut [u8; 8];
- let slice = unsafe { std::slice::from_raw_parts_mut(start_ptr, (count - 1) / 2) };
- for rp in slice.iter_mut() {
- let ptr = rp as *mut [u8; 8] as *mut u64;
- let d1 = unsafe { *ptr };
- let mut h = (d1 >> 8) & M;
- let mut l = d1 & M;
-
- #[cfg(target_endian = "little")]
- {
- h = (h * (256 - a) + q * a) & N;
- l = ((l * (256 - a) + p * a) & N) >> 8;
- }
-
- #[cfg(target_endian = "big")]
- {
- h = (h * (256 - a) + p * a) & N;
- l = ((l * (256 - a) + q * a) & N) >> 8;
- }
-
- unsafe {
- *ptr = h | l;
- }
- }
-
- let mut iter = dst[((start + slice.len() * 2) * Self::PIXEL_SIZE)
- ..((start + count) * Self::PIXEL_SIZE)]
- .iter_mut();
- for _ in (slice.len() * 2)..count {
- blend(iter.next().unwrap(), b, a);
- blend(iter.next().unwrap(), g, a);
- blend(iter.next().unwrap(), r, a);
- iter.next();
- }
- }
- }
-
- #[allow(clippy::many_single_char_names, clippy::cast_ptr_alignment)]
- fn fill_rect_fast(
- target: &mut BitMapBackend<'_, Self>,
- upper_left: (i32, i32),
- bottom_right: (i32, i32),
- r: u8,
- g: u8,
- b: u8,
- ) {
- let (w, h) = target.get_size();
- let (x0, y0) = (
- upper_left.0.min(bottom_right.0).max(0),
- upper_left.1.min(bottom_right.1).max(0),
- );
- let (x1, y1) = (
- upper_left.0.max(bottom_right.0).min(w as i32 - 1),
- upper_left.1.max(bottom_right.1).min(h as i32 - 1),
- );
-
- // This may happen when the minimal value is larger than the limit.
- // Thus we just have something that is completely out-of-range
- if x0 > x1 || y0 > y1 {
- return;
- }
-
- let dst = target.get_raw_pixel_buffer();
-
- if r == g && g == b {
- // If r == g == b, then we can use memset
- if x0 != 0 || x1 != w as i32 - 1 {
- // If it's not the entire row is filled, we can only do
- // memset per row
- for y in y0..=y1 {
- let start = (y * w as i32 + x0) as usize;
- let count = (x1 - x0 + 1) as usize;
- dst[(start * Self::PIXEL_SIZE)..((start + count) * Self::PIXEL_SIZE)]
- .iter_mut()
- .for_each(|e| *e = r);
- }
- } else {
- // If the entire memory block is going to be filled, just use single memset
- dst[Self::PIXEL_SIZE * (y0 * w as i32) as usize
- ..((y1 + 1) * w as i32) as usize * Self::PIXEL_SIZE]
- .iter_mut()
- .for_each(|e| *e = r);
- }
- } else {
- let count = (x1 - x0 + 1) as usize;
- if count < 8 {
- for y in y0..=y1 {
- let start = (y * w as i32 + x0) as usize;
- let mut iter = dst
- [(start * Self::PIXEL_SIZE)..((start + count) * Self::PIXEL_SIZE)]
- .iter_mut();
- for _ in 0..=(x1 - x0) {
- *iter.next().unwrap() = b;
- *iter.next().unwrap() = g;
- *iter.next().unwrap() = r;
- iter.next();
- }
- }
- } else {
- for y in y0..=y1 {
- let start = (y * w as i32 + x0) as usize;
- let start_ptr = &mut dst[start * Self::PIXEL_SIZE] as *mut u8 as *mut [u8; 8];
- let slice =
- unsafe { std::slice::from_raw_parts_mut(start_ptr, (count - 1) / 2) };
- for p in slice.iter_mut() {
- // In this case, we can actually fill 8 pixels in one iteration with
- // only 3 movq instructions.
- // TODO: Consider using AVX instructions when possible
- let ptr = p as *mut [u8; 8] as *mut u64;
- unsafe {
- let d: u64 = std::mem::transmute([
- b, g, r, 0, b, g, r, 0, // QW1
- ]);
- *ptr = d;
- }
- }
-
- for idx in (slice.len() * 2)..count {
- dst[start * Self::PIXEL_SIZE + idx * Self::PIXEL_SIZE] = b;
- dst[start * Self::PIXEL_SIZE + idx * Self::PIXEL_SIZE + 1] = g;
- dst[start * Self::PIXEL_SIZE + idx * Self::PIXEL_SIZE + 2] = r;
- }
- }
- }
- }
- }
-}
-
-/// The backend that drawing a bitmap
-pub struct BitMapBackend<'a, P: PixelFormat = RGBPixel> {
- /// The path to the image
- #[allow(dead_code)]
- target: Target<'a>,
- /// The size of the image
- size: (u32, u32),
- /// The data buffer of the image
- buffer: Buffer<'a>,
- /// Flag indicates if the bitmap has been saved
- saved: bool,
- _pantomdata: PhantomData<P>,
-}
-
-impl<'a, P: PixelFormat> BitMapBackend<'a, P> {
- /// The number of bytes per pixel
- const PIXEL_SIZE: usize = P::PIXEL_SIZE;
-}
-
-impl<'a> BitMapBackend<'a, RGBPixel> {
- /// Create a new bitmap backend
- #[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
- pub fn new<T: AsRef<Path> + ?Sized>(path: &'a T, (w, h): (u32, u32)) -> Self {
- Self {
- target: Target::File(path.as_ref()),
- size: (w, h),
- buffer: Buffer::Owned(vec![0; Self::PIXEL_SIZE * (w * h) as usize]),
- saved: false,
- _pantomdata: PhantomData,
- }
- }
-
- /// Create a new bitmap backend that generate GIF animation
- ///
- /// When this is used, the bitmap backend acts similar to a real-time rendering backend.
- /// When the program finished drawing one frame, use `present` function to flush the frame
- /// into the GIF file.
- ///
- /// - `path`: The path to the GIF file to create
- /// - `dimension`: The size of the GIF image
- /// - `speed`: The amount of time for each frame to display
- #[cfg(all(feature = "gif", not(target_arch = "wasm32"), feature = "image"))]
- pub fn gif<T: AsRef<Path>>(
- path: T,
- (w, h): (u32, u32),
- frame_delay: u32,
- ) -> Result<Self, BitMapBackendError> {
- Ok(Self {
- target: Target::Gif(Box::new(gif_support::GifFile::new(
- path,
- (w, h),
- frame_delay,
- )?)),
- size: (w, h),
- buffer: Buffer::Owned(vec![0; Self::PIXEL_SIZE * (w * h) as usize]),
- saved: false,
- _pantomdata: PhantomData,
- })
- }
-
- /// Create a new bitmap backend which only lives in-memory
- ///
- /// When this is used, the bitmap backend will write to a user provided [u8] array (or Vec<u8>)
- /// in RGB pixel format.
- ///
- /// Note: This function provides backward compatibility for those code that assumes Plotters
- /// uses RGB pixel format and maniuplates the in-memory framebuffer.
- /// For more pixel format option, use `with_buffer_and_format` instead.
- ///
- /// - `buf`: The buffer to operate
- /// - `dimension`: The size of the image in pixels
- /// - **returns**: The newly created bitmap backend
- pub fn with_buffer(buf: &'a mut [u8], (w, h): (u32, u32)) -> Self {
- Self::with_buffer_and_format(buf, (w, h)).expect("Wrong buffer size")
- }
-}
-
-impl<'a, P: PixelFormat> BitMapBackend<'a, P> {
- /// Create a new bitmap backend with a in-memory buffer with specific pixel format.
- ///
- /// Note: This can be used as a way to manipulate framebuffer, `mmap` can be used on the top of this
- /// as well.
- ///
- /// - `buf`: The buffer to operate
- /// - `dimension`: The size of the image in pixels
- /// - **returns**: The newly created bitmap backend
- pub fn with_buffer_and_format(
- buf: &'a mut [u8],
- (w, h): (u32, u32),
- ) -> Result<Self, BitMapBackendError> {
- if (w * h) as usize * Self::PIXEL_SIZE > buf.len() {
- return Err(BitMapBackendError::InvalidBuffer);
- }
-
- Ok(Self {
- target: Target::Buffer(PhantomData),
- size: (w, h),
- buffer: Buffer::Borrowed(buf),
- saved: false,
- _pantomdata: PhantomData,
- })
- }
-
- #[inline(always)]
- fn get_raw_pixel_buffer(&mut self) -> &mut [u8] {
- self.buffer.borrow_buffer()
- }
-
- /// Split a bitmap backend vertically into several sub drawing area which allows
- /// multi-threading rendering.
- ///
- /// - `area_size`: The size of the area
- /// - **returns**: The splitted backends that can be rendered in parallel
- pub fn split(&mut self, area_size: &[u32]) -> Vec<BitMapBackend<P>> {
- let (w, h) = self.get_size();
- let buf = self.get_raw_pixel_buffer();
-
- let base_addr = &mut buf[0] as *mut u8;
- let mut split_points = vec![0];
- for size in area_size {
- let next = split_points.last().unwrap() + size;
- if next >= h {
- break;
- }
- split_points.push(next);
- }
- split_points.push(h);
-
- split_points
- .iter()
- .zip(split_points.iter().skip(1))
- .map(|(begin, end)| {
- let actual_buf = unsafe {
- std::slice::from_raw_parts_mut(
- base_addr.offset((begin * w) as isize * Self::PIXEL_SIZE as isize),
- ((end - begin) * w) as usize * Self::PIXEL_SIZE,
- )
- };
- Self::with_buffer_and_format(actual_buf, (w, end - begin)).unwrap()
- })
- .collect()
- }
-}
-
-impl<'a, P: PixelFormat> DrawingBackend for BitMapBackend<'a, P> {
- type ErrorType = BitMapBackendError;
-
- fn get_size(&self) -> (u32, u32) {
- self.size
- }
-
- fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<BitMapBackendError>> {
- self.saved = false;
- Ok(())
- }
-
- #[cfg(any(target_arch = "wasm32", not(feature = "image")))]
- fn present(&mut self) -> Result<(), DrawingErrorKind<BitMapBackendError>> {
- Ok(())
- }
-
- #[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
- fn present(&mut self) -> Result<(), DrawingErrorKind<BitMapBackendError>> {
- if !P::can_be_saved() {
- return Ok(());
- }
- let (w, h) = self.get_size();
- match &mut self.target {
- Target::File(path) => {
- if let Some(img) = BorrowedImage::from_raw(w, h, self.buffer.borrow_buffer()) {
- img.save(&path).map_err(|x| {
- DrawingErrorKind::DrawingError(match x {
- ImageError::IoError(x) => BitMapBackendError::IOError(x),
- whatever => BitMapBackendError::IOError(std::io::Error::new(
- std::io::ErrorKind::Other,
- format!("{}", whatever),
- )),
- })
- })?;
- self.saved = true;
- Ok(())
- } else {
- Err(DrawingErrorKind::DrawingError(
- BitMapBackendError::InvalidBuffer,
- ))
- }
- }
- Target::Buffer(_) => Ok(()),
-
- #[cfg(all(feature = "gif", not(target_arch = "wasm32"), feature = "image"))]
- Target::Gif(target) => {
- target
- .flush_frame(self.buffer.borrow_buffer())
- .map_err(DrawingErrorKind::DrawingError)?;
- self.saved = true;
- Ok(())
- }
- }
- }
-
- fn draw_pixel(
- &mut self,
- point: BackendCoord,
- color: &RGBAColor,
- ) -> Result<(), DrawingErrorKind<BitMapBackendError>> {
- if point.0 < 0 || point.1 < 0 {
- return Ok(());
- }
-
- let alpha = color.alpha();
- let rgb = color.rgb();
-
- P::draw_pixel(self, point, rgb, alpha);
-
- Ok(())
- }
-
- fn draw_line<S: BackendStyle>(
- &mut self,
- from: (i32, i32),
- to: (i32, i32),
- style: &S,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- let alpha = style.as_color().alpha();
- let (r, g, b) = style.as_color().rgb();
-
- if (from.0 == to.0 || from.1 == to.1) && style.stroke_width() == 1 {
- if alpha >= 1.0 {
- if from.1 == to.1 {
- P::fill_rect_fast(self, from, to, r, g, b);
- } else {
- P::fill_vertical_line_fast(self, from.0, (from.1, to.1), r, g, b);
- }
- } else {
- P::blend_rect_fast(self, from, to, r, g, b, alpha);
- }
- return Ok(());
- }
-
- crate::drawing::rasterizer::draw_line(self, from, to, style)
- }
-
- fn draw_rect<S: BackendStyle>(
- &mut self,
- upper_left: (i32, i32),
- bottom_right: (i32, i32),
- style: &S,
- fill: bool,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- let alpha = style.as_color().alpha();
- let (r, g, b) = style.as_color().rgb();
- if fill {
- if alpha >= 1.0 {
- P::fill_rect_fast(self, upper_left, bottom_right, r, g, b);
- } else {
- P::blend_rect_fast(self, upper_left, bottom_right, r, g, b, alpha);
- }
- return Ok(());
- }
- crate::drawing::rasterizer::draw_rect(self, upper_left, bottom_right, style, fill)
- }
-
- fn blit_bitmap<'b>(
- &mut self,
- pos: BackendCoord,
- (sw, sh): (u32, u32),
- src: &'b [u8],
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- let (dw, dh) = self.get_size();
-
- let (x0, y0) = pos;
- let (x1, y1) = (x0 + sw as i32, y0 + sh as i32);
-
- let (x0, y0, x1, y1) = (x0.max(0), y0.max(0), x1.min(dw as i32), y1.min(dh as i32));
-
- if x0 == x1 || y0 == y1 {
- return Ok(());
- }
-
- let mut chunk_size = (x1 - x0) as usize;
- let mut num_chunks = (y1 - y0) as usize;
- let dst_gap = dw as usize - chunk_size;
- let src_gap = sw as usize - chunk_size;
-
- let dst_start = Self::PIXEL_SIZE * (y0 as usize * dw as usize + x0 as usize);
-
- let mut dst = &mut self.get_raw_pixel_buffer()[dst_start..];
-
- let src_start =
- Self::PIXEL_SIZE * ((sh as i32 + y0 - y1) * sw as i32 + (sw as i32 + x0 - x1)) as usize;
- let mut src = &src[src_start..];
-
- if src_gap == 0 && dst_gap == 0 {
- chunk_size *= num_chunks;
- num_chunks = 1;
- }
- for i in 0..num_chunks {
- dst[0..(chunk_size * Self::PIXEL_SIZE)]
- .copy_from_slice(&src[0..(chunk_size * Self::PIXEL_SIZE)]);
- if i != num_chunks - 1 {
- dst = &mut dst[((chunk_size + dst_gap) * Self::PIXEL_SIZE)..];
- src = &src[((chunk_size + src_gap) * Self::PIXEL_SIZE)..];
- }
- }
-
- Ok(())
- }
-}
-
-impl<P: PixelFormat> Drop for BitMapBackend<'_, P> {
- fn drop(&mut self) {
- if !self.saved {
- // drop should not panic, so we ignore a failed present
- let _ = self.present();
- }
- }
-}
-
-#[cfg(test)]
-#[test]
-fn test_bitmap_backend() {
- use crate::prelude::*;
- let mut buffer = vec![0; 10 * 10 * 3];
-
- {
- let back = BitMapBackend::with_buffer(&mut buffer, (10, 10));
-
- let area = back.into_drawing_area();
- area.fill(&WHITE).unwrap();
- area.draw(&PathElement::new(vec![(0, 0), (10, 10)], RED.filled()))
- .unwrap();
- area.present().unwrap();
- }
-
- for i in 0..10 {
- assert_eq!(buffer[i * 33], 255);
- assert_eq!(buffer[i * 33 + 1], 0);
- assert_eq!(buffer[i * 33 + 2], 0);
- buffer[i * 33] = 255;
- buffer[i * 33 + 1] = 255;
- buffer[i * 33 + 2] = 255;
- }
-
- assert!(buffer.into_iter().all(|x| x == 255));
-}
-
-#[cfg(test)]
-#[test]
-fn test_bitmap_backend_fill_half() {
- use crate::prelude::*;
- let mut buffer = vec![0; 10 * 10 * 3];
-
- {
- let back = BitMapBackend::with_buffer(&mut buffer, (10, 10));
-
- let area = back.into_drawing_area();
- area.draw(&Rectangle::new([(0, 0), (5, 10)], RED.filled()))
- .unwrap();
- area.present().unwrap();
- }
- for x in 0..10 {
- for y in 0..10 {
- assert_eq!(
- buffer[(y * 10 + x) as usize * 3 + 0],
- if x <= 5 { 255 } else { 0 }
- );
- assert_eq!(buffer[(y * 10 + x) as usize * 3 + 1], 0);
- assert_eq!(buffer[(y * 10 + x) as usize * 3 + 2], 0);
- }
- }
-
- let mut buffer = vec![0; 10 * 10 * 3];
-
- {
- let back = BitMapBackend::with_buffer(&mut buffer, (10, 10));
-
- let area = back.into_drawing_area();
- area.draw(&Rectangle::new([(0, 0), (10, 5)], RED.filled()))
- .unwrap();
- area.present().unwrap();
- }
- for x in 0..10 {
- for y in 0..10 {
- assert_eq!(
- buffer[(y * 10 + x) as usize * 3 + 0],
- if y <= 5 { 255 } else { 0 }
- );
- assert_eq!(buffer[(y * 10 + x) as usize * 3 + 1], 0);
- assert_eq!(buffer[(y * 10 + x) as usize * 3 + 2], 0);
- }
- }
-}
-
-#[cfg(test)]
-#[test]
-fn test_bitmap_backend_blend() {
- use crate::prelude::*;
- let mut buffer = vec![255; 10 * 10 * 3];
-
- {
- let back = BitMapBackend::with_buffer(&mut buffer, (10, 10));
-
- let area = back.into_drawing_area();
- area.draw(&Rectangle::new(
- [(0, 0), (5, 10)],
- RGBColor(0, 100, 200).mix(0.2).filled(),
- ))
- .unwrap();
- area.present().unwrap();
- }
-
- for x in 0..10 {
- for y in 0..10 {
- let (r, g, b) = if x <= 5 {
- (205, 225, 245)
- } else {
- (255, 255, 255)
- };
- assert_eq!(buffer[(y * 10 + x) as usize * 3 + 0], r);
- assert_eq!(buffer[(y * 10 + x) as usize * 3 + 1], g);
- assert_eq!(buffer[(y * 10 + x) as usize * 3 + 2], b);
- }
- }
-}
-
-#[cfg(test)]
-#[test]
-fn test_bitmap_backend_split_and_fill() {
- use crate::prelude::*;
- let mut buffer = vec![255; 10 * 10 * 3];
-
- {
- let mut back = BitMapBackend::with_buffer(&mut buffer, (10, 10));
-
- for (sub_backend, color) in back.split(&[5]).into_iter().zip([&RED, &GREEN].iter()) {
- sub_backend.into_drawing_area().fill(*color).unwrap();
- }
- }
-
- for x in 0..10 {
- for y in 0..10 {
- let (r, g, b) = if y < 5 { (255, 0, 0) } else { (0, 255, 0) };
- assert_eq!(buffer[(y * 10 + x) as usize * 3 + 0], r);
- assert_eq!(buffer[(y * 10 + x) as usize * 3 + 1], g);
- assert_eq!(buffer[(y * 10 + x) as usize * 3 + 2], b);
- }
- }
-}
-
-#[cfg(test)]
-#[test]
-fn test_draw_rect_out_of_range() {
- use crate::prelude::*;
- let mut buffer = vec![0; 1099 * 1000 * 3];
-
- {
- let mut back = BitMapBackend::with_buffer(&mut buffer, (1000, 1000));
-
- back.draw_line((1100, 0), (1100, 999), &RED.to_rgba())
- .unwrap();
- back.draw_line((0, 1100), (999, 1100), &RED.to_rgba())
- .unwrap();
- back.draw_rect((1100, 0), (1100, 999), &RED.to_rgba(), true)
- .unwrap();
- }
-
- for x in 0..1000 {
- for y in 0..1000 {
- assert_eq!(buffer[(y * 1000 + x) as usize * 3 + 0], 0);
- assert_eq!(buffer[(y * 1000 + x) as usize * 3 + 1], 0);
- assert_eq!(buffer[(y * 1000 + x) as usize * 3 + 2], 0);
- }
- }
-}
-
-#[cfg(test)]
-#[test]
-fn test_draw_line_out_of_range() {
- use crate::prelude::*;
- let mut buffer = vec![0; 1000 * 1000 * 3];
-
- {
- let mut back = BitMapBackend::with_buffer(&mut buffer, (1000, 1000));
-
- back.draw_line((-1000, -1000), (2000, 2000), &WHITE.to_rgba())
- .unwrap();
-
- back.draw_line((999, -1000), (999, 2000), &WHITE.to_rgba())
- .unwrap();
- }
-
- for x in 0..1000 {
- for y in 0..1000 {
- let expected_value = if x == y || x == 999 { 255 } else { 0 };
- assert_eq!(buffer[(y * 1000 + x) as usize * 3 + 0], expected_value);
- assert_eq!(buffer[(y * 1000 + x) as usize * 3 + 1], expected_value);
- assert_eq!(buffer[(y * 1000 + x) as usize * 3 + 2], expected_value);
- }
- }
-}
-
-#[cfg(test)]
-#[test]
-fn test_bitmap_blend_large() {
- use crate::prelude::*;
- let mut buffer = vec![0; 1000 * 1000 * 3];
-
- for fill_color in [RED, GREEN, BLUE].iter() {
- buffer.iter_mut().for_each(|x| *x = 0);
-
- {
- let mut back = BitMapBackend::with_buffer(&mut buffer, (1000, 1000));
-
- back.draw_rect((0, 0), (1000, 1000), &WHITE.mix(0.1), true)
- .unwrap(); // should be (24, 24, 24)
- back.draw_rect((0, 0), (100, 100), &fill_color.mix(0.5), true)
- .unwrap(); // should be (139, 24, 24)
- }
-
- for x in 0..1000 {
- for y in 0..1000 {
- let expected_value = if x <= 100 && y <= 100 {
- let (r, g, b) = fill_color.to_rgba().rgb();
- (
- if r > 0 { 139 } else { 12 },
- if g > 0 { 139 } else { 12 },
- if b > 0 { 139 } else { 12 },
- )
- } else {
- (24, 24, 24)
- };
- assert_eq!(buffer[(y * 1000 + x) as usize * 3 + 0], expected_value.0);
- assert_eq!(buffer[(y * 1000 + x) as usize * 3 + 1], expected_value.1);
- assert_eq!(buffer[(y * 1000 + x) as usize * 3 + 2], expected_value.2);
- }
- }
- }
-}
-
-#[cfg(test)]
-#[test]
-fn test_bitmap_bgrx_pixel_format() {
- use crate::drawing::bitmap_pixel::BGRXPixel;
- use crate::prelude::*;
- let mut rgb_buffer = vec![0; 1000 * 1000 * 3];
- let mut bgrx_buffer = vec![0; 1000 * 1000 * 4];
-
- {
- let mut rgb_back = BitMapBackend::with_buffer(&mut rgb_buffer, (1000, 1000));
- let mut bgrx_back =
- BitMapBackend::<BGRXPixel>::with_buffer_and_format(&mut bgrx_buffer, (1000, 1000))
- .unwrap();
-
- rgb_back
- .draw_rect((0, 0), (1000, 1000), &BLACK, true)
- .unwrap();
- bgrx_back
- .draw_rect((0, 0), (1000, 1000), &BLACK, true)
- .unwrap();
-
- rgb_back
- .draw_rect(
- (0, 0),
- (1000, 1000),
- &RGBColor(0xaa, 0xbb, 0xcc).mix(0.85),
- true,
- )
- .unwrap();
- bgrx_back
- .draw_rect(
- (0, 0),
- (1000, 1000),
- &RGBColor(0xaa, 0xbb, 0xcc).mix(0.85),
- true,
- )
- .unwrap();
-
- rgb_back
- .draw_rect((0, 0), (1000, 1000), &RED.mix(0.85), true)
- .unwrap();
- bgrx_back
- .draw_rect((0, 0), (1000, 1000), &RED.mix(0.85), true)
- .unwrap();
-
- rgb_back.draw_circle((300, 300), 100, &GREEN, true).unwrap();
- bgrx_back
- .draw_circle((300, 300), 100, &GREEN, true)
- .unwrap();
-
- rgb_back.draw_rect((10, 10), (50, 50), &BLUE, true).unwrap();
- bgrx_back
- .draw_rect((10, 10), (50, 50), &BLUE, true)
- .unwrap();
-
- rgb_back
- .draw_rect((10, 10), (50, 50), &WHITE, true)
- .unwrap();
- bgrx_back
- .draw_rect((10, 10), (50, 50), &WHITE, true)
- .unwrap();
-
- rgb_back
- .draw_rect((10, 10), (15, 50), &YELLOW, true)
- .unwrap();
- bgrx_back
- .draw_rect((10, 10), (15, 50), &YELLOW, true)
- .unwrap();
- }
-
- for x in 0..1000 {
- for y in 0..1000 {
- assert!(
- (rgb_buffer[y * 3000 + x * 3 + 0] as i32
- - bgrx_buffer[y * 4000 + x * 4 + 2] as i32)
- .abs()
- <= 1
- );
- assert!(
- (rgb_buffer[y * 3000 + x * 3 + 1] as i32
- - bgrx_buffer[y * 4000 + x * 4 + 1] as i32)
- .abs()
- <= 1
- );
- assert!(
- (rgb_buffer[y * 3000 + x * 3 + 2] as i32
- - bgrx_buffer[y * 4000 + x * 4 + 0] as i32)
- .abs()
- <= 1
- );
- }
- }
-}
-#[cfg(test)]
-#[test]
-fn test_draw_simple_lines() {
- use crate::prelude::*;
- let mut buffer = vec![0; 1000 * 1000 * 3];
-
- {
- let mut back = BitMapBackend::with_buffer(&mut buffer, (1000, 1000));
- back.draw_line((500, 0), (500, 1000), &WHITE.filled().stroke_width(5))
- .unwrap();
- }
-
- let nz_count = buffer.into_iter().filter(|x| *x != 0).count();
-
- assert_eq!(nz_count, 6 * 1000 * 3);
-}
-
-#[cfg(test)]
-#[test]
-fn test_bitmap_blit() {
- let src_bitmap: Vec<u8> = (0..100)
- .map(|y| (0..300).map(move |x| ((x * y) % 253) as u8))
- .flatten()
- .collect();
-
- use crate::prelude::*;
- let mut buffer = vec![0; 1000 * 1000 * 3];
-
- {
- let mut back = BitMapBackend::with_buffer(&mut buffer, (1000, 1000));
- back.blit_bitmap((500, 500), (100, 100), &src_bitmap[..])
- .unwrap();
- }
-
- for y in 0..1000 {
- for x in 0..1000 {
- if x >= 500 && x < 600 && y >= 500 && y < 600 {
- let lx = x - 500;
- let ly = y - 500;
- assert_eq!(buffer[y * 3000 + x * 3 + 0] as usize, (ly * lx * 3) % 253);
- assert_eq!(
- buffer[y * 3000 + x * 3 + 1] as usize,
- (ly * (lx * 3 + 1)) % 253
- );
- assert_eq!(
- buffer[y * 3000 + x * 3 + 2] as usize,
- (ly * (lx * 3 + 2)) % 253
- );
- } else {
- assert_eq!(buffer[y * 3000 + x * 3 + 0], 0);
- assert_eq!(buffer[y * 3000 + x * 3 + 1], 0);
- assert_eq!(buffer[y * 3000 + x * 3 + 2], 0);
- }
- }
- }
-}
-
-#[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
-#[cfg(test)]
-mod test {
- use crate::prelude::*;
- use crate::style::text_anchor::{HPos, Pos, VPos};
- use image::{ImageBuffer, Rgb};
- use std::fs;
- use std::path::Path;
-
- static DST_DIR: &str = "target/test/bitmap";
-
- fn checked_save_file(name: &str, content: &[u8], w: u32, h: u32) {
- /*
- Please use the PNG file to manually verify the results.
- */
- assert!(content.iter().any(|x| *x != 0));
- fs::create_dir_all(DST_DIR).unwrap();
- let file_name = format!("{}.png", name);
- let file_path = Path::new(DST_DIR).join(file_name);
- println!("{:?} created", file_path);
- let img = ImageBuffer::<Rgb<u8>, &[u8]>::from_raw(w, h, content).unwrap();
- img.save(&file_path).unwrap();
- }
-
- fn draw_mesh_with_custom_ticks(tick_size: i32, test_name: &str) {
- let (width, height) = (500, 500);
- let mut buffer = vec![0; (width * height * 3) as usize];
- {
- let root = BitMapBackend::with_buffer(&mut buffer, (width, height)).into_drawing_area();
- root.fill(&WHITE).unwrap();
-
- let mut chart = ChartBuilder::on(&root)
- .caption("This is a test", ("sans-serif", 20))
- .set_all_label_area_size(40)
- .build_ranged(0..10, 0..10)
- .unwrap();
-
- chart
- .configure_mesh()
- .set_all_tick_mark_size(tick_size)
- .draw()
- .unwrap();
- }
- checked_save_file(test_name, &buffer, width, height);
- }
-
- #[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_draw() {
- let (width, height) = (1500, 800);
- let mut buffer = vec![0; (width * height * 3) as usize];
- {
- let root = BitMapBackend::with_buffer(&mut buffer, (width, height)).into_drawing_area();
- root.fill(&WHITE).unwrap();
- 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", 20))
- .set_all_label_area_size(40)
- .build_ranged(0..100, 0..50)
- .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", &buffer, width, height);
- }
-
- #[test]
- fn test_text_clipping() {
- let (width, height) = (500_i32, 500_i32);
- let mut buffer = vec![0; (width * height * 3) as usize];
- {
- let root = BitMapBackend::with_buffer(&mut buffer, (width as u32, height as u32))
- .into_drawing_area();
- root.fill(&WHITE).unwrap();
-
- 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", &buffer, width as u32, height as u32);
- }
-
- #[test]
- fn test_series_labels() {
- let (width, height) = (500, 500);
- let mut buffer = vec![0; (width * height * 3) as usize];
- {
- let root = BitMapBackend::with_buffer(&mut buffer, (width, height)).into_drawing_area();
- root.fill(&WHITE).unwrap();
-
- let mut chart = ChartBuilder::on(&root)
- .caption("All series label positions", ("sans-serif", 20))
- .set_all_label_area_size(40)
- .build_ranged(0..50, 0..50)
- .unwrap();
-
- chart
- .configure_mesh()
- .disable_x_mesh()
- .disable_y_mesh()
- .draw()
- .unwrap();
-
- chart
- .draw_series(std::iter::once(Circle::new((5, 15), 5, &RED)))
- .expect("Drawing error")
- .label("Series 1")
- .legend(|(x, y)| Circle::new((x, y), 3, RED.filled()));
-
- chart
- .draw_series(std::iter::once(Circle::new((5, 15), 10, &BLUE)))
- .expect("Drawing error")
- .label("Series 2")
- .legend(|(x, y)| Circle::new((x, y), 3, 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", &buffer, width, height);
- }
-
- #[test]
- fn test_draw_pixel_alphas() {
- let (width, height) = (100_i32, 100_i32);
- let mut buffer = vec![0; (width * height * 3) as usize];
- {
- let root = BitMapBackend::with_buffer(&mut buffer, (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",
- &buffer,
- width as u32,
- height as u32,
- );
- }
-}
diff --git a/src/drawing/backend_impl/cairo.rs b/src/drawing/backend_impl/cairo.rs
deleted file mode 100644
index a65bb7b..0000000
--- a/src/drawing/backend_impl/cairo.rs
+++ /dev/null
@@ -1,567 +0,0 @@
-use cairo::{Context as CairoContext, FontSlant, FontWeight, Status as CairoStatus};
-
-#[allow(unused_imports)]
-use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind};
-use crate::style::text_anchor::{HPos, VPos};
-#[allow(unused_imports)]
-use crate::style::{Color, FontDesc, FontStyle, FontTransform, RGBAColor, TextStyle};
-
-/// The drawing backend that is backed with a Cairo context
-pub struct CairoBackend<'a> {
- context: &'a CairoContext,
- width: u32,
- height: u32,
- init_flag: bool,
-}
-
-#[derive(Debug)]
-pub struct CairoError(CairoStatus);
-
-impl std::fmt::Display for CairoError {
- fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
- write!(fmt, "{:?}", self)
- }
-}
-
-impl std::error::Error for CairoError {}
-
-impl<'a> CairoBackend<'a> {
- /// Call cairo functions and verify the cairo status afterward.
- ///
- /// All major cairo objects retain an error status internally
- /// which can be queried anytime by the users using status() method.
- /// In the mean time, it is safe to call all cairo functions normally even
- /// if the underlying object is in an error status.
- /// This means that no error handling code is required before or after
- /// each individual cairo function call.
- ///
- /// - `f`: The function to call
- /// - *Returns* The wrapped result of the function
- fn call_cairo<T, F: Fn(&CairoContext) -> T>(
- &self,
- f: F,
- ) -> Result<T, DrawingErrorKind<CairoError>> {
- let result = f(self.context);
- let status = self.context.status();
- if status == CairoStatus::Success {
- Ok(result)
- } else {
- Err(DrawingErrorKind::DrawingError(CairoError(status)))
- }
- }
-
- fn set_color(&self, color: &RGBAColor) -> Result<(), DrawingErrorKind<CairoError>> {
- self.call_cairo(|c| {
- c.set_source_rgba(
- f64::from(color.rgb().0) / 255.0,
- f64::from(color.rgb().1) / 255.0,
- f64::from(color.rgb().2) / 255.0,
- color.alpha(),
- )
- })
- }
-
- fn set_stroke_width(&self, width: u32) -> Result<(), DrawingErrorKind<CairoError>> {
- self.call_cairo(|c| c.set_line_width(f64::from(width)))
- }
-
- fn set_font<'b>(&self, font: &FontDesc<'b>) -> Result<(), DrawingErrorKind<CairoError>> {
- let actual_size = font.get_size();
- self.call_cairo(|c| {
- match font.get_style() {
- FontStyle::Normal => {
- c.select_font_face(font.get_name(), FontSlant::Normal, FontWeight::Normal)
- }
- FontStyle::Bold => {
- c.select_font_face(font.get_name(), FontSlant::Normal, FontWeight::Bold)
- }
- FontStyle::Oblique => {
- c.select_font_face(font.get_name(), FontSlant::Oblique, FontWeight::Normal)
- }
- FontStyle::Italic => {
- c.select_font_face(font.get_name(), FontSlant::Italic, FontWeight::Normal)
- }
- };
- c.set_font_size(actual_size);
- })
- }
-
- pub fn new(context: &'a CairoContext, (w, h): (u32, u32)) -> Result<Self, CairoError> {
- let ret = Self {
- context,
- width: w,
- height: h,
- init_flag: false,
- };
- Ok(ret)
- }
-}
-
-impl<'a> DrawingBackend for CairoBackend<'a> {
- type ErrorType = CairoError;
-
- fn get_size(&self) -> (u32, u32) {
- (self.width, self.height)
- }
-
- fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- if !self.init_flag {
- self.call_cairo(|c| {
- let (x0, y0, x1, y1) = c.clip_extents();
- c.scale(
- (x1 - x0) / f64::from(self.width),
- (y1 - y0) / f64::from(self.height),
- )
- })?;
- self.init_flag = true;
- }
- Ok(())
- }
-
- fn present(&mut self) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- Ok(())
- }
-
- fn draw_pixel(
- &mut self,
- point: BackendCoord,
- color: &RGBAColor,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- self.call_cairo(|c| {
- c.rectangle(f64::from(point.0), f64::from(point.1), 1.0, 1.0);
- c.set_source_rgba(
- f64::from(color.rgb().0) / 255.0,
- f64::from(color.rgb().1) / 255.0,
- f64::from(color.rgb().2) / 255.0,
- color.alpha(),
- );
- c.fill();
- })
- }
-
- fn draw_line<S: BackendStyle>(
- &mut self,
- from: BackendCoord,
- to: BackendCoord,
- style: &S,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- self.set_color(&style.as_color())?;
- self.set_stroke_width(style.stroke_width())?;
-
- self.call_cairo(|c| {
- c.move_to(f64::from(from.0), f64::from(from.1));
- c.line_to(f64::from(to.0), f64::from(to.1));
- c.stroke();
- })
- }
-
- fn draw_rect<S: BackendStyle>(
- &mut self,
- upper_left: BackendCoord,
- bottom_right: BackendCoord,
- style: &S,
- fill: bool,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- self.set_color(&style.as_color())?;
- self.set_stroke_width(style.stroke_width())?;
-
- self.call_cairo(|c| {
- c.rectangle(
- f64::from(upper_left.0),
- f64::from(upper_left.1),
- f64::from(bottom_right.0 - upper_left.0),
- f64::from(bottom_right.1 - upper_left.1),
- );
- if fill {
- c.fill();
- } else {
- c.stroke();
- }
- })
- }
-
- fn draw_path<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
- &mut self,
- path: I,
- style: &S,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- self.set_color(&style.as_color())?;
- self.set_stroke_width(style.stroke_width())?;
-
- let mut path = path.into_iter();
- if let Some((x, y)) = path.next() {
- self.call_cairo(|c| c.move_to(f64::from(x), f64::from(y)))?;
- }
-
- for (x, y) in path {
- self.call_cairo(|c| c.line_to(f64::from(x), f64::from(y)))?;
- }
-
- self.call_cairo(|c| c.stroke())
- }
-
- fn fill_polygon<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
- &mut self,
- path: I,
- style: &S,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- self.set_color(&style.as_color())?;
- self.set_stroke_width(style.stroke_width())?;
-
- let mut path = path.into_iter();
-
- if let Some((x, y)) = path.next() {
- self.call_cairo(|c| c.move_to(f64::from(x), f64::from(y)))?;
-
- for (x, y) in path {
- self.call_cairo(|c| c.line_to(f64::from(x), f64::from(y)))?;
- }
-
- self.call_cairo(|c| {
- c.close_path();
- c.fill();
- })
- } else {
- Ok(())
- }
- }
-
- fn draw_circle<S: BackendStyle>(
- &mut self,
- center: BackendCoord,
- radius: u32,
- style: &S,
- fill: bool,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- self.set_color(&style.as_color())?;
- self.set_stroke_width(style.stroke_width())?;
-
- self.call_cairo(|c| {
- c.new_sub_path();
- c.arc(
- f64::from(center.0),
- f64::from(center.1),
- f64::from(radius),
- 0.0,
- std::f64::consts::PI * 2.0,
- );
-
- if fill {
- c.fill();
- } else {
- c.stroke();
- }
- })
- }
-
- fn estimate_text_size<'b>(
- &self,
- text: &str,
- font: &FontDesc<'b>,
- ) -> Result<(u32, u32), DrawingErrorKind<Self::ErrorType>> {
- self.set_font(&font)?;
- self.call_cairo(|c| {
- let extents = c.text_extents(text);
- (extents.width as u32, extents.height as u32)
- })
- }
-
- fn draw_text(
- &mut self,
- text: &str,
- style: &TextStyle,
- pos: BackendCoord,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- let font = &style.font;
- let color = &style.color;
- let (mut x, mut y) = (pos.0, pos.1);
-
- let degree = match font.get_transform() {
- FontTransform::None => 0.0,
- FontTransform::Rotate90 => 90.0,
- FontTransform::Rotate180 => 180.0,
- FontTransform::Rotate270 => 270.0,
- } / 180.0
- * std::f64::consts::PI;
-
- if degree != 0.0 {
- self.call_cairo(|c| {
- c.save();
- c.translate(f64::from(x), f64::from(y));
- c.rotate(degree);
- })?;
- x = 0;
- y = 0;
- }
-
- self.set_font(&font)?;
- self.set_color(&color)?;
-
- self.call_cairo(|c| {
- let extents = c.text_extents(text);
- let dx = match style.pos.h_pos {
- HPos::Left => 0.0,
- HPos::Right => -extents.width,
- HPos::Center => -extents.width / 2.0,
- };
- let dy = match style.pos.v_pos {
- VPos::Top => extents.height,
- VPos::Center => extents.height / 2.0,
- VPos::Bottom => 0.0,
- };
- c.move_to(
- f64::from(x) + dx - extents.x_bearing,
- f64::from(y) + dy - extents.y_bearing - extents.height,
- );
- c.show_text(text);
- if degree != 0.0 {
- c.restore();
- }
- })
- }
-}
-
-#[cfg(test)]
-mod test {
- use super::*;
- use crate::prelude::*;
- use crate::style::text_anchor::{HPos, Pos, VPos};
- use std::fs;
- use std::path::Path;
-
- static DST_DIR: &str = "target/test/cairo";
-
- fn checked_save_file(name: &str, content: &str) {
- /*
- Please use the PS file to manually verify the results.
-
- You may want to use Ghostscript to view the file.
- */
- assert!(!content.is_empty());
- fs::create_dir_all(DST_DIR).unwrap();
- let file_name = format!("{}.ps", 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 buffer: Vec<u8> = vec![];
- let surface = cairo::PsSurface::for_stream(500.0, 500.0, buffer).unwrap();
- let cr = CairoContext::new(&surface);
- let root = CairoBackend::new(&cr, (500, 500))
- .unwrap()
- .into_drawing_area();
-
- // Text could be rendered to different elements if has whitespaces
- let mut chart = ChartBuilder::on(&root)
- .caption("this-is-a-test", ("sans-serif", 20))
- .set_all_label_area_size(40)
- .build_ranged(0..10, 0..10)
- .unwrap();
-
- chart
- .configure_mesh()
- .set_all_tick_mark_size(tick_size)
- .draw()
- .unwrap();
-
- let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap();
- let content = String::from_utf8(buffer).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_draw() {
- let buffer: Vec<u8> = vec![];
- let (width, height) = (1500, 800);
- let surface = cairo::PsSurface::for_stream(width.into(), height.into(), buffer).unwrap();
- let cr = CairoContext::new(&surface);
- let root = CairoBackend::new(&cr, (width, height))
- .unwrap()
- .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", 20))
- .set_all_label_area_size(40)
- .build_ranged(0..100, 0..50)
- .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");
- }
- }
- }
-
- let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap();
- let content = String::from_utf8(buffer).unwrap();
- 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 buffer: Vec<u8> = vec![];
- let (width, height) = (500_i32, 500_i32);
- let surface = cairo::PsSurface::for_stream(width.into(), height.into(), buffer).unwrap();
- let cr = CairoContext::new(&surface);
- let root = CairoBackend::new(&cr, (width as u32, height as u32))
- .unwrap()
- .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();
-
- let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap();
- let content = String::from_utf8(buffer).unwrap();
- checked_save_file("test_text_clipping", &content);
- }
-
- #[test]
- fn test_series_labels() {
- let buffer: Vec<u8> = vec![];
- let (width, height) = (500, 500);
- let surface = cairo::PsSurface::for_stream(width.into(), height.into(), buffer).unwrap();
- let cr = CairoContext::new(&surface);
- let root = CairoBackend::new(&cr, (width, height))
- .unwrap()
- .into_drawing_area();
-
- let mut chart = ChartBuilder::on(&root)
- .caption("All series label positions", ("sans-serif", 20))
- .set_all_label_area_size(40)
- .build_ranged(0..50, 0..50)
- .unwrap();
-
- chart
- .configure_mesh()
- .disable_x_mesh()
- .disable_y_mesh()
- .draw()
- .unwrap();
-
- chart
- .draw_series(std::iter::once(Circle::new((5, 15), 5, &RED)))
- .expect("Drawing error")
- .label("Series 1")
- .legend(|(x, y)| Circle::new((x, y), 3, RED.filled()));
-
- chart
- .draw_series(std::iter::once(Circle::new((5, 15), 10, &BLUE)))
- .expect("Drawing error")
- .label("Series 2")
- .legend(|(x, y)| Circle::new((x, y), 3, 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");
- }
-
- let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap();
- let content = String::from_utf8(buffer).unwrap();
- checked_save_file("test_series_labels", &content);
- }
-
- #[test]
- fn test_draw_pixel_alphas() {
- let buffer: Vec<u8> = vec![];
- let (width, height) = (100_i32, 100_i32);
- let surface = cairo::PsSurface::for_stream(width.into(), height.into(), buffer).unwrap();
- let cr = CairoContext::new(&surface);
- let root = CairoBackend::new(&cr, (width as u32, height as u32))
- .unwrap()
- .into_drawing_area();
-
- for i in -20..20 {
- let alpha = i as f64 * 0.1;
- root.draw_pixel((50 + i, 50 + i), &BLACK.mix(alpha))
- .unwrap();
- }
-
- let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap();
- let content = String::from_utf8(buffer).unwrap();
- checked_save_file("test_draw_pixel_alphas", &content);
- }
-}
diff --git a/src/drawing/backend_impl/canvas.rs b/src/drawing/backend_impl/canvas.rs
deleted file mode 100644
index f57639f..0000000
--- a/src/drawing/backend_impl/canvas.rs
+++ /dev/null
@@ -1,530 +0,0 @@
-use js_sys::JSON;
-use wasm_bindgen::{JsCast, JsValue};
-use web_sys::{window, CanvasRenderingContext2d, HtmlCanvasElement};
-
-use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind};
-use crate::style::text_anchor::{HPos, VPos};
-use crate::style::{Color, FontTransform, RGBAColor, TextStyle};
-
-/// The backend that is drawing on the HTML canvas
-/// TODO: Support double buffering
-pub struct CanvasBackend {
- canvas: HtmlCanvasElement,
- context: CanvasRenderingContext2d,
-}
-
-pub struct CanvasError(String);
-
-impl std::fmt::Display for CanvasError {
- fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
- return write!(fmt, "Canvas Error: {}", self.0);
- }
-}
-
-impl std::fmt::Debug for CanvasError {
- fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
- return write!(fmt, "CanvasError({})", self.0);
- }
-}
-
-impl From<JsValue> for DrawingErrorKind<CanvasError> {
- fn from(e: JsValue) -> DrawingErrorKind<CanvasError> {
- DrawingErrorKind::DrawingError(CanvasError(
- JSON::stringify(&e)
- .map(|s| Into::<String>::into(&s))
- .unwrap_or_else(|_| "Unknown".to_string()),
- ))
- }
-}
-
-impl std::error::Error for CanvasError {}
-
-impl CanvasBackend {
- fn init_backend(canvas: HtmlCanvasElement) -> Option<Self> {
- let context: CanvasRenderingContext2d = canvas.get_context("2d").ok()??.dyn_into().ok()?;
- Some(CanvasBackend { canvas, context })
- }
-
- /// Create a new drawing backend backed with an HTML5 canvas object with given Id
- /// - `elem_id` The element id for the canvas
- /// - Return either some drawing backend has been created, or none in error case
- pub fn new(elem_id: &str) -> Option<Self> {
- let document = window()?.document()?;
- let canvas = document.get_element_by_id(elem_id)?;
- let canvas: HtmlCanvasElement = canvas.dyn_into().ok()?;
- Self::init_backend(canvas)
- }
-
- /// Create a new drawing backend backend with a HTML5 canvas object passed in
- /// - `canvas` The object we want to use as backend
- /// - Return either the drawing backend or None for error
- pub fn with_canvas_object(canvas: HtmlCanvasElement) -> Option<Self> {
- Self::init_backend(canvas)
- }
-}
-
-fn make_canvas_color(color: RGBAColor) -> JsValue {
- let (r, g, b) = color.rgb();
- let a = color.alpha();
- format!("rgba({},{},{},{})", r, g, b, a).into()
-}
-
-impl DrawingBackend for CanvasBackend {
- type ErrorType = CanvasError;
-
- fn get_size(&self) -> (u32, u32) {
- // Getting just canvas.width gives poor results on HighDPI screens.
- let rect = self.canvas.get_bounding_client_rect();
- (rect.width() as u32, rect.height() as u32)
- }
-
- fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<CanvasError>> {
- Ok(())
- }
-
- fn present(&mut self) -> Result<(), DrawingErrorKind<CanvasError>> {
- Ok(())
- }
-
- fn draw_pixel(
- &mut self,
- point: BackendCoord,
- style: &RGBAColor,
- ) -> Result<(), DrawingErrorKind<CanvasError>> {
- if style.alpha() == 0.0 {
- return Ok(());
- }
-
- self.context
- .set_fill_style(&make_canvas_color(style.as_color()));
- self.context
- .fill_rect(f64::from(point.0), f64::from(point.1), 1.0, 1.0);
- Ok(())
- }
-
- fn draw_line<S: BackendStyle>(
- &mut self,
- from: BackendCoord,
- to: BackendCoord,
- style: &S,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- if style.as_color().alpha() == 0.0 {
- return Ok(());
- }
-
- self.context
- .set_stroke_style(&make_canvas_color(style.as_color()));
- self.context.set_line_width(style.stroke_width() as f64);
- self.context.begin_path();
- self.context.move_to(f64::from(from.0), f64::from(from.1));
- self.context.line_to(f64::from(to.0), f64::from(to.1));
- self.context.stroke();
- Ok(())
- }
-
- fn draw_rect<S: BackendStyle>(
- &mut self,
- upper_left: BackendCoord,
- bottom_right: BackendCoord,
- style: &S,
- fill: bool,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- if style.as_color().alpha() == 0.0 {
- return Ok(());
- }
- if fill {
- self.context
- .set_fill_style(&make_canvas_color(style.as_color()));
- self.context.fill_rect(
- f64::from(upper_left.0),
- f64::from(upper_left.1),
- f64::from(bottom_right.0 - upper_left.0),
- f64::from(bottom_right.1 - upper_left.1),
- );
- } else {
- self.context
- .set_stroke_style(&make_canvas_color(style.as_color()));
- self.context.stroke_rect(
- f64::from(upper_left.0),
- f64::from(upper_left.1),
- f64::from(bottom_right.0 - upper_left.0),
- f64::from(bottom_right.1 - upper_left.1),
- );
- }
- Ok(())
- }
-
- fn draw_path<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
- &mut self,
- path: I,
- style: &S,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- if style.as_color().alpha() == 0.0 {
- return Ok(());
- }
- let mut path = path.into_iter();
- self.context.begin_path();
- if let Some(start) = path.next() {
- self.context
- .set_stroke_style(&make_canvas_color(style.as_color()));
- self.context.move_to(f64::from(start.0), f64::from(start.1));
- for next in path {
- self.context.line_to(f64::from(next.0), f64::from(next.1));
- }
- }
- self.context.stroke();
- Ok(())
- }
-
- fn fill_polygon<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
- &mut self,
- path: I,
- style: &S,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- if style.as_color().alpha() == 0.0 {
- return Ok(());
- }
- let mut path = path.into_iter();
- self.context.begin_path();
- if let Some(start) = path.next() {
- self.context
- .set_fill_style(&make_canvas_color(style.as_color()));
- self.context.move_to(f64::from(start.0), f64::from(start.1));
- for next in path {
- self.context.line_to(f64::from(next.0), f64::from(next.1));
- }
- self.context.close_path();
- }
- self.context.fill();
- Ok(())
- }
-
- fn draw_circle<S: BackendStyle>(
- &mut self,
- center: BackendCoord,
- radius: u32,
- style: &S,
- fill: bool,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- if style.as_color().alpha() == 0.0 {
- return Ok(());
- }
- if fill {
- self.context
- .set_fill_style(&make_canvas_color(style.as_color()));
- } else {
- self.context
- .set_stroke_style(&make_canvas_color(style.as_color()));
- }
- self.context.begin_path();
- self.context.arc(
- f64::from(center.0),
- f64::from(center.1),
- f64::from(radius),
- 0.0,
- std::f64::consts::PI * 2.0,
- )?;
- if fill {
- self.context.fill();
- } else {
- self.context.stroke();
- }
- Ok(())
- }
-
- fn draw_text(
- &mut self,
- text: &str,
- style: &TextStyle,
- pos: BackendCoord,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- let font = &style.font;
- let color = &style.color;
- if color.alpha() == 0.0 {
- return Ok(());
- }
-
- let (mut x, mut y) = (pos.0, pos.1);
-
- let degree = match font.get_transform() {
- FontTransform::None => 0.0,
- FontTransform::Rotate90 => 90.0,
- FontTransform::Rotate180 => 180.0,
- FontTransform::Rotate270 => 270.0,
- } / 180.0
- * std::f64::consts::PI;
-
- if degree != 0.0 {
- self.context.save();
- self.context.translate(f64::from(x), f64::from(y))?;
- self.context.rotate(degree)?;
- x = 0;
- y = 0;
- }
-
- let text_baseline = match style.pos.v_pos {
- VPos::Top => "top",
- VPos::Center => "middle",
- VPos::Bottom => "bottom",
- };
- self.context.set_text_baseline(text_baseline);
-
- let text_align = match style.pos.h_pos {
- HPos::Left => "start",
- HPos::Right => "end",
- HPos::Center => "center",
- };
- self.context.set_text_align(text_align);
-
- self.context
- .set_fill_style(&make_canvas_color(color.clone()));
- self.context.set_font(&format!(
- "{} {}px {}",
- font.get_style().as_str(),
- font.get_size(),
- font.get_name()
- ));
- self.context.fill_text(text, f64::from(x), f64::from(y))?;
-
- if degree != 0.0 {
- self.context.restore();
- }
-
- Ok(())
- }
-}
-
-#[cfg(test)]
-mod test {
- use super::*;
- use crate::element::Circle;
- use crate::prelude::*;
- use crate::style::text_anchor::Pos;
- use wasm_bindgen_test::wasm_bindgen_test_configure;
- use wasm_bindgen_test::*;
- use web_sys::Document;
-
- wasm_bindgen_test_configure!(run_in_browser);
-
- fn create_canvas(document: &Document, id: &str, width: u32, height: u32) -> HtmlCanvasElement {
- let canvas = document
- .create_element("canvas")
- .unwrap()
- .dyn_into::<HtmlCanvasElement>()
- .unwrap();
- let div = document.create_element("div").unwrap();
- div.append_child(&canvas).unwrap();
- document.body().unwrap().append_child(&div).unwrap();
- canvas.set_attribute("id", id).unwrap();
- canvas.set_width(width);
- canvas.set_height(height);
- canvas
- }
-
- fn check_content(document: &Document, id: &str) {
- let canvas = document
- .get_element_by_id(id)
- .unwrap()
- .dyn_into::<HtmlCanvasElement>()
- .unwrap();
- let data_uri = canvas.to_data_url().unwrap();
- let prefix = "data:image/png;base64,";
- assert!(&data_uri.starts_with(prefix));
- }
-
- fn draw_mesh_with_custom_ticks(tick_size: i32, test_name: &str) {
- let document = window().unwrap().document().unwrap();
- let canvas = create_canvas(&document, test_name, 500, 500);
- let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
- let root = backend.into_drawing_area();
-
- let mut chart = ChartBuilder::on(&root)
- .caption("This is a test", ("sans-serif", 20))
- .set_all_label_area_size(40)
- .build_ranged(0..10, 0..10)
- .unwrap();
-
- chart
- .configure_mesh()
- .set_all_tick_mark_size(tick_size)
- .draw()
- .unwrap();
-
- check_content(&document, test_name);
- }
-
- #[wasm_bindgen_test]
- fn test_draw_mesh_no_ticks() {
- draw_mesh_with_custom_ticks(0, "test_draw_mesh_no_ticks");
- }
-
- #[wasm_bindgen_test]
- fn test_draw_mesh_negative_ticks() {
- draw_mesh_with_custom_ticks(-10, "test_draw_mesh_negative_ticks");
- }
-
- #[wasm_bindgen_test]
- fn test_text_draw() {
- let document = window().unwrap().document().unwrap();
- let canvas = create_canvas(&document, "test_text_draw", 1500, 800);
- let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
- let root = backend.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", 20))
- .set_all_label_area_size(40)
- .build_ranged(0..100, 0..50)
- .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");
- }
- }
- }
- check_content(&document, "test_text_draw");
- }
-
- #[wasm_bindgen_test]
- fn test_text_clipping() {
- let (width, height) = (500_i32, 500_i32);
- let document = window().unwrap().document().unwrap();
- let canvas = create_canvas(&document, "test_text_clipping", width as u32, height as u32);
- let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
- let root = backend.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();
-
- check_content(&document, "test_text_clipping");
- }
-
- #[wasm_bindgen_test]
- fn test_series_labels() {
- let (width, height) = (500, 500);
- let document = window().unwrap().document().unwrap();
- let canvas = create_canvas(&document, "test_series_labels", width, height);
- let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
- let root = backend.into_drawing_area();
-
- let mut chart = ChartBuilder::on(&root)
- .caption("All series label positions", ("sans-serif", 20))
- .set_all_label_area_size(40)
- .build_ranged(0..50, 0..50)
- .unwrap();
-
- chart
- .configure_mesh()
- .disable_x_mesh()
- .disable_y_mesh()
- .draw()
- .unwrap();
-
- chart
- .draw_series(std::iter::once(Circle::new((5, 15), 5, &RED)))
- .expect("Drawing error")
- .label("Series 1")
- .legend(|(x, y)| Circle::new((x, y), 3, RED.filled()));
-
- chart
- .draw_series(std::iter::once(Circle::new((5, 15), 10, &BLUE)))
- .expect("Drawing error")
- .label("Series 2")
- .legend(|(x, y)| Circle::new((x, y), 3, 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");
- }
-
- check_content(&document, "test_series_labels");
- }
-
- #[wasm_bindgen_test]
- fn test_draw_pixel_alphas() {
- let (width, height) = (100_i32, 100_i32);
- let document = window().unwrap().document().unwrap();
- let canvas = create_canvas(
- &document,
- "test_draw_pixel_alphas",
- width as u32,
- height as u32,
- );
- let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
- let root = backend.into_drawing_area();
-
- for i in -20..20 {
- let alpha = i as f64 * 0.1;
- root.draw_pixel((50 + i, 50 + i), &BLACK.mix(alpha))
- .unwrap();
- }
-
- check_content(&document, "test_draw_pixel_alphas");
- }
-}
diff --git a/src/drawing/backend_impl/mocked.rs b/src/drawing/backend_impl/mocked.rs
index b416ba9..da6bfec 100644
--- a/src/drawing/backend_impl/mocked.rs
+++ b/src/drawing/backend_impl/mocked.rs
@@ -1,11 +1,20 @@
use crate::coord::Shift;
use crate::drawing::area::IntoDrawingArea;
-use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind};
use crate::drawing::DrawingArea;
-use crate::style::{Color, RGBAColor, TextStyle};
+use crate::style::RGBAColor;
+use plotters_backend::{
+ BackendColor, BackendCoord, BackendStyle, BackendTextStyle, DrawingBackend, DrawingErrorKind,
+};
use std::collections::VecDeque;
+pub fn check_color(left: BackendColor, right: RGBAColor) {
+ assert_eq!(
+ RGBAColor(left.rgb.0, left.rgb.1, left.rgb.2, left.alpha),
+ right
+ );
+}
+
pub struct MockedBackend {
height: u32,
width: u32,
@@ -122,11 +131,11 @@ impl DrawingBackend for MockedBackend {
fn draw_pixel(
&mut self,
point: BackendCoord,
- color: &RGBAColor,
+ color: BackendColor,
) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
self.check_before_draw();
self.num_draw_pixel_call += 1;
- let color = color.to_rgba();
+ let color = RGBAColor(color.rgb.0, color.rgb.1, color.rgb.2, color.alpha);
if let Some(mut checker) = self.check_draw_pixel.pop_front() {
checker(color, point);
@@ -145,7 +154,8 @@ impl DrawingBackend for MockedBackend {
) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
self.check_before_draw();
self.num_draw_line_call += 1;
- let color = style.as_color().to_rgba();
+ let color = style.color();
+ let color = RGBAColor(color.rgb.0, color.rgb.1, color.rgb.2, color.alpha);
if let Some(mut checker) = self.check_draw_line.pop_front() {
checker(color, style.stroke_width(), from, to);
@@ -165,7 +175,8 @@ impl DrawingBackend for MockedBackend {
) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
self.check_before_draw();
self.num_draw_rect_call += 1;
- let color = style.as_color().to_rgba();
+ let color = style.color();
+ let color = RGBAColor(color.rgb.0, color.rgb.1, color.rgb.2, color.alpha);
if let Some(mut checker) = self.check_draw_rect.pop_front() {
checker(color, style.stroke_width(), fill, upper_left, bottom_right);
@@ -183,7 +194,8 @@ impl DrawingBackend for MockedBackend {
) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
self.check_before_draw();
self.num_draw_path_call += 1;
- let color = style.as_color().to_rgba();
+ let color = style.color();
+ let color = RGBAColor(color.rgb.0, color.rgb.1, color.rgb.2, color.alpha);
if let Some(mut checker) = self.check_draw_path.pop_front() {
checker(color, style.stroke_width(), path.into_iter().collect());
@@ -203,7 +215,8 @@ impl DrawingBackend for MockedBackend {
) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
self.check_before_draw();
self.num_draw_circle_call += 1;
- let color = style.as_color().to_rgba();
+ let color = style.color();
+ let color = RGBAColor(color.rgb.0, color.rgb.1, color.rgb.2, color.alpha);
if let Some(mut checker) = self.check_draw_circle.pop_front() {
checker(color, style.stroke_width(), fill, center, radius);
@@ -221,7 +234,8 @@ impl DrawingBackend for MockedBackend {
) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
self.check_before_draw();
self.num_fill_polygon_call += 1;
- let color = style.as_color().to_rgba();
+ let color = style.color();
+ let color = RGBAColor(color.rgb.0, color.rgb.1, color.rgb.2, color.alpha);
if let Some(mut checker) = self.check_fill_polygon.pop_front() {
checker(color, path.into_iter().collect());
@@ -232,19 +246,18 @@ impl DrawingBackend for MockedBackend {
Ok(())
}
- fn draw_text(
+ fn draw_text<S: BackendTextStyle>(
&mut self,
text: &str,
- style: &TextStyle,
+ style: &S,
pos: BackendCoord,
) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- let font = &style.font;
- let color = &style.color;
+ let color = style.color();
+ let color = RGBAColor(color.rgb.0, color.rgb.1, color.rgb.2, color.alpha);
self.check_before_draw();
self.num_draw_text_call += 1;
- let color = color.to_rgba();
if let Some(mut checker) = self.check_draw_text.pop_front() {
- checker(color, font.get_name(), font.get_size(), pos, text);
+ checker(color, style.family().as_str(), style.size(), pos, text);
if self.check_draw_text.is_empty() {
self.check_draw_text.push_back(checker);
diff --git a/src/drawing/backend_impl/mod.rs b/src/drawing/backend_impl/mod.rs
index 719f375..59daa8d 100644
--- a/src/drawing/backend_impl/mod.rs
+++ b/src/drawing/backend_impl/mod.rs
@@ -1,37 +1,7 @@
-#[cfg(feature = "svg")]
-mod svg;
-#[cfg(feature = "svg")]
-pub use self::svg::SVGBackend;
-
-#[cfg(feature = "bitmap")]
-mod bitmap;
-#[cfg(feature = "bitmap")]
-pub use bitmap::BitMapBackend;
-
-#[cfg(feature = "bitmap")]
-pub mod bitmap_pixel {
- pub use super::bitmap::{BGRXPixel, PixelFormat, RGBPixel};
-}
-
-#[cfg(target_arch = "wasm32")]
-mod canvas;
-#[cfg(target_arch = "wasm32")]
-pub use canvas::CanvasBackend;
-
#[cfg(test)]
mod mocked;
#[cfg(test)]
-pub use mocked::{create_mocked_drawing_area, MockedBackend};
-
-#[cfg(all(not(target_arch = "wasm32"), feature = "piston"))]
-mod piston;
-#[cfg(all(not(target_arch = "wasm32"), feature = "piston"))]
-pub use piston::{draw_piston_window, PistonBackend};
-
-#[cfg(all(not(target_arch = "wasm32"), feature = "cairo-rs"))]
-mod cairo;
-#[cfg(all(not(target_arch = "wasm32"), feature = "cairo-rs"))]
-pub use self::cairo::CairoBackend;
+pub use mocked::{check_color, create_mocked_drawing_area, MockedBackend};
/// This is the dummy backend placeholder for the backend that never fails
#[derive(Debug)]
diff --git a/src/drawing/backend_impl/piston.rs b/src/drawing/backend_impl/piston.rs
deleted file mode 100644
index 4a57905..0000000
--- a/src/drawing/backend_impl/piston.rs
+++ /dev/null
@@ -1,206 +0,0 @@
-use piston_window::context::Context;
-use piston_window::ellipse::circle;
-use piston_window::{circle_arc, ellipse, line, rectangle, Event, Loop};
-use piston_window::{G2d, PistonWindow};
-
-use super::DummyBackendError;
-use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind};
-use crate::style::{Color, RGBAColor};
-
-pub struct PistonBackend<'a, 'b> {
- size: (u32, u32),
- scale: f64,
- context: Context,
- graphics: &'b mut G2d<'a>,
-}
-
-fn make_piston_rgba(color: &RGBAColor) -> [f32; 4] {
- let (r, g, b) = color.rgb();
- let a = color.alpha();
-
- [
- r as f32 / 255.0,
- g as f32 / 255.0,
- b as f32 / 255.0,
- a as f32,
- ]
-}
-fn make_point_pair(a: BackendCoord, b: BackendCoord, scale: f64) -> [f64; 4] {
- [
- a.0 as f64 * scale,
- a.1 as f64 * scale,
- b.0 as f64 * scale,
- b.1 as f64 * scale,
- ]
-}
-
-impl<'a, 'b> PistonBackend<'a, 'b> {
- pub fn new(size: (u32, u32), scale: f64, context: Context, graphics: &'b mut G2d<'a>) -> Self {
- Self {
- size,
- context,
- graphics,
- scale,
- }
- }
-}
-
-impl<'a, 'b> DrawingBackend for PistonBackend<'a, 'b> {
- type ErrorType = DummyBackendError;
-
- fn get_size(&self) -> (u32, u32) {
- self.size
- }
-
- fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<DummyBackendError>> {
- Ok(())
- }
-
- fn present(&mut self) -> Result<(), DrawingErrorKind<DummyBackendError>> {
- Ok(())
- }
-
- fn draw_pixel(
- &mut self,
- point: BackendCoord,
- color: &RGBAColor,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- piston_window::rectangle(
- make_piston_rgba(color),
- make_point_pair(point, (1, 1), self.scale),
- self.context.transform,
- self.graphics,
- );
- Ok(())
- }
-
- fn draw_line<S: BackendStyle>(
- &mut self,
- from: BackendCoord,
- to: BackendCoord,
- style: &S,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- line(
- make_piston_rgba(&style.as_color()),
- self.scale,
- make_point_pair(from, to, self.scale),
- self.context.transform,
- self.graphics,
- );
- Ok(())
- }
-
- fn draw_rect<S: BackendStyle>(
- &mut self,
- upper_left: BackendCoord,
- bottom_right: BackendCoord,
- style: &S,
- fill: bool,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- if fill {
- rectangle(
- make_piston_rgba(&style.as_color()),
- make_point_pair(
- upper_left,
- (bottom_right.0 - upper_left.0, bottom_right.1 - upper_left.1),
- self.scale,
- ),
- self.context.transform,
- self.graphics,
- );
- } else {
- let color = make_piston_rgba(&style.as_color());
- let [x0, y0, x1, y1] = make_point_pair(upper_left, bottom_right, self.scale);
- line(
- color,
- self.scale,
- [x0, y0, x0, y1],
- self.context.transform,
- self.graphics,
- );
- line(
- color,
- self.scale,
- [x0, y1, x1, y1],
- self.context.transform,
- self.graphics,
- );
- line(
- color,
- self.scale,
- [x1, y1, x1, y0],
- self.context.transform,
- self.graphics,
- );
- line(
- color,
- self.scale,
- [x1, y0, x0, y0],
- self.context.transform,
- self.graphics,
- );
- }
- Ok(())
- }
-
- fn draw_circle<S: BackendStyle>(
- &mut self,
- center: BackendCoord,
- radius: u32,
- style: &S,
- fill: bool,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- let rect = circle(center.0 as f64, center.1 as f64, radius as f64);
- if fill {
- ellipse(
- make_piston_rgba(&style.as_color()),
- rect,
- self.context.transform,
- self.graphics,
- );
- } else {
- circle_arc(
- make_piston_rgba(&style.as_color()),
- self.scale,
- std::f64::consts::PI,
- 0.0,
- rect,
- self.context.transform,
- self.graphics,
- );
- circle_arc(
- make_piston_rgba(&style.as_color()),
- self.scale,
- 0.0,
- std::f64::consts::PI,
- rect,
- self.context.transform,
- self.graphics,
- );
- }
- Ok(())
- }
-}
-
-#[allow(clippy::single_match)]
-pub fn draw_piston_window<F: FnOnce(PistonBackend) -> Result<(), Box<dyn std::error::Error>>>(
- window: &mut PistonWindow,
- draw: F,
-) -> Option<Event> {
- if let Some(event) = window.next() {
- window.draw_2d(&event, |c, g, _| match event {
- Event::Loop(Loop::Render(arg)) => {
- draw(PistonBackend::new(
- (arg.draw_size[0], arg.draw_size[1]),
- arg.window_size[0] / arg.draw_size[0] as f64,
- c,
- g,
- ))
- .ok();
- }
- _ => {}
- });
- return Some(event);
- }
- None
-}
diff --git a/src/drawing/backend_impl/svg.rs b/src/drawing/backend_impl/svg.rs
deleted file mode 100644
index 53e49bc..0000000
--- a/src/drawing/backend_impl/svg.rs
+++ /dev/null
@@ -1,832 +0,0 @@
-/*!
-The SVG image drawing backend
-*/
-
-use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind};
-use crate::style::text_anchor::{HPos, VPos};
-use crate::style::{Color, FontStyle, FontTransform, RGBAColor, TextStyle};
-
-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<C: Color>(color: &C) -> String {
- let (r, g, b) = color.rgb();
- return format!("#{:02X}{:02X}{:02X}", r, g, b);
-}
-
-fn make_svg_opacity<C: Color>(color: &C) -> 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("&lt;"),
- '>' => buf.push_str("&gt;"),
- '&' => buf.push_str("&amp;"),
- '"' => buf.push_str("&quot;"),
- '\'' => buf.push_str("&apos;"),
- other => buf.push(other),
- });
- }
- fn open_tag(&mut self, tag: SVGTag, attr: &[(&str, &str)], close: bool) {
- let buf = self.target.get_mut();
- buf.push_str("<");
- buf.push_str(tag.to_tag_name());
- for (key, value) in attr {
- buf.push_str(" ");
- buf.push_str(key);
- buf.push_str("=\"");
- Self::escape_and_push(buf, value);
- buf.push_str("\"");
- }
- 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: &RGBAColor,
- ) -> 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.as_color().alpha() == 0.0 {
- return Ok(());
- }
- self.open_tag(
- SVGTag::Line,
- &[
- ("opacity", &make_svg_opacity(&style.as_color())),
- ("stroke", &make_svg_color(&style.as_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.as_color().alpha() == 0.0 {
- return Ok(());
- }
-
- let (fill, stroke) = if !fill {
- ("none".to_string(), make_svg_color(&style.as_color()))
- } else {
- (make_svg_color(&style.as_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.as_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.as_color().alpha() == 0.0 {
- return Ok(());
- }
- self.open_tag(
- SVGTag::Polyline,
- &[
- ("fill", "none"),
- ("opacity", &make_svg_opacity(&style.as_color())),
- ("stroke", &make_svg_color(&style.as_color())),
- ("stroke-width", &format!("{}", style.stroke_width())),
- (
- "points",
- &path.into_iter().fold(String::new(), |mut s, (x, y)| {
- s.push_str(&format!("{},{} ", x, y));
- s
- }),
- ),
- ],
- true,
- );
- Ok(())
- }
-
- fn fill_polygon<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
- &mut self,
- path: I,
- style: &S,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- if style.as_color().alpha() == 0.0 {
- return Ok(());
- }
- self.open_tag(
- SVGTag::Polygon,
- &[
- ("opacity", &make_svg_opacity(&style.as_color())),
- ("fill", &make_svg_color(&style.as_color())),
- (
- "points",
- &path.into_iter().fold(String::new(), |mut s, (x, y)| {
- s.push_str(&format!("{},{} ", x, y));
- s
- }),
- ),
- ],
- true,
- );
- Ok(())
- }
-
- fn draw_circle<S: BackendStyle>(
- &mut self,
- center: BackendCoord,
- radius: u32,
- style: &S,
- fill: bool,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- if style.as_color().alpha() == 0.0 {
- return Ok(());
- }
- let (stroke, fill) = if !fill {
- (make_svg_color(&style.as_color()), "none".to_string())
- } else {
- ("none".to_string(), make_svg_color(&style.as_color()))
- };
- self.open_tag(
- SVGTag::Circle,
- &[
- ("cx", &format!("{}", center.0)),
- ("cy", &format!("{}", center.1)),
- ("r", &format!("{}", radius)),
- ("opacity", &make_svg_opacity(&style.as_color())),
- ("fill", &fill),
- ("stroke", &stroke),
- ],
- true,
- );
- Ok(())
- }
-
- fn draw_text(
- &mut self,
- text: &str,
- style: &TextStyle,
- pos: BackendCoord,
- ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
- let font = &style.font;
- let color = &style.color;
- if color.alpha() == 0.0 {
- return Ok(());
- }
-
- let (x0, y0) = pos;
- let text_anchor = match style.pos.h_pos {
- HPos::Left => "start",
- HPos::Right => "end",
- HPos::Center => "middle",
- };
-
- let dy = match style.pos.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.pos.h_pos {
- HPos::Left => x0,
- HPos::Center => x0 - fx1 / 2 + fx0 / 2,
- HPos::Right => x0 - fx1 + fx0,
- };
- let y0 = match style.pos.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", font.get_name().to_string()),
- ("font-size", format!("{}", font.get_size() / 1.24)),
- ("opacity", make_svg_opacity(color)),
- ("fill", make_svg_color(color)),
- ];
-
- match font.get_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 = font.get_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_str("\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::png::PNGEncoder;
-
- let mut data = vec![0; 0];
-
- {
- let cursor = Cursor::new(&mut data);
-
- let encoder = PNGEncoder::new(cursor);
-
- let color = image::ColorType::Rgb8;
-
- encoder.encode(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 crate::element::Circle;
- use crate::prelude::*;
- use crate::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", 20))
- .set_all_label_area_size(40)
- .build_ranged(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", 20))
- .set_all_label_area_size(40)
- .build_ranged(0..100, 0..50)
- .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", 20))
- .set_all_label_area_size(40)
- .build_ranged(0..50, 0..50)
- .unwrap();
-
- chart
- .configure_mesh()
- .disable_x_mesh()
- .disable_y_mesh()
- .draw()
- .unwrap();
-
- chart
- .draw_series(std::iter::once(Circle::new((5, 15), 5, &RED)))
- .expect("Drawing error")
- .label("Series 1")
- .legend(|(x, y)| Circle::new((x, y), 3, RED.filled()));
-
- chart
- .draw_series(std::iter::once(Circle::new((5, 15), 10, &BLUE)))
- .expect("Drawing error")
- .label("Series 2")
- .legend(|(x, y)| Circle::new((x, y), 3, 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);
- }
-}
diff --git a/src/drawing/mod.rs b/src/drawing/mod.rs
index e2c59bd..af845ea 100644
--- a/src/drawing/mod.rs
+++ b/src/drawing/mod.rs
@@ -1,31 +1,18 @@
/*!
-The drawing utils for Plotter. Which handles the both low-level and high-level
-drawing.
+The drawing utils for Plotters. In Plotters, we have two set of drawing APIs: low-level API and
+high-level API.
-For the low-level drawing abstraction, the module defines the `DrawingBackend` trait,
-which handles low-level drawing of different shapes, such as, pixels, lines, rectangles, etc.
-
-On the top of drawing backend, one or more drawing area can be defined and different coordinate
-system can be applied to the drawing areas. And the drawing area implement the high-level drawing
-interface, which draws an element.
-
-Currently we have following backend implemented:
-
-- `BitMapBackend`: The backend that creates bitmap, this is based on `image` crate
-- `SVGBackend`: The backend that creates SVG image, based on `svg` crate.
-- `PistonBackend`: The backend that uses Piston Window for real time rendering. Disabled by default, use feature `piston` to turn on.
-- `CanvasBackend`: The backend that operates HTML5 Canvas, this is available when `Plotters` is targeting WASM.
+The low-level drawing abstraction, the module defines the `DrawingBackend` trait from the `plotters-backend` create.
+It exposes a set of functions which allows basic shape, such as pixels, lines, rectangles, circles, to be drawn on the screen.
+The low-level API uses the pixel based coordinate.
+The high-level API is built on the top of high-level API. The `DrawingArea` type exposes the high-level drawing API to the remianing part
+of Plotters. The basic drawing blocks are composable elements, which can be defined in logic coordinate. To learn more details
+about the [coordinate abstraction](../coord/index.html) and [element system](../element/index.html).
*/
mod area;
mod backend_impl;
-pub mod rasterizer;
-
-pub mod backend;
-
pub use area::{DrawingArea, DrawingAreaErrorKind, IntoDrawingArea};
pub use backend_impl::*;
-
-pub use backend::DrawingBackend;
diff --git a/src/drawing/rasterizer/circle.rs b/src/drawing/rasterizer/circle.rs
deleted file mode 100644
index d38e00a..0000000
--- a/src/drawing/rasterizer/circle.rs
+++ /dev/null
@@ -1,67 +0,0 @@
-use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingErrorKind};
-use crate::drawing::DrawingBackend;
-
-use crate::style::Color;
-
-pub fn draw_circle<B: DrawingBackend, S: BackendStyle>(
- b: &mut B,
- center: BackendCoord,
- radius: u32,
- style: &S,
- fill: bool,
-) -> Result<(), DrawingErrorKind<B::ErrorType>> {
- if style.as_color().alpha() == 0.0 {
- return Ok(());
- }
-
- if !fill && style.stroke_width() != 1 {
- // FIXME: We are currently ignore the stroke width for circles
- }
-
- let min = (f64::from(radius) * (1.0 - (2f64).sqrt() / 2.0)).ceil() as i32;
- let max = (f64::from(radius) * (1.0 + (2f64).sqrt() / 2.0)).floor() as i32;
-
- let range = min..=max;
-
- let (up, down) = (
- range.start() + center.1 - radius as i32,
- range.end() + center.1 - radius as i32,
- );
-
- for dy in range {
- let dy = dy - radius as i32;
- let y = center.1 + dy;
-
- let lx = (f64::from(radius) * f64::from(radius)
- - (f64::from(dy) * f64::from(dy)).max(1e-5))
- .sqrt();
-
- let left = center.0 - lx.floor() as i32;
- let right = center.0 + lx.floor() as i32;
-
- let v = lx - lx.floor();
-
- let x = center.0 + dy;
- let top = center.1 - lx.floor() as i32;
- let bottom = center.1 + lx.floor() as i32;
-
- if fill {
- check_result!(b.draw_line((left, y), (right, y), &style.as_color()));
- check_result!(b.draw_line((x, top), (x, up), &style.as_color()));
- check_result!(b.draw_line((x, down), (x, bottom), &style.as_color()));
- } else {
- check_result!(b.draw_pixel((left, y), &style.as_color().mix(1.0 - v)));
- check_result!(b.draw_pixel((right, y), &style.as_color().mix(1.0 - v)));
-
- check_result!(b.draw_pixel((x, top), &style.as_color().mix(1.0 - v)));
- check_result!(b.draw_pixel((x, bottom), &style.as_color().mix(1.0 - v)));
- }
-
- check_result!(b.draw_pixel((left - 1, y), &style.as_color().mix(v)));
- check_result!(b.draw_pixel((right + 1, y), &style.as_color().mix(v)));
- check_result!(b.draw_pixel((x, top - 1), &style.as_color().mix(v)));
- check_result!(b.draw_pixel((x, bottom + 1), &style.as_color().mix(v)));
- }
-
- Ok(())
-}
diff --git a/src/drawing/rasterizer/line.rs b/src/drawing/rasterizer/line.rs
deleted file mode 100644
index e1f9e5f..0000000
--- a/src/drawing/rasterizer/line.rs
+++ /dev/null
@@ -1,126 +0,0 @@
-use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingErrorKind};
-use crate::drawing::DrawingBackend;
-
-use crate::style::Color;
-
-pub fn draw_line<DB: DrawingBackend, S: BackendStyle>(
- back: &mut DB,
- mut from: BackendCoord,
- mut to: BackendCoord,
- style: &S,
-) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
- if style.as_color().alpha() == 0.0 {
- return Ok(());
- }
-
- if style.stroke_width() != 1 {
- // If the line is wider than 1px, then we need to make it a polygon
- let v = (i64::from(to.0 - from.0), i64::from(to.1 - from.1));
- let l = ((v.0 * v.0 + v.1 * v.1) as f64).sqrt();
-
- if l < 1e-5 {
- return Ok(());
- }
-
- let v = (v.0 as f64 / l, v.1 as f64 / l);
-
- let r = f64::from(style.stroke_width()) / 2.0;
- let mut trans = [(v.1 * r, -v.0 * r), (-v.1 * r, v.0 * r)];
- let mut vertices = vec![];
-
- for point in [from, to].iter() {
- for t in trans.iter() {
- vertices.push((
- (f64::from(point.0) + t.0) as i32,
- (f64::from(point.1) + t.1) as i32,
- ))
- }
-
- trans.swap(0, 1);
- }
-
- return back.fill_polygon(vertices, &style.as_color());
- }
-
- if from.0 == to.0 {
- if from.1 > to.1 {
- std::mem::swap(&mut from, &mut to);
- }
- for y in from.1..=to.1 {
- check_result!(back.draw_pixel((from.0, y), &style.as_color()));
- }
- return Ok(());
- }
-
- if from.1 == to.1 {
- if from.0 > to.0 {
- std::mem::swap(&mut from, &mut to);
- }
- for x in from.0..=to.0 {
- check_result!(back.draw_pixel((x, from.1), &style.as_color()));
- }
- return Ok(());
- }
-
- let steep = (from.0 - to.0).abs() < (from.1 - to.1).abs();
-
- if steep {
- from = (from.1, from.0);
- to = (to.1, to.0);
- }
-
- let (from, to) = if from.0 > to.0 {
- (to, from)
- } else {
- (from, to)
- };
-
- let mut size_limit = back.get_size();
-
- if steep {
- size_limit = (size_limit.1, size_limit.0);
- }
-
- let grad = f64::from(to.1 - from.1) / f64::from(to.0 - from.0);
-
- let mut put_pixel = |(x, y): BackendCoord, b: f64| {
- if steep {
- back.draw_pixel((y, x), &style.as_color().mix(b))
- } else {
- back.draw_pixel((x, y), &style.as_color().mix(b))
- }
- };
-
- let y_step_limit =
- (f64::from(to.1.min(size_limit.1 as i32 - 1).max(0) - from.1) / grad).floor() as i32;
-
- let batch_start = (f64::from(from.1.min(size_limit.1 as i32 - 2).max(0) - from.1) / grad)
- .abs()
- .ceil() as i32
- + from.0;
-
- let batch_limit =
- to.0.min(size_limit.0 as i32 - 2)
- .min(from.0 + y_step_limit - 1);
-
- let mut y = f64::from(from.1) + f64::from(batch_start - from.0) * grad;
-
- for x in batch_start..=batch_limit {
- check_result!(put_pixel((x, y as i32), 1.0 + y.floor() - y));
- check_result!(put_pixel((x, y as i32 + 1), y - y.floor()));
-
- y += grad;
- }
-
- if to.0 > batch_limit && y < f64::from(to.1) {
- let x = batch_limit as i32 + 1;
- if 1.0 + y.floor() - y > 1e-5 {
- check_result!(put_pixel((x, y as i32), 1.0 + y.floor() - y));
- }
- if y - y.floor() > 1e-5 && y + 1.0 < f64::from(to.1) {
- check_result!(put_pixel((x, y as i32 + 1), y - y.floor()));
- }
- }
-
- Ok(())
-}
diff --git a/src/drawing/rasterizer/mod.rs b/src/drawing/rasterizer/mod.rs
deleted file mode 100644
index 1fba804..0000000
--- a/src/drawing/rasterizer/mod.rs
+++ /dev/null
@@ -1,24 +0,0 @@
-// TODO: ? operator is very slow. See issue #58 for details
-macro_rules! check_result {
- ($e:expr) => {
- let result = $e;
- if result.is_err() {
- return result;
- }
- };
-}
-
-mod line;
-pub use line::draw_line;
-
-mod rect;
-pub use rect::draw_rect;
-
-mod circle;
-pub use circle::draw_circle;
-
-mod polygon;
-pub use polygon::fill_polygon;
-
-mod path;
-pub use path::polygonize;
diff --git a/src/drawing/rasterizer/path.rs b/src/drawing/rasterizer/path.rs
deleted file mode 100644
index ff0be10..0000000
--- a/src/drawing/rasterizer/path.rs
+++ /dev/null
@@ -1,115 +0,0 @@
-use crate::drawing::backend::BackendCoord;
-
-fn get_dir_vector(from: BackendCoord, to: BackendCoord, flag: bool) -> ((f64, f64), (f64, f64)) {
- let v = (i64::from(to.0 - from.0), i64::from(to.1 - from.1));
- let l = ((v.0 * v.0 + v.1 * v.1) as f64).sqrt();
-
- let v = (v.0 as f64 / l, v.1 as f64 / l);
-
- if flag {
- (v, (v.1, -v.0))
- } else {
- (v, (-v.1, v.0))
- }
-}
-
-fn compute_polygon_vertex(triple: &[BackendCoord; 3], d: f64) -> BackendCoord {
- let (a_t, a_n) = get_dir_vector(triple[0], triple[1], false);
- let (b_t, b_n) = get_dir_vector(triple[2], triple[1], true);
-
- let a_p = (
- f64::from(triple[1].0) + d * a_n.0,
- f64::from(triple[1].1) + d * a_n.1,
- );
- let b_p = (
- f64::from(triple[1].0) + d * b_n.0,
- f64::from(triple[1].1) + d * b_n.1,
- );
-
- // u * a_t + a_p = v * b_t + b_p
- // u * a_t.0 - v * b_t.0 = b_p.0 - a_p.0
- // u * a_t.1 - v * b_t.1 = b_p.1 - a_p.1
- if a_p.0 as i32 == b_p.0 as i32 && a_p.1 as i32 == b_p.1 as i32 {
- return (a_p.0 as i32, a_p.1 as i32);
- }
-
- let a0 = a_t.0;
- let b0 = -b_t.0;
- let c0 = b_p.0 - a_p.0;
- let a1 = a_t.1;
- let b1 = -b_t.1;
- let c1 = b_p.1 - a_p.1;
-
- // This is the coner case that
- if (a0 * b1 - a1 * b0).abs() < 1e-10 {
- return (a_p.0 as i32, a_p.1 as i32);
- }
-
- let u = (c0 * b1 - c1 * b0) / (a0 * b1 - a1 * b0);
-
- let x = a_p.0 + u * a_t.0;
- let y = a_p.1 + u * a_t.1;
-
- (x.round() as i32, y.round() as i32)
-}
-
-fn traverse_vertices<'a>(
- mut vertices: impl Iterator<Item = &'a BackendCoord>,
- width: u32,
- mut op: impl FnMut(BackendCoord),
-) {
- let mut a = vertices.next().unwrap();
- let mut b = vertices.next().unwrap();
-
- while a == b {
- a = b;
- if let Some(new_b) = vertices.next() {
- b = new_b;
- } else {
- return;
- }
- }
-
- let (_, n) = get_dir_vector(*a, *b, false);
-
- op((
- (f64::from(a.0) + n.0 * f64::from(width) / 2.0).round() as i32,
- (f64::from(a.1) + n.1 * f64::from(width) / 2.0).round() as i32,
- ));
-
- let mut recent = [(0, 0), *a, *b];
-
- for p in vertices {
- if *p == recent[2] {
- continue;
- }
- recent.swap(0, 1);
- recent.swap(1, 2);
- recent[2] = *p;
-
- op(compute_polygon_vertex(&recent, f64::from(width) / 2.0));
- }
-
- let b = recent[1];
- let a = recent[2];
-
- let (_, n) = get_dir_vector(a, b, true);
-
- op((
- (f64::from(a.0) + n.0 * f64::from(width) / 2.0).round() as i32,
- (f64::from(a.1) + n.1 * f64::from(width) / 2.0).round() as i32,
- ));
-}
-
-pub fn polygonize(vertices: &[BackendCoord], stroke_width: u32) -> Vec<BackendCoord> {
- if vertices.len() < 2 {
- return vec![];
- }
-
- let mut ret = vec![];
-
- traverse_vertices(vertices.iter(), stroke_width, |v| ret.push(v));
- traverse_vertices(vertices.iter().rev(), stroke_width, |v| ret.push(v));
-
- ret
-}
diff --git a/src/drawing/rasterizer/polygon.rs b/src/drawing/rasterizer/polygon.rs
deleted file mode 100644
index 169b83a..0000000
--- a/src/drawing/rasterizer/polygon.rs
+++ /dev/null
@@ -1,245 +0,0 @@
-use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingErrorKind};
-use crate::drawing::DrawingBackend;
-
-use crate::style::Color;
-
-use std::cmp::{Ord, Ordering, PartialOrd};
-
-#[derive(Clone, Debug)]
-struct Edge {
- epoch: u32,
- total_epoch: u32,
- slave_begin: i32,
- slave_end: i32,
-}
-
-impl Edge {
- fn horizontal_sweep(mut from: BackendCoord, mut to: BackendCoord) -> Option<Edge> {
- if from.0 == to.0 {
- return None;
- }
-
- if from.0 > to.0 {
- std::mem::swap(&mut from, &mut to);
- }
-
- Some(Edge {
- epoch: 0,
- total_epoch: (to.0 - from.0) as u32,
- slave_begin: from.1,
- slave_end: to.1,
- })
- }
-
- fn vertical_sweep(from: BackendCoord, to: BackendCoord) -> Option<Edge> {
- Edge::horizontal_sweep((from.1, from.0), (to.1, to.0))
- }
-
- fn get_master_pos(&self) -> i32 {
- (self.total_epoch - self.epoch) as i32
- }
-
- fn inc_epoch(&mut self) {
- self.epoch += 1;
- }
-
- fn get_slave_pos(&self) -> f64 {
- f64::from(self.slave_begin)
- + (i64::from(self.slave_end - self.slave_begin) * i64::from(self.epoch)) as f64
- / f64::from(self.total_epoch)
- }
-}
-
-impl PartialOrd for Edge {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- self.get_slave_pos().partial_cmp(&other.get_slave_pos())
- }
-}
-
-impl PartialEq for Edge {
- fn eq(&self, other: &Self) -> bool {
- self.get_slave_pos() == other.get_slave_pos()
- }
-}
-
-impl Eq for Edge {}
-
-impl Ord for Edge {
- fn cmp(&self, other: &Self) -> Ordering {
- self.get_slave_pos()
- .partial_cmp(&other.get_slave_pos())
- .unwrap()
- }
-}
-
-pub fn fill_polygon<DB: DrawingBackend, S: BackendStyle>(
- back: &mut DB,
- vertices: &[BackendCoord],
- style: &S,
-) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
- if let Some((x_span, y_span)) =
- vertices
- .iter()
- .fold(None, |res: Option<((i32, i32), (i32, i32))>, (x, y)| {
- Some(
- res.map(|((min_x, max_x), (min_y, max_y))| {
- (
- (min_x.min(*x), max_x.max(*x)),
- (min_y.min(*y), max_y.max(*y)),
- )
- })
- .unwrap_or(((*x, *x), (*y, *y))),
- )
- })
- {
- // First of all, let's handle the case that all the points is in a same vertical or
- // horizontal line
- if x_span.0 == x_span.1 || y_span.0 == y_span.1 {
- return back.draw_line((x_span.0, y_span.0), (x_span.1, y_span.1), style);
- }
-
- let horizontal_sweep = x_span.1 - x_span.0 > y_span.1 - y_span.0;
-
- let mut edges: Vec<_> = vertices
- .iter()
- .zip(vertices.iter().skip(1))
- .map(|(a, b)| (*a, *b))
- .collect();
- edges.push((vertices[vertices.len() - 1], vertices[0]));
- edges.sort_by_key(|((x1, y1), (x2, y2))| {
- if horizontal_sweep {
- *x1.min(x2)
- } else {
- *y1.min(y2)
- }
- });
-
- for edge in &mut edges.iter_mut() {
- if horizontal_sweep {
- if (edge.0).0 > (edge.1).0 {
- std::mem::swap(&mut edge.0, &mut edge.1);
- }
- } else if (edge.0).1 > (edge.1).1 {
- std::mem::swap(&mut edge.0, &mut edge.1);
- }
- }
-
- let (low, high) = if horizontal_sweep { x_span } else { y_span };
-
- let mut idx = 0;
-
- let mut active_edge: Vec<Edge> = vec![];
-
- for sweep_line in low..=high {
- let mut new_vec = vec![];
-
- for mut e in active_edge {
- if e.get_master_pos() > 0 {
- e.inc_epoch();
- new_vec.push(e);
- }
- }
-
- active_edge = new_vec;
-
- loop {
- if idx >= edges.len() {
- break;
- }
- let line = if horizontal_sweep {
- (edges[idx].0).0
- } else {
- (edges[idx].0).1
- };
- if line > sweep_line {
- break;
- }
-
- let edge_obj = if horizontal_sweep {
- Edge::horizontal_sweep(edges[idx].0, edges[idx].1)
- } else {
- Edge::vertical_sweep(edges[idx].0, edges[idx].1)
- };
-
- if let Some(edge_obj) = edge_obj {
- active_edge.push(edge_obj);
- }
-
- idx += 1;
- }
-
- active_edge.sort();
-
- let mut first = None;
- let mut second = None;
-
- for edge in active_edge.iter() {
- if first.is_none() {
- first = Some(edge.clone())
- } else if second.is_none() {
- second = Some(edge.clone())
- }
-
- if let Some(a) = first.clone() {
- if let Some(b) = second.clone() {
- if a.get_master_pos() == 0 && b.get_master_pos() != 0 {
- first = Some(b);
- second = None;
- continue;
- }
-
- if a.get_master_pos() != 0 && b.get_master_pos() == 0 {
- first = Some(a);
- second = None;
- continue;
- }
-
- let from = a.get_slave_pos();
- let to = b.get_slave_pos();
-
- if a.get_master_pos() == 0 && b.get_master_pos() == 0 && to - from > 1.0 {
- first = None;
- second = None;
- continue;
- }
-
- if horizontal_sweep {
- check_result!(back.draw_line(
- (sweep_line, from.ceil() as i32),
- (sweep_line, to.floor() as i32),
- &style.as_color(),
- ));
- check_result!(back.draw_pixel(
- (sweep_line, from.floor() as i32),
- &style.as_color().mix(from.ceil() - from),
- ));
- check_result!(back.draw_pixel(
- (sweep_line, to.ceil() as i32),
- &style.as_color().mix(to - to.floor()),
- ));
- } else {
- check_result!(back.draw_line(
- (from.ceil() as i32, sweep_line),
- (to.floor() as i32, sweep_line),
- &style.as_color(),
- ));
- check_result!(back.draw_pixel(
- (from.floor() as i32, sweep_line),
- &style.as_color().mix(from.ceil() - from),
- ));
- check_result!(back.draw_pixel(
- (to.ceil() as i32, sweep_line),
- &style.as_color().mix(to.floor() - to),
- ));
- }
-
- first = None;
- second = None;
- }
- }
- }
- }
- }
-
- Ok(())
-}
diff --git a/src/drawing/rasterizer/rect.rs b/src/drawing/rasterizer/rect.rs
deleted file mode 100644
index 659fbba..0000000
--- a/src/drawing/rasterizer/rect.rs
+++ /dev/null
@@ -1,60 +0,0 @@
-use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingErrorKind};
-use crate::drawing::DrawingBackend;
-
-use crate::style::Color;
-
-pub fn draw_rect<B: DrawingBackend, S: BackendStyle>(
- b: &mut B,
- upper_left: BackendCoord,
- bottom_right: BackendCoord,
- style: &S,
- fill: bool,
-) -> Result<(), DrawingErrorKind<B::ErrorType>> {
- if style.as_color().alpha() == 0.0 {
- return Ok(());
- }
- let (upper_left, bottom_right) = (
- (
- upper_left.0.min(bottom_right.0),
- upper_left.1.min(bottom_right.1),
- ),
- (
- upper_left.0.max(bottom_right.0),
- upper_left.1.max(bottom_right.1),
- ),
- );
-
- if fill {
- if bottom_right.0 - upper_left.0 < bottom_right.1 - upper_left.1 {
- for x in upper_left.0..=bottom_right.0 {
- check_result!(b.draw_line((x, upper_left.1), (x, bottom_right.1), style));
- }
- } else {
- for y in upper_left.1..=bottom_right.1 {
- check_result!(b.draw_line((upper_left.0, y), (bottom_right.0, y), style));
- }
- }
- } else {
- b.draw_line(
- (upper_left.0, upper_left.1),
- (upper_left.0, bottom_right.1),
- style,
- )?;
- b.draw_line(
- (upper_left.0, upper_left.1),
- (bottom_right.0, upper_left.1),
- style,
- )?;
- b.draw_line(
- (bottom_right.0, bottom_right.1),
- (upper_left.0, bottom_right.1),
- style,
- )?;
- b.draw_line(
- (bottom_right.0, bottom_right.1),
- (bottom_right.0, upper_left.1),
- style,
- )?;
- }
- Ok(())
-}
diff --git a/src/element/basic_shapes.rs b/src/element/basic_shapes.rs
index c0a453f..e0a3548 100644
--- a/src/element/basic_shapes.rs
+++ b/src/element/basic_shapes.rs
@@ -1,6 +1,6 @@
use super::{Drawable, PointCollection};
-use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
use crate::style::{ShapeStyle, SizeDesc};
+use plotters_backend::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind};
/// An element of a single pixel
pub struct Pixel<Coord> {
@@ -18,7 +18,7 @@ impl<Coord> Pixel<Coord> {
}
impl<'a, Coord> PointCollection<'a, Coord> for &'a Pixel<Coord> {
- type Borrow = &'a Coord;
+ type Point = &'a Coord;
type IntoIter = std::iter::Once<&'a Coord>;
fn point_iter(self) -> Self::IntoIter {
std::iter::once(&self.pos)
@@ -33,7 +33,7 @@ impl<Coord, DB: DrawingBackend> Drawable<DB> for Pixel<Coord> {
_: (u32, u32),
) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
if let Some((x, y)) = points.next() {
- return backend.draw_pixel((x, y), &self.style.color);
+ return backend.draw_pixel((x, y), self.style.color());
}
Ok(())
}
@@ -81,7 +81,7 @@ impl<Coord> PathElement<Coord> {
}
impl<'a, Coord> PointCollection<'a, Coord> for &'a PathElement<Coord> {
- type Borrow = &'a Coord;
+ type Point = &'a Coord;
type IntoIter = &'a [Coord];
fn point_iter(self) -> &'a [Coord] {
&self.points
@@ -153,7 +153,7 @@ impl<Coord> Rectangle<Coord> {
}
impl<'a, Coord> PointCollection<'a, Coord> for &'a Rectangle<Coord> {
- type Borrow = &'a Coord;
+ type Point = &'a Coord;
type IntoIter = &'a [Coord];
fn point_iter(self) -> &'a [Coord] {
&self.points
@@ -200,7 +200,7 @@ fn test_rect_element() {
});
da.draw(&Rectangle::new(
[(100, 101), (105, 107)],
- BLUE.stroke_width(5),
+ Color::stroke_width(&BLUE, 5),
))
.expect("Drawing Failure");
}
@@ -245,7 +245,7 @@ impl<Coord, Size: SizeDesc> Circle<Coord, Size> {
}
impl<'a, Coord, Size: SizeDesc> PointCollection<'a, Coord> for &'a Circle<Coord, Size> {
- type Borrow = &'a Coord;
+ type Point = &'a Coord;
type IntoIter = std::iter::Once<&'a Coord>;
fn point_iter(self) -> std::iter::Once<&'a Coord> {
std::iter::once(&self.center)
@@ -306,7 +306,7 @@ impl<Coord> Polygon<Coord> {
}
impl<'a, Coord> PointCollection<'a, Coord> for &'a Polygon<Coord> {
- type Borrow = &'a Coord;
+ type Point = &'a Coord;
type IntoIter = &'a [Coord];
fn point_iter(self) -> &'a [Coord] {
&self.points
diff --git a/src/element/boxplot.rs b/src/element/boxplot.rs
index c8040bc..76679b3 100644
--- a/src/element/boxplot.rs
+++ b/src/element/boxplot.rs
@@ -1,9 +1,9 @@
use std::marker::PhantomData;
use crate::data::Quartiles;
-use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
use crate::element::{Drawable, PointCollection};
use crate::style::{ShapeStyle, BLACK};
+use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
/// The boxplot orientation trait
pub trait BoxplotOrient<K, V> {
@@ -177,11 +177,11 @@ impl<K, O: BoxplotOrient<K, f32>> Boxplot<K, O> {
}
}
-impl<'a, K: 'a + Clone, O: BoxplotOrient<K, f32>> PointCollection<'a, (O::XType, O::YType)>
+impl<'a, K: Clone, O: BoxplotOrient<K, f32>> PointCollection<'a, (O::XType, O::YType)>
for &'a Boxplot<K, O>
{
- type Borrow = (O::XType, O::YType);
- type IntoIter = Vec<Self::Borrow>;
+ type Point = (O::XType, O::YType);
+ type IntoIter = Vec<Self::Point>;
fn point_iter(self) -> Self::IntoIter {
self.values
.iter()
@@ -257,7 +257,7 @@ mod test {
fn test_draw_v() {
let root = MockedBackend::new(1024, 768).into_drawing_area();
let chart = ChartBuilder::on(&root)
- .build_ranged(0..2, 0f32..100f32)
+ .build_cartesian_2d(0..2, 0f32..100f32)
.unwrap();
let values = Quartiles::new(&[6]);
@@ -271,7 +271,7 @@ mod test {
fn test_draw_h() {
let root = MockedBackend::new(1024, 768).into_drawing_area();
let chart = ChartBuilder::on(&root)
- .build_ranged(0f32..100f32, 0..2)
+ .build_cartesian_2d(0f32..100f32, 0..2)
.unwrap();
let values = Quartiles::new(&[6]);
diff --git a/src/element/candlestick.rs b/src/element/candlestick.rs
index b026425..6157cb6 100644
--- a/src/element/candlestick.rs
+++ b/src/element/candlestick.rs
@@ -4,9 +4,9 @@
use std::cmp::Ordering;
-use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
use crate::element::{Drawable, PointCollection};
use crate::style::ShapeStyle;
+use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
/// The candlestick data point element
pub struct CandleStick<X, Y: PartialOrd> {
@@ -62,7 +62,7 @@ impl<X: Clone, Y: PartialOrd> CandleStick<X, Y> {
}
impl<'a, X: 'a, Y: PartialOrd + 'a> PointCollection<'a, (X, Y)> for &'a CandleStick<X, Y> {
- type Borrow = &'a (X, Y);
+ type Point = &'a (X, Y);
type IntoIter = &'a [(X, Y)];
fn point_iter(self) -> &'a [(X, Y)] {
&self.points
diff --git a/src/element/composable.rs b/src/element/composable.rs
index 95ff380..33b08c9 100644
--- a/src/element/composable.rs
+++ b/src/element/composable.rs
@@ -1,5 +1,5 @@
use super::*;
-use crate::drawing::backend::DrawingBackend;
+use plotters_backend::DrawingBackend;
use std::borrow::Borrow;
use std::iter::{once, Once};
use std::marker::PhantomData;
@@ -36,7 +36,7 @@ where
}
impl<'a, Coord, DB: DrawingBackend> PointCollection<'a, Coord> for &'a EmptyElement<Coord, DB> {
- type Borrow = &'a Coord;
+ type Point = &'a Coord;
type IntoIter = Once<&'a Coord>;
fn point_iter(self) -> Self::IntoIter {
once(&self.coord)
@@ -64,7 +64,7 @@ pub struct BoxedElement<Coord, DB: DrawingBackend, A: Drawable<DB>> {
impl<'b, Coord, DB: DrawingBackend, A: Drawable<DB>> PointCollection<'b, Coord>
for &'b BoxedElement<Coord, DB, A>
{
- type Borrow = &'b Coord;
+ type Point = &'b Coord;
type IntoIter = Once<&'b Coord>;
fn point_iter(self) -> Self::IntoIter {
once(&self.offset)
@@ -132,7 +132,7 @@ where
A: Drawable<DB>,
B: Drawable<DB>,
{
- type Borrow = &'b Coord;
+ type Point = &'b Coord;
type IntoIter = Once<&'b Coord>;
fn point_iter(self) -> Self::IntoIter {
once(&self.offset)
diff --git a/src/element/dynelem.rs b/src/element/dynelem.rs
index d32c06d..b2bd178 100644
--- a/src/element/dynelem.rs
+++ b/src/element/dynelem.rs
@@ -1,5 +1,5 @@
use super::{Drawable, PointCollection};
-use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
+use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
use std::borrow::Borrow;
@@ -36,7 +36,7 @@ where
impl<'a, 'b: 'a, DB: DrawingBackend, Coord: Clone> PointCollection<'a, Coord>
for &'a DynElement<'b, DB, Coord>
{
- type Borrow = &'a Coord;
+ type Point = &'a Coord;
type IntoIter = &'a Vec<Coord>;
fn point_iter(self) -> Self::IntoIter {
&self.points
diff --git a/src/element/errorbar.rs b/src/element/errorbar.rs
index 855cd72..846474e 100644
--- a/src/element/errorbar.rs
+++ b/src/element/errorbar.rs
@@ -1,8 +1,8 @@
use std::marker::PhantomData;
-use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
use crate::element::{Drawable, PointCollection};
use crate::style::ShapeStyle;
+use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
pub trait ErrorBarOrient<K, V> {
type XType;
@@ -94,11 +94,11 @@ impl<K, V> ErrorBar<K, V, ErrorBarOrientH<K, V>> {
}
}
-impl<'a, K: 'a + Clone, V: 'a + Clone, O: ErrorBarOrient<K, V>>
- PointCollection<'a, (O::XType, O::YType)> for &'a ErrorBar<K, V, O>
+impl<'a, K: Clone, V: Clone, O: ErrorBarOrient<K, V>> PointCollection<'a, (O::XType, O::YType)>
+ for &'a ErrorBar<K, V, O>
{
- type Borrow = (O::XType, O::YType);
- type IntoIter = Vec<Self::Borrow>;
+ type Point = (O::XType, O::YType);
+ type IntoIter = Vec<Self::Point>;
fn point_iter(self) -> Self::IntoIter {
self.values
.iter()
diff --git a/src/element/image.rs b/src/element/image.rs
index 12f3f30..dbddb19 100644
--- a/src/element/image.rs
+++ b/src/element/image.rs
@@ -2,10 +2,15 @@
use image::{DynamicImage, GenericImageView};
use super::{Drawable, PointCollection};
-use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
-use crate::drawing::bitmap_pixel::{PixelFormat, RGBPixel};
+use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
+
+use plotters_bitmap::bitmap_pixel::{PixelFormat, RGBPixel};
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
+use plotters_bitmap::bitmap_pixel::BGRXPixel;
+
+use plotters_bitmap::BitMapBackend;
-use crate::drawing::BitMapBackend;
use std::borrow::Borrow;
use std::marker::PhantomData;
@@ -174,9 +179,7 @@ impl<'a, Coord> From<(Coord, DynamicImage)> for BitMapElement<'a, Coord, RGBPixe
}
#[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
-impl<'a, Coord> From<(Coord, DynamicImage)>
- for BitMapElement<'a, Coord, crate::drawing::bitmap_pixel::BGRXPixel>
-{
+impl<'a, Coord> From<(Coord, DynamicImage)> for BitMapElement<'a, Coord, BGRXPixel> {
fn from((pos, image): (Coord, DynamicImage)) -> Self {
let (w, h) = image.dimensions();
let rgb_image = image.to_bgra().into_raw();
@@ -190,7 +193,7 @@ impl<'a, Coord> From<(Coord, DynamicImage)>
}
impl<'a, 'b, Coord> PointCollection<'a, Coord> for &'a BitMapElement<'b, Coord> {
- type Borrow = &'a Coord;
+ type Point = &'a Coord;
type IntoIter = std::iter::Once<&'a Coord>;
fn point_iter(self) -> Self::IntoIter {
std::iter::once(&self.pos)
diff --git a/src/element/mod.rs b/src/element/mod.rs
index 99b7a62..4b39ef4 100644
--- a/src/element/mod.rs
+++ b/src/element/mod.rs
@@ -21,7 +21,7 @@
```rust
use std::iter::{Once, once};
use plotters::element::{PointCollection, Drawable};
- use plotters::drawing::backend::{BackendCoord, DrawingErrorKind};
+ use plotters_backend::{BackendCoord, DrawingErrorKind, BackendStyle};
use plotters::style::IntoTextStyle;
use plotters::prelude::*;
@@ -30,7 +30,7 @@
// For any reference to RedX, we can convert it into an iterator of points
impl <'a> PointCollection<'a, (i32, i32)> for &'a RedBoxedX {
- type Borrow = &'a (i32, i32);
+ type Point = &'a (i32, i32);
type IntoIter = Once<&'a (i32, i32)>;
fn point_iter(self) -> Self::IntoIter {
once(&self.0)
@@ -46,10 +46,9 @@
_: (u32, u32),
) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
let pos = pos.next().unwrap();
- let color = RED.to_rgba();
- backend.draw_rect(pos, (pos.0 + 10, pos.1 + 12), &color, false)?;
- let text_style = &("sans-serif", 20).into_text_style(backend).color(&color);
- backend.draw_text("X", &text_style, pos)
+ backend.draw_rect(pos, (pos.0 + 10, pos.1 + 12), &RED, false)?;
+ let text_style = &("sans-serif", 20).into_text_style(&backend.get_size()).color(&RED);
+ backend.draw_text("X", text_style, pos)
}
}
@@ -155,7 +154,7 @@
```
![](https://plotters-rs.github.io/plotters-doc-data/element-3.png)
*/
-use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
+use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
use std::borrow::Borrow;
mod basic_shapes;
@@ -185,26 +184,52 @@ mod boxplot;
#[cfg(feature = "boxplot")]
pub use boxplot::Boxplot;
-#[cfg(feature = "bitmap")]
+#[cfg(feature = "bitmap_backend")]
mod image;
-#[cfg(feature = "bitmap")]
+#[cfg(feature = "bitmap_backend")]
pub use self::image::BitMapElement;
mod dynelem;
pub use dynelem::{DynElement, IntoDynElement};
-/// A type which is logically a collection of points, under any given coordinate system
+/// A type which is logically a collection of points, under any given coordinate system.
+/// Note: Ideally, a point collection trait should be any type of which coordinate elements can be
+/// iterated. This is similar to `iter` method of many collection types in std.
+///
+/// ```ignore
+/// trait PointCollection<Coord> {
+/// type PointIter<'a> : Iterator<Item = &'a Coord>;
+/// fn iter(&self) -> PointIter<'a>;
+/// }
+/// ```
+///
+/// However,
+/// [Generic Associated Types](https://github.com/rust-lang/rfcs/blob/master/text/1598-generic_associated_types.md)
+/// is far away from stablize.
+/// So currently we have the following workaround:
+///
+/// Instead of implement the PointCollection trait on the element type itself, it implements on the
+/// reference to the element. By doing so, we now have a well-defined lifetime for the iterator.
+///
+/// In addition, for some element, the coordinate is computed on the fly, thus we can't hard-code
+/// the iterator's return type is `&'a Coord`.
+/// `Borrow` trait seems to strict in this case, since we don't need the order and hash
+/// preservation properties at this point. However, `AsRef` doesn't work with `Coord`
+///
+/// This workaround also leads overly strict lifetime bound on `ChartContext::draw_series`.
+///
+/// TODO: Once GAT is ready on stable Rust, we should simplify the design.
+///
pub trait PointCollection<'a, Coord> {
/// The item in point iterator
- type Borrow: Borrow<Coord>;
+ type Point: Borrow<Coord> + 'a;
/// The point iterator
- type IntoIter: IntoIterator<Item = Self::Borrow>;
+ type IntoIter: IntoIterator<Item = Self::Point>;
/// framework to do the coordinate mapping
fn point_iter(self) -> Self::IntoIter;
}
-
/// The trait indicates we are able to draw it on a drawing area
pub trait Drawable<DB: DrawingBackend> {
/// Actually draws the element. The key points is already translated into the
diff --git a/src/element/points.rs b/src/element/points.rs
index aa07de3..2b5346c 100644
--- a/src/element/points.rs
+++ b/src/element/points.rs
@@ -1,7 +1,7 @@
use super::*;
use super::{Drawable, PointCollection};
-use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
use crate::style::{ShapeStyle, SizeDesc};
+use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
/// The element that used to describe a point
pub trait PointElement<Coord, Size: SizeDesc> {
@@ -26,7 +26,7 @@ impl<Coord, Size: SizeDesc> Cross<Coord, Size> {
}
impl<'a, Coord: 'a, Size: SizeDesc> PointCollection<'a, Coord> for &'a Cross<Coord, Size> {
- type Borrow = &'a Coord;
+ type Point = &'a Coord;
type IntoIter = std::iter::Once<&'a Coord>;
fn point_iter(self) -> std::iter::Once<&'a Coord> {
std::iter::once(&self.center)
@@ -69,7 +69,7 @@ impl<Coord, Size: SizeDesc> TriangleMarker<Coord, Size> {
}
impl<'a, Coord: 'a, Size: SizeDesc> PointCollection<'a, Coord> for &'a TriangleMarker<Coord, Size> {
- type Borrow = &'a Coord;
+ type Point = &'a Coord;
type IntoIter = std::iter::Once<&'a Coord>;
fn point_iter(self) -> std::iter::Once<&'a Coord> {
std::iter::once(&self.center)
diff --git a/src/element/text.rs b/src/element/text.rs
index 3acaac3..ca813c7 100644
--- a/src/element/text.rs
+++ b/src/element/text.rs
@@ -2,8 +2,8 @@ use std::borrow::Borrow;
use std::i32;
use super::{Drawable, PointCollection};
-use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
use crate::style::{FontDesc, FontResult, LayoutBox, TextStyle};
+use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
/// A single line text element. This can be owned or borrowed string, dependents on
/// `String` or `str` moved into.
@@ -29,7 +29,7 @@ impl<'a, Coord, T: Borrow<str>> Text<'a, Coord, T> {
}
impl<'b, 'a, Coord: 'a, T: Borrow<str> + 'a> PointCollection<'a, Coord> for &'a Text<'b, Coord, T> {
- type Borrow = &'a Coord;
+ type Point = &'a Coord;
type IntoIter = std::iter::Once<&'a Coord>;
fn point_iter(self) -> Self::IntoIter {
std::iter::once(&self.coord)
@@ -216,7 +216,7 @@ impl<'a, Coord> MultiLineText<'a, Coord, String> {
impl<'b, 'a, Coord: 'a, T: Borrow<str> + 'a> PointCollection<'a, Coord>
for &'a MultiLineText<'b, Coord, T>
{
- type Borrow = &'a Coord;
+ type Point = &'a Coord;
type IntoIter = std::iter::Once<&'a Coord>;
fn point_iter(self) -> Self::IntoIter {
std::iter::once(&self.coord)
diff --git a/src/evcxr.rs b/src/evcxr.rs
index 1a17077..44734b7 100644
--- a/src/evcxr.rs
+++ b/src/evcxr.rs
@@ -1,5 +1,6 @@
use crate::coord::Shift;
-use crate::drawing::{DrawingArea, IntoDrawingArea, SVGBackend};
+use crate::drawing::{DrawingArea, IntoDrawingArea};
+use plotters_svg::SVGBackend;
/// The wrapper for the generated SVG
pub struct SVGWrapper(String, String);
diff --git a/src/lib.rs b/src/lib.rs
index 06c6d6c..590bf89 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -264,7 +264,7 @@ including bitmap, vector graph, piston window, GTK/Cairo and WebAssembly.
To use Plotters, you can simply add Plotters into your `Cargo.toml`
```toml
[dependencies]
-plotters = "^0.2.15"
+plotters = "^0.3.0"
```
And the following code draws a quadratic function. `src/main.rs`,
@@ -484,13 +484,14 @@ For example, we can have an element which includes a dot and its coordinate.
```rust
use plotters::prelude::*;
+use plotters::coord::types::RangedCoordf32;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let root = BitMapBackend::new("plotters-doc-data/4.png", (640, 480)).into_drawing_area();
root.fill(&RGBColor(240, 200, 200))?;
- let root = root.apply_coord_spec(RangedCoord::<RangedCoordf32, RangedCoordf32>::new(
+ let root = root.apply_coord_spec(Cartesian2d::<RangedCoordf32, RangedCoordf32>::new(
0f32..1f32,
0f32..1f32,
(0..640, 0..480),
@@ -536,7 +537,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.x_label_area_size(20)
.y_label_area_size(40)
// Finally attach a coordinate on the drawing area and make a chart context
- .build_ranged(0f32..10f32, 0f32..10f32)?;
+ .build_cartesian_2d(0f32..10f32, 0f32..10f32)?;
// Then we can draw a mesh
chart
@@ -607,15 +608,13 @@ By doing so, you can minimize the number of dependencies down to only `itertools
The following list is a complete list of features that can be opt in and out.
-- Drawing backends related features
+- Tier 1 drawing backends
| Name | Description | Additional Dependency |Default?|
|---------|--------------|--------|------------|
-| image\_encoder | Allow `BitMapBackend` save the result to bitmap files | image, rusttype, font-kit | Yes |
-| svg | Enable `SVGBackend` Support | None | Yes |
-| gif\_backend| Opt-in GIF animation Rendering support for `BitMapBackend`, implies `bitmap` enabled | gif | Yes |
-| piston | Enable `PistonWindowBackend` | piston\_window, rusttype, font-kit | No |
-| cairo | Enable `CairoBackend` | cairo-rs, rusttype, font-kit | No |
+| bitmap\_encoder | Allow `BitMapBackend` save the result to bitmap files | image, rusttype, font-kit | Yes |
+| svg\_backend | Enable `SVGBackend` Support | None | Yes |
+| bitmap\_gif| Opt-in GIF animation Rendering support for `BitMapBackend`, implies `bitmap` enabled | gif | Yes |
- Font manipulation features
@@ -696,24 +695,39 @@ pub mod style;
pub mod evcxr;
#[cfg(test)]
-pub use crate::drawing::create_mocked_drawing_area;
+pub use crate::drawing::{check_color, create_mocked_drawing_area};
#[cfg(feature = "palette_ext")]
pub use palette;
/// The module imports the most commonly used types and modules in Plotters
pub mod prelude {
+ // Chart related types
pub use crate::chart::{ChartBuilder, ChartContext, LabelAreaPosition, SeriesLabelPosition};
+
+ // Coordinates
pub use crate::coord::{
- Category, CoordTranslate, GroupBy, IntoCentric, IntoPartialAxis, LogCoord, LogRange,
- LogScalable, Ranged, RangedCoord, RangedCoordf32, RangedCoordf64, RangedCoordi32,
- RangedCoordi64, RangedCoordu32, RangedCoordu64, ToGroupByRange,
+ cartesian::Cartesian2d,
+ combinators::{
+ make_partial_axis, BindKeyPointMethod, BindKeyPoints, BuildNestedCoord, GroupBy,
+ IntoLinspace, IntoLogRange, IntoPartialAxis, Linspace, LogCoord, LogRange, LogScalable,
+ NestedRange, NestedValue, ToGroupByRange,
+ },
+ ranged1d::{DiscreteRanged, IntoSegmentedCoord, Ranged, SegmentValue},
+ CoordTranslate,
};
#[cfg(feature = "chrono")]
- pub use crate::coord::{make_partial_axis, RangedDate, RangedDateTime, RangedDuration};
+ pub use crate::coord::types::{
+ IntoMonthly, IntoYearly, RangedDate, RangedDateTime, RangedDuration,
+ };
+
+ // Re-export the backend for backward compatibility
+ pub use plotters_backend::DrawingBackend;
pub use crate::drawing::*;
+
+ // Series helpers
#[cfg(feature = "area_series")]
pub use crate::series::AreaSeries;
#[cfg(feature = "histogram")]
@@ -722,14 +736,17 @@ pub mod prelude {
pub use crate::series::LineSeries;
#[cfg(feature = "point_series")]
pub use crate::series::PointSeries;
+ #[cfg(feature = "surface_series")]
+ pub use crate::series::SurfaceSeries;
+ // Styles
pub use crate::style::{
AsRelative, Color, FontDesc, FontFamily, FontStyle, FontTransform, HSLColor, IntoFont,
- Palette, Palette100, Palette99, Palette9999, PaletteColor, RGBColor, ShapeStyle,
- SimpleColor, TextStyle,
+ Palette, Palette100, Palette99, Palette9999, PaletteColor, RGBColor, ShapeStyle, TextStyle,
};
pub use crate::style::{BLACK, BLUE, CYAN, GREEN, MAGENTA, RED, TRANSPARENT, WHITE, YELLOW};
+ // Elements
pub use crate::element::{
Circle, Cross, DynElement, EmptyElement, IntoDynElement, MultiLineText, PathElement, Pixel,
Polygon, Rectangle, Text, TriangleMarker,
@@ -742,9 +759,10 @@ pub mod prelude {
#[cfg(feature = "errorbar")]
pub use crate::element::ErrorBar;
- #[cfg(feature = "bitmap")]
+ #[cfg(feature = "bitmap_backend")]
pub use crate::element::BitMapElement;
+ // Data
pub use crate::data::Quartiles;
// TODO: This should be deprecated and completely removed
@@ -761,4 +779,11 @@ pub mod prelude {
#[cfg(feature = "evcxr")]
pub use crate::evcxr::evcxr_figure;
+
+ // Re-export tier 1 backends for backward compatibility
+ #[cfg(feature = "bitmap_backend")]
+ pub use plotters_bitmap::BitMapBackend;
+
+ #[cfg(feature = "svg_backend")]
+ pub use plotters_svg::SVGBackend;
}
diff --git a/src/series/area_series.rs b/src/series/area_series.rs
index f6dce0c..df601cf 100644
--- a/src/series/area_series.rs
+++ b/src/series/area_series.rs
@@ -1,7 +1,7 @@
-use crate::drawing::DrawingBackend;
use crate::element::{DynElement, IntoDynElement, PathElement, Polygon};
use crate::style::colors::TRANSPARENT;
use crate::style::ShapeStyle;
+use plotters_backend::DrawingBackend;
/// An area series is similar to a line series but use a filled polygon
pub struct AreaSeries<DB: DrawingBackend, X: Clone, Y: Clone> {
diff --git a/src/series/histogram.rs b/src/series/histogram.rs
index 75c2fb2..477e4ad 100644
--- a/src/series/histogram.rs
+++ b/src/series/histogram.rs
@@ -1,13 +1,13 @@
use std::collections::{hash_map::IntoIter as HashMapIter, HashMap};
-use std::hash::Hash;
use std::marker::PhantomData;
use std::ops::AddAssign;
use crate::chart::ChartContext;
-use crate::coord::{DiscreteRanged, Ranged, RangedCoord};
-use crate::drawing::DrawingBackend;
+use crate::coord::cartesian::Cartesian2d;
+use crate::coord::ranged1d::{DiscreteRanged, Ranged};
use crate::element::Rectangle;
use crate::style::{Color, ShapeStyle, GREEN};
+use plotters_backend::DrawingBackend;
pub trait HistogramType {}
pub struct Vertical;
@@ -20,32 +20,30 @@ impl HistogramType for Horizontal {}
pub struct Histogram<'a, BR, A, Tag = Vertical>
where
BR: DiscreteRanged,
- BR::ValueType: Eq + Hash,
A: AddAssign<A> + Default,
Tag: HistogramType,
{
style: Box<dyn Fn(&BR::ValueType, &A) -> ShapeStyle + 'a>,
margin: u32,
- iter: HashMapIter<BR::ValueType, A>,
- baseline: Box<dyn Fn(BR::ValueType) -> A + 'a>,
- br_param: BR::RangeParameter,
- _p: PhantomData<(BR, Tag)>,
+ iter: HashMapIter<usize, A>,
+ baseline: Box<dyn Fn(&BR::ValueType) -> A + 'a>,
+ br: BR,
+ _p: PhantomData<Tag>,
}
impl<'a, BR, A, Tag> Histogram<'a, BR, A, Tag>
where
- BR: DiscreteRanged,
- BR::ValueType: Eq + Hash,
+ BR: DiscreteRanged + Clone,
A: AddAssign<A> + Default + 'a,
Tag: HistogramType,
{
- fn empty(br_param: BR::RangeParameter) -> Self {
+ fn empty(br: &BR) -> Self {
Self {
style: Box::new(|_, _| GREEN.filled()),
margin: 5,
iter: HashMap::new().into_iter(),
baseline: Box::new(|_| A::default()),
- br_param,
+ br: br.clone(),
_p: PhantomData,
}
}
@@ -75,7 +73,7 @@ where
}
/// Set a function that defines variant baseline
- pub fn baseline_func(mut self, func: impl Fn(BR::ValueType) -> A + 'a) -> Self {
+ pub fn baseline_func(mut self, func: impl Fn(&BR::ValueType) -> A + 'a) -> Self {
self.baseline = Box::new(func);
self
}
@@ -87,68 +85,33 @@ where
}
/// Set the data iterator
- pub fn data<I: IntoIterator<Item = (BR::ValueType, A)>>(mut self, iter: I) -> Self {
- let mut buffer = HashMap::<BR::ValueType, A>::new();
+ pub fn data<TB: Into<BR::ValueType>, I: IntoIterator<Item = (TB, A)>>(
+ mut self,
+ iter: I,
+ ) -> Self {
+ let mut buffer = HashMap::<usize, A>::new();
for (x, y) in iter.into_iter() {
- *buffer.entry(x).or_insert_with(Default::default) += y;
+ if let Some(x) = self.br.index_of(&x.into()) {
+ *buffer.entry(x).or_insert_with(Default::default) += y;
+ }
}
self.iter = buffer.into_iter();
self
}
}
-pub trait UseDefaultParameter: Default {
- fn new() -> Self {
- Default::default()
- }
-}
-
-impl UseDefaultParameter for () {}
-
impl<'a, BR, A> Histogram<'a, BR, A, Vertical>
where
- BR: DiscreteRanged,
- BR::ValueType: Eq + Hash,
+ BR: DiscreteRanged + Clone,
A: AddAssign<A> + Default + 'a,
{
- /// Create a new histogram series.
- ///
- /// - `iter`: The data iterator
- /// - `margin`: The margin between bars
- /// - `style`: The style of bars
- ///
- /// Returns the newly created histogram series
- #[allow(clippy::redundant_closure)]
- pub fn new<S: Into<ShapeStyle>, I: IntoIterator<Item = (BR::ValueType, A)>>(
- iter: I,
- margin: u32,
- style: S,
- ) -> Self
- where
- BR::RangeParameter: UseDefaultParameter,
- {
- let mut buffer = HashMap::<BR::ValueType, A>::new();
- for (x, y) in iter.into_iter() {
- *buffer.entry(x).or_insert_with(Default::default) += y;
- }
- let style = style.into();
- Self {
- style: Box::new(move |_, _| style.clone()),
- margin,
- iter: buffer.into_iter(),
- baseline: Box::new(|_| A::default()),
- br_param: BR::RangeParameter::new(),
- _p: PhantomData,
- }
- }
-
pub fn vertical<ACoord, DB: DrawingBackend + 'a>(
- parent: &ChartContext<DB, RangedCoord<BR, ACoord>>,
+ parent: &ChartContext<DB, Cartesian2d<BR, ACoord>>,
) -> Self
where
ACoord: Ranged<ValueType = A>,
{
- let dp = parent.as_coord_spec().x_spec().get_range_parameter();
+ let dp = parent.as_coord_spec().x_spec();
Self::empty(dp)
}
@@ -156,17 +119,16 @@ where
impl<'a, BR, A> Histogram<'a, BR, A, Horizontal>
where
- BR: DiscreteRanged,
- BR::ValueType: Eq + Hash,
+ BR: DiscreteRanged + Clone,
A: AddAssign<A> + Default + 'a,
{
pub fn horizontal<ACoord, DB: DrawingBackend>(
- parent: &ChartContext<DB, RangedCoord<ACoord, BR>>,
+ parent: &ChartContext<DB, Cartesian2d<ACoord, BR>>,
) -> Self
where
ACoord: Ranged<ValueType = A>,
{
- let dp = parent.as_coord_spec().y_spec().get_range_parameter();
+ let dp = parent.as_coord_spec().y_spec();
Self::empty(dp)
}
}
@@ -174,18 +136,22 @@ where
impl<'a, BR, A> Iterator for Histogram<'a, BR, A, Vertical>
where
BR: DiscreteRanged,
- BR::ValueType: Eq + Hash,
A: AddAssign<A> + Default,
{
type Item = Rectangle<(BR::ValueType, A)>;
fn next(&mut self) -> Option<Self::Item> {
- if let Some((x, y)) = self.iter.next() {
- let nx = BR::next_value(&x, &self.br_param);
- let base = (self.baseline)(BR::previous_value(&nx, &self.br_param));
- let style = (self.style)(&x, &y);
- let mut rect = Rectangle::new([(x, y), (nx, base)], style);
- rect.set_margin(0, 0, self.margin, self.margin);
- return Some(rect);
+ while let Some((x, y)) = self.iter.next() {
+ if let Some((x, Some(nx))) = self
+ .br
+ .from_index(x)
+ .map(|v| (v, self.br.from_index(x + 1)))
+ {
+ let base = (self.baseline)(&x);
+ let style = (self.style)(&x, &y);
+ let mut rect = Rectangle::new([(x, y), (nx, base)], style);
+ rect.set_margin(0, 0, self.margin, self.margin);
+ return Some(rect);
+ }
}
None
}
@@ -194,19 +160,22 @@ where
impl<'a, BR, A> Iterator for Histogram<'a, BR, A, Horizontal>
where
BR: DiscreteRanged,
- BR::ValueType: Eq + Hash,
A: AddAssign<A> + Default,
{
type Item = Rectangle<(A, BR::ValueType)>;
fn next(&mut self) -> Option<Self::Item> {
- if let Some((y, x)) = self.iter.next() {
- let ny = BR::next_value(&y, &self.br_param);
- // With this trick we can avoid the clone trait bound
- let base = (self.baseline)(BR::previous_value(&ny, &self.br_param));
- let style = (self.style)(&y, &x);
- let mut rect = Rectangle::new([(x, y), (base, ny)], style);
- rect.set_margin(self.margin, self.margin, 0, 0);
- return Some(rect);
+ while let Some((y, x)) = self.iter.next() {
+ if let Some((y, Some(ny))) = self
+ .br
+ .from_index(y)
+ .map(|v| (v, self.br.from_index(y + 1)))
+ {
+ let base = (self.baseline)(&y);
+ let style = (self.style)(&y, &x);
+ let mut rect = Rectangle::new([(x, y), (base, ny)], style);
+ rect.set_margin(0, 0, self.margin, self.margin);
+ return Some(rect);
+ }
}
None
}
diff --git a/src/series/line_series.rs b/src/series/line_series.rs
index d3a5971..f1e08e8 100644
--- a/src/series/line_series.rs
+++ b/src/series/line_series.rs
@@ -1,6 +1,6 @@
-use crate::drawing::DrawingBackend;
use crate::element::{Circle, DynElement, IntoDynElement, PathElement};
use crate::style::ShapeStyle;
+use plotters_backend::DrawingBackend;
use std::marker::PhantomData;
/// The line series object, which takes an iterator of points in guest coordinate system
@@ -73,7 +73,7 @@ mod test {
});
let mut chart = ChartBuilder::on(&drawing_area)
- .build_ranged(0..100, 0..100)
+ .build_cartesian_2d(0..100, 0..100)
.expect("Build chart error");
chart
diff --git a/src/series/mod.rs b/src/series/mod.rs
index 103135e..a0c8f19 100644
--- a/src/series/mod.rs
+++ b/src/series/mod.rs
@@ -18,6 +18,8 @@ mod histogram;
mod line_series;
#[cfg(feature = "point_series")]
mod point_series;
+#[cfg(feature = "surface_series")]
+mod surface;
#[cfg(feature = "area_series")]
pub use area_series::AreaSeries;
@@ -27,3 +29,5 @@ pub use histogram::Histogram;
pub use line_series::LineSeries;
#[cfg(feature = "point_series")]
pub use point_series::PointSeries;
+#[cfg(feature = "surface_series")]
+pub use surface::SurfaceSeries;
diff --git a/src/series/surface.rs b/src/series/surface.rs
new file mode 100644
index 0000000..04792dc
--- /dev/null
+++ b/src/series/surface.rs
@@ -0,0 +1,82 @@
+use crate::element::Polygon;
+use crate::style::ShapeStyle;
+/// The surface series.
+///
+/// Currently the surface is representing any surface in form
+/// y = f(x,z)
+///
+/// TODO: make this more general
+pub struct SurfaceSeries<X, Y, Z> {
+ x_data: Vec<X>,
+ y_data: Vec<Y>,
+ z_data: Vec<Z>,
+ style: ShapeStyle,
+ size: usize,
+ state: usize,
+}
+
+impl<X, Y, Z> SurfaceSeries<X, Y, Z> {
+ pub fn new<XS, ZS, YF, S>(xs: XS, zs: ZS, y_func: YF, style: S) -> Self
+ where
+ YF: Fn(&X, &Z) -> Y,
+ XS: Iterator<Item = X>,
+ ZS: Iterator<Item = Z>,
+ S: Into<ShapeStyle>,
+ {
+ let x_data: Vec<_> = xs.collect();
+ let z_data: Vec<_> = zs.collect();
+ let y_data: Vec<_> = x_data
+ .iter()
+ .map(|x| z_data.iter().map(move |z| (x, z)))
+ .flatten()
+ .map(|(x, z)| y_func(x, z))
+ .collect();
+ let size = (x_data.len().max(1) - 1) * (z_data.len().max(1) - 1);
+ Self {
+ x_data,
+ y_data,
+ z_data,
+ style: style.into(),
+ size,
+ state: 0,
+ }
+ }
+
+ fn point_at(&self, x: usize, z: usize) -> (X, Y, Z)
+ where
+ X: Clone,
+ Y: Clone,
+ Z: Clone,
+ {
+ (
+ self.x_data[x].clone(),
+ self.y_data[x * self.z_data.len() + z].clone(),
+ self.z_data[z].clone(),
+ )
+ }
+}
+
+impl<X: Clone, Y: Clone, Z: Clone> Iterator for SurfaceSeries<X, Y, Z> {
+ type Item = Polygon<(X, Y, Z)>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.size <= self.state {
+ return None;
+ }
+
+ let x = self.state / (self.z_data.len() - 1);
+ let z = self.state % (self.z_data.len() - 1);
+
+ self.state += 1;
+
+ Some(Polygon::new(
+ vec![
+ self.point_at(x, z),
+ self.point_at(x, z + 1),
+ self.point_at(x + 1, z + 1),
+ self.point_at(x + 1, z),
+ ],
+ self.style.clone(),
+ ))
+ }
+}
diff --git a/src/style/color.rs b/src/style/color.rs
index f43f863..d75add3 100644
--- a/src/style/color.rs
+++ b/src/style/color.rs
@@ -1,15 +1,23 @@
use super::palette::Palette;
use super::ShapeStyle;
+use plotters_backend::{BackendColor, BackendStyle};
+
use std::marker::PhantomData;
/// Any color representation
-pub trait Color {
+pub trait Color: BackendStyle {
/// Convert the RGB representation to the standard RGB tuple
- fn rgb(&self) -> (u8, u8, u8);
+ #[inline(always)]
+ fn rgb(&self) -> (u8, u8, u8) {
+ self.color().rgb
+ }
/// Get the alpha channel of the color
- fn alpha(&self) -> f64;
+ #[inline(always)]
+ fn alpha(&self) -> f64 {
+ self.color().alpha
+ }
/// Mix the color with given opacity
fn mix(&self, value: f64) -> RGBAColor {
@@ -45,38 +53,18 @@ pub trait Color {
/// The RGBA representation of the color, Plotters use RGBA as the internal representation
/// of color
#[derive(Clone, PartialEq, Debug)]
-pub struct RGBAColor(pub(super) u8, pub(super) u8, pub(super) u8, pub(super) f64);
+pub struct RGBAColor(pub(crate) u8, pub(crate) u8, pub(crate) u8, pub(crate) f64);
-impl Color for RGBAColor {
+impl BackendStyle for RGBAColor {
#[inline(always)]
- fn rgb(&self) -> (u8, u8, u8) {
- (self.0, self.1, self.2)
- }
-
- #[inline(always)]
- fn alpha(&self) -> f64 {
- self.3
- }
-
- fn to_rgba(&self) -> RGBAColor {
- self.clone()
- }
-}
-
-/// Color without alpha channel
-pub trait SimpleColor {
- fn rgb(&self) -> (u8, u8, u8);
-}
-
-impl<T: SimpleColor> Color for T {
- fn rgb(&self) -> (u8, u8, u8) {
- SimpleColor::rgb(self)
- }
-
- fn alpha(&self) -> f64 {
- 1.0
+ fn color(&self) -> BackendColor {
+ BackendColor {
+ rgb: (self.0, self.1, self.2),
+ alpha: self.3,
+ }
}
}
+impl Color for RGBAColor {}
/// A color in the given palette
pub struct PaletteColor<P: Palette>(usize, PhantomData<P>);
@@ -88,28 +76,41 @@ impl<P: Palette> PaletteColor<P> {
}
}
-impl<P: Palette> SimpleColor for PaletteColor<P> {
- fn rgb(&self) -> (u8, u8, u8) {
- P::COLORS[self.0]
+impl<P: Palette> BackendStyle for PaletteColor<P> {
+ #[inline(always)]
+ fn color(&self) -> BackendColor {
+ BackendColor {
+ rgb: P::COLORS[self.0],
+ alpha: 1.0,
+ }
}
}
+impl<P: Palette> Color for PaletteColor<P> {}
+
/// The color described by its RGB value
#[derive(Debug)]
pub struct RGBColor(pub u8, pub u8, pub u8);
-impl SimpleColor for RGBColor {
- fn rgb(&self) -> (u8, u8, u8) {
- (self.0, self.1, self.2)
+impl BackendStyle for RGBColor {
+ #[inline(always)]
+ fn color(&self) -> BackendColor {
+ BackendColor {
+ rgb: (self.0, self.1, self.2),
+ alpha: 1.0,
+ }
}
}
+impl Color for RGBColor {}
+
/// The color described by HSL color space
pub struct HSLColor(pub f64, pub f64, pub f64);
-impl SimpleColor for HSLColor {
+impl BackendStyle for HSLColor {
+ #[inline(always)]
#[allow(clippy::many_single_char_names)]
- fn rgb(&self) -> (u8, u8, u8) {
+ fn color(&self) -> BackendColor {
let (h, s, l) = (
self.0.min(1.0).max(0.0),
self.1.min(1.0).max(0.0),
@@ -118,7 +119,10 @@ impl SimpleColor for HSLColor {
if s == 0.0 {
let value = (l * 255.0).round() as u8;
- return (value, value, value);
+ return BackendColor {
+ rgb: (value, value, value),
+ alpha: 1.0,
+ };
}
let q = if l < 0.5 {
@@ -147,6 +151,11 @@ impl SimpleColor for HSLColor {
(value * 255.0).round() as u8
};
- (cvt(h + 1.0 / 3.0), cvt(h), cvt(h - 1.0 / 3.0))
+ BackendColor {
+ rgb: (cvt(h + 1.0 / 3.0), cvt(h), cvt(h - 1.0 / 3.0)),
+ alpha: 1.0,
+ }
}
}
+
+impl Color for HSLColor {}
diff --git a/src/style/colors.rs b/src/style/colors.rs
index 4854da8..3b9ce07 100644
--- a/src/style/colors.rs
+++ b/src/style/colors.rs
@@ -22,36 +22,3 @@ predefined_color!(YELLOW, 255, 255, 0, "The predefined yellow color");
predefined_color!(CYAN, 0, 255, 255, "The predefined cyan color");
predefined_color!(MAGENTA, 255, 0, 255, "The predefined magenta color");
predefined_color!(TRANSPARENT, 0, 0, 0, 0.0, "The predefined transparent");
-
-/// Predefined Color definitions using the [palette](https://docs.rs/palette/) color types
-#[cfg(feature = "palette_ext")]
-pub mod palette_ext {
- use palette::rgb::Srgb;
- use palette::Alpha;
-
- use std::marker::PhantomData;
-
- macro_rules! predefined_color_pal {
- ($name:ident, $r:expr, $g:expr, $b:expr, $doc:expr) => {
- #[doc = $doc]
- pub const $name: Srgb<u8> = predefined_color_pal!(@gen_c $r, $g, $b);
- };
- ($name:ident, $r:expr, $g:expr, $b:expr, $a:expr, $doc:expr) => {
- #[doc = $doc]
- pub const $name: Alpha<Srgb<u8>, f64> = Alpha{ alpha: $a, color: predefined_color_pal!(@gen_c $r, $g, $b) };
- };
- (@gen_c $r:expr, $g:expr, $b:expr) => {
- Srgb { red: $r, green: $g, blue: $b, standard: PhantomData }
- };
- }
-
- predefined_color_pal!(WHITE, 255, 255, 255, "The predefined white color");
- predefined_color_pal!(BLACK, 0, 0, 0, "The predefined black color");
- predefined_color_pal!(RED, 255, 0, 0, "The predefined red color");
- predefined_color_pal!(GREEN, 0, 255, 0, "The predefined green color");
- predefined_color_pal!(BLUE, 0, 0, 255, "The predefined blue color");
- predefined_color_pal!(YELLOW, 255, 255, 0, "The predefined yellow color");
- predefined_color_pal!(CYAN, 0, 255, 255, "The predefined cyan color");
- predefined_color_pal!(MAGENTA, 255, 0, 255, "The predefined magenta color");
- predefined_color_pal!(TRANSPARENT, 0, 0, 0, 0.0, "The predefined transparent");
-}
diff --git a/src/style/font/font_desc.rs b/src/style/font/font_desc.rs
index 7caa4e4..20d8b7c 100644
--- a/src/style/font/font_desc.rs
+++ b/src/style/font/font_desc.rs
@@ -4,41 +4,14 @@ use crate::style::{Color, TextStyle};
use std::convert::From;
+pub use plotters_backend::{FontFamily, FontStyle, FontTransform};
+
/// The error type for the font implementation
pub type FontError = <FontDataInternal as FontData>::ErrorType;
/// The type we used to represent a result of any font operations
pub type FontResult<T> = Result<T, FontError>;
-/// Specifying text transformations
-#[derive(Clone)]
-pub enum FontTransform {
- /// Nothing to transform
- None,
- /// Rotating the text 90 degree clockwise
- Rotate90,
- /// Rotating the text 180 degree clockwise
- Rotate180,
- /// Rotating the text 270 degree clockwise
- Rotate270,
-}
-
-impl FontTransform {
- /// Transform the coordinate to perform the rotation
- ///
- /// - `x`: The x coordinate in pixels before transform
- /// - `y`: The y coordinate in pixels before transform
- /// - **returns**: The coordinate after transform
- pub fn transform(&self, x: i32, y: i32) -> (i32, i32) {
- match self {
- FontTransform::None => (x, y),
- FontTransform::Rotate90 => (-y, x),
- FontTransform::Rotate180 => (-x, -y),
- FontTransform::Rotate270 => (y, -x),
- }
- }
-}
-
/// Describes a font
#[derive(Clone)]
pub struct FontDesc<'a> {
@@ -49,82 +22,6 @@ pub struct FontDesc<'a> {
style: FontStyle,
}
-/// Describes font family.
-/// This can be either a specific font family name, such as "arial",
-/// or a general font family class, such as "serif" and "sans-serif"
-#[derive(Clone, Copy)]
-pub enum FontFamily<'a> {
- /// The system default serif font family
- Serif,
- /// The system default sans-serif font family
- SansSerif,
- /// The system default monospace font
- Monospace,
- /// A specific font family name
- Name(&'a str),
-}
-
-impl<'a> FontFamily<'a> {
- /// Make a CSS compatible string for the font family name.
- /// This can be used as the value of `font-family` attribute in SVG.
- pub fn as_str(&self) -> &str {
- match self {
- FontFamily::Serif => "serif",
- FontFamily::SansSerif => "sans-serif",
- FontFamily::Monospace => "monospace",
- FontFamily::Name(face) => face,
- }
- }
-}
-
-impl<'a> From<&'a str> for FontFamily<'a> {
- fn from(from: &'a str) -> FontFamily<'a> {
- match from.to_lowercase().as_str() {
- "serif" => FontFamily::Serif,
- "sans-serif" => FontFamily::SansSerif,
- "monospace" => FontFamily::Monospace,
- _ => FontFamily::Name(from),
- }
- }
-}
-
-/// Describes the font style. Such as Italic, Oblique, etc.
-#[derive(Clone, Copy)]
-pub enum FontStyle {
- /// The normal style
- Normal,
- /// The oblique style
- Oblique,
- /// The italic style
- Italic,
- /// The bold style
- Bold,
-}
-
-impl FontStyle {
- /// Convert the font style into a CSS compatible string which can be used in `font-style` attribute.
- pub fn as_str(&self) -> &str {
- match self {
- FontStyle::Normal => "normal",
- FontStyle::Italic => "italic",
- FontStyle::Oblique => "oblique",
- FontStyle::Bold => "bold",
- }
- }
-}
-
-impl<'a> From<&'a str> for FontStyle {
- fn from(from: &'a str) -> FontStyle {
- match from.to_lowercase().as_str() {
- "normal" => FontStyle::Normal,
- "italic" => FontStyle::Italic,
- "oblique" => FontStyle::Oblique,
- "bold" => FontStyle::Bold,
- _ => FontStyle::Normal,
- }
- }
-}
-
impl<'a> From<&'a str> for FontDesc<'a> {
fn from(from: &'a str) -> FontDesc<'a> {
FontDesc::new(from.into(), 1.0, FontStyle::Normal)
@@ -241,11 +138,15 @@ impl<'a> FontDesc<'a> {
pub fn color<C: Color>(&self, color: &C) -> TextStyle<'a> {
TextStyle {
font: self.clone(),
- color: color.to_rgba(),
+ color: color.color(),
pos: Pos::default(),
}
}
+ pub fn get_family(&self) -> FontFamily {
+ self.family
+ }
+
/// Get the name of the font
pub fn get_name(&self) -> &str {
self.family.as_str()
diff --git a/src/style/mod.rs b/src/style/mod.rs
index 39c171d..635fd10 100644
--- a/src/style/mod.rs
+++ b/src/style/mod.rs
@@ -9,12 +9,9 @@ mod shape;
mod size;
mod text;
-#[cfg(feature = "palette_ext")]
-mod palette_ext;
-
/// Definitions of palettes of accessibility
pub use self::palette::*;
-pub use color::{Color, HSLColor, PaletteColor, RGBAColor, RGBColor, SimpleColor};
+pub use color::{Color, HSLColor, PaletteColor, RGBAColor, RGBColor};
pub use colors::{BLACK, BLUE, CYAN, GREEN, MAGENTA, RED, TRANSPARENT, WHITE, YELLOW};
pub use font::{
FontDesc, FontError, FontFamily, FontResult, FontStyle, FontTransform, IntoFont, LayoutBox,
diff --git a/src/style/palette_ext.rs b/src/style/palette_ext.rs
deleted file mode 100644
index 35e15ff..0000000
--- a/src/style/palette_ext.rs
+++ /dev/null
@@ -1,136 +0,0 @@
-use num_traits::Float;
-
-use palette::encoding::Linear;
-use palette::luma::{Luma, LumaStandard};
-use palette::rgb::RgbStandard;
-use palette::rgb::{Rgb, RgbSpace};
-use palette::white_point::D65;
-use palette::{Alpha, Component, Hsl, Hsv, Hwb, Lab, Lch, LinSrgb, Xyz, Yxy};
-
-use super::color::Color;
-
-impl<S: RgbStandard, T: Component> Color for Rgb<S, T> {
- fn rgb(&self) -> (u8, u8, u8) {
- self.into_format::<u8>().into_components()
- }
-
- #[inline]
- fn alpha(&self) -> f64 {
- 1.0
- }
-}
-
-impl<S: LumaStandard, T: Component> Color for Luma<S, T> {
- fn rgb(&self) -> (u8, u8, u8) {
- let (luma,) = self.into_format::<u8>().into_components();
- (luma, luma, luma)
- }
-
- #[inline]
- fn alpha(&self) -> f64 {
- 1.0
- }
-}
-
-impl<S: RgbSpace, T: Component + Float> Color for Hsl<S, T> {
- fn rgb(&self) -> (u8, u8, u8) {
- Rgb::<Linear<S>, T>::from(*self)
- .into_format::<u8>()
- .into_components()
- }
-
- #[inline]
- fn alpha(&self) -> f64 {
- 1.0
- }
-}
-
-impl<S: RgbSpace, T: Component + Float> Color for Hsv<S, T> {
- fn rgb(&self) -> (u8, u8, u8) {
- Rgb::<Linear<S>, T>::from(*self)
- .into_format::<u8>()
- .into_components()
- }
-
- #[inline]
- fn alpha(&self) -> f64 {
- 1.0
- }
-}
-
-impl<S: RgbSpace, T: Component + Float> Color for Hwb<S, T> {
- fn rgb(&self) -> (u8, u8, u8) {
- Rgb::<Linear<S>, T>::from(*self)
- .into_format::<u8>()
- .into_components()
- }
-
- #[inline]
- fn alpha(&self) -> f64 {
- 1.0
- }
-}
-
-impl<T: Component + Float> Color for Lab<D65, T> {
- fn rgb(&self) -> (u8, u8, u8) {
- LinSrgb::<T>::from(*self)
- .into_format::<u8>()
- .into_components()
- }
-
- #[inline]
- fn alpha(&self) -> f64 {
- 1.0
- }
-}
-
-impl<T: Component + Float> Color for Lch<D65, T> {
- fn rgb(&self) -> (u8, u8, u8) {
- LinSrgb::<T>::from(*self)
- .into_format::<u8>()
- .into_components()
- }
-
- #[inline]
- fn alpha(&self) -> f64 {
- 1.0
- }
-}
-
-impl<T: Component + Float> Color for Xyz<D65, T> {
- fn rgb(&self) -> (u8, u8, u8) {
- LinSrgb::<T>::from(*self)
- .into_format::<u8>()
- .into_components()
- }
-
- #[inline]
- fn alpha(&self) -> f64 {
- 1.0
- }
-}
-
-impl<T: Component + Float> Color for Yxy<D65, T> {
- fn rgb(&self) -> (u8, u8, u8) {
- LinSrgb::<T>::from(*self)
- .into_format::<u8>()
- .into_components()
- }
-
- #[inline]
- fn alpha(&self) -> f64 {
- 1.0
- }
-}
-
-impl<C: Color, T: Component> Color for Alpha<C, T> {
- #[inline]
- fn rgb(&self) -> (u8, u8, u8) {
- self.color.rgb()
- }
-
- #[inline]
- fn alpha(&self) -> f64 {
- self.alpha.convert()
- }
-}
diff --git a/src/style/shape.rs b/src/style/shape.rs
index 4a56a1d..a829f3f 100644
--- a/src/style/shape.rs
+++ b/src/style/shape.rs
@@ -1,4 +1,5 @@
use super::color::{Color, RGBAColor};
+use plotters_backend::{BackendColor, BackendStyle};
/// Style for any of shape
#[derive(Clone)]
@@ -36,3 +37,12 @@ impl<'a, T: Color> From<&'a T> for ShapeStyle {
}
}
}
+
+impl BackendStyle for ShapeStyle {
+ fn color(&self) -> BackendColor {
+ self.color.color()
+ }
+ fn stroke_width(&self) -> u32 {
+ self.stroke_width
+ }
+}
diff --git a/src/style/size.rs b/src/style/size.rs
index 22e436b..500993f 100644
--- a/src/style/size.rs
+++ b/src/style/size.rs
@@ -1,6 +1,6 @@
use crate::coord::CoordTranslate;
use crate::drawing::DrawingArea;
-use crate::drawing::DrawingBackend;
+use plotters_backend::DrawingBackend;
/// The trait indicates that the type has a dimensional data.
/// This is the abstraction for the relative sizing model.
@@ -11,12 +11,6 @@ pub trait HasDimension {
fn dim(&self) -> (u32, u32);
}
-impl<T: DrawingBackend> HasDimension for T {
- fn dim(&self) -> (u32, u32) {
- self.get_size()
- }
-}
-
impl<D: DrawingBackend, C: CoordTranslate> HasDimension for DrawingArea<D, C> {
fn dim(&self) -> (u32, u32) {
self.dim_in_pixel()
diff --git a/src/style/text.rs b/src/style/text.rs
index a609767..d84d5a5 100644
--- a/src/style/text.rs
+++ b/src/style/text.rs
@@ -1,91 +1,9 @@
-use super::color::{Color, RGBAColor};
-use super::font::{FontDesc, FontFamily, FontStyle, FontTransform};
+use super::color::Color;
+use super::font::{FontDesc, FontError, FontFamily, FontStyle, FontTransform};
use super::size::{HasDimension, SizeDesc};
use super::BLACK;
-
-/// Text anchor attributes are used to properly position the text.
-///
-/// # Examples
-///
-/// In the example below, the text anchor (X) position is `Pos::new(HPos::Right, VPos::Center)`.
-/// ```text
-/// ***** X
-/// ```
-/// The position is always relative to the text regardless of its rotation.
-/// In the example below, the text has style
-/// `style.transform(FontTransform::Rotate90).pos(Pos::new(HPos::Center, VPos::Top))`.
-/// ```text
-/// *
-/// *
-/// * X
-/// *
-/// *
-/// ```
-pub mod text_anchor {
- /// The horizontal position of the anchor point relative to the text.
- #[derive(Clone, Copy)]
- pub enum HPos {
- /// Anchor point is on the left side of the text
- Left,
- /// Anchor point is on the right side of the text
- Right,
- /// Anchor point is in the horizontal center of the text
- Center,
- }
-
- /// The vertical position of the anchor point relative to the text.
- #[derive(Clone, Copy)]
- pub enum VPos {
- /// Anchor point is on the top of the text
- Top,
- /// Anchor point is in the vertical center of the text
- Center,
- /// Anchor point is on the bottom of the text
- Bottom,
- }
-
- /// The text anchor position.
- #[derive(Clone, Copy)]
- pub struct Pos {
- /// The horizontal position of the anchor point
- pub h_pos: HPos,
- /// The vertical position of the anchor point
- pub v_pos: VPos,
- }
-
- impl Pos {
- /// Create a new text anchor position.
- ///
- /// - `h_pos`: The horizontal position of the anchor point
- /// - `v_pos`: The vertical position of the anchor point
- /// - **returns** The newly created text anchor position
- ///
- /// ```rust
- /// use plotters::style::text_anchor::{Pos, HPos, VPos};
- ///
- /// let pos = Pos::new(HPos::Left, VPos::Top);
- /// ```
- pub fn new(h_pos: HPos, v_pos: VPos) -> Self {
- Pos { h_pos, v_pos }
- }
-
- /// Create a default text anchor position (top left).
- ///
- /// - **returns** The default text anchor position
- ///
- /// ```rust
- /// use plotters::style::text_anchor::{Pos, HPos, VPos};
- ///
- /// let pos = Pos::default();
- /// ```
- pub fn default() -> Self {
- Pos {
- h_pos: HPos::Left,
- v_pos: VPos::Top,
- }
- }
- }
-}
+pub use plotters_backend::text_anchor;
+use plotters_backend::{BackendColor, BackendCoord, BackendStyle, BackendTextStyle};
/// Style of a text
#[derive(Clone)]
@@ -93,7 +11,7 @@ pub struct TextStyle<'a> {
/// The font description
pub font: FontDesc<'a>,
/// The text color
- pub color: RGBAColor,
+ pub color: BackendColor,
/// The anchor point position
pub pos: text_anchor::Pos,
}
@@ -158,7 +76,7 @@ impl<'a> TextStyle<'a> {
pub fn color<C: Color>(&self, color: &'a C) -> Self {
Self {
font: self.font.clone(),
- color: color.to_rgba(),
+ color: color.color(),
pos: self.pos,
}
}
@@ -176,7 +94,7 @@ impl<'a> TextStyle<'a> {
pub fn transform(&self, trans: FontTransform) -> Self {
Self {
font: self.font.clone().transform(trans),
- color: self.color.clone(),
+ color: self.color,
pos: self.pos,
}
}
@@ -196,7 +114,7 @@ impl<'a> TextStyle<'a> {
pub fn pos(&self, pos: text_anchor::Pos) -> Self {
Self {
font: self.font.clone(),
- color: self.color.clone(),
+ color: self.color,
pos,
}
}
@@ -213,8 +131,53 @@ impl<'a, T: Into<FontDesc<'a>>> From<T> for TextStyle<'a> {
fn from(font: T) -> Self {
Self {
font: font.into(),
- color: BLACK.to_rgba(),
+ color: BLACK.color(),
pos: text_anchor::Pos::default(),
}
}
}
+
+impl<'a> BackendTextStyle for TextStyle<'a> {
+ type FontError = FontError;
+ fn color(&self) -> BackendColor {
+ self.color.color()
+ }
+
+ fn size(&self) -> f64 {
+ self.font.get_size()
+ }
+
+ fn transform(&self) -> FontTransform {
+ self.font.get_transform()
+ }
+
+ fn style(&self) -> FontStyle {
+ self.font.get_style()
+ }
+
+ #[allow(clippy::type_complexity)]
+ fn layout_box(&self, text: &str) -> Result<((i32, i32), (i32, i32)), Self::FontError> {
+ self.font.layout_box(text)
+ }
+
+ fn anchor(&self) -> text_anchor::Pos {
+ self.pos
+ }
+
+ fn family(&self) -> FontFamily {
+ self.font.get_family()
+ }
+
+ fn draw<E, DrawFunc: FnMut(i32, i32, BackendColor) -> Result<(), E>>(
+ &self,
+ text: &str,
+ pos: BackendCoord,
+ mut draw: DrawFunc,
+ ) -> Result<Result<(), E>, Self::FontError> {
+ let color = self.color.color();
+ self.font.draw(text, pos, move |x, y, a| {
+ let mix_color = color.mix(a as f64);
+ draw(x, y, mix_color)
+ })
+ }
+}