From 3591cb5b1f7df389f924188cd66e65fa373bd875 Mon Sep 17 00:00:00 2001 From: Hao Hou Date: Sat, 30 Nov 2019 10:36:25 -0700 Subject: [PATCH 1/8] Start working on the discrete refactoring --- examples/normal-dist2.rs | 2 + src/coord/datetime.rs | 94 ++++++++++++++++++---------- src/coord/mod.rs | 3 +- src/coord/numeric.rs | 128 +++++++-------------------------------- src/coord/ranged.rs | 97 +++++++---------------------- src/series/histogram.rs | 107 ++++++++++---------------------- 6 files changed, 142 insertions(+), 289 deletions(-) diff --git a/examples/normal-dist2.rs b/examples/normal-dist2.rs index 6155ea65..72323aa3 100644 --- a/examples/normal-dist2.rs +++ b/examples/normal-dist2.rs @@ -6,6 +6,8 @@ use rand_xorshift::XorShiftRng; use num_traits::sign::Signed; +fn main() {} +#[cfg(feature = "disabled")] fn main() -> Result<(), Box> { let sd = 0.60; diff --git a/src/coord/datetime.rs b/src/coord/datetime.rs index cb96f937..45340a0b 100644 --- a/src/coord/datetime.rs +++ b/src/coord/datetime.rs @@ -135,14 +135,20 @@ impl Ranged for RangedDate { } impl DiscreteRanged for RangedDate { - type RangeParameter = (); - fn get_range_parameter(&self) {} - fn next_value(this: &Date, _: &()) -> Date { - this.clone() + Duration::days(1) + fn size(&self) -> usize { + ((self.1.clone() - self.0.clone()).num_days().max(0) + 1) as usize } - fn previous_value(this: &Date, _: &()) -> Date { - this.clone() - Duration::days(1) + fn index_of(&self, value: &Date) -> Option { + 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> { + Some(self.0.clone() + Duration::days(index as i64)) } } @@ -267,28 +273,39 @@ impl Ranged for Monthly { } impl DiscreteRanged for Monthly { - type RangeParameter = (); - fn get_range_parameter(&self) {} - fn next_value(this: &T, _: &()) -> T { - let mut year = this.date_ceil().year(); - let mut month = this.date_ceil().month(); - month += 1; - if month == 13 { - month = 1; - year += 1; + 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 + (start_month as i32) + (end_month as i32 - 1) + 1).max(0) as usize + } + + fn index_of(&self, value: &T) -> Option { + 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); } - T::earliest_after_date(this.timezone().ymd(year, month, this.date_ceil().day())) + None } - fn previous_value(this: &T, _: &()) -> T { - let mut year = this.clone().date_floor().year(); - let mut month = this.clone().date_floor().month(); - month -= 1; - if month == 0 { - month = 12; - year -= 1; - } - T::earliest_after_date(this.timezone().ymd(year, month, this.date_floor().day())) + fn from_index(&self, index: usize) -> Option { + 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.timezone().ymd(year, month as u32, self.0.start.date_ceil().day()) + )) } } @@ -380,14 +397,29 @@ impl Ranged for Yearly { } impl DiscreteRanged for Yearly { - type RangeParameter = (); - fn get_range_parameter(&self) {} - fn next_value(this: &T, _: &()) -> T { - T::earliest_after_date(this.timezone().ymd(this.date_floor().year() + 1, 1, 1)) + 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 + 1) as usize + } + + fn index_of(&self, value: &T) -> Option { + 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 previous_value(this: &T, _: &()) -> T { - T::earliest_after_date(this.timezone().ymd(this.date_ceil().year() - 1, 1, 1)) + fn from_index(&self, index: usize) -> Option { + let year = self.0.start.date_ceil().year() + index as i32; + let ret = T::earliest_after_date(self.0.start.timezone().ymd(year, 1, 1)); + if ret.date_ceil() <= self.0.start.date_floor() { + return Some(self.0.start.clone()) + } + Some(ret) } } diff --git a/src/coord/mod.rs b/src/coord/mod.rs index 0afafa32..77d0df1c 100644 --- a/src/coord/mod.rs +++ b/src/coord/mod.rs @@ -28,6 +28,7 @@ mod datetime; mod logarithmic; mod numeric; mod ranged; +mod group_by; #[cfg(feature = "chrono")] pub use datetime::{IntoMonthly, IntoYearly, RangedDate, RangedDateTime, RangedDuration}; @@ -44,7 +45,7 @@ pub use ranged::make_partial_axis; pub use logarithmic::{LogCoord, LogRange, LogScalable}; -pub use numeric::group_integer_by::{GroupBy, ToGroupByRange}; +pub use group_by::{GroupBy, ToGroupByRange}; use std::rc::Rc; use std::sync::Arc; diff --git a/src/coord/numeric.rs b/src/coord/numeric.rs index 6a9f72d2..bb375158 100644 --- a/src/coord/numeric.rs +++ b/src/coord/numeric.rs @@ -1,17 +1,32 @@ use std::ops::Range; +use std::convert::TryFrom; use super::{AsRangedCoord, DiscreteRanged, Ranged, ReversibleRanged}; macro_rules! impl_discrete_trait { ($name:ident) => { impl DiscreteRanged for $name { - type RangeParameter = (); - fn get_range_parameter(&self) -> () {} - fn next_value(this: &Self::ValueType, _: &()) -> Self::ValueType { - return *this + 1; + fn size(&self) -> usize { + if &self.1 < &self.0 { + return 0; + } + let values = self.1 - self.0; + (values + 1) as usize } - fn previous_value(this: &Self::ValueType, _: &()) -> Self::ValueType { - return *this - 1; + + fn index_of(&self, value: &Self::ValueType) -> Option { + if value < &self.0 { + return None; + } + let ret = value - self.0; + Some(ret as usize) + } + + fn from_index(&self, index: usize) -> Option { + if let Ok(index) = Self::ValueType::try_from(index){ + return Some(self.0 + index); + } + None } } }; @@ -256,107 +271,6 @@ impl_ranged_type_trait!(u128, RangedCoordu128); impl_ranged_type_trait!(isize, RangedCoordisize); impl_ranged_type_trait!(usize, RangedCoordusize); -// TODO: Think about how to re-organize this part -pub mod group_integer_by { - use super::Ranged; - use super::{AsRangedCoord, DiscreteRanged}; - use num_traits::{FromPrimitive, PrimInt, ToPrimitive}; - use std::ops::{Mul, Range}; - - /// The ranged value spec that needs to be grouped. - /// This is useful, for example, when we have an X axis is a integer and denotes days. - /// And we are expecting the tick mark denotes weeks, in this way we can make the range - /// spec grouping by 7 elements. - pub struct GroupBy(T, T::ValueType) - where - T::ValueType: PrimInt + ToPrimitive + FromPrimitive + Mul, - T: Ranged; - - /// The trait that provides method `Self::group_by` function which creates a - /// `GroupBy` decorated ranged value. - pub trait ToGroupByRange - where - Self: AsRangedCoord, - ::Value: PrimInt + ToPrimitive + FromPrimitive + Mul, - <::CoordDescType as Ranged>::ValueType: - PrimInt + ToPrimitive + FromPrimitive + Mul, - { - /// Make a grouping ranged value, see the documentation for `GroupBy` for details. - /// - /// - `value`: The number of values we want to group it - /// - **return**: The newly created grouping range sepcification - fn group_by( - self, - value: <::CoordDescType as Ranged>::ValueType, - ) -> GroupBy<::CoordDescType> { - GroupBy(self.into(), value) - } - } - - impl ToGroupByRange for T - where - Self: AsRangedCoord, - ::Value: PrimInt + FromPrimitive + ToPrimitive + Mul, - <::CoordDescType as Ranged>::ValueType: - PrimInt + FromPrimitive + ToPrimitive + Mul, - { - } - - impl AsRangedCoord for GroupBy - where - T::ValueType: PrimInt + ToPrimitive + FromPrimitive + Mul, - T: Ranged, - { - type Value = T::ValueType; - type CoordDescType = Self; - } - - impl DiscreteRanged for GroupBy - where - T::ValueType: PrimInt + ToPrimitive + FromPrimitive + Mul, - T: Ranged + DiscreteRanged, - { - type RangeParameter = ::RangeParameter; - fn get_range_parameter(&self) -> Self::RangeParameter { - self.0.get_range_parameter() - } - fn previous_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType { - ::previous_value(this, param) - } - fn next_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType { - ::next_value(this, param) - } - } - - impl Ranged for GroupBy - where - T::ValueType: PrimInt + ToPrimitive + FromPrimitive + Mul, - T: Ranged, - { - type ValueType = T::ValueType; - fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> i32 { - self.0.map(value, limit) - } - fn range(&self) -> Range { - self.0.range() - } - fn key_points(&self, max_points: usize) -> Vec { - let actual_range = self.0.range(); - let from = ((actual_range.start + self.1 - T::ValueType::from_u8(1).unwrap()) / self.1) - .to_isize() - .unwrap(); - let to = (actual_range.end / self.1).to_isize().unwrap(); - let logic_range: super::RangedCoordisize = (from..to).into(); - - logic_range - .key_points(max_points) - .into_iter() - .map(|x| T::ValueType::from_isize(x).unwrap() * self.1) - .collect() - } - } -} - #[cfg(test)] mod test { use super::*; diff --git a/src/coord/ranged.rs b/src/coord/ranged.rs index b7f3ffd7..5071aa68 100644 --- a/src/coord/ranged.rs +++ b/src/coord/ranged.rs @@ -177,15 +177,23 @@ pub trait DiscreteRanged where Self: Ranged, { - type RangeParameter; - - fn get_range_parameter(&self) -> Self::RangeParameter; + /// 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; - /// Get the smallest value that is larger than the `this` value - fn next_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType; + /// Map a value to the index + /// + /// - `value`: The value to map + /// - **returns** The index of the value + fn index_of(&self, value: &Self::ValueType) -> Option; - /// Get the largest value that is smaller than `this` value - fn previous_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType; + /// Reverse map the index to the value + /// + /// - `value`: The index to map + /// - **returns** The value + fn from_index(&self, index: usize) -> Option; } /// The trait for the type that can be converted into a ranged coordinate axis @@ -223,67 +231,6 @@ where } } -impl IntoCentric for T -where - T::CoordDescType: DiscreteRanged, - ::ValueType: Eq, -{ -} - -impl Clone for CentricDiscreteRange -where - ::ValueType: Eq, -{ - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - -impl Ranged for CentricDiscreteRange -where - ::ValueType: Eq, -{ - type ValueType = ::ValueType; - - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { - let prev = ::previous_value(&value, &self.0.get_range_parameter()); - (self.0.map(&prev, limit) + self.0.map(value, limit)) / 2 - } - - fn key_points(&self, max_points: usize) -> Vec { - self.0.key_points(max_points) - } - - fn range(&self) -> Range { - self.0.range() - } -} - -impl DiscreteRanged for CentricDiscreteRange -where - ::ValueType: Eq, -{ - type RangeParameter = ::RangeParameter; - fn get_range_parameter(&self) -> Self::RangeParameter { - self.0.get_range_parameter() - } - fn next_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType { - ::next_value(this, param) - } - - fn previous_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType { - ::previous_value(this, param) - } -} - -impl AsRangedCoord for CentricDiscreteRange -where - ::ValueType: Eq, -{ - type CoordDescType = Self; - type Value = ::ValueType; -} - /// This axis decorator will make the axis partially display on the axis. /// At some time, we want the axis only covers some part of the value. /// This decorator will have an additional display range defined. @@ -345,16 +292,16 @@ where R: Ranged, ::ValueType: Eq + Clone, { - type RangeParameter = ::RangeParameter; - fn get_range_parameter(&self) -> Self::RangeParameter { - self.0.get_range_parameter() + fn size(&self) -> usize { + self.0.size() } - fn next_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType { - ::next_value(this, param) + + fn index_of(&self, value: &R::ValueType) -> Option { + self.0.index_of(value) } - fn previous_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType { - ::previous_value(this, param) + fn from_index(&self, index: usize) -> Option { + self.0.from_index(index) } } diff --git a/src/series/histogram.rs b/src/series/histogram.rs index 75c2fb2a..14354e35 100644 --- a/src/series/histogram.rs +++ b/src/series/histogram.rs @@ -1,5 +1,4 @@ use std::collections::{hash_map::IntoIter as HashMapIter, HashMap}; -use std::hash::Hash; use std::marker::PhantomData; use std::ops::AddAssign; @@ -20,32 +19,30 @@ impl HistogramType for Horizontal {} pub struct Histogram<'a, BR, A, Tag = Vertical> where BR: DiscreteRanged, - BR::ValueType: Eq + Hash, A: AddAssign + Default, Tag: HistogramType, { style: Box ShapeStyle + 'a>, margin: u32, - iter: HashMapIter, - baseline: Box A + 'a>, - br_param: BR::RangeParameter, - _p: PhantomData<(BR, Tag)>, + iter: HashMapIter, + baseline: Box A + 'a>, + br: BR, + _p: PhantomData, } impl<'a, BR, A, Tag> Histogram<'a, BR, A, Tag> where - BR: DiscreteRanged, - BR::ValueType: Eq + Hash, + BR: DiscreteRanged + Clone, A: AddAssign + Default + 'a, Tag: HistogramType, { - fn empty(br_param: BR::RangeParameter) -> Self { + fn empty(br: &BR) -> Self { Self { style: Box::new(|_, _| GREEN.filled()), margin: 5, iter: HashMap::new().into_iter(), baseline: Box::new(|_| A::default()), - br_param, + br: br.clone(), _p: PhantomData, } } @@ -75,7 +72,7 @@ where } /// Set a function that defines variant baseline - pub fn baseline_func(mut self, func: impl Fn(BR::ValueType) -> A + 'a) -> Self { + pub fn baseline_func(mut self, func: impl Fn(&BR::ValueType) -> A + 'a) -> Self { self.baseline = Box::new(func); self } @@ -88,67 +85,29 @@ where /// Set the data iterator pub fn data>(mut self, iter: I) -> Self { - let mut buffer = HashMap::::new(); + let mut buffer = HashMap::::new(); for (x, y) in iter.into_iter() { - *buffer.entry(x).or_insert_with(Default::default) += y; + if let Some(x) = self.br.index_of(&x) { + *buffer.entry(x).or_insert_with(Default::default) += y; + } } self.iter = buffer.into_iter(); self } } -pub trait UseDefaultParameter: Default { - fn new() -> Self { - Default::default() - } -} - -impl UseDefaultParameter for () {} - impl<'a, BR, A> Histogram<'a, BR, A, Vertical> where - BR: DiscreteRanged, - BR::ValueType: Eq + Hash, + BR: DiscreteRanged + Clone, A: AddAssign + Default + 'a, { - /// Create a new histogram series. - /// - /// - `iter`: The data iterator - /// - `margin`: The margin between bars - /// - `style`: The style of bars - /// - /// Returns the newly created histogram series - #[allow(clippy::redundant_closure)] - pub fn new, I: IntoIterator>( - iter: I, - margin: u32, - style: S, - ) -> Self - where - BR::RangeParameter: UseDefaultParameter, - { - let mut buffer = HashMap::::new(); - for (x, y) in iter.into_iter() { - *buffer.entry(x).or_insert_with(Default::default) += y; - } - let style = style.into(); - Self { - style: Box::new(move |_, _| style.clone()), - margin, - iter: buffer.into_iter(), - baseline: Box::new(|_| A::default()), - br_param: BR::RangeParameter::new(), - _p: PhantomData, - } - } - pub fn vertical( parent: &ChartContext>, ) -> Self where ACoord: Ranged, { - let dp = parent.as_coord_spec().x_spec().get_range_parameter(); + let dp = parent.as_coord_spec().x_spec(); Self::empty(dp) } @@ -156,8 +115,7 @@ where impl<'a, BR, A> Histogram<'a, BR, A, Horizontal> where - BR: DiscreteRanged, - BR::ValueType: Eq + Hash, + BR: DiscreteRanged + Clone, A: AddAssign + Default + 'a, { pub fn horizontal( @@ -166,7 +124,7 @@ where where ACoord: Ranged, { - let dp = parent.as_coord_spec().y_spec().get_range_parameter(); + let dp = parent.as_coord_spec().y_spec(); Self::empty(dp) } } @@ -174,18 +132,18 @@ where impl<'a, BR, A> Iterator for Histogram<'a, BR, A, Vertical> where BR: DiscreteRanged, - BR::ValueType: Eq + Hash, A: AddAssign + Default, { type Item = Rectangle<(BR::ValueType, A)>; fn next(&mut self) -> Option { - if let Some((x, y)) = self.iter.next() { - let nx = BR::next_value(&x, &self.br_param); - let base = (self.baseline)(BR::previous_value(&nx, &self.br_param)); - let style = (self.style)(&x, &y); - let mut rect = Rectangle::new([(x, y), (nx, base)], style); - rect.set_margin(0, 0, self.margin, self.margin); - return Some(rect); + while let Some((x, y)) = self.iter.next() { + if let Some((x, Some(nx))) = self.br.from_index(x).map(|v| (v, self.br.from_index(x + 1))) { + let base = (self.baseline)(&x); + let style = (self.style)(&x, &y); + let mut rect = Rectangle::new([(x, y), (nx, base)], style); + rect.set_margin(0, 0, self.margin, self.margin); + return Some(rect) + } } None } @@ -194,19 +152,18 @@ where impl<'a, BR, A> Iterator for Histogram<'a, BR, A, Horizontal> where BR: DiscreteRanged, - BR::ValueType: Eq + Hash, A: AddAssign + Default, { type Item = Rectangle<(A, BR::ValueType)>; fn next(&mut self) -> Option { - if let Some((y, x)) = self.iter.next() { - let ny = BR::next_value(&y, &self.br_param); - // With this trick we can avoid the clone trait bound - let base = (self.baseline)(BR::previous_value(&ny, &self.br_param)); - let style = (self.style)(&y, &x); - let mut rect = Rectangle::new([(x, y), (base, ny)], style); - rect.set_margin(self.margin, self.margin, 0, 0); - return Some(rect); + while let Some((y, x)) = self.iter.next() { + if let Some((y, Some(ny))) = self.br.from_index(y).map(|v| (v, self.br.from_index(y + 1))) { + let base = (self.baseline)(&y); + let style = (self.style)(&y, &x); + let mut rect = Rectangle::new([(x,y), (base, ny)], style); + rect.set_margin(0, 0, self.margin, self.margin); + return Some(rect); + } } None } From 4a328d769adc945bea48bfa52bdf7e85b552e91e Mon Sep 17 00:00:00 2001 From: Hao Hou Date: Sat, 30 Nov 2019 10:37:31 -0700 Subject: [PATCH 2/8] Add missing files --- src/coord/group_by.rs | 73 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/coord/group_by.rs diff --git a/src/coord/group_by.rs b/src/coord/group_by.rs new file mode 100644 index 00000000..0a438642 --- /dev/null +++ b/src/coord/group_by.rs @@ -0,0 +1,73 @@ +use super::{Ranged, AsRangedCoord, DiscreteRanged}; +use super::numeric::RangedCoordusize; +use std::ops::Range; + +/// The ranged value spec that needs to be grouped. +/// This is useful, for example, when we have an X axis is a integer and denotes days. +/// And we are expecting the tick mark denotes weeks, in this way we can make the range +/// spec grouping by 7 elements. +pub struct GroupBy(T, 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<::CoordDescType> { + GroupBy(self.into(), value) + } +} + +impl ToGroupByRange for T +where + T::CoordDescType: DiscreteRanged +{ +} + +impl AsRangedCoord for GroupBy +{ + type Value = T::ValueType; + type CoordDescType = Self; +} + +impl DiscreteRanged for GroupBy +{ + fn size(&self) -> usize { + (self.0.size() + self.1 - 1) / self.1 + } + fn index_of(&self, value: &Self::ValueType) -> Option { + self.0.index_of(value).map(|idx| idx / self.1) + } + fn from_index(&self, index: usize) -> Option { + self.0.from_index(index * self.1) + } +} + +impl Ranged for GroupBy +{ + type ValueType = T::ValueType; + fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> i32 { + self.0.map(value, limit) + } + fn range(&self) -> Range { + self.0.range() + } + fn key_points(&self, max_points: usize) -> Vec { + let range = 0..(self.0.size() + self.1 - 1) / self.1; + let logic_range: RangedCoordusize = range.into(); + + logic_range + .key_points(max_points) + .into_iter() + .map(|x| self.0.from_index(x * self.1).unwrap()) + .collect() + } +} \ No newline at end of file From c7b2aac7a9a741ce7e9d0718ae4d5a1839bbb54d Mon Sep 17 00:00:00 2001 From: Hao Hou Date: Sat, 30 Nov 2019 14:04:02 -0700 Subject: [PATCH 3/8] New discrete coord trait now works! --- examples/histogram.rs | 4 +- examples/normal-dist2.rs | 4 +- src/coord/category.rs | 7 +- src/coord/datetime.rs | 27 +++---- src/coord/discrete.rs | 138 +++++++++++++++++++++++++++++++++ src/coord/group_by.rs | 33 +++----- src/coord/mod.rs | 13 ++-- src/coord/numeric.rs | 4 +- src/coord/partial_axis.rs | 106 +++++++++++++++++++++++++ src/coord/ranged.rs | 157 -------------------------------------- src/series/histogram.rs | 23 ++++-- 11 files changed, 294 insertions(+), 222 deletions(-) create mode 100644 src/coord/discrete.rs create mode 100644 src/coord/partial_axis.rs diff --git a/examples/histogram.rs b/examples/histogram.rs index c33363f8..2fa6aeef 100644 --- a/examples/histogram.rs +++ b/examples/histogram.rs @@ -10,13 +10,13 @@ fn main() -> Result<(), Box> { .y_label_area_size(40) .margin(5) .caption("Histogram Test", ("sans-serif", 50.0).into_font()) - .build_ranged(0u32..10u32, 0u32..10u32)?; + .build_ranged((0u32..10u32).into_centric(), 0u32..10u32)?; chart .configure_mesh() .disable_x_mesh() .line_style_1(&WHITE.mix(0.3)) - .x_label_offset(30) + //.x_label_offset(30) .y_desc("Count") .x_desc("Bucket") .axis_desc_style(("sans-serif", 15).into_font()) diff --git a/examples/normal-dist2.rs b/examples/normal-dist2.rs index 72323aa3..eaebfdc5 100644 --- a/examples/normal-dist2.rs +++ b/examples/normal-dist2.rs @@ -6,8 +6,6 @@ use rand_xorshift::XorShiftRng; use num_traits::sign::Signed; -fn main() {} -#[cfg(feature = "disabled")] fn main() -> Result<(), Box> { let sd = 0.60; @@ -30,7 +28,7 @@ fn main() -> Result<(), Box> { .set_label_area_size(LabelAreaPosition::Bottom, 60) .set_label_area_size(LabelAreaPosition::Right, 60) .build_ranged(-4f64..4f64, 0f64..0.1)? - .set_secondary_coord((-40i32..40i32).into_centric(), 0u32..500u32); + .set_secondary_coord(-40i32..40i32, 0u32..500u32); chart .configure_mesh() diff --git a/src/coord/category.rs b/src/coord/category.rs index 805bad2c..f6dc6864 100644 --- a/src/coord/category.rs +++ b/src/coord/category.rs @@ -2,7 +2,7 @@ use std::fmt; use std::ops::Range; use std::rc::Rc; -use super::{AsRangedCoord, Ranged}; +use super::Ranged; /// The category coordinate pub struct Category { @@ -172,11 +172,6 @@ impl Ranged for Category { } } -impl AsRangedCoord for Category { - type CoordDescType = Self; - type Value = Category; -} - #[cfg(test)] mod test { use super::*; diff --git a/src/coord/datetime.rs b/src/coord/datetime.rs index 45340a0b..bc869b98 100644 --- a/src/coord/datetime.rs +++ b/src/coord/datetime.rs @@ -165,11 +165,6 @@ impl AsRangedCoord for Range> { #[derive(Clone)] pub struct Monthly(Range); -impl AsRangedCoord for Monthly { - type CoordDescType = Monthly; - type Value = T; -} - impl Ranged for Monthly { type ValueType = T; @@ -282,7 +277,8 @@ impl DiscreteRanged for Monthly { let floor = self.0.end.date_floor(); (floor.year(), floor.month()) }; - ((end_year - start_year).max(0) * 12 + (start_month as i32) + (end_month as i32 - 1) + 1).max(0) as usize + ((end_year - start_year).max(0) * 12 + (start_month as i32) + (end_month as i32 - 1) + 1) + .max(0) as usize } fn index_of(&self, value: &T) -> Option { @@ -292,7 +288,9 @@ impl DiscreteRanged for Monthly { 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); + 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); } @@ -303,9 +301,11 @@ impl DiscreteRanged for Monthly { 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.timezone().ymd(year, month as u32, self.0.start.date_ceil().day()) - )) + Some(T::earliest_after_date(self.0.start.timezone().ymd( + year, + month as u32, + self.0.start.date_ceil().day(), + ))) } } @@ -313,11 +313,6 @@ impl DiscreteRanged for Monthly { #[derive(Clone)] pub struct Yearly(Range); -impl AsRangedCoord for Yearly { - type CoordDescType = Yearly; - type Value = T; -} - fn generate_yearly_keypoints( max_points: usize, mut start_year: i32, @@ -417,7 +412,7 @@ impl DiscreteRanged for Yearly { let year = self.0.start.date_ceil().year() + index as i32; let ret = T::earliest_after_date(self.0.start.timezone().ymd(year, 1, 1)); if ret.date_ceil() <= self.0.start.date_floor() { - return Some(self.0.start.clone()) + return Some(self.0.start.clone()); } Some(ret) } diff --git a/src/coord/discrete.rs b/src/coord/discrete.rs new file mode 100644 index 00000000..5169a8a3 --- /dev/null +++ b/src/coord/discrete.rs @@ -0,0 +1,138 @@ +use super::{AsRangedCoord, Ranged}; +use std::fmt::{Debug, Formatter, Result as FmtResult}; +use std::ops::Range; + +/// The trait indicates the coordinate is discrete, so that we can draw histogram on it +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 + /// + /// - `value`: The value to map + /// - **returns** The index of the value + fn index_of(&self, value: &Self::ValueType) -> Option; + + /// Reverse map the index to the value + /// + /// - `value`: The index to map + /// - **returns** The value + fn from_index(&self, index: usize) -> Option; +} + +/// The axis decorator that makes key-point in the center of the value range +/// This is useful when we draw a histogram, since we want the axis value label +/// to be shown in the middle of the range rather than exactly the location where +/// the value mapped to. +pub struct CentricDiscreteRange(D); + +impl Clone for CentricDiscreteRange { + fn clone(&self) -> Self { + CentricDiscreteRange(self.0.clone()) + } +} + +/// The trait for types that can decorated by `CentricDiscreteRange` decorator +pub trait IntoCentric: AsRangedCoord +where + Self::CoordDescType: DiscreteRanged, +{ + /// Convert current ranged value into a centric ranged value + fn into_centric(self) -> CentricDiscreteRange { + CentricDiscreteRange(self.into()) + } +} + +impl IntoCentric for R where R::CoordDescType: DiscreteRanged {} + +/// The value that used by the centric coordinate +pub enum CentricValues { + Exact(T), + CenterOf(T), + Last, +} + +impl Debug for CentricValues { + fn fmt(&self, formatter: &mut Formatter) -> FmtResult { + match self { + CentricValues::Exact(value) => write!(formatter, "{:?}", value), + CentricValues::CenterOf(value) => write!(formatter, "{:?}", value), + CentricValues::Last => Ok(()), + } + } +} + +impl Ranged for CentricDiscreteRange { + type ValueType = CentricValues; + + 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 { + CentricValues::Exact(coord) => self.0.map(coord, (limit.0, limit.1 - margin)), + CentricValues::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 + } + CentricValues::Last => limit.1, + } + } + + fn key_points(&self, max_points: usize) -> Vec { + self.0 + .key_points(max_points) + .into_iter() + .map(CentricValues::CenterOf) + .collect() + } + + fn range(&self) -> Range { + let range = self.0.range(); + CentricValues::Exact(range.start)..CentricValues::Exact(range.end) + } +} + +impl DiscreteRanged for CentricDiscreteRange { + fn size(&self) -> usize { + self.0.size() + 1 + } + + fn index_of(&self, value: &Self::ValueType) -> Option { + match value { + CentricValues::Exact(value) => self.0.index_of(value), + CentricValues::CenterOf(value) => self.0.index_of(value), + CentricValues::Last => Some(self.0.size()), + } + } + + fn from_index(&self, idx: usize) -> Option { + if idx < self.0.size() { + self.0.from_index(idx).map(|x| CentricValues::Exact(x)) + } else if idx == self.0.size() { + Some(CentricValues::Last) + } else { + None + } + } +} + +impl From for CentricValues { + fn from(this: T) -> CentricValues { + CentricValues::Exact(this) + } +} diff --git a/src/coord/group_by.rs b/src/coord/group_by.rs index 0a438642..c38275ef 100644 --- a/src/coord/group_by.rs +++ b/src/coord/group_by.rs @@ -1,45 +1,31 @@ -use super::{Ranged, AsRangedCoord, DiscreteRanged}; use super::numeric::RangedCoordusize; +use super::{AsRangedCoord, DiscreteRanged, Ranged}; use std::ops::Range; /// The ranged value spec that needs to be grouped. /// This is useful, for example, when we have an X axis is a integer and denotes days. /// And we are expecting the tick mark denotes weeks, in this way we can make the range /// spec grouping by 7 elements. -pub struct GroupBy(T, usize); +pub struct GroupBy(T, usize); /// The trait that provides method `Self::group_by` function which creates a /// `GroupBy` decorated ranged value. -pub trait ToGroupByRange : AsRangedCoord + Sized +pub trait ToGroupByRange: AsRangedCoord + Sized where - Self::CoordDescType : DiscreteRanged + 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<::CoordDescType> { + fn group_by(self, value: usize) -> GroupBy<::CoordDescType> { GroupBy(self.into(), value) } } -impl ToGroupByRange for T -where - T::CoordDescType: DiscreteRanged -{ -} +impl ToGroupByRange for T where T::CoordDescType: DiscreteRanged {} -impl AsRangedCoord for GroupBy -{ - type Value = T::ValueType; - type CoordDescType = Self; -} - -impl DiscreteRanged for GroupBy -{ +impl DiscreteRanged for GroupBy { fn size(&self) -> usize { (self.0.size() + self.1 - 1) / self.1 } @@ -51,8 +37,7 @@ impl DiscreteRanged for GroupBy } } -impl Ranged for GroupBy -{ +impl Ranged for GroupBy { type ValueType = T::ValueType; fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> i32 { self.0.map(value, limit) @@ -70,4 +55,4 @@ impl Ranged for GroupBy .map(|x| self.0.from_index(x * self.1).unwrap()) .collect() } -} \ No newline at end of file +} diff --git a/src/coord/mod.rs b/src/coord/mod.rs index 77d0df1c..f66321f8 100644 --- a/src/coord/mod.rs +++ b/src/coord/mod.rs @@ -25,10 +25,12 @@ use crate::drawing::backend::BackendCoord; mod category; #[cfg(feature = "chrono")] mod datetime; +mod discrete; +mod group_by; mod logarithmic; mod numeric; +mod partial_axis; mod ranged; -mod group_by; #[cfg(feature = "chrono")] pub use datetime::{IntoMonthly, IntoYearly, RangedDate, RangedDateTime, RangedDuration}; @@ -36,12 +38,11 @@ pub use numeric::{ RangedCoordf32, RangedCoordf64, RangedCoordi128, RangedCoordi32, RangedCoordi64, RangedCoordu128, RangedCoordu32, RangedCoordu64, }; -pub use ranged::{ - AsRangedCoord, DiscreteRanged, IntoCentric, IntoPartialAxis, MeshLine, Ranged, RangedCoord, - ReversibleRanged, -}; +pub use ranged::{AsRangedCoord, MeshLine, Ranged, RangedCoord, ReversibleRanged}; + +pub use partial_axis::{make_partial_axis, IntoPartialAxis}; -pub use ranged::make_partial_axis; +pub use discrete::{DiscreteRanged, IntoCentric}; pub use logarithmic::{LogCoord, LogRange, LogScalable}; diff --git a/src/coord/numeric.rs b/src/coord/numeric.rs index bb375158..9b51c1b5 100644 --- a/src/coord/numeric.rs +++ b/src/coord/numeric.rs @@ -1,5 +1,5 @@ -use std::ops::Range; use std::convert::TryFrom; +use std::ops::Range; use super::{AsRangedCoord, DiscreteRanged, Ranged, ReversibleRanged}; @@ -23,7 +23,7 @@ macro_rules! impl_discrete_trait { } fn from_index(&self, index: usize) -> Option { - if let Ok(index) = Self::ValueType::try_from(index){ + if let Ok(index) = Self::ValueType::try_from(index) { return Some(self.0 + index); } None diff --git a/src/coord/partial_axis.rs b/src/coord/partial_axis.rs new file mode 100644 index 00000000..290c6017 --- /dev/null +++ b/src/coord/partial_axis.rs @@ -0,0 +1,106 @@ +use super::{AsRangedCoord, DiscreteRanged, 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. +pub struct PartialAxis(R, Range); + +/// 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<::ValueType>, + ) -> PartialAxis { + PartialAxis(self.into(), axis_range) + } +} + +impl IntoPartialAxis for R {} + +impl Clone for PartialAxis +where + ::ValueType: Clone, +{ + fn clone(&self) -> Self { + PartialAxis(self.0.clone(), self.1.clone()) + } +} + +impl Ranged for PartialAxis +where + R::ValueType: Clone, +{ + type ValueType = R::ValueType; + + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { + self.0.map(value, limit) + } + + fn key_points(&self, max_points: usize) -> Vec { + self.0.key_points(max_points) + } + + fn range(&self) -> Range { + self.0.range() + } + + fn axis_pixel_range(&self, limit: (i32, i32)) -> Range { + let left = self.map(&self.1.start, limit); + let right = self.map(&self.1.end, limit); + + left.min(right)..left.max(right) + } +} + +impl DiscreteRanged for PartialAxis +where + R: Ranged, + ::ValueType: Eq + Clone, +{ + fn size(&self) -> usize { + self.0.size() + } + + fn index_of(&self, value: &R::ValueType) -> Option { + self.0.index_of(value) + } + + fn from_index(&self, index: usize) -> Option { + 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( + axis_range: Range, + part: Range, +) -> Option as AsRangedCoord>::CoordDescType>> +where + Range: 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 = num_traits::cast(full_left)?..num_traits::cast(full_right)?; + + let axis_range: as AsRangedCoord>::CoordDescType = axis_range.into(); + + Some(PartialAxis(full_range.into(), axis_range.range())) +} diff --git a/src/coord/ranged.rs b/src/coord/ranged.rs index 5071aa68..a8a34339 100644 --- a/src/coord/ranged.rs +++ b/src/coord/ranged.rs @@ -172,30 +172,6 @@ impl<'a, X: Ranged, Y: Ranged> MeshLine<'a, X, Y> { } } -/// The trait indicates the coordinate is discrete, so that we can draw histogram on it -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 - /// - /// - `value`: The value to map - /// - **returns** The index of the value - fn index_of(&self, value: &Self::ValueType) -> Option; - - /// Reverse map the index to the value - /// - /// - `value`: The index to map - /// - **returns** The value - fn from_index(&self, index: usize) -> Option; -} - /// The trait for the type that can be converted into a ranged coordinate axis pub trait AsRangedCoord: Sized { type CoordDescType: Ranged + From; @@ -205,140 +181,7 @@ pub trait AsRangedCoord: Sized { impl AsRangedCoord for T where T: Ranged, - Range: Into, { type CoordDescType = T; type Value = T::ValueType; } - -/// The axis decorator that makes key-point in the center of the value range -/// This is useful when we draw a histogram, since we want the axis value label -/// to be shown in the middle of the range rather than exactly the location where -/// the value mapped to. -pub struct CentricDiscreteRange(D) -where - ::ValueType: Eq; - -/// The trait for types that can decorated by `CentricDiscreteRange` decorator -pub trait IntoCentric: AsRangedCoord -where - Self::CoordDescType: DiscreteRanged, - ::ValueType: Eq, -{ - /// Convert current ranged value into a centric ranged value - fn into_centric(self) -> CentricDiscreteRange { - CentricDiscreteRange(self.into()) - } -} - -/// This axis decorator will make the axis partially display on the axis. -/// At some time, we want the axis only covers some part of the value. -/// This decorator will have an additional display range defined. -pub struct PartialAxis(R, Range); - -/// 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<::ValueType>, - ) -> PartialAxis { - PartialAxis(self.into(), axis_range) - } -} - -impl IntoPartialAxis for R {} - -impl Clone for PartialAxis -where - ::ValueType: Clone, -{ - fn clone(&self) -> Self { - PartialAxis(self.0.clone(), self.1.clone()) - } -} - -impl Ranged for PartialAxis -where - R::ValueType: Clone, -{ - type ValueType = R::ValueType; - - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { - self.0.map(value, limit) - } - - fn key_points(&self, max_points: usize) -> Vec { - self.0.key_points(max_points) - } - - fn range(&self) -> Range { - self.0.range() - } - - fn axis_pixel_range(&self, limit: (i32, i32)) -> Range { - let left = self.map(&self.1.start, limit); - let right = self.map(&self.1.end, limit); - - left.min(right)..left.max(right) - } -} - -impl DiscreteRanged for PartialAxis -where - R: Ranged, - ::ValueType: Eq + Clone, -{ - fn size(&self) -> usize { - self.0.size() - } - - fn index_of(&self, value: &R::ValueType) -> Option { - self.0.index_of(value) - } - - fn from_index(&self, index: usize) -> Option { - self.0.from_index(index) - } -} - -impl AsRangedCoord for PartialAxis -where - ::ValueType: Clone, -{ - type CoordDescType = Self; - type Value = ::ValueType; -} - -/// Make a partial axis based on the percentage of visible portion. -/// We can use `into_partial_axis` to create a partial axis range specification. -/// But sometimes, we want to directly specify the percentage visible to the user. -/// -/// - `axis_range`: The range specification -/// - `part`: The visible part of the axis. Each value is from [0.0, 1.0] -/// - **returns**: The partial axis created from the input, or `None` when not possible -pub fn make_partial_axis( - axis_range: Range, - part: Range, -) -> Option as AsRangedCoord>::CoordDescType>> -where - Range: 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 = num_traits::cast(full_left)?..num_traits::cast(full_right)?; - - let axis_range: as AsRangedCoord>::CoordDescType = axis_range.into(); - - Some(PartialAxis(full_range.into(), axis_range.range())) -} diff --git a/src/series/histogram.rs b/src/series/histogram.rs index 14354e35..d4220e0b 100644 --- a/src/series/histogram.rs +++ b/src/series/histogram.rs @@ -84,10 +84,13 @@ where } /// Set the data iterator - pub fn data>(mut self, iter: I) -> Self { + pub fn data, I: IntoIterator>( + mut self, + iter: I, + ) -> Self { let mut buffer = HashMap::::new(); for (x, y) in iter.into_iter() { - if let Some(x) = self.br.index_of(&x) { + if let Some(x) = self.br.index_of(&x.into()) { *buffer.entry(x).or_insert_with(Default::default) += y; } } @@ -137,12 +140,16 @@ where type Item = Rectangle<(BR::ValueType, A)>; fn next(&mut self) -> Option { while let Some((x, y)) = self.iter.next() { - if let Some((x, Some(nx))) = self.br.from_index(x).map(|v| (v, self.br.from_index(x + 1))) { + if let Some((x, Some(nx))) = self + .br + .from_index(x) + .map(|v| (v, self.br.from_index(x + 1))) + { let base = (self.baseline)(&x); let style = (self.style)(&x, &y); let mut rect = Rectangle::new([(x, y), (nx, base)], style); rect.set_margin(0, 0, self.margin, self.margin); - return Some(rect) + return Some(rect); } } None @@ -157,10 +164,14 @@ where type Item = Rectangle<(A, BR::ValueType)>; fn next(&mut self) -> Option { while let Some((y, x)) = self.iter.next() { - if let Some((y, Some(ny))) = self.br.from_index(y).map(|v| (v, self.br.from_index(y + 1))) { + if let Some((y, Some(ny))) = self + .br + .from_index(y) + .map(|v| (v, self.br.from_index(y + 1))) + { let base = (self.baseline)(&y); let style = (self.style)(&y, &x); - let mut rect = Rectangle::new([(x,y), (base, ny)], style); + let mut rect = Rectangle::new([(x, y), (base, ny)], style); rect.set_margin(0, 0, self.margin, self.margin); return Some(rect); } From d58e801cc41ca819f55f15ccd37208bdcb3f0798 Mon Sep 17 00:00:00 2001 From: Hao Hou Date: Sat, 30 Nov 2019 14:46:40 -0700 Subject: [PATCH 4/8] Implemente the value iterator for discrete coordinate and add test cases --- src/coord/datetime.rs | 44 +++++++++++++++++++++++++++++++++++++++---- src/coord/discrete.rs | 40 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/coord/datetime.rs b/src/coord/datetime.rs index bc869b98..829e2356 100644 --- a/src/coord/datetime.rs +++ b/src/coord/datetime.rs @@ -277,8 +277,11 @@ impl DiscreteRanged for Monthly { let floor = self.0.end.date_floor(); (floor.year(), floor.month()) }; - ((end_year - start_year).max(0) * 12 + (start_month as i32) + (end_month as i32 - 1) + 1) - .max(0) as usize + ((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 { @@ -298,13 +301,16 @@ impl DiscreteRanged for Monthly { } fn from_index(&self, index: usize) -> Option { + 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.timezone().ymd( year, - month as u32, - self.0.start.date_ceil().day(), + month as u32 + 1, + 1, ))) } } @@ -978,4 +984,34 @@ mod test { assert!(max == min); assert_eq!(max, 3600 * 2); } + + #[test] + fn test_date_discrete() { + let coord: RangedDate = (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); + } + } + + #[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); + } + } } diff --git a/src/coord/discrete.rs b/src/coord/discrete.rs index 5169a8a3..9254cf45 100644 --- a/src/coord/discrete.rs +++ b/src/coord/discrete.rs @@ -24,6 +24,15 @@ where /// - `value`: The index to map /// - **returns** The value fn from_index(&self, index: usize) -> Option; + + /// 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()) + } } /// The axis decorator that makes key-point in the center of the value range @@ -136,3 +145,34 @@ impl From for CentricValues { CentricValues::Exact(this) } } + +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 { + 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::numeric::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) + } + } +} From e98fb25ef3c3a5ff17e183ed84184bc474af15f8 Mon Sep 17 00:00:00 2001 From: Hao Hou Date: Sat, 30 Nov 2019 19:39:02 -0700 Subject: [PATCH 5/8] Add test cases --- Cargo.toml | 1 - src/coord/discrete.rs | 36 +++++++++++++++++++++++++++++++----- src/coord/group_by.rs | 14 ++++++++++++++ 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 45dd2391..f6febe33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,7 +79,6 @@ rand_distr = "0.2.2" criterion = "0.3.0" rayon = "1.2.0" rand_xorshift = "0.2.0" -cpuprofiler = "*" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "0.3.4" diff --git a/src/coord/discrete.rs b/src/coord/discrete.rs index 9254cf45..c113ef22 100644 --- a/src/coord/discrete.rs +++ b/src/coord/discrete.rs @@ -28,8 +28,9 @@ where /// Return a iterator that iterates over the all possible values /// /// - **returns** The value iterator - fn values(&self) -> DiscreteValueIter<'_, Self> - where Self:Sized + fn values(&self) -> DiscreteValueIter<'_, Self> + where + Self: Sized, { DiscreteValueIter(self, 0, self.size()) } @@ -148,7 +149,7 @@ impl From for CentricValues { pub struct DiscreteValueIter<'a, T: DiscreteRanged>(&'a T, usize, usize); -impl <'a, T:DiscreteRanged> Iterator for DiscreteValueIter<'a, T> { +impl<'a, T: DiscreteRanged> Iterator for DiscreteValueIter<'a, T> { type Item = T::ValueType; fn next(&mut self) -> Option { if self.1 >= self.2 { @@ -165,9 +166,9 @@ mod test { use super::*; #[test] fn test_value_iter() { - let range:crate::coord::numeric::RangedCoordi32 = (-10..10).into(); + let range: crate::coord::numeric::RangedCoordi32 = (-10..10).into(); - let values:Vec<_> = range.values().collect(); + let values: Vec<_> = range.values().collect(); assert_eq!(21, values.len()); @@ -175,4 +176,29 @@ mod test { assert_eq!(expected, value) } } + + #[test] + fn test_centric_coord() { + let coord = (0..10).into_centric(); + + assert_eq!(coord.size(), 12); + for i in 0..=11 { + match coord.from_index(i as usize) { + Some(CentricValues::Exact(value)) => assert_eq!(i, value), + Some(CentricValues::Last) => assert_eq!(i, 11), + _ => panic!(), + } + } + + for (kps, idx) in coord.key_points(20).into_iter().zip(0..) { + match kps { + CentricValues::CenterOf(value) if value <= 10 => assert_eq!(value, idx), + _ => panic!(), + } + } + + assert_eq!(coord.map(&CentricValues::CenterOf(0), (0, 24)), 1); + assert_eq!(coord.map(&CentricValues::Exact(0), (0, 24)), 0); + assert_eq!(coord.map(&CentricValues::Exact(1), (0, 24)), 2); + } } diff --git a/src/coord/group_by.rs b/src/coord/group_by.rs index c38275ef..5cda0fb8 100644 --- a/src/coord/group_by.rs +++ b/src/coord/group_by.rs @@ -56,3 +56,17 @@ impl Ranged for GroupBy { .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)); + } + } +} From 33444aa40cea694e648a1d35df27e89b40429b11 Mon Sep 17 00:00:00 2001 From: Hao Hou Date: Sat, 30 Nov 2019 21:58:14 -0700 Subject: [PATCH 6/8] Add more test --- examples/histogram.rs | 1 - src/coord/datetime.rs | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/histogram.rs b/examples/histogram.rs index 2fa6aeef..7e1d31f1 100644 --- a/examples/histogram.rs +++ b/examples/histogram.rs @@ -16,7 +16,6 @@ fn main() -> Result<(), Box> { .configure_mesh() .disable_x_mesh() .line_style_1(&WHITE.mix(0.3)) - //.x_label_offset(30) .y_desc("Count") .x_desc("Bucket") .axis_desc_style(("sans-serif", 15).into_font()) diff --git a/src/coord/datetime.rs b/src/coord/datetime.rs index 829e2356..be4e75dc 100644 --- a/src/coord/datetime.rs +++ b/src/coord/datetime.rs @@ -1002,6 +1002,10 @@ mod test { 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 + ); } } @@ -1012,6 +1016,7 @@ mod test { 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); } } } From d8b6cb3613f648746da8aa796e9f5e3a30a92c47 Mon Sep 17 00:00:00 2001 From: Hao Hou Date: Sun, 1 Dec 2019 08:06:27 -0700 Subject: [PATCH 7/8] Add more test cases --- src/coord/numeric.rs | 8 ++++++++ src/coord/partial_axis.rs | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/coord/numeric.rs b/src/coord/numeric.rs index 9b51c1b5..e1f5cd8f 100644 --- a/src/coord/numeric.rs +++ b/src/coord/numeric.rs @@ -306,4 +306,12 @@ mod test { let _coord = RangedCoord::::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/partial_axis.rs b/src/coord/partial_axis.rs index 290c6017..e3e3a518 100644 --- a/src/coord/partial_axis.rs +++ b/src/coord/partial_axis.rs @@ -104,3 +104,15 @@ where 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); + } +} From 473adc50bf79121431facf91062764c78f8008e4 Mon Sep 17 00:00:00 2001 From: Hao Hou Date: Sun, 1 Dec 2019 11:00:23 -0700 Subject: [PATCH 8/8] Add previous and next function to discrete trait --- src/coord/discrete.rs | 54 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/src/coord/discrete.rs b/src/coord/discrete.rs index c113ef22..c63a3f40 100644 --- a/src/coord/discrete.rs +++ b/src/coord/discrete.rs @@ -2,7 +2,11 @@ use super::{AsRangedCoord, Ranged}; use std::fmt::{Debug, Formatter, Result as FmtResult}; use std::ops::Range; -/// The trait indicates the coordinate is discrete, so that we can draw histogram on it +/// 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, @@ -15,12 +19,18 @@ where /// 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; /// 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; @@ -34,6 +44,42 @@ where { 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 { + 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 { + if let Some(idx) = self.index_of(value) { + if idx + 1 < self.size() { + return self.from_index(idx + 1); + } + } + None + } } /// The axis decorator that makes key-point in the center of the value range @@ -173,8 +219,12 @@ mod test { assert_eq!(21, values.len()); for (expected, value) in (-10..=10).zip(values) { - assert_eq!(expected, value) + 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]