colorimetry_plot/chart/
range.rs

1// Copyright (c) 2025, Harbers Bik LLC
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3//
4use std::fmt::{self, Formatter, Result as FmtResult};
5use std::ops::{Bound, Range, RangeBounds};
6
7pub struct ScaleValue(pub f64, pub f64);
8
9impl fmt::Display for ScaleValue {
10    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
11        format_axis_tick(self.0, self.1, f)
12    }
13}
14
15/// Round value to the nearest tick step and format using the formatter's precision.
16pub fn format_axis_tick(value: f64, step: f64, f: &mut Formatter<'_>) -> FmtResult {
17    let rounded = (value / step).round() * step;
18    if approx::abs_diff_eq!(rounded, 0.0, epsilon = 1e-10) {
19        // Handle zero case explicitly to avoid formatting issues
20        return write!(f, "0");
21    }
22    // Determine precision from tick step if not explicitly set
23    let precision = match f.precision() {
24        Some(p) => p,
25        None => {
26            if step >= 1.0 {
27                0
28            } else {
29                // Count decimal digits in step: e.g. 0.01 → 2
30                let mut digits = 0;
31                let mut s = step;
32                while s < 1.0 {
33                    s *= 10.0;
34                    digits += 1;
35                    if digits > 10 {
36                        break;
37                    } // prevent infinite loops
38                }
39                digits
40            }
41        }
42    };
43
44    write!(f, "{rounded:.precision$}")
45}
46
47#[derive(Debug, Clone, Copy)]
48pub struct ScaleRange {
49    pub start: f64,
50    pub end: f64,
51    pub end_included: bool,
52}
53
54impl ScaleRange {
55    pub fn new<R: RangeBounds<f64>>(range: R) -> Self {
56        let start = match range.start_bound() {
57            Bound::Included(&s) => s,
58            Bound::Excluded(&s) => s,
59            Bound::Unbounded => 0.0,
60        };
61        let (end, end_included) = match range.end_bound() {
62            Bound::Included(&e) => (e, true),
63            Bound::Excluded(&e) => (e, false),
64            Bound::Unbounded => panic!("Unbounded end not allowed"),
65        };
66
67        Self {
68            start,
69            end,
70            end_included,
71        }
72    }
73
74    pub fn as_range(&self) -> Range<f64> {
75        self.start..self.end
76    }
77
78    pub fn span(&self) -> f64 {
79        self.end - self.start
80    }
81
82    pub fn scale(&self, value: f64) -> f64 {
83        (value - self.start) / self.span()
84    }
85
86    pub fn unscale(&self, value: f64) -> f64 {
87        self.start + value * self.span()
88    }
89
90    pub fn scale_descent(&self, value: f64) -> f64 {
91        (self.end - value) / self.span()
92    }
93
94    pub fn unscale_descent(&self, value: f64) -> f64 {
95        self.end - value * self.span()
96    }
97
98    /// Creates an iterator over the range with a specified step size.
99    /// Produces values aligned with the step size, for use to draw grid lines and ticks.
100    pub fn iter_with_step(&self, step: f64) -> ScaleRangeIterator {
101        let start = (self.start / step).ceil() * step; // Start from the first tick
102
103        ScaleRangeIterator {
104            start,
105            end: self.end,
106            step,
107            end_included: self.end_included,
108        }
109    }
110}
111
112pub struct ScaleRangeIterator {
113    start: f64,
114    end: f64,
115    end_included: bool,
116    step: f64,
117}
118
119impl Iterator for ScaleRangeIterator {
120    type Item = f64;
121
122    fn next(&mut self) -> Option<Self::Item> {
123        if self.start < self.end
124            || (self.end_included && approx::abs_diff_eq!(self.start, self.end, epsilon = 1e-10))
125        {
126            let current = self.start;
127            self.start += self.step;
128            Some(current)
129        } else {
130            None
131        }
132    }
133}
134
135pub struct ScaleRangeWithStep {
136    pub range: ScaleRange,
137    pub step: f64,
138}
139
140impl ScaleRangeWithStep {
141    pub fn new(range: ScaleRange, step: f64) -> Self {
142        Self { range, step }
143    }
144
145    pub fn iter(&self) -> ScaleRangeIterator {
146        self.range.iter_with_step(self.step)
147    }
148}
149
150impl<R: RangeBounds<f64>> From<(R, f64)> for ScaleRangeWithStep {
151    fn from((range, step): (R, f64)) -> Self {
152        Self {
153            range: ScaleRange::new(range),
154            step,
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use approx::assert_abs_diff_eq;
162
163    use super::*;
164
165    #[test]
166    fn test_chart_range() {
167        let range = ScaleRange::new(0.0..1.0);
168        assert_abs_diff_eq!(range.end, 1.0);
169        assert_abs_diff_eq!(range.span(), 1.0);
170        assert_abs_diff_eq!(range.scale(0.5), 0.5);
171        assert_abs_diff_eq!(range.scale_descent(0.5), 0.5);
172    }
173
174    #[test]
175    fn test_iter_with_step() {
176        let range = ScaleRange::new(0.0..=1.0);
177        let mut iter = range.iter_with_step(0.2);
178        assert_abs_diff_eq!(iter.next().unwrap(), 0.0);
179        assert_abs_diff_eq!(iter.next().unwrap(), 0.2);
180        assert_abs_diff_eq!(iter.next().unwrap(), 0.4);
181        assert_abs_diff_eq!(iter.next().unwrap(), 0.6);
182        assert_abs_diff_eq!(iter.next().unwrap(), 0.8);
183        assert_abs_diff_eq!(iter.next().unwrap(), 1.0);
184        assert_eq!(iter.next(), None);
185    }
186
187    #[test]
188    fn test_iter_with_step_excluding_end() {
189        let range = ScaleRange::new(0.0..1.0);
190        let mut iter = range.iter_with_step(0.2);
191        assert_abs_diff_eq!(iter.next().unwrap(), 0.0);
192        assert_abs_diff_eq!(iter.next().unwrap(), 0.2);
193        assert_abs_diff_eq!(iter.next().unwrap(), 0.4);
194        assert_abs_diff_eq!(iter.next().unwrap(), 0.6);
195        assert_abs_diff_eq!(iter.next().unwrap(), 0.8);
196        assert_eq!(iter.next(), None);
197    }
198
199    #[test]
200    fn test_iter_with_open_start() {
201        let range = ScaleRange::new(..1.0);
202        let mut iter = range.iter_with_step(0.2);
203        assert_abs_diff_eq!(iter.next().unwrap(), 0.0);
204        assert_abs_diff_eq!(iter.next().unwrap(), 0.2);
205        assert_abs_diff_eq!(iter.next().unwrap(), 0.4);
206        assert_abs_diff_eq!(iter.next().unwrap(), 0.6);
207        assert_abs_diff_eq!(iter.next().unwrap(), 0.8);
208        assert_eq!(iter.next(), None);
209    }
210
211    #[test]
212    fn test_range_with_step() {
213        let rws: ScaleRangeWithStep = (0.0..=1.0, 0.2).into();
214        let mut iter = rws.iter();
215        assert_abs_diff_eq!(iter.next().unwrap(), 0.0);
216        assert_abs_diff_eq!(iter.next().unwrap(), 0.2);
217        assert_abs_diff_eq!(iter.next().unwrap(), 0.4);
218        assert_abs_diff_eq!(iter.next().unwrap(), 0.6);
219        assert_abs_diff_eq!(iter.next().unwrap(), 0.8);
220        assert_abs_diff_eq!(iter.next().unwrap(), 1.0);
221        assert_eq!(iter.next(), None);
222    }
223}