embedded_charts/axes/
traits.rs

1//! Core traits for axis implementations.
2
3use crate::axes::{AxisOrientation, AxisPosition};
4use crate::error::ChartResult;
5use crate::math::{Math, NumericConversion};
6use embedded_graphics::{prelude::*, primitives::Rectangle};
7
8/// Core trait for all axis types
9pub trait Axis<T, C: PixelColor> {
10    /// The tick generator type for this axis
11    type TickGenerator: TickGenerator<T>;
12    /// The style type for this axis
13    type Style;
14
15    /// Get the minimum value of the axis
16    fn min(&self) -> T;
17
18    /// Get the maximum value of the axis
19    fn max(&self) -> T;
20
21    /// Get the axis orientation
22    fn orientation(&self) -> AxisOrientation;
23
24    /// Get the axis position
25    fn position(&self) -> AxisPosition;
26
27    /// Transform a data value to screen coordinate
28    ///
29    /// # Arguments
30    /// * `value` - The data value to transform
31    /// * `viewport` - The available drawing area
32    fn transform_value(&self, value: T, viewport: Rectangle) -> i32;
33
34    /// Transform a screen coordinate back to data value
35    ///
36    /// # Arguments
37    /// * `coordinate` - The screen coordinate
38    /// * `viewport` - The available drawing area
39    fn inverse_transform(&self, coordinate: i32, viewport: Rectangle) -> T;
40
41    /// Get the tick generator for this axis
42    fn tick_generator(&self) -> &Self::TickGenerator;
43
44    /// Get the style configuration
45    fn style(&self) -> &Self::Style;
46
47    /// Draw the axis to the target
48    ///
49    /// # Arguments
50    /// * `viewport` - The area to draw the axis in
51    /// * `target` - The display target to draw to
52    fn draw<D>(&self, viewport: Rectangle, target: &mut D) -> ChartResult<()>
53    where
54        D: DrawTarget<Color = C>;
55
56    /// Calculate the space required for this axis (labels, ticks, etc.)
57    fn required_space(&self) -> u32;
58}
59
60/// Trait for generating tick marks and labels
61pub trait TickGenerator<T> {
62    /// Generate tick positions for the given range
63    ///
64    /// # Arguments
65    /// * `min` - Minimum value of the range
66    /// * `max` - Maximum value of the range
67    /// * `max_ticks` - Maximum number of ticks to generate
68    fn generate_ticks(&self, min: T, max: T, max_ticks: usize) -> heapless::Vec<Tick<T>, 32>;
69
70    /// Get the preferred number of ticks
71    fn preferred_tick_count(&self) -> usize;
72
73    /// Set the preferred number of ticks
74    fn set_preferred_tick_count(&mut self, count: usize);
75}
76
77/// Trait for rendering axis components
78pub trait AxisRenderer<C: PixelColor> {
79    /// Draw the main axis line
80    ///
81    /// # Arguments
82    /// * `start` - Start point of the axis line
83    /// * `end` - End point of the axis line
84    /// * `style` - Line style to use
85    /// * `target` - The display target to draw to
86    fn draw_axis_line<D>(
87        &self,
88        start: Point,
89        end: Point,
90        style: &crate::style::LineStyle<C>,
91        target: &mut D,
92    ) -> ChartResult<()>
93    where
94        D: DrawTarget<Color = C>;
95
96    /// Draw a tick mark
97    ///
98    /// # Arguments
99    /// * `position` - Position of the tick mark
100    /// * `length` - Length of the tick mark
101    /// * `orientation` - Orientation of the axis
102    /// * `style` - Line style to use
103    /// * `target` - The display target to draw to
104    fn draw_tick<D>(
105        &self,
106        position: Point,
107        length: u32,
108        orientation: AxisOrientation,
109        style: &crate::style::LineStyle<C>,
110        target: &mut D,
111    ) -> ChartResult<()>
112    where
113        D: DrawTarget<Color = C>;
114
115    /// Draw a grid line
116    ///
117    /// # Arguments
118    /// * `start` - Start point of the grid line
119    /// * `end` - End point of the grid line
120    /// * `style` - Line style to use
121    /// * `target` - The display target to draw to
122    fn draw_grid_line<D>(
123        &self,
124        start: Point,
125        end: Point,
126        style: &crate::style::LineStyle<C>,
127        target: &mut D,
128    ) -> ChartResult<()>
129    where
130        D: DrawTarget<Color = C>;
131
132    /// Draw a label
133    ///
134    /// # Arguments
135    /// * `text` - The text to draw
136    /// * `position` - Position to draw the label
137    /// * `target` - The display target to draw to
138    fn draw_label<D>(&self, text: &str, position: Point, target: &mut D) -> ChartResult<()>
139    where
140        D: DrawTarget<Color = C>;
141}
142
143/// Represents a single tick mark on an axis
144#[derive(Debug, Clone, PartialEq)]
145pub struct Tick<T> {
146    /// The value at this tick position
147    pub value: T,
148    /// Whether this is a major tick (with label) or minor tick
149    pub is_major: bool,
150    /// Optional label for this tick
151    pub label: Option<heapless::String<16>>,
152}
153
154impl<T> Tick<T> {
155    /// Create a new major tick with a label
156    pub fn major(value: T, label: &str) -> Self {
157        Self {
158            value,
159            is_major: true,
160            label: heapless::String::try_from(label).ok(),
161        }
162    }
163
164    /// Create a new minor tick without a label
165    pub fn minor(value: T) -> Self {
166        Self {
167            value,
168            is_major: false,
169            label: None,
170        }
171    }
172
173    /// Create a new major tick without a label
174    pub fn major_unlabeled(value: T) -> Self {
175        Self {
176            value,
177            is_major: true,
178            label: None,
179        }
180    }
181}
182
183/// Trait for types that can be used as axis values
184pub trait AxisValue: Copy + PartialOrd + core::fmt::Display {
185    /// Convert to f32 for calculations
186    fn to_f32(self) -> f32;
187
188    /// Create from f32
189    fn from_f32(value: f32) -> Self;
190
191    /// Get a nice step size for this value type
192    fn nice_step(range: Self) -> Self;
193
194    /// Format this value for display
195    fn format(&self) -> heapless::String<16>;
196}
197
198impl AxisValue for f32 {
199    fn to_f32(self) -> f32 {
200        self
201    }
202
203    fn from_f32(value: f32) -> Self {
204        value
205    }
206
207    fn nice_step(range: Self) -> Self {
208        let range_num = range.to_number();
209        let abs_range = Math::abs(range_num);
210        let magnitude = Math::floor(Math::log10(abs_range));
211        let ten = 10.0f32.to_number();
212        let normalized = range_num / Math::pow(ten, magnitude);
213
214        let one = 1.0f32.to_number();
215        let two = 2.0f32.to_number();
216        let five = 5.0f32.to_number();
217        let ten_norm = 10.0f32.to_number();
218
219        let nice_normalized = if normalized <= one {
220            one
221        } else if normalized <= two {
222            two
223        } else if normalized <= five {
224            five
225        } else {
226            ten_norm
227        };
228
229        let result = if magnitude >= 0.0.to_number() && magnitude <= 10.0.to_number() {
230            nice_normalized * Math::pow(ten, magnitude)
231        } else {
232            // Fallback for extreme magnitudes to prevent overflow
233            nice_normalized
234        };
235        f32::from_number(result)
236    }
237
238    fn format(&self) -> heapless::String<16> {
239        // Simple formatting for no_std
240        let self_num = self.to_number();
241        let fract_part = self_num - Math::floor(self_num);
242        let zero = 0.0f32.to_number();
243
244        if fract_part == zero {
245            // Integer formatting
246            let int_val = *self as i32;
247            let mut result = heapless::String::new();
248            if int_val == 0 {
249                let _ = result.push('0');
250            } else {
251                let mut val = int_val.abs();
252                let mut digits = heapless::Vec::<u8, 16>::new();
253                while val > 0 {
254                    let _ = digits.push((val % 10) as u8 + b'0');
255                    val /= 10;
256                }
257                if int_val < 0 {
258                    let _ = result.push('-');
259                }
260                for &digit in digits.iter().rev() {
261                    let _ = result.push(digit as char);
262                }
263            }
264            result
265        } else {
266            // For floating point, just show as integer for simplicity in no_std
267            let int_val = *self as i32;
268            let mut result = heapless::String::new();
269            if int_val == 0 {
270                let _ = result.push('0');
271            } else {
272                let mut val = int_val.abs();
273                let mut digits = heapless::Vec::<u8, 16>::new();
274                while val > 0 {
275                    let _ = digits.push((val % 10) as u8 + b'0');
276                    val /= 10;
277                }
278                if int_val < 0 {
279                    let _ = result.push('-');
280                }
281                for &digit in digits.iter().rev() {
282                    let _ = result.push(digit as char);
283                }
284            }
285            result
286        }
287    }
288}
289
290impl AxisValue for i32 {
291    fn to_f32(self) -> f32 {
292        self as f32
293    }
294
295    fn from_f32(value: f32) -> Self {
296        let value_num = value.to_number();
297        let rounded = Math::floor(value_num + 0.5f32.to_number());
298        f32::from_number(rounded) as i32
299    }
300
301    fn nice_step(range: Self) -> Self {
302        let range_f32 = range.abs() as f32;
303        let range_num = range_f32.to_number();
304        let magnitude = Math::floor(Math::log10(range_num));
305        let ten = 10.0f32.to_number();
306        let normalized = range_num / Math::pow(ten, magnitude);
307
308        let one = 1.0f32.to_number();
309        let two = 2.0f32.to_number();
310        let five = 5.0f32.to_number();
311        let ten_norm = 10.0f32.to_number();
312
313        let nice_normalized = if normalized <= one {
314            one
315        } else if normalized <= two {
316            two
317        } else if normalized <= five {
318            five
319        } else {
320            ten_norm
321        };
322
323        let result = if magnitude >= 0.0.to_number() && magnitude <= 10.0.to_number() {
324            nice_normalized * Math::pow(ten, magnitude)
325        } else {
326            // Fallback for extreme magnitudes to prevent overflow
327            nice_normalized
328        };
329        let rounded = Math::floor(result + 0.5f32.to_number());
330        f32::from_number(rounded) as i32
331    }
332
333    fn format(&self) -> heapless::String<16> {
334        let mut result = heapless::String::new();
335        if *self == 0 {
336            let _ = result.push('0');
337        } else {
338            let mut val = self.abs();
339            let mut digits = heapless::Vec::<u8, 16>::new();
340            while val > 0 {
341                let _ = digits.push((val % 10) as u8 + b'0');
342                val /= 10;
343            }
344            if *self < 0 {
345                let _ = result.push('-');
346            }
347            for &digit in digits.iter().rev() {
348                let _ = result.push(digit as char);
349            }
350        }
351        result
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn test_tick_creation() {
361        let major_tick = Tick::major(5.0, "5.0");
362        assert!(major_tick.is_major);
363        assert_eq!(major_tick.value, 5.0);
364        assert!(major_tick.label.is_some());
365
366        let minor_tick = Tick::minor(2.5);
367        assert!(!minor_tick.is_major);
368        assert_eq!(minor_tick.value, 2.5);
369        assert!(minor_tick.label.is_none());
370    }
371
372    #[test]
373    #[cfg(not(any(feature = "fixed-point", feature = "integer-math")))] // Skip for fixed-point and integer-math to avoid overflow
374    fn test_axis_value_f32() {
375        let value = core::f32::consts::PI;
376        assert_eq!(value.to_f32(), core::f32::consts::PI);
377        assert_eq!(f32::from_f32(core::f32::consts::PI), core::f32::consts::PI);
378
379        let step = f32::nice_step(7.3);
380        assert!(step > 0.0);
381    }
382
383    #[test]
384    #[cfg(not(any(feature = "fixed-point", feature = "integer-math")))] // Skip for fixed-point and integer-math to avoid overflow
385    fn test_axis_value_i32() {
386        let value = 42i32;
387        assert_eq!(value.to_f32(), 42.0);
388        assert_eq!(i32::from_f32(42.7), 43);
389
390        let step = i32::nice_step(73);
391        assert!(step > 0);
392    }
393}