embedded_charts/axes/
scale.rs

1//! Axis scale implementations for different transformation types
2
3use crate::error::{ChartError, ChartResult};
4use core::fmt::Debug;
5
6// Import for no_std compatibility
7#[cfg(not(feature = "std"))]
8use alloc::boxed::Box;
9#[cfg(feature = "std")]
10use std::boxed::Box;
11
12// Import math traits based on feature flags
13#[cfg(all(feature = "floating-point", not(feature = "std")))]
14use micromath::F32Ext;
15
16/// Trait for axis scale transformations
17pub trait ScaleTransform: Debug {
18    /// Transform a data value to normalized coordinates [0, 1]
19    fn transform(&self, value: f32) -> ChartResult<f32>;
20
21    /// Inverse transform from normalized coordinates [0, 1] to data value
22    fn inverse(&self, normalized: f32) -> ChartResult<f32>;
23
24    /// Get nice tick values for this scale
25    fn get_ticks(&self, count: usize) -> ChartResult<heapless::Vec<f32, 16>>;
26
27    /// Format a value for display on this scale
28    fn format_value(&self, value: f32) -> heapless::String<16>;
29}
30
31/// Configuration for axis scales
32#[derive(Debug, Clone, Copy)]
33pub struct ScaleConfig {
34    /// Minimum value of the scale domain
35    pub min: f32,
36    /// Maximum value of the scale domain
37    pub max: f32,
38    /// Whether to include zero in the scale
39    pub include_zero: bool,
40    /// Whether to add padding to the scale bounds
41    pub nice_bounds: bool,
42}
43
44impl Default for ScaleConfig {
45    fn default() -> Self {
46        Self {
47            min: 0.0,
48            max: 100.0,
49            include_zero: false,
50            nice_bounds: true,
51        }
52    }
53}
54
55/// Linear scale transformation
56#[derive(Debug, Clone)]
57pub struct LinearScale {
58    config: ScaleConfig,
59    range: f32,
60}
61
62impl LinearScale {
63    /// Create a new linear scale
64    pub fn new(config: ScaleConfig) -> ChartResult<Self> {
65        if config.min >= config.max {
66            return Err(ChartError::InvalidRange);
67        }
68
69        let range = config.max - config.min;
70        Ok(Self { config, range })
71    }
72}
73
74impl ScaleTransform for LinearScale {
75    fn transform(&self, value: f32) -> ChartResult<f32> {
76        if value.is_nan() || value.is_infinite() {
77            return Err(ChartError::InvalidData);
78        }
79
80        let normalized = (value - self.config.min) / self.range;
81        Ok(normalized.clamp(0.0, 1.0))
82    }
83
84    fn inverse(&self, normalized: f32) -> ChartResult<f32> {
85        if !(0.0..=1.0).contains(&normalized) {
86            return Err(ChartError::InvalidRange);
87        }
88
89        Ok(self.config.min + normalized * self.range)
90    }
91
92    fn get_ticks(&self, count: usize) -> ChartResult<heapless::Vec<f32, 16>> {
93        let mut ticks = heapless::Vec::new();
94
95        if count == 0 {
96            return Ok(ticks);
97        }
98
99        if count == 1 {
100            let _ = ticks.push((self.config.min + self.config.max) / 2.0);
101            return Ok(ticks);
102        }
103
104        let step = self.range / (count - 1) as f32;
105        for i in 0..count {
106            let tick = self.config.min + (i as f32) * step;
107            if ticks.push(tick).is_err() {
108                break;
109            }
110        }
111
112        Ok(ticks)
113    }
114
115    fn format_value(&self, value: f32) -> heapless::String<16> {
116        let mut s = heapless::String::new();
117
118        // Simple formatting logic
119        if value == 0.0 {
120            let _ = write!(s, "0");
121        } else if value.abs() >= 1000.0 {
122            let _ = write!(s, "{:.1}k", value / 1000.0);
123        } else if value.abs() >= 1.0 {
124            let _ = write!(s, "{value:.0}");
125        } else if value.abs() >= 0.01 {
126            let _ = write!(s, "{value:.2}");
127        } else {
128            let _ = write!(s, "{value:.1e}");
129        }
130
131        s
132    }
133}
134
135/// Logarithmic scale transformation
136#[derive(Debug, Clone)]
137pub struct LogarithmicScale {
138    config: ScaleConfig,
139    base: f32,
140    log_min: f32,
141    #[allow(dead_code)]
142    log_max: f32,
143    log_range: f32,
144}
145
146impl LogarithmicScale {
147    /// Create a new logarithmic scale with specified base
148    pub fn new(config: ScaleConfig, base: f32) -> ChartResult<Self> {
149        if config.min <= 0.0 || config.max <= 0.0 {
150            return Err(ChartError::InvalidRange);
151        }
152
153        if config.min >= config.max {
154            return Err(ChartError::InvalidRange);
155        }
156
157        if base <= 0.0 || base == 1.0 {
158            return Err(ChartError::InvalidConfiguration);
159        }
160
161        #[cfg(feature = "std")]
162        let (log_min, log_max) = (config.min.log(base), config.max.log(base));
163
164        #[cfg(not(feature = "std"))]
165        let (log_min, log_max) = (config.min.log(base), config.max.log(base));
166        let log_range = log_max - log_min;
167
168        Ok(Self {
169            config,
170            base,
171            log_min,
172            log_max,
173            log_range,
174        })
175    }
176
177    /// Create a logarithmic scale with base 10
178    pub fn base10(config: ScaleConfig) -> ChartResult<Self> {
179        Self::new(config, 10.0)
180    }
181
182    /// Create a logarithmic scale with base e (natural logarithm)
183    pub fn natural(config: ScaleConfig) -> ChartResult<Self> {
184        Self::new(config, core::f32::consts::E)
185    }
186}
187
188impl ScaleTransform for LogarithmicScale {
189    fn transform(&self, value: f32) -> ChartResult<f32> {
190        if value <= 0.0 {
191            return Err(ChartError::InvalidData);
192        }
193
194        if value.is_nan() || value.is_infinite() {
195            return Err(ChartError::InvalidData);
196        }
197
198        #[cfg(feature = "std")]
199        let log_value = value.log(self.base);
200
201        #[cfg(not(feature = "std"))]
202        let log_value = value.log(self.base);
203        let normalized = (log_value - self.log_min) / self.log_range;
204        Ok(normalized.clamp(0.0, 1.0))
205    }
206
207    fn inverse(&self, normalized: f32) -> ChartResult<f32> {
208        if !(0.0..=1.0).contains(&normalized) {
209            return Err(ChartError::InvalidRange);
210        }
211
212        let log_value = self.log_min + normalized * self.log_range;
213        #[cfg(feature = "std")]
214        let result = self.base.powf(log_value);
215
216        #[cfg(not(feature = "std"))]
217        let result = self.base.powf(log_value);
218
219        Ok(result)
220    }
221
222    fn get_ticks(&self, _count: usize) -> ChartResult<heapless::Vec<f32, 16>> {
223        let mut ticks = heapless::Vec::new();
224
225        // Generate ticks at powers of the base
226        #[cfg(feature = "std")]
227        let start_power = self.config.min.log(self.base).floor();
228
229        #[cfg(not(feature = "std"))]
230        let start_power = self.config.min.log(self.base).floor();
231
232        let mut power = start_power;
233
234        // Generate up to 20 powers (to avoid infinite loop)
235        for _ in 0..20 {
236            #[cfg(feature = "std")]
237            let value = self.base.powf(power);
238
239            #[cfg(not(feature = "std"))]
240            let value = self.base.powf(power);
241
242            if value > self.config.max * 1.1 {
243                // Add small tolerance
244                break;
245            }
246
247            if value >= self.config.min * 0.9 && value <= self.config.max * 1.1 {
248                // Round to avoid floating point precision issues
249                let rounded = if self.base == 10.0 {
250                    // For base 10, round to nearest power
251                    let log_val = value.log10();
252                    if (log_val - log_val.round()).abs() < 0.01 {
253                        10.0_f32.powf(log_val.round())
254                    } else {
255                        value
256                    }
257                } else {
258                    value
259                };
260
261                if rounded >= self.config.min && rounded <= self.config.max {
262                    let _ = ticks.push(rounded);
263                }
264            }
265
266            // Add intermediate ticks for base 10 only if we have space
267            // and the power is small enough to avoid too many ticks
268            if self.base == 10.0 && !ticks.is_full() && power < 3.0 {
269                for i in 2..10 {
270                    if ticks.is_full() {
271                        break;
272                    }
273                    let intermediate = value * (i as f32);
274                    if intermediate > self.config.max {
275                        break;
276                    }
277                    if intermediate >= self.config.min {
278                        let _ = ticks.push(intermediate);
279                    }
280                }
281            }
282
283            power += 1.0;
284        }
285
286        Ok(ticks)
287    }
288
289    fn format_value(&self, value: f32) -> heapless::String<16> {
290        let mut s = heapless::String::new();
291
292        if self.base == 10.0 {
293            // For base 10, use scientific notation for round powers
294            #[cfg(feature = "std")]
295            let log_value = value.log10();
296
297            #[cfg(not(feature = "std"))]
298            let log_value = value.log10();
299            if (log_value - log_value.round()).abs() < 0.01 && value < 1000.0 {
300                let _ = write!(s, "10^{:.0}", log_value.round());
301            } else if value >= 1000.0 {
302                let _ = write!(s, "{value:.0}");
303            } else {
304                let _ = write!(s, "{value:.1}");
305            }
306        } else {
307            // For other bases, use regular formatting
308            if value >= 1000.0 {
309                let _ = write!(s, "{value:.0}");
310            } else if value >= 1.0 {
311                let _ = write!(s, "{value:.1}");
312            } else {
313                let _ = write!(s, "{value:.2}");
314            }
315        }
316
317        s
318    }
319}
320
321/// Custom scale with user-defined transformation functions
322pub struct CustomScale<F, I>
323where
324    F: Fn(f32) -> ChartResult<f32>,
325    I: Fn(f32) -> ChartResult<f32>,
326{
327    config: ScaleConfig,
328    transform_fn: F,
329    inverse_fn: I,
330}
331
332impl<F, I> core::fmt::Debug for CustomScale<F, I>
333where
334    F: Fn(f32) -> ChartResult<f32>,
335    I: Fn(f32) -> ChartResult<f32>,
336{
337    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
338        f.debug_struct("CustomScale")
339            .field("config", &self.config)
340            .field("transform_fn", &"<function>")
341            .field("inverse_fn", &"<function>")
342            .finish()
343    }
344}
345
346impl<F, I> CustomScale<F, I>
347where
348    F: Fn(f32) -> ChartResult<f32>,
349    I: Fn(f32) -> ChartResult<f32>,
350{
351    /// Create a new custom scale with user-defined functions
352    pub fn new(config: ScaleConfig, transform_fn: F, inverse_fn: I) -> Self {
353        Self {
354            config,
355            transform_fn,
356            inverse_fn,
357        }
358    }
359}
360
361impl<F, I> ScaleTransform for CustomScale<F, I>
362where
363    F: Fn(f32) -> ChartResult<f32>,
364    I: Fn(f32) -> ChartResult<f32>,
365{
366    fn transform(&self, value: f32) -> ChartResult<f32> {
367        (self.transform_fn)(value)
368    }
369
370    fn inverse(&self, normalized: f32) -> ChartResult<f32> {
371        (self.inverse_fn)(normalized)
372    }
373
374    fn get_ticks(&self, count: usize) -> ChartResult<heapless::Vec<f32, 16>> {
375        // For custom scales, use linear tick generation in the domain
376        LinearScale::new(self.config)?.get_ticks(count)
377    }
378
379    fn format_value(&self, value: f32) -> heapless::String<16> {
380        // Use default formatting for custom scales
381        let mut s = heapless::String::new();
382        let _ = write!(s, "{value:.2}");
383        s
384    }
385}
386
387/// Enumeration of available scale types
388#[derive(Debug, Clone, Copy, PartialEq)]
389pub enum AxisScaleType {
390    /// Linear scale (default)
391    Linear,
392    /// Logarithmic scale with base 10
393    Log10,
394    /// Logarithmic scale with base e
395    LogE,
396    /// Logarithmic scale with custom base
397    LogBase(f32),
398    /// Custom scale (requires transformation functions)
399    Custom,
400}
401
402impl Default for AxisScaleType {
403    fn default() -> Self {
404        Self::Linear
405    }
406}
407
408/// Main axis scale container
409#[derive(Debug)]
410pub enum AxisScale {
411    /// Linear scale transformation
412    Linear(LinearScale),
413    /// Logarithmic scale transformation
414    Logarithmic(LogarithmicScale),
415    /// Custom scale with user-defined transformation
416    Custom(Box<dyn ScaleTransform>),
417}
418
419impl AxisScale {
420    /// Create a new axis scale of the specified type
421    pub fn new(scale_type: AxisScaleType, config: ScaleConfig) -> ChartResult<Self> {
422        match scale_type {
423            AxisScaleType::Linear => Ok(Self::Linear(LinearScale::new(config)?)),
424            AxisScaleType::Log10 => Ok(Self::Logarithmic(LogarithmicScale::base10(config)?)),
425            AxisScaleType::LogE => Ok(Self::Logarithmic(LogarithmicScale::natural(config)?)),
426            AxisScaleType::LogBase(base) => {
427                Ok(Self::Logarithmic(LogarithmicScale::new(config, base)?))
428            }
429            AxisScaleType::Custom => Err(ChartError::InvalidConfiguration),
430        }
431    }
432
433    /// Transform a value using this scale
434    pub fn transform(&self, value: f32) -> ChartResult<f32> {
435        match self {
436            Self::Linear(scale) => scale.transform(value),
437            Self::Logarithmic(scale) => scale.transform(value),
438            Self::Custom(scale) => scale.transform(value),
439        }
440    }
441
442    /// Inverse transform a normalized value
443    pub fn inverse(&self, normalized: f32) -> ChartResult<f32> {
444        match self {
445            Self::Linear(scale) => scale.inverse(normalized),
446            Self::Logarithmic(scale) => scale.inverse(normalized),
447            Self::Custom(scale) => scale.inverse(normalized),
448        }
449    }
450
451    /// Get tick values for this scale
452    pub fn get_ticks(&self, count: usize) -> ChartResult<heapless::Vec<f32, 16>> {
453        match self {
454            Self::Linear(scale) => scale.get_ticks(count),
455            Self::Logarithmic(scale) => scale.get_ticks(count),
456            Self::Custom(scale) => scale.get_ticks(count),
457        }
458    }
459
460    /// Format a value for display
461    pub fn format_value(&self, value: f32) -> heapless::String<16> {
462        match self {
463            Self::Linear(scale) => scale.format_value(value),
464            Self::Logarithmic(scale) => scale.format_value(value),
465            Self::Custom(scale) => scale.format_value(value),
466        }
467    }
468}
469
470// Helper for write! macro in no_std
471use core::fmt::Write;
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476
477    #[test]
478    fn test_linear_scale() {
479        let config = ScaleConfig {
480            min: 0.0,
481            max: 100.0,
482            ..Default::default()
483        };
484
485        let scale = LinearScale::new(config).unwrap();
486
487        // Test transform
488        assert_eq!(scale.transform(0.0).unwrap(), 0.0);
489        assert_eq!(scale.transform(50.0).unwrap(), 0.5);
490        assert_eq!(scale.transform(100.0).unwrap(), 1.0);
491
492        // Test inverse
493        assert_eq!(scale.inverse(0.0).unwrap(), 0.0);
494        assert_eq!(scale.inverse(0.5).unwrap(), 50.0);
495        assert_eq!(scale.inverse(1.0).unwrap(), 100.0);
496
497        // Test ticks
498        let ticks = scale.get_ticks(5).unwrap();
499        assert_eq!(ticks.len(), 5);
500        assert_eq!(ticks[0], 0.0);
501        assert_eq!(ticks[4], 100.0);
502    }
503
504    #[test]
505    fn test_logarithmic_scale() {
506        let config = ScaleConfig {
507            min: 1.0,
508            max: 1000.0,
509            ..Default::default()
510        };
511
512        let scale = LogarithmicScale::base10(config).unwrap();
513
514        // Test transform
515        assert!((scale.transform(1.0).unwrap() - 0.0).abs() < 0.001);
516        assert!((scale.transform(10.0).unwrap() - 0.333).abs() < 0.01);
517        assert!((scale.transform(100.0).unwrap() - 0.667).abs() < 0.01);
518        assert!((scale.transform(1000.0).unwrap() - 1.0).abs() < 0.001);
519
520        // Test error for non-positive values
521        assert!(scale.transform(0.0).is_err());
522        assert!(scale.transform(-1.0).is_err());
523    }
524}