aboutsummaryrefslogtreecommitdiff
path: root/src/coord
diff options
context:
space:
mode:
Diffstat (limited to 'src/coord')
-rw-r--r--src/coord/category.rs209
-rw-r--r--src/coord/mod.rs129
-rw-r--r--src/coord/ranged.rs397
-rw-r--r--src/coord/ranged1d/combinators/ckps.rs266
-rw-r--r--src/coord/ranged1d/combinators/group_by.rs120
-rw-r--r--src/coord/ranged1d/combinators/linspace.rs432
-rw-r--r--src/coord/ranged1d/combinators/logarithmic.rs (renamed from src/coord/logarithmic.rs)28
-rw-r--r--src/coord/ranged1d/combinators/mod.rs17
-rw-r--r--src/coord/ranged1d/combinators/nested.rs204
-rw-r--r--src/coord/ranged1d/combinators/partial_axis.rs113
-rw-r--r--src/coord/ranged1d/discrete.rs270
-rw-r--r--src/coord/ranged1d/mod.rs234
-rw-r--r--src/coord/ranged1d/types/datetime.rs (renamed from src/coord/datetime.rs)445
-rw-r--r--src/coord/ranged1d/types/mod.rs15
-rw-r--r--src/coord/ranged1d/types/numeric.rs (renamed from src/coord/numeric.rs)220
-rw-r--r--src/coord/ranged1d/types/slice.rs100
-rw-r--r--src/coord/ranged2d/cartesian.rs152
-rw-r--r--src/coord/ranged2d/mod.rs1
-rw-r--r--src/coord/ranged3d/cartesian3d.rs105
-rw-r--r--src/coord/ranged3d/mod.rs5
-rw-r--r--src/coord/ranged3d/projection.rs198
-rw-r--r--src/coord/translate.rs32
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>;
+}