plotters/coord/ranged1d/combinators/
linspace.rs

1use crate::coord::ranged1d::types::RangedCoordusize;
2use crate::coord::ranged1d::{
3    AsRangedCoord, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, ValueFormatter,
4};
5use std::cmp::{Ordering, PartialOrd};
6use std::marker::PhantomData;
7use std::ops::{Add, Range, Sub};
8
9/// The type marker used to denote the rounding method.
10/// Since we are mapping any range to a discrete range thus not all values are
11/// perfect mapped to the grid points. In this case, this type marker gives hints
12/// for the linspace coord for how to treat the non-grid-point values.
13pub trait LinspaceRoundingMethod<V> {
14    /// Search for the value within the given values array and rounding method
15    ///
16    /// - `values`: The values we want to search
17    /// - `target`: The target value
18    /// - `returns`: The index if we found the matching item, otherwise none
19    fn search(values: &[V], target: &V) -> Option<usize>;
20}
21
22/// This type marker means linspace do the exact match for searching
23/// which means if there's no value strictly equals to the target, the coord spec
24/// reports not found result.
25#[derive(Clone)]
26pub struct Exact<V>(PhantomData<V>);
27
28impl<V: PartialOrd> LinspaceRoundingMethod<V> for Exact<V> {
29    fn search(values: &[V], target: &V) -> Option<usize> {
30        values.iter().position(|x| target == x)
31    }
32}
33
34/// This type marker means we round up the value. Which means we try to find a
35/// minimal value in the values array that is greater or equal to the target.
36#[derive(Clone)]
37pub struct Ceil<V>(PhantomData<V>);
38
39impl<V: PartialOrd> LinspaceRoundingMethod<V> for Ceil<V> {
40    fn search(values: &[V], target: &V) -> Option<usize> {
41        let ascending = if values.len() < 2 {
42            true
43        } else {
44            values[0].partial_cmp(&values[1]) == Some(Ordering::Less)
45        };
46
47        match values.binary_search_by(|probe| {
48            if ascending {
49                probe.partial_cmp(target).unwrap()
50            } else {
51                target.partial_cmp(probe).unwrap()
52            }
53        }) {
54            Ok(idx) => Some(idx),
55            Err(idx) => {
56                let offset = if ascending { 0 } else { 1 };
57
58                if idx < offset || idx >= values.len() + offset {
59                    return None;
60                }
61                Some(idx - offset)
62            }
63        }
64    }
65}
66
67/// This means we use the round down. Which means we try to find a
68/// maximum value in the values array that is less or equal to the target.
69#[derive(Clone)]
70pub struct Floor<V>(PhantomData<V>);
71
72impl<V: PartialOrd> LinspaceRoundingMethod<V> for Floor<V> {
73    fn search(values: &[V], target: &V) -> Option<usize> {
74        let ascending = if values.len() < 2 {
75            true
76        } else {
77            values[0].partial_cmp(&values[1]) == Some(Ordering::Less)
78        };
79
80        match values.binary_search_by(|probe| {
81            if ascending {
82                probe.partial_cmp(target).unwrap()
83            } else {
84                target.partial_cmp(probe).unwrap()
85            }
86        }) {
87            Ok(idx) => Some(idx),
88            Err(idx) => {
89                let offset = if ascending { 1 } else { 0 };
90
91                if idx < offset || idx >= values.len() + offset {
92                    return None;
93                }
94                Some(idx - offset)
95            }
96        }
97    }
98}
99
100/// This means we use the rounding. Which means we try to find the closet
101/// value in the array that matches the target
102#[derive(Clone)]
103pub struct Round<V, S>(PhantomData<(V, S)>);
104
105impl<V, S> LinspaceRoundingMethod<V> for Round<V, S>
106where
107    V: Add<S, Output = V> + PartialOrd + Sub<V, Output = S> + Clone,
108    S: PartialOrd + Clone,
109{
110    fn search(values: &[V], target: &V) -> Option<usize> {
111        let ascending = if values.len() < 2 {
112            true
113        } else {
114            values[0].partial_cmp(&values[1]) == Some(Ordering::Less)
115        };
116
117        match values.binary_search_by(|probe| {
118            if ascending {
119                probe.partial_cmp(target).unwrap()
120            } else {
121                target.partial_cmp(probe).unwrap()
122            }
123        }) {
124            Ok(idx) => Some(idx),
125            Err(idx) => {
126                if idx == 0 {
127                    return Some(0);
128                }
129
130                if idx == values.len() {
131                    return Some(idx - 1);
132                }
133
134                let left_delta = if ascending {
135                    target.clone() - values[idx - 1].clone()
136                } else {
137                    values[idx - 1].clone() - target.clone()
138                };
139                let right_delta = if ascending {
140                    values[idx].clone() - target.clone()
141                } else {
142                    target.clone() - values[idx].clone()
143                };
144
145                if left_delta.partial_cmp(&right_delta) == Some(Ordering::Less) {
146                    Some(idx - 1)
147                } else {
148                    Some(idx)
149                }
150            }
151        }
152    }
153}
154
155/// The coordinate combinator that transform a continuous coordinate to a discrete coordinate
156/// to a discrete coordinate by a giving step.
157///
158/// For example, range `0f32..100f32` is a continuous coordinate, thus this prevent us having a
159/// histogram on it since Plotters doesn't know how to segment the range into buckets.
160/// In this case, to get a histogram, we need to split the original range to a
161/// set of discrete buckets (for example, 0.5 per bucket).
162///
163/// The linspace decorate abstracting this method. For example, we can have a discrete coordinate:
164/// `(0f32..100f32).step(0.5)`.
165///
166/// Linspace also supports different types of bucket matching method - This configuration alters the behavior of
167/// [DiscreteCoord::index_of](../trait.DiscreteCoord.html#tymethod.index_of) for Linspace coord spec
168/// - **Flooring**, the value falls into the nearst bucket smaller than it. See [Linspace::use_floor](struct.Linspace.html#method.use_floor)
169/// - **Round**,   the value falls into the nearst bucket. See [Linearspace::use_round](struct.Linspace.html#method.use_round)
170/// - **Ceiling**, the value falls into the nearst bucket larger than itself. See [Linspace::use_ceil](struct.Linspace.html#method.use_ceil)
171/// - **Exact Matchting**, the value must be exactly same as the butcket value.  See [Linspace::use_exact](struct.Linspace.html#method.use_exact)
172#[derive(Clone)]
173pub struct Linspace<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>>
174where
175    T::ValueType: Add<S, Output = T::ValueType> + PartialOrd + Clone,
176{
177    step: S,
178    inner: T,
179    grid_value: Vec<T::ValueType>,
180    _phatom: PhantomData<R>,
181}
182
183impl<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>> Linspace<T, S, R>
184where
185    T::ValueType: Add<S, Output = T::ValueType> + PartialOrd + Clone,
186{
187    fn compute_grid_values(&mut self) {
188        let range = self.inner.range();
189
190        match (
191            range.start.partial_cmp(&range.end),
192            (range.start.clone() + self.step.clone()).partial_cmp(&range.end),
193        ) {
194            (Some(a), Some(b)) if a != b || a == Ordering::Equal || b == Ordering::Equal => (),
195            (Some(a), Some(_)) => {
196                let mut current = range.start;
197                while current.partial_cmp(&range.end) == Some(a) {
198                    self.grid_value.push(current.clone());
199                    current = current + self.step.clone();
200                }
201            }
202            _ => (),
203        }
204    }
205
206    /// Set the linspace use the round up method for value matching
207    ///
208    /// - **returns**: The newly created linspace that uses new matching method
209    pub fn use_ceil(self) -> Linspace<T, S, Ceil<T::ValueType>> {
210        Linspace {
211            step: self.step,
212            inner: self.inner,
213            grid_value: self.grid_value,
214            _phatom: PhantomData,
215        }
216    }
217
218    /// Set the linspace use the round down method for value matching
219    ///
220    /// - **returns**: The newly created linspace that uses new matching method
221    pub fn use_floor(self) -> Linspace<T, S, Floor<T::ValueType>> {
222        Linspace {
223            step: self.step,
224            inner: self.inner,
225            grid_value: self.grid_value,
226            _phatom: PhantomData,
227        }
228    }
229
230    /// Set the linspace use the best match method for value matching
231    ///
232    /// - **returns**: The newly created linspace that uses new matching method
233    pub fn use_round(self) -> Linspace<T, S, Round<T::ValueType, S>>
234    where
235        T::ValueType: Sub<T::ValueType, Output = S>,
236        S: PartialOrd,
237    {
238        Linspace {
239            step: self.step,
240            inner: self.inner,
241            grid_value: self.grid_value,
242            _phatom: PhantomData,
243        }
244    }
245
246    /// Set the linspace use the exact match method for value matching
247    ///
248    /// - **returns**: The newly created linspace that uses new matching method
249    pub fn use_exact(self) -> Linspace<T, S, Exact<T::ValueType>>
250    where
251        T::ValueType: Sub<T::ValueType, Output = S>,
252        S: PartialOrd,
253    {
254        Linspace {
255            step: self.step,
256            inner: self.inner,
257            grid_value: self.grid_value,
258            _phatom: PhantomData,
259        }
260    }
261}
262
263impl<T, R, S, RM> ValueFormatter<T> for Linspace<R, S, RM>
264where
265    R: Ranged<ValueType = T> + ValueFormatter<T>,
266    RM: LinspaceRoundingMethod<T>,
267    T: Add<S, Output = T> + PartialOrd + Clone,
268    S: Clone,
269{
270    fn format(value: &T) -> String {
271        R::format(value)
272    }
273}
274
275impl<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>> Ranged for Linspace<T, S, R>
276where
277    T::ValueType: Add<S, Output = T::ValueType> + PartialOrd + Clone,
278{
279    type FormatOption = NoDefaultFormatting;
280    type ValueType = T::ValueType;
281
282    fn range(&self) -> Range<T::ValueType> {
283        self.inner.range()
284    }
285
286    fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> i32 {
287        self.inner.map(value, limit)
288    }
289
290    fn key_points<Hint: KeyPointHint>(&self, hint: Hint) -> Vec<T::ValueType> {
291        if self.grid_value.is_empty() {
292            return vec![];
293        }
294        let idx_range: RangedCoordusize = (0..(self.grid_value.len() - 1)).into();
295
296        idx_range
297            .key_points(hint)
298            .into_iter()
299            .map(|x| self.grid_value[x].clone())
300            .collect()
301    }
302}
303
304impl<T: Ranged, S: Clone, R: LinspaceRoundingMethod<T::ValueType>> DiscreteRanged
305    for Linspace<T, S, R>
306where
307    T::ValueType: Add<S, Output = T::ValueType> + PartialOrd + Clone,
308{
309    fn size(&self) -> usize {
310        self.grid_value.len()
311    }
312
313    fn index_of(&self, value: &T::ValueType) -> Option<usize> {
314        R::search(self.grid_value.as_ref(), value)
315    }
316
317    fn from_index(&self, idx: usize) -> Option<T::ValueType> {
318        self.grid_value.get(idx).cloned()
319    }
320}
321
322/// Makes a linspace coordinate from the ranged coordinates.
323pub trait IntoLinspace: AsRangedCoord {
324    /// Set the step value, make a linspace coordinate from the given range.
325    /// By default the matching method use the exact match
326    ///
327    /// - `val`: The step value
328    /// - **returns*: The newly created linspace
329    fn step<S: Clone>(self, val: S) -> Linspace<Self::CoordDescType, S, Exact<Self::Value>>
330    where
331        Self::Value: Add<S, Output = Self::Value> + PartialOrd + Clone,
332    {
333        let mut ret = Linspace {
334            step: val,
335            inner: self.into(),
336            grid_value: vec![],
337            _phatom: PhantomData,
338        };
339
340        ret.compute_grid_values();
341
342        ret
343    }
344}
345
346impl<T: AsRangedCoord> IntoLinspace for T {}
347
348#[cfg(test)]
349mod test {
350    use super::*;
351
352    #[test]
353    fn test_float_linspace() {
354        let coord = (0.0f64..100.0f64).step(0.1);
355
356        assert_eq!(coord.map(&23.12, (0, 10000)), 2312);
357        assert_eq!(coord.range(), 0.0..100.0);
358        assert_eq!(coord.key_points(100000).len(), 1001);
359        assert_eq!(coord.size(), 1001);
360        assert_eq!(coord.index_of(&coord.from_index(230).unwrap()), Some(230));
361        assert!((coord.from_index(230).unwrap() - 23.0).abs() < 1e-5);
362    }
363
364    #[test]
365    fn test_rounding_methods() {
366        let coord = (0.0f64..100.0f64).step(1.0);
367
368        assert_eq!(coord.index_of(&1.0), Some(1));
369        assert_eq!(coord.index_of(&1.2), None);
370
371        let coord = coord.use_floor();
372        assert_eq!(coord.index_of(&1.0), Some(1));
373        assert_eq!(coord.index_of(&1.2), Some(1));
374        assert_eq!(coord.index_of(&23.9), Some(23));
375        assert_eq!(coord.index_of(&10000.0), Some(99));
376        assert_eq!(coord.index_of(&-1.0), None);
377
378        let coord = coord.use_ceil();
379        assert_eq!(coord.index_of(&1.0), Some(1));
380        assert_eq!(coord.index_of(&1.2), Some(2));
381        assert_eq!(coord.index_of(&23.9), Some(24));
382        assert_eq!(coord.index_of(&10000.0), None);
383        assert_eq!(coord.index_of(&-1.0), Some(0));
384
385        let coord = coord.use_round();
386        assert_eq!(coord.index_of(&1.0), Some(1));
387        assert_eq!(coord.index_of(&1.2), Some(1));
388        assert_eq!(coord.index_of(&1.7), Some(2));
389        assert_eq!(coord.index_of(&23.9), Some(24));
390        assert_eq!(coord.index_of(&10000.0), Some(99));
391        assert_eq!(coord.index_of(&-1.0), Some(0));
392
393        let coord = (0.0f64..-100.0f64).step(-1.0);
394
395        assert_eq!(coord.index_of(&-1.0), Some(1));
396        assert_eq!(coord.index_of(&-1.2), None);
397
398        let coord = coord.use_floor();
399        assert_eq!(coord.index_of(&-1.0), Some(1));
400        assert_eq!(coord.index_of(&-1.2), Some(2));
401        assert_eq!(coord.index_of(&-23.9), Some(24));
402        assert_eq!(coord.index_of(&-10000.0), None);
403        assert_eq!(coord.index_of(&1.0), Some(0));
404
405        let coord = coord.use_ceil();
406        assert_eq!(coord.index_of(&-1.0), Some(1));
407        assert_eq!(coord.index_of(&-1.2), Some(1));
408        assert_eq!(coord.index_of(&-23.9), Some(23));
409        assert_eq!(coord.index_of(&-10000.0), Some(99));
410        assert_eq!(coord.index_of(&1.0), None);
411
412        let coord = coord.use_round();
413        assert_eq!(coord.index_of(&-1.0), Some(1));
414        assert_eq!(coord.index_of(&-1.2), Some(1));
415        assert_eq!(coord.index_of(&-1.7), Some(2));
416        assert_eq!(coord.index_of(&-23.9), Some(24));
417        assert_eq!(coord.index_of(&-10000.0), Some(99));
418        assert_eq!(coord.index_of(&1.0), Some(0));
419    }
420
421    #[cfg(feature = "chrono")]
422    #[test]
423    fn test_duration_linspace() {
424        use chrono::Duration;
425        let coord = (Duration::seconds(0)..Duration::seconds(100)).step(Duration::milliseconds(1));
426
427        assert_eq!(coord.size(), 100_000);
428        assert_eq!(coord.index_of(&coord.from_index(230).unwrap()), Some(230));
429        assert_eq!(coord.key_points(10000000).len(), 100_000);
430        assert_eq!(coord.range(), Duration::seconds(0)..Duration::seconds(100));
431        assert_eq!(coord.map(&Duration::seconds(25), (0, 100_000)), 25000);
432    }
433}