diff options
Diffstat (limited to 'src/coord')
22 files changed, 2744 insertions, 948 deletions
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>; +} |