diff options
author | Joel Galenson <jgalenson@google.com> | 2021-04-12 23:43:24 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2021-04-12 23:43:24 +0000 |
commit | b64df24e47441d6ec44a063c665ac39d21cfb141 (patch) | |
tree | f5309a0441cebfeb180fa9e396526b41dc0ed911 /src | |
parent | 201b37a7aa6ab96dbcd39df7f0a1072845a55fa0 (diff) | |
parent | eecbedf4bd5479cfae7af7e1038ce8c8580c5840 (diff) | |
download | plotters-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')
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("<"), - '>' => buf.push_str(">"), - '&' => buf.push_str("&"), - '"' => buf.push_str("""), - '\'' => buf.push_str("'"), - other => buf.push(other), - }); - } - fn open_tag(&mut self, tag: SVGTag, attr: &[(&str, &str)], close: bool) { - let buf = self.target.get_mut(); - buf.push_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); @@ -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) + }) + } +} |