aboutsummaryrefslogtreecommitdiff
path: root/src/coord/ranged1d
diff options
context:
space:
mode:
Diffstat (limited to 'src/coord/ranged1d')
-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.rs160
-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.rs1163
-rw-r--r--src/coord/ranged1d/types/mod.rs15
-rw-r--r--src/coord/ranged1d/types/numeric.rs361
-rw-r--r--src/coord/ranged1d/types/slice.rs100
13 files changed, 3455 insertions, 0 deletions
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/ranged1d/combinators/logarithmic.rs b/src/coord/ranged1d/combinators/logarithmic.rs
new file mode 100644
index 0000000..d29c73e
--- /dev/null
+++ b/src/coord/ranged1d/combinators/logarithmic.rs
@@ -0,0 +1,160 @@
+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.
+/// 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;
+ /// Convert a floating point number to the scale
+ fn from_f64(f: f64) -> Self;
+}
+
+macro_rules! impl_log_scalable {
+ (i, $t:ty) => {
+ impl LogScalable for $t {
+ fn as_f64(&self) -> f64 {
+ if *self != 0 {
+ return *self as f64;
+ }
+ // If this is an integer, we should allow zero point to be shown
+ // on the chart, thus we can't map the zero point to inf.
+ // So we just assigning a value smaller than 1 as the alternative
+ // of the zero point.
+ return 0.5;
+ }
+ fn from_f64(f: f64) -> $t {
+ f.round() as $t
+ }
+ }
+ };
+ (f, $t:ty) => {
+ impl LogScalable for $t {
+ fn as_f64(&self) -> f64 {
+ *self as f64
+ }
+ fn from_f64(f: f64) -> $t {
+ f as $t
+ }
+ }
+ };
+}
+
+impl_log_scalable!(i, u8);
+impl_log_scalable!(i, u16);
+impl_log_scalable!(i, u32);
+impl_log_scalable!(i, u64);
+impl_log_scalable!(f, f32);
+impl_log_scalable!(f, f64);
+
+pub trait IntoLogRange {
+ type ValueType: LogScalable;
+ fn log_scale(self) -> LogRange<Self::ValueType>;
+}
+
+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 {
+ linear: (range.0.start.as_f64().ln()..range.0.end.as_f64().ln()).into(),
+ logic: range.0,
+ marker: PhantomData,
+ }
+ }
+}
+
+impl<V: LogScalable> AsRangedCoord for LogRange<V> {
+ type CoordDescType = LogCoord<V>;
+ type Value = V;
+}
+
+/// A log scaled coordinate axis
+pub struct LogCoord<V: LogScalable> {
+ linear: RangedCoordf64,
+ logic: Range<V>,
+ marker: PhantomData<V>,
+}
+
+impl<V: LogScalable> Ranged for LogCoord<V> {
+ type FormatOption = DefaultFormatting;
+ type ValueType = V;
+
+ fn map(&self, value: &V, limit: (i32, i32)) -> i32 {
+ let value = value.as_f64();
+ let value = value.max(self.logic.start.as_f64()).ln();
+ self.linear.map(&value, limit)
+ }
+
+ 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()
+ .floor()
+ .max(1.0) as usize;
+
+ let tier_2_density = if max_points < tier_1 {
+ 0
+ } else {
+ let density = 1 + (max_points - tier_1) / tier_1;
+ let mut exp = 1;
+ while exp * 10 <= density {
+ exp *= 10;
+ }
+ exp - 1
+ };
+
+ let mut multiplier = 10.0;
+ let mut cnt = 1;
+ while max_points < tier_1 / cnt {
+ multiplier *= 10.0;
+ cnt += 1;
+ }
+
+ let mut ret = vec![];
+ let mut val = (10f64).powf(self.logic.start.as_f64().log10().ceil());
+
+ while val <= self.logic.end.as_f64() {
+ ret.push(V::from_f64(val));
+ for i in 1..=tier_2_density {
+ let v = val
+ * (1.0
+ + multiplier / f64::from(tier_2_density as u32 + 1) * f64::from(i as u32));
+ if v > self.logic.end.as_f64() {
+ break;
+ }
+ ret.push(V::from_f64(v));
+ }
+ val *= multiplier;
+ }
+
+ ret
+ }
+
+ fn range(&self) -> Range<V> {
+ self.logic.clone()
+ }
+}
+#[cfg(test)]
+mod test {
+ use super::*;
+ #[test]
+ fn regression_test_issue_143() {
+ let range: LogCoord<f64> = LogRange(1.0..5.0).into();
+
+ range.key_points(100);
+ }
+}
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/ranged1d/types/datetime.rs b/src/coord/ranged1d/types/datetime.rs
new file mode 100644
index 0000000..f6b5717
--- /dev/null
+++ b/src/coord/ranged1d/types/datetime.rs
@@ -0,0 +1,1163 @@
+/// The datetime coordinates
+use chrono::{Date, DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, TimeZone, Timelike};
+use std::ops::{Add, Range, Sub};
+
+use crate::coord::ranged1d::{
+ AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged,
+ ValueFormatter,
+};
+
+/// 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 DateType: Datelike + PartialOrd;
+
+ /// Returns the date that is no later than the time
+ fn date_floor(&self) -> Self::DateType;
+ /// Returns the date that is no earlier than the time
+ fn date_ceil(&self) -> Self::DateType;
+ /// Returns the maximum value that is earlier than the given date
+ fn earliest_after_date(date: Self::DateType) -> Self;
+ /// Returns the duration between two time value
+ fn subtract(&self, other: &Self) -> Duration;
+ /// 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 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);
+
+ // First, lets try the nanoseconds precision
+ if let Some(total_ns) = total_span.num_nanoseconds() {
+ if let Some(value_ns) = value_span.num_nanoseconds() {
+ return (f64::from(limit.1 - limit.0) * value_ns as f64 / total_ns as f64) as i32
+ + limit.0;
+ }
+ }
+
+ // 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;
+ let value_days = value_span.num_days() as f64;
+
+ (f64::from(limit.1 - limit.0) * value_days / total_days) as i32 + limit.0
+ }
+}
+
+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 DateType = Date<Z>;
+ fn date_floor(&self) -> Date<Z> {
+ self.clone()
+ }
+ fn date_ceil(&self) -> Date<Z> {
+ self.clone()
+ }
+ fn earliest_after_date(date: Date<Z>) -> Self {
+ date
+ }
+ fn subtract(&self, other: &Date<Z>) -> Duration {
+ self.clone() - other.clone()
+ }
+
+ 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 DateType = Date<Z>;
+ fn date_floor(&self) -> Date<Z> {
+ self.date()
+ }
+ fn date_ceil(&self) -> Date<Z> {
+ if self.time().num_seconds_from_midnight() > 0 {
+ self.date() + Duration::days(1)
+ } else {
+ self.date()
+ }
+ }
+ fn earliest_after_date(date: Date<Z>) -> DateTime<Z> {
+ date.and_hms(0, 0, 0)
+ }
+
+ fn subtract(&self, other: &DateTime<Z>) -> Duration {
+ self.clone() - other.clone()
+ }
+
+ 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<D: Datelike>(D, D);
+
+impl<D: Datelike> From<Range<D>> for RangedDate<D> {
+ fn from(range: Range<D>) -> Self {
+ Self(range.start, range.end)
+ }
+}
+
+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<D> {
+ self.0.clone()..self.1.clone()
+ }
+
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
+ TimeValue::map_coord(value, &self.0, &self.1, limit)
+ }
+
+ 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();
+ let total_weeks = (self.1.clone() - self.0.clone()).num_weeks();
+
+ if total_days > 0 && total_days as usize <= max_points {
+ for day_idx in 0..=total_days {
+ ret.push(self.0.clone() + Duration::days(day_idx));
+ }
+ return ret;
+ }
+
+ if total_weeks > 0 && total_weeks as usize <= max_points {
+ for day_idx in 0..=total_weeks {
+ ret.push(self.0.clone() + Duration::weeks(day_idx));
+ }
+ return ret;
+ }
+
+ let week_per_point = ((total_weeks as f64) / (max_points as f64)).ceil() as usize;
+
+ for idx in 0..=(total_weeks as usize / week_per_point) {
+ ret.push(self.0.clone() + Duration::weeks((idx * week_per_point) as i64));
+ }
+
+ ret
+ }
+}
+
+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 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<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.
+/// We can't use a simple granularity to describe it. Thus we have
+/// this axis decorator to make it yield monthly key-points.
+#[derive(Clone)]
+pub struct Monthly<T: TimeValue>(Range<T>);
+
+impl<T: TimeValue + Datelike + Clone> ValueFormatter<T> for Monthly<T> {
+ fn format(value: &T) -> String {
+ format!("{}-{}", value.year(), value.month())
+ }
+}
+
+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();
+
+ let mut start_year = start_date.year();
+ let mut start_month = start_date.month();
+ let start_day = start_date.day();
+
+ let end_year = end_date.year();
+ let end_month = end_date.month();
+
+ if start_day != 1 {
+ start_month += 1;
+ if start_month == 13 {
+ start_month = 1;
+ start_year += 1;
+ }
+ }
+
+ let total_month = (end_year - start_year) * 12 + end_month as i32 - start_month as i32;
+
+ fn generate_key_points<T: TimeValue>(
+ mut start_year: i32,
+ mut start_month: i32,
+ end_year: i32,
+ end_month: i32,
+ step: u32,
+ 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(builder.ymd(
+ start_year,
+ start_month as u32,
+ 1,
+ )));
+ start_month += step as i32;
+
+ if start_month >= 13 {
+ start_year += start_month / 12;
+ start_month %= 12;
+ }
+ }
+
+ ret
+ }
+
+ if total_month as usize <= max_points {
+ // Monthly
+ return generate_key_points(
+ start_year,
+ start_month as i32,
+ end_year,
+ end_month as i32,
+ 1,
+ &self.0.start,
+ );
+ } else if total_month as usize <= max_points * 3 {
+ // Quarterly
+ return generate_key_points(
+ start_year,
+ start_month as i32,
+ end_year,
+ end_month as i32,
+ 3,
+ &self.0.start,
+ );
+ } else if total_month as usize <= max_points * 6 {
+ // Biyearly
+ return generate_key_points(
+ start_year,
+ start_month as i32,
+ end_year,
+ end_month as i32,
+ 6,
+ &self.0.start,
+ );
+ }
+
+ // Otherwise we could generate the yearly keypoints
+ generate_yearly_keypoints(
+ max_points,
+ start_year,
+ start_month,
+ end_year,
+ end_month,
+ &self.0.start,
+ )
+ }
+}
+
+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;
+ }
+ self.bold_key_points(&hint)
+ }
+}
+
+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()));
+ }
+ 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,
+ )))
+ }
+}
+
+/// Indicate the coord has a yearly granularity.
+#[derive(Clone)]
+pub struct Yearly<T: TimeValue>(Range<T>);
+
+fn generate_yearly_keypoints<T: TimeValue>(
+ max_points: usize,
+ mut start_year: i32,
+ start_month: u32,
+ mut end_year: i32,
+ end_month: u32,
+ builder: &T,
+) -> Vec<T> {
+ if start_month > end_month {
+ end_year -= 1;
+ }
+
+ let mut exp10 = 1;
+
+ while (end_year - start_year + 1) as usize / (exp10 * 10) > max_points {
+ exp10 *= 10;
+ }
+
+ let mut freq = exp10;
+
+ for try_freq in &[1, 2, 5, 10] {
+ freq = *try_freq * exp10;
+ if (end_year - start_year + 1) as usize / (exp10 * *try_freq) <= max_points {
+ break;
+ }
+ }
+
+ let mut ret = vec![];
+
+ while start_year <= end_year {
+ ret.push(T::earliest_after_date(builder.ymd(
+ start_year,
+ start_month,
+ 1,
+ )));
+ start_year += freq as i32;
+ }
+
+ ret
+}
+
+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> {
+ 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 {
+ 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();
+
+ let mut start_year = start_date.year();
+ let mut start_month = start_date.month();
+ let start_day = start_date.day();
+
+ let end_year = end_date.year();
+ let end_month = end_date.month();
+
+ if start_day != 1 {
+ start_month += 1;
+ if start_month == 13 {
+ start_month = 1;
+ start_year += 1;
+ }
+ }
+
+ generate_yearly_keypoints(
+ max_points,
+ start_year,
+ start_month,
+ end_year,
+ end_month,
+ &self.0.start,
+ )
+ }
+}
+
+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 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)
+ }
+}
+
+/// The trait that converts a normal date coord into a yearly one
+pub trait IntoMonthly<T: TimeValue> {
+ fn monthly(self) -> Monthly<T>;
+}
+
+/// The trait that converts a normal date coord into a yearly one
+pub trait IntoYearly<T: TimeValue> {
+ fn yearly(self) -> Yearly<T>;
+}
+
+impl<T: TimeValue> IntoMonthly<T> for Range<T> {
+ fn monthly(self) -> Monthly<T> {
+ Monthly(self)
+ }
+}
+
+impl<T: TimeValue> IntoYearly<T> for Range<T> {
+ fn yearly(self) -> Yearly<T> {
+ Yearly(self)
+ }
+}
+
+/// The ranged coordinate for the date and time
+#[derive(Clone)]
+pub struct RangedDateTime<DT: Datelike + Timelike + TimeValue>(DT, DT);
+
+impl<Z: TimeZone> AsRangedCoord for Range<DateTime<Z>> {
+ type CoordDescType = RangedDateTime<DateTime<Z>>;
+ type Value = DateTime<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 From<Range<NaiveDateTime>> for RangedDateTime<NaiveDateTime> {
+ fn from(range: Range<NaiveDateTime>) -> Self {
+ Self(range.start, range.end)
+ }
+}
+
+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()
+ }
+
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
+ TimeValue::map_coord(value, &self.0, &self.1, limit)
+ }
+
+ 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.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![];
+
+ while start_time < self.1 {
+ ret.push(start_time.clone());
+ start_time = start_time + Duration::nanoseconds(actual_ns_per_point as i64);
+ }
+
+ return ret;
+ }
+ }
+
+ // Otherwise, it actually behaves like a date
+ let date_range = RangedDate(self.0.date_ceil(), self.1.date_floor());
+
+ date_range
+ .key_points(max_points)
+ .into_iter()
+ .map(DT::from_date)
+ .collect()
+ }
+}
+
+/// The coordinate that for duration of time
+#[derive(Clone)]
+pub struct RangedDuration(Duration, Duration);
+
+impl AsRangedCoord for Range<Duration> {
+ type CoordDescType = RangedDuration;
+ type Value = Duration;
+}
+
+impl From<Range<Duration>> for RangedDuration {
+ fn from(range: Range<Duration>) -> Self {
+ Self(range.start, range.end)
+ }
+}
+
+impl Ranged for RangedDuration {
+ type FormatOption = DefaultFormatting;
+ type ValueType = Duration;
+
+ fn range(&self) -> Range<Duration> {
+ self.0..self.1
+ }
+
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
+ let total_span = self.1 - self.0;
+ let value_span = *value - self.0;
+
+ if let Some(total_ns) = total_span.num_nanoseconds() {
+ if let Some(value_ns) = value_span.num_nanoseconds() {
+ return limit.0
+ + (f64::from(limit.1 - limit.0) * value_ns as f64 / total_ns as f64 + 1e-10)
+ as i32;
+ }
+ return limit.1;
+ }
+
+ let total_days = total_span.num_days();
+ let value_days = value_span.num_days();
+
+ limit.0
+ + (f64::from(limit.1 - limit.0) * value_days as f64 / total_days as f64 + 1e-10) as i32
+ }
+
+ 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() {
+ if let Some(period) = compute_period_per_point(total_ns as u64, max_points, false) {
+ let mut start_ns = self.0.num_nanoseconds().unwrap();
+
+ if start_ns as u64 % period > 0 {
+ if start_ns > 0 {
+ start_ns += period as i64 - (start_ns % period as i64);
+ } else {
+ start_ns -= start_ns % period as i64;
+ }
+ }
+
+ let mut current = Duration::nanoseconds(start_ns);
+ let mut ret = vec![];
+
+ while current < self.1 {
+ ret.push(current);
+ current = current + Duration::nanoseconds(period as i64);
+ }
+
+ return ret;
+ }
+ }
+
+ let begin_days = self.0.num_days();
+ let end_days = self.1.num_days();
+
+ let mut days_per_tick = 1;
+ let mut idx = 0;
+ const MULTIPLIER: &[i32] = &[1, 2, 5];
+
+ while (end_days - begin_days) / i64::from(days_per_tick * MULTIPLIER[idx])
+ > max_points as i64
+ {
+ idx += 1;
+ if idx == MULTIPLIER.len() {
+ idx = 0;
+ days_per_tick *= 10;
+ }
+ }
+
+ days_per_tick *= MULTIPLIER[idx];
+
+ let mut ret = vec![];
+
+ let mut current = Duration::days(
+ self.0.num_days()
+ + if Duration::days(self.0.num_days()) != self.0 {
+ 1
+ } else {
+ 0
+ },
+ );
+
+ while current < self.1 {
+ ret.push(current);
+ current = current + Duration::days(i64::from(days_per_tick));
+ }
+
+ ret
+ }
+}
+
+#[allow(clippy::inconsistent_digit_grouping)]
+fn compute_period_per_point(total_ns: u64, max_points: usize, sub_daily: bool) -> Option<u64> {
+ let min_ns_per_point = total_ns as f64 / max_points as f64;
+ let actual_ns_per_point: u64 = (10u64).pow((min_ns_per_point as f64).log10().floor() as u32);
+
+ fn determine_actual_ns_per_point(
+ total_ns: u64,
+ mut actual_ns_per_point: u64,
+ units: &[u64],
+ base: u64,
+ max_points: usize,
+ ) -> u64 {
+ let mut unit_per_point_idx = 0;
+ while total_ns / actual_ns_per_point > max_points as u64 * units[unit_per_point_idx] {
+ unit_per_point_idx += 1;
+ if unit_per_point_idx == units.len() {
+ unit_per_point_idx = 0;
+ actual_ns_per_point *= base;
+ }
+ }
+ units[unit_per_point_idx] * actual_ns_per_point
+ }
+
+ if actual_ns_per_point < 1_000_000_000 {
+ Some(determine_actual_ns_per_point(
+ total_ns as u64,
+ actual_ns_per_point,
+ &[1, 2, 5],
+ 10,
+ max_points,
+ ))
+ } else if actual_ns_per_point < 3600_000_000_000 {
+ Some(determine_actual_ns_per_point(
+ total_ns as u64,
+ 1_000_000_000,
+ &[1, 2, 5, 10, 15, 20, 30],
+ 60,
+ max_points,
+ ))
+ } else if actual_ns_per_point < 3600_000_000_000 * 24 {
+ Some(determine_actual_ns_per_point(
+ total_ns as u64,
+ 3600_000_000_000,
+ &[1, 2, 4, 8, 12],
+ 24,
+ max_points,
+ ))
+ } else if !sub_daily {
+ if actual_ns_per_point < 3600_000_000_000 * 24 * 10 {
+ Some(determine_actual_ns_per_point(
+ total_ns as u64,
+ 3600_000_000_000 * 24,
+ &[1, 2, 5, 7],
+ 10,
+ max_points,
+ ))
+ } else {
+ Some(determine_actual_ns_per_point(
+ total_ns as u64,
+ 3600_000_000_000 * 24 * 10,
+ &[1, 2, 5],
+ 10,
+ max_points,
+ ))
+ }
+ } else {
+ None
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use chrono::{TimeZone, Utc};
+
+ #[test]
+ fn test_date_range_long() {
+ let range = Utc.ymd(1000, 1, 1)..Utc.ymd(2999, 1, 1);
+
+ let ranged_coord = Into::<RangedDate<_>>::into(range);
+
+ assert_eq!(ranged_coord.map(&Utc.ymd(1000, 8, 10), (0, 100)), 0);
+ assert_eq!(ranged_coord.map(&Utc.ymd(2999, 8, 10), (0, 100)), 100);
+
+ let kps = ranged_coord.key_points(23);
+
+ assert!(kps.len() <= 23);
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_days())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_days())
+ .min()
+ .unwrap();
+ assert_eq!(max, min);
+ assert_eq!(max % 7, 0);
+ }
+
+ #[test]
+ fn test_date_range_short() {
+ let range = Utc.ymd(2019, 1, 1)..Utc.ymd(2019, 1, 21);
+ let ranged_coord = Into::<RangedDate<_>>::into(range);
+
+ let kps = ranged_coord.key_points(4);
+
+ assert_eq!(kps.len(), 3);
+
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_days())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_days())
+ .min()
+ .unwrap();
+ assert_eq!(max, min);
+ assert_eq!(max, 7);
+
+ let kps = ranged_coord.key_points(30);
+ assert_eq!(kps.len(), 21);
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_days())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_days())
+ .min()
+ .unwrap();
+ assert_eq!(max, min);
+ assert_eq!(max, 1);
+ }
+
+ #[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();
+
+ assert_eq!(ranged_coord.map(&Utc.ymd(1000, 8, 10), (0, 100)), 0);
+ assert_eq!(ranged_coord.map(&Utc.ymd(2999, 8, 10), (0, 100)), 100);
+
+ let kps = ranged_coord.key_points(23);
+
+ assert!(kps.len() <= 23);
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_days())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_days())
+ .min()
+ .unwrap();
+ assert!(max != min);
+
+ assert!(kps.into_iter().all(|x| x.month() == 9 && x.day() == 1));
+
+ let range = Utc.ymd(2019, 8, 5)..Utc.ymd(2020, 1, 1);
+ let ranged_coord = range.yearly();
+ let kps = ranged_coord.key_points(BoldPoints(23));
+ assert!(kps.len() == 1);
+ }
+
+ #[test]
+ fn test_monthly_date_range() {
+ let range = Utc.ymd(2019, 8, 5)..Utc.ymd(2020, 9, 1);
+ let ranged_coord = range.monthly();
+
+ 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(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();
+ assert_eq!(kps, vec![9, 12, 3, 6, 9]);
+
+ // TODO: Investigate why max_point = 1 breaks the contract
+ let kps = ranged_coord.key_points(3);
+ assert!(kps.len() == 3);
+ assert!(kps.iter().all(|x| x.day() == 1));
+ let kps: Vec<_> = kps.into_iter().map(|x| x.month()).collect();
+ assert_eq!(kps, vec![9, 3, 9]);
+ }
+
+ #[test]
+ fn test_datetime_long_range() {
+ let coord: RangedDateTime<_> =
+ (Utc.ymd(1000, 1, 1).and_hms(0, 0, 0)..Utc.ymd(3000, 1, 1).and_hms(0, 0, 0)).into();
+
+ assert_eq!(
+ coord.map(&Utc.ymd(1000, 1, 1).and_hms(0, 0, 0), (0, 100)),
+ 0
+ );
+ assert_eq!(
+ coord.map(&Utc.ymd(3000, 1, 1).and_hms(0, 0, 0), (0, 100)),
+ 100
+ );
+
+ let kps = coord.key_points(23);
+
+ assert!(kps.len() <= 23);
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .min()
+ .unwrap();
+ assert!(max == min);
+ assert!(max % (24 * 3600 * 7) == 0);
+ }
+
+ #[test]
+ fn test_datetime_medium_range() {
+ let coord: RangedDateTime<_> =
+ (Utc.ymd(2019, 1, 1).and_hms(0, 0, 0)..Utc.ymd(2019, 1, 11).and_hms(0, 0, 0)).into();
+
+ let kps = coord.key_points(23);
+
+ assert!(kps.len() <= 23);
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .min()
+ .unwrap();
+ assert!(max == min);
+ assert_eq!(max, 12 * 3600);
+ }
+
+ #[test]
+ fn test_datetime_short_range() {
+ let coord: RangedDateTime<_> =
+ (Utc.ymd(2019, 1, 1).and_hms(0, 0, 0)..Utc.ymd(2019, 1, 2).and_hms(0, 0, 0)).into();
+
+ let kps = coord.key_points(50);
+
+ assert!(kps.len() <= 50);
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .min()
+ .unwrap();
+ assert!(max == min);
+ assert_eq!(max, 1800);
+ }
+
+ #[test]
+ fn test_datetime_nano_range() {
+ let start = Utc.ymd(2019, 1, 1).and_hms(0, 0, 0);
+ let end = start.clone() + Duration::nanoseconds(100);
+ let coord: RangedDateTime<_> = (start..end).into();
+
+ let kps = coord.key_points(50);
+
+ assert!(kps.len() <= 50);
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_nanoseconds().unwrap())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_nanoseconds().unwrap())
+ .min()
+ .unwrap();
+ assert!(max == min);
+ assert_eq!(max, 2);
+ }
+
+ #[test]
+ fn test_duration_long_range() {
+ let coord: RangedDuration = (Duration::days(-1000000)..Duration::days(1000000)).into();
+
+ assert_eq!(coord.map(&Duration::days(-1000000), (0, 100)), 0);
+ assert_eq!(coord.map(&Duration::days(1000000), (0, 100)), 100);
+
+ let kps = coord.key_points(23);
+
+ assert!(kps.len() <= 23);
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .min()
+ .unwrap();
+ assert!(max == min);
+ assert!(max % (24 * 3600 * 10000) == 0);
+ }
+
+ #[test]
+ fn test_duration_daily_range() {
+ let coord: RangedDuration = (Duration::days(0)..Duration::hours(25)).into();
+
+ let kps = coord.key_points(23);
+
+ assert!(kps.len() <= 23);
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .min()
+ .unwrap();
+ 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/ranged1d/types/numeric.rs b/src/coord/ranged1d/types/numeric.rs
new file mode 100644
index 0000000..6de2bdf
--- /dev/null
+++ b/src/coord/ranged1d/types/numeric.rs
@@ -0,0 +1,361 @@
+use std::convert::TryFrom;
+use std::ops::Range;
+
+use crate::coord::ranged1d::{
+ AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged,
+ ReversibleRanged, ValueFormatter,
+};
+
+macro_rules! impl_discrete_trait {
+ ($name:ident) => {
+ impl DiscreteRanged for $name {
+ 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 from_index(&self, index: usize) -> Option<Self::ValueType> {
+ if let Ok(index) = Self::ValueType::try_from(index) {
+ return Some(self.0 + index);
+ }
+ None
+ }
+ }
+ };
+}
+
+macro_rules! impl_ranged_type_trait {
+ ($value:ty, $coord:ident) => {
+ impl AsRangedCoord for Range<$value> {
+ type CoordDescType = $coord;
+ type Value = $value;
+ }
+ };
+}
+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, $fmt: ident) => {
+ #[doc = $doc]
+ #[derive(Clone)]
+ pub struct $name($type, $type);
+ impl From<Range<$type>> for $name {
+ fn from(range: Range<$type>) -> Self {
+ return $name(range.start, range.end);
+ }
+ }
+ 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 {
+ return limit.1;
+ }
+
+ return limit.0 + (actual_length as f64 * logic_length + 1e-3).floor() as i32;
+ }
+ 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;
+ }
+ }
+ };
+ ($type:ty, $name:ident, $key_points:ident, $doc: expr) => {
+ make_numeric_coord!($type, $name, $key_points, $doc, DefaultFormatting);
+ }
+}
+
+macro_rules! gen_key_points_comp {
+ (float, $name:ident, $type:ty) => {
+ fn $name(range: ($type, $type), max_points: usize) -> Vec<$type> {
+ if max_points == 0 {
+ return vec![];
+ }
+
+ let range = (range.0 as f64, range.1 as f64);
+ let mut scale = (10f64).powf((range.1 - range.0).log(10.0).floor());
+ let mut digits = -(range.1 - range.0).log(10.0).floor() as i32 + 1;
+ fn rem_euclid(a: f64, b: f64) -> f64 {
+ if b > 0.0 {
+ a - (a / b).floor() * b
+ } else {
+ a - (a / b).ceil() * b
+ }
+ }
+
+ // At this point we need to make sure that the loop invariant:
+ // The scale must yield number of points than requested
+ if 1 + ((range.1 - range.0) / scale).floor() as usize > max_points {
+ scale *= 10.0;
+ }
+
+ 'outer: loop {
+ let old_scale = scale;
+ for nxt in [2.0, 5.0, 10.0].iter() {
+ let new_left = range.0 + scale / nxt - rem_euclid(range.0, scale / nxt);
+ let new_right = range.1 - rem_euclid(range.1, scale / nxt);
+
+ let npoints = 1 + ((new_right - new_left) / old_scale * nxt) as usize;
+
+ if npoints > max_points {
+ break 'outer;
+ }
+
+ scale = old_scale / nxt;
+ }
+ scale = old_scale / 10.0;
+ if scale < 1.0 {
+ digits += 1;
+ }
+ }
+
+ let mut ret = vec![];
+ let mut left = range.0 + scale - rem_euclid(range.0, scale);
+ let right = range.1 - rem_euclid(range.1, scale);
+ while left <= right {
+ let size = (10f64).powf(digits as f64 + 1.0);
+ let new_left = (left * size).abs() + 1e-3;
+ if left < 0.0 {
+ left = -new_left.round() / size;
+ } else {
+ left = new_left.round() / size;
+ }
+ ret.push(left as $type);
+ left += scale;
+ }
+ return ret;
+ }
+ };
+ (integer, $name:ident, $type:ty) => {
+ fn $name(range: ($type, $type), max_points: usize) -> Vec<$type> {
+ let mut scale: $type = 1;
+ let range = (range.0.min(range.1), range.0.max(range.1));
+ 'outer: while (range.1 - range.0 + scale - 1) as usize / (scale as usize) > max_points {
+ let next_scale = scale * 10;
+ for new_scale in [scale * 2, scale * 5, scale * 10].iter() {
+ scale = *new_scale;
+ if (range.1 - range.0 + *new_scale - 1) as usize / (*new_scale as usize)
+ < max_points
+ {
+ break 'outer;
+ }
+ }
+ scale = next_scale;
+ }
+
+ let (mut left, right) = (
+ range.0 + (scale - range.0 % scale) % scale,
+ range.1 - range.1 % scale,
+ );
+
+ let mut ret = vec![];
+ while left <= right {
+ ret.push(left as $type);
+ left += scale;
+ }
+
+ return ret;
+ }
+ };
+}
+
+gen_key_points_comp!(float, compute_f32_key_points, f32);
+gen_key_points_comp!(float, compute_f64_key_points, f64);
+gen_key_points_comp!(integer, compute_i32_key_points, i32);
+gen_key_points_comp!(integer, compute_u32_key_points, u32);
+gen_key_points_comp!(integer, compute_i64_key_points, i64);
+gen_key_points_comp!(integer, compute_u64_key_points, u64);
+gen_key_points_comp!(integer, compute_i128_key_points, i128);
+gen_key_points_comp!(integer, compute_u128_key_points, u128);
+gen_key_points_comp!(integer, compute_isize_key_points, isize);
+gen_key_points_comp!(integer, compute_usize_key_points, usize);
+
+make_numeric_coord!(
+ f32,
+ RangedCoordf32,
+ compute_f32_key_points,
+ "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",
+ 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,
+ compute_u32_key_points,
+ "The ranged coordinate for type u32"
+);
+make_numeric_coord!(
+ i32,
+ RangedCoordi32,
+ compute_i32_key_points,
+ "The ranged coordinate for type i32"
+);
+make_numeric_coord!(
+ u64,
+ RangedCoordu64,
+ compute_u64_key_points,
+ "The ranged coordinate for type u64"
+);
+make_numeric_coord!(
+ i64,
+ RangedCoordi64,
+ compute_i64_key_points,
+ "The ranged coordinate for type i64"
+);
+make_numeric_coord!(
+ u128,
+ RangedCoordu128,
+ compute_u128_key_points,
+ "The ranged coordinate for type u128"
+);
+make_numeric_coord!(
+ i128,
+ RangedCoordi128,
+ compute_i128_key_points,
+ "The ranged coordinate for type i128"
+);
+make_numeric_coord!(
+ usize,
+ RangedCoordusize,
+ compute_usize_key_points,
+ "The ranged coordinate for type usize"
+);
+make_numeric_coord!(
+ isize,
+ RangedCoordisize,
+ compute_isize_key_points,
+ "The ranged coordinate for type isize"
+);
+
+impl_discrete_trait!(RangedCoordu32);
+impl_discrete_trait!(RangedCoordi32);
+impl_discrete_trait!(RangedCoordu64);
+impl_discrete_trait!(RangedCoordi64);
+impl_discrete_trait!(RangedCoordu128);
+impl_discrete_trait!(RangedCoordi128);
+impl_discrete_trait!(RangedCoordusize);
+impl_discrete_trait!(RangedCoordisize);
+
+impl_ranged_type_trait!(f32, RangedCoordf32);
+impl_ranged_type_trait!(f64, RangedCoordf64);
+impl_ranged_type_trait!(i32, RangedCoordi32);
+impl_ranged_type_trait!(u32, RangedCoordu32);
+impl_ranged_type_trait!(i64, RangedCoordi64);
+impl_ranged_type_trait!(u64, RangedCoordu64);
+impl_ranged_type_trait!(i128, RangedCoordi128);
+impl_ranged_type_trait!(u128, RangedCoordu128);
+impl_ranged_type_trait!(isize, RangedCoordisize);
+impl_ranged_type_trait!(usize, RangedCoordusize);
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ #[test]
+ fn test_key_points() {
+ let kp = compute_i32_key_points((0, 999), 28);
+
+ assert!(kp.len() > 0);
+ assert!(kp.len() <= 28);
+
+ let kp = compute_f64_key_points((-1.2, 1.2), 1);
+ assert!(kp.len() == 1);
+
+ let kp = compute_f64_key_points((-1.2, 1.2), 0);
+ assert!(kp.len() == 0);
+ }
+
+ #[test]
+ fn test_linear_coord_map() {
+ let coord: RangedCoordu32 = (0..20).into();
+ assert_eq!(coord.key_points(11).len(), 11);
+ assert_eq!(coord.key_points(11)[0], 0);
+ assert_eq!(coord.key_points(11)[10], 20);
+ assert_eq!(coord.map(&5, (0, 100)), 25);
+
+ let coord: RangedCoordf32 = (0f32..20f32).into();
+ assert_eq!(coord.map(&5.0, (0, 100)), 25);
+ }
+
+ #[test]
+ fn test_linear_coord_system() {
+ let _coord =
+ 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));
+ }
+}