plotters/coord/ranged1d/combinators/
logarithmic.rs

1use crate::coord::ranged1d::types::RangedCoordf64;
2use crate::coord::ranged1d::{AsRangedCoord, DefaultFormatting, KeyPointHint, Ranged};
3use std::marker::PhantomData;
4use std::ops::Range;
5
6/// The trait for the type that is able to be presented in the log scale.
7/// This trait is primarily used by [LogRangeExt](struct.LogRangeExt.html).
8pub trait LogScalable: Clone {
9    /// Make the conversion from the type to the floating point number
10    fn as_f64(&self) -> f64;
11    /// Convert a floating point number to the scale
12    fn from_f64(f: f64) -> Self;
13}
14
15macro_rules! impl_log_scalable {
16    (i, $t:ty) => {
17        impl LogScalable for $t {
18            fn as_f64(&self) -> f64 {
19                if *self != 0 {
20                    return *self as f64;
21                }
22                // If this is an integer, we should allow zero point to be shown
23                // on the chart, thus we can't map the zero point to inf.
24                // So we just assigning a value smaller than 1 as the alternative
25                // of the zero point.
26                return 0.5;
27            }
28            fn from_f64(f: f64) -> $t {
29                f.round() as $t
30            }
31        }
32    };
33    (f, $t:ty) => {
34        impl LogScalable for $t {
35            fn as_f64(&self) -> f64 {
36                *self as f64
37            }
38            fn from_f64(f: f64) -> $t {
39                f as $t
40            }
41        }
42    };
43}
44
45impl_log_scalable!(i, u8);
46impl_log_scalable!(i, u16);
47impl_log_scalable!(i, u32);
48impl_log_scalable!(i, u64);
49impl_log_scalable!(i, usize);
50
51impl_log_scalable!(i, i8);
52impl_log_scalable!(i, i16);
53impl_log_scalable!(i, i32);
54impl_log_scalable!(i, i64);
55impl_log_scalable!(i, i128);
56impl_log_scalable!(i, isize);
57
58impl_log_scalable!(f, f32);
59impl_log_scalable!(f, f64);
60
61/// Convert a range to a log scale coordinate spec
62pub trait IntoLogRange {
63    /// The type of the value
64    type ValueType: LogScalable;
65
66    /// Make the log scale coordinate
67    fn log_scale(self) -> LogRangeExt<Self::ValueType>;
68}
69
70impl<T: LogScalable> IntoLogRange for Range<T> {
71    type ValueType = T;
72    fn log_scale(self) -> LogRangeExt<T> {
73        LogRangeExt {
74            range: self,
75            zero: 0.0,
76            base: 10.0,
77        }
78    }
79}
80
81/// The logarithmic coordinate decorator.
82/// This decorator is used to make the axis rendered as logarithmically.
83#[derive(Clone)]
84pub struct LogRangeExt<V: LogScalable> {
85    range: Range<V>,
86    zero: f64,
87    base: f64,
88}
89
90impl<V: LogScalable> LogRangeExt<V> {
91    /// Set the zero point of the log scale coordinate. Zero point is the point where we map -inf
92    /// of the axis to the coordinate
93    pub fn zero_point(mut self, value: V) -> Self
94    where
95        V: PartialEq,
96    {
97        self.zero = if V::from_f64(0.0) == value {
98            0.0
99        } else {
100            value.as_f64()
101        };
102
103        self
104    }
105
106    /// Set the base multiplier
107    pub fn base(mut self, base: f64) -> Self {
108        if self.base > 1.0 {
109            self.base = base;
110        }
111        self
112    }
113}
114
115impl<V: LogScalable> From<LogRangeExt<V>> for LogCoord<V> {
116    fn from(spec: LogRangeExt<V>) -> LogCoord<V> {
117        let zero_point = spec.zero;
118        let mut start = spec.range.start.as_f64() - zero_point;
119        let mut end = spec.range.end.as_f64() - zero_point;
120        let negative = if start < 0.0 || end < 0.0 {
121            start = -start;
122            end = -end;
123            true
124        } else {
125            false
126        };
127
128        if start < end {
129            if start == 0.0 {
130                start = start.max(end * 1e-5);
131            }
132        } else if end == 0.0 {
133            end = end.max(start * 1e-5);
134        }
135
136        LogCoord {
137            linear: (start.ln()..end.ln()).into(),
138            logic: spec.range,
139            normalized: start..end,
140            base: spec.base,
141            zero_point,
142            negative,
143            marker: PhantomData,
144        }
145    }
146}
147
148impl<V: LogScalable> AsRangedCoord for LogRangeExt<V> {
149    type CoordDescType = LogCoord<V>;
150    type Value = V;
151}
152
153/// A log scaled coordinate axis
154pub struct LogCoord<V: LogScalable> {
155    linear: RangedCoordf64,
156    logic: Range<V>,
157    normalized: Range<f64>,
158    base: f64,
159    zero_point: f64,
160    negative: bool,
161    marker: PhantomData<V>,
162}
163
164impl<V: LogScalable> LogCoord<V> {
165    fn value_to_f64(&self, value: &V) -> f64 {
166        let fv = value.as_f64() - self.zero_point;
167        if self.negative {
168            -fv
169        } else {
170            fv
171        }
172    }
173
174    fn f64_to_value(&self, fv: f64) -> V {
175        let fv = if self.negative { -fv } else { fv };
176        V::from_f64(fv + self.zero_point)
177    }
178
179    fn is_inf(&self, fv: f64) -> bool {
180        let fv = if self.negative { -fv } else { fv };
181        let a = V::from_f64(fv + self.zero_point);
182        let b = V::from_f64(self.zero_point);
183
184        (V::as_f64(&a) - V::as_f64(&b)).abs() < f64::EPSILON
185    }
186}
187
188impl<V: LogScalable> Ranged for LogCoord<V> {
189    type FormatOption = DefaultFormatting;
190    type ValueType = V;
191
192    fn map(&self, value: &V, limit: (i32, i32)) -> i32 {
193        let fv = self.value_to_f64(value);
194        let value_ln = fv.ln();
195        self.linear.map(&value_ln, limit)
196    }
197
198    fn key_points<Hint: KeyPointHint>(&self, hint: Hint) -> Vec<Self::ValueType> {
199        let max_points = hint.max_num_points();
200
201        let base = self.base;
202        let base_ln = base.ln();
203
204        let Range { mut start, mut end } = self.normalized;
205
206        if start > end {
207            std::mem::swap(&mut start, &mut end);
208        }
209
210        let bold_count = ((end / start).ln().abs() / base_ln).floor().max(1.0) as usize;
211
212        let light_density = if max_points < bold_count {
213            0
214        } else {
215            let density = 1 + (max_points - bold_count) / bold_count;
216            let mut exp = 1;
217            while exp * 10 <= density {
218                exp *= 10;
219            }
220            exp - 1
221        };
222
223        let mut multiplier = base;
224        let mut cnt = 1;
225        while max_points < bold_count / cnt {
226            multiplier *= base;
227            cnt += 1;
228        }
229
230        let mut ret = vec![];
231        let mut val = (base).powf((start.ln() / base_ln).ceil());
232
233        while val <= end {
234            if !self.is_inf(val) {
235                ret.push(self.f64_to_value(val));
236            }
237            for i in 1..=light_density {
238                let v = val
239                    * (1.0
240                        + multiplier / f64::from(light_density as u32 + 1) * f64::from(i as u32));
241                if v > end {
242                    break;
243                }
244                if !self.is_inf(val) {
245                    ret.push(self.f64_to_value(v));
246                }
247            }
248            val *= multiplier;
249        }
250
251        ret
252    }
253
254    fn range(&self) -> Range<V> {
255        self.logic.clone()
256    }
257}
258
259/// The logarithmic coordinate decorator.
260/// This decorator is used to make the axis rendered as logarithmically.
261#[deprecated(note = "LogRange is deprecated, use IntoLogRange trait method instead")]
262#[derive(Clone)]
263pub struct LogRange<V: LogScalable>(pub Range<V>);
264
265#[allow(deprecated)]
266impl<V: LogScalable> AsRangedCoord for LogRange<V> {
267    type CoordDescType = LogCoord<V>;
268    type Value = V;
269}
270
271#[allow(deprecated)]
272impl<V: LogScalable> From<LogRange<V>> for LogCoord<V> {
273    fn from(range: LogRange<V>) -> LogCoord<V> {
274        range.0.log_scale().into()
275    }
276}
277
278#[cfg(test)]
279mod test {
280    use super::*;
281    #[test]
282    fn regression_test_issue_143() {
283        let range: LogCoord<f64> = (1.0..5.0).log_scale().into();
284
285        range.key_points(100);
286    }
287}