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/examples/histogram.rs b/examples/histogram.rs index c33363f8..7e1d31f1 100644 --- a/examples/histogram.rs +++ b/examples/histogram.rs @@ -10,13 +10,12 @@ 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) .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 6155ea65..eaebfdc5 100644 --- a/examples/normal-dist2.rs +++ b/examples/normal-dist2.rs @@ -28,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 cb96f937..be4e75dc 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)) } } @@ -159,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; @@ -267,28 +268,50 @@ 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 + + (1 - 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; + fn from_index(&self, index: usize) -> Option { + if index == 0 { + return Some(T::earliest_after_date(self.0.start.date_ceil())); } - T::earliest_after_date(this.timezone().ymd(year, month, this.date_floor().day())) + let index_from_start_year = index + (self.0.start.date_ceil().month() - 1) as usize; + let year = self.0.start.date_ceil().year() + index_from_start_year as i32 / 12; + let month = index_from_start_year % 12; + Some(T::earliest_after_date(self.0.start.timezone().ymd( + year, + month as u32 + 1, + 1, + ))) } } @@ -296,11 +319,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, @@ -380,14 +398,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) } } @@ -951,4 +984,39 @@ 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); + 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/discrete.rs b/src/coord/discrete.rs new file mode 100644 index 00000000..c63a3f40 --- /dev/null +++ b/src/coord/discrete.rs @@ -0,0 +1,254 @@ +use super::{AsRangedCoord, Ranged}; +use std::fmt::{Debug, Formatter, Result as FmtResult}; +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; + + /// 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; + + /// 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 { + 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 +/// 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) + } +} + +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); + } + 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_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 new file mode 100644 index 00000000..5cda0fb8 --- /dev/null +++ b/src/coord/group_by.rs @@ -0,0 +1,72 @@ +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); + +/// 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 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() + } +} + +#[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/mod.rs b/src/coord/mod.rs index 0afafa32..f66321f8 100644 --- a/src/coord/mod.rs +++ b/src/coord/mod.rs @@ -25,8 +25,11 @@ 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; #[cfg(feature = "chrono")] @@ -35,16 +38,15 @@ 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}; -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..e1f5cd8f 100644 --- a/src/coord/numeric.rs +++ b/src/coord/numeric.rs @@ -1,3 +1,4 @@ +use std::convert::TryFrom; use std::ops::Range; use super::{AsRangedCoord, DiscreteRanged, Ranged, ReversibleRanged}; @@ -5,13 +6,27 @@ 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 index_of(&self, value: &Self::ValueType) -> Option { + if value < &self.0 { + return None; + } + let ret = value - self.0; + Some(ret as usize) } - fn previous_value(this: &Self::ValueType, _: &()) -> Self::ValueType { - return *this - 1; + + fn from_index(&self, index: usize) -> Option { + 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::*; @@ -392,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 new file mode 100644 index 00000000..e3e3a518 --- /dev/null +++ b/src/coord/partial_axis.rs @@ -0,0 +1,118 @@ +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())) +} + +#[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/ranged.rs b/src/coord/ranged.rs index b7f3ffd7..a8a34339 100644 --- a/src/coord/ranged.rs +++ b/src/coord/ranged.rs @@ -172,22 +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, -{ - type RangeParameter; - - fn get_range_parameter(&self) -> Self::RangeParameter; - - /// Get the smallest value that is larger than the `this` value - fn next_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType; - - /// Get the largest value that is smaller than `this` value - fn previous_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType; -} - /// The trait for the type that can be converted into a ranged coordinate axis pub trait AsRangedCoord: Sized { type CoordDescType: Ranged + From; @@ -197,201 +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()) - } -} - -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. -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, -{ - 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 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 75c2fb2a..d4220e0b 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 } @@ -87,68 +84,33 @@ where } /// Set the data iterator - pub fn data>(mut self, iter: I) -> Self { - let mut buffer = HashMap::::new(); + pub fn data, I: IntoIterator>( + mut self, + iter: I, + ) -> Self { + 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.into()) { + *buffer.entry(x).or_insert_with(Default::default) += y; + } } self.iter = buffer.into_iter(); self } } -pub trait UseDefaultParameter: Default { - fn new() -> Self { - Default::default() - } -} - -impl UseDefaultParameter for () {} - impl<'a, BR, A> Histogram<'a, BR, A, Vertical> where - BR: DiscreteRanged, - BR::ValueType: Eq + Hash, + BR: DiscreteRanged + Clone, A: AddAssign + 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 +118,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 +127,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 +135,22 @@ 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 +159,22 @@ 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 }