yew_chart/
linear_axis_scale.rs

1/// A LinearScale represents a linear scale for floating point values within a fixed range.
2/// A step is also expressed and indicates the interval to be used for each tick on the axis.
3use std::{ops::Range, rc::Rc};
4
5use crate::axis::{NormalisedValue, Scale, Tick};
6
7/// An axis labeller is a closure that produces a string given a value within the axis scale
8pub trait Labeller: Fn(f32) -> String {}
9
10impl<T: Fn(f32) -> String> Labeller for T {}
11
12fn labeller() -> impl Labeller {
13    |v| (v as i32).to_string()
14}
15
16#[derive(Clone)]
17pub struct LinearScale {
18    range: Range<f32>,
19    step: f32,
20    scale: f32,
21    labeller: Option<Rc<dyn Labeller>>,
22}
23
24impl LinearScale {
25    /// Create a new scale with a range and step and labels as a integers
26    pub fn new(range: Range<f32>, step: f32) -> LinearScale {
27        Self::with_labeller(range, step, Some(Rc::from(labeller())))
28    }
29
30    /// Create a new scale with a range and step and a custom labeller
31    pub fn with_labeller(
32        range: Range<f32>,
33        step: f32,
34        labeller: Option<Rc<dyn Labeller>>,
35    ) -> LinearScale {
36        let delta = range.end - range.start;
37        let scale = if delta != 0.0 { 1.0 / delta } else { 1.0 };
38        LinearScale {
39            range,
40            step,
41            scale,
42            labeller,
43        }
44    }
45}
46
47impl Scale for LinearScale {
48    type Scalar = f32;
49
50    fn ticks(&self) -> Vec<Tick> {
51        LinearScaleInclusiveIter {
52            from: self.range.start,
53            to: self.range.end,
54            step: self.step,
55            first_time: true,
56            last_time: false,
57        }
58        .map(move |v| {
59            let location = (v - self.range.start) * self.scale;
60            Tick {
61                location: NormalisedValue(location),
62                label: self.labeller.as_ref().map(|l| (l)(v)),
63            }
64        })
65        .collect()
66    }
67
68    fn normalise(&self, value: Self::Scalar) -> NormalisedValue {
69        NormalisedValue((value - self.range.start) * self.scale)
70    }
71}
72
73struct LinearScaleInclusiveIter {
74    pub from: f32,
75    pub to: f32,
76    pub step: f32,
77    pub first_time: bool,
78    pub last_time: bool,
79}
80
81impl Iterator for LinearScaleInclusiveIter {
82    type Item = f32;
83
84    fn next(&mut self) -> Option<Self::Item> {
85        if !self.first_time {
86            self.from += self.step;
87        } else {
88            self.first_time = false;
89        };
90        if (self.step >= 0.0 && self.from < self.to) || (self.step < 0.0 && self.from > self.to) {
91            Some(self.from)
92        } else if !self.last_time {
93            self.last_time = true;
94            Some(self.to)
95        } else {
96            None
97        }
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_scale() {
107        let scale = LinearScale::new(0.0..100.0, 25.0);
108
109        assert_eq!(
110            scale.ticks(),
111            vec![
112                Tick {
113                    location: NormalisedValue(0.0),
114                    label: Some("0".to_string())
115                },
116                Tick {
117                    location: NormalisedValue(0.25),
118                    label: Some("25".to_string())
119                },
120                Tick {
121                    location: NormalisedValue(0.5),
122                    label: Some("50".to_string())
123                },
124                Tick {
125                    location: NormalisedValue(0.75),
126                    label: Some("75".to_string())
127                },
128                Tick {
129                    location: NormalisedValue(1.0),
130                    label: Some("100".to_string())
131                }
132            ]
133        );
134
135        assert_eq!(scale.normalise(50.0), NormalisedValue(0.5));
136    }
137
138    #[test]
139    fn test_backward_scale() {
140        let scale = LinearScale::new(100.0..0.0, -25.0);
141
142        assert_eq!(
143            scale.ticks(),
144            vec![
145                Tick {
146                    location: NormalisedValue(-0.0),
147                    label: Some("100".to_string())
148                },
149                Tick {
150                    location: NormalisedValue(0.25),
151                    label: Some("75".to_string())
152                },
153                Tick {
154                    location: NormalisedValue(0.5),
155                    label: Some("50".to_string())
156                },
157                Tick {
158                    location: NormalisedValue(0.75),
159                    label: Some("25".to_string())
160                },
161                Tick {
162                    location: NormalisedValue(1.0),
163                    label: Some("0".to_string())
164                },
165            ]
166        );
167
168        assert_eq!(scale.normalise(50.0), NormalisedValue(0.5));
169    }
170
171    #[test]
172    fn test_precise_scale() {
173        fn float_labeller() -> impl Labeller {
174            |v| format!("{:3.2}", v)
175        }
176
177        let scale = LinearScale::with_labeller(0.0..1.0, 0.25, Some(Rc::from(float_labeller())));
178
179        assert_eq!(
180            scale.ticks(),
181            vec![
182                Tick {
183                    location: NormalisedValue(0.0),
184                    label: Some("0.00".to_string())
185                },
186                Tick {
187                    location: NormalisedValue(0.25),
188                    label: Some("0.25".to_string())
189                },
190                Tick {
191                    location: NormalisedValue(0.5),
192                    label: Some("0.50".to_string())
193                },
194                Tick {
195                    location: NormalisedValue(0.75),
196                    label: Some("0.75".to_string())
197                },
198                Tick {
199                    location: NormalisedValue(1.0),
200                    label: Some("1.00".to_string())
201                }
202            ]
203        );
204
205        assert_eq!(scale.normalise(0.5), NormalisedValue(0.5));
206    }
207
208    #[test]
209    fn test_zero_range() {
210        let scale = LinearScale::new(1.0..1.0, 0.25);
211
212        assert_eq!(
213            scale.ticks(),
214            vec![Tick {
215                location: NormalisedValue(0.0),
216                label: Some("1".to_string())
217            },]
218        );
219
220        assert_eq!(scale.normalise(1.0), NormalisedValue(0.0));
221    }
222
223    #[test]
224    fn test_zero_duration() {
225        let scale = LinearScale::new(1.0..1.0, 0.0);
226
227        assert_eq!(
228            scale.ticks(),
229            vec![Tick {
230                location: NormalisedValue(0.0),
231                label: Some("1".to_string())
232            },]
233        );
234
235        assert_eq!(scale.normalise(1.0), NormalisedValue(0.0));
236    }
237}