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