embedded_charts/grid/
types.rs

1//! Grid type implementations.
2
3use crate::error::ChartResult;
4use crate::grid::style::GridStyle;
5use crate::grid::traits::{DefaultGridRenderer, Grid, GridOrientation, GridRenderer};
6use embedded_graphics::{prelude::*, primitives::Rectangle};
7
8use crate::axes::traits::TickGenerator;
9use crate::grid::traits::TickAlignedGrid;
10
11/// Grid spacing configuration
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum GridSpacing {
14    /// Fixed spacing in pixels
15    Pixels(u32),
16    /// Fixed spacing in data units
17    DataUnits(f32),
18    /// Automatic spacing based on viewport size
19    Auto,
20}
21
22/// Grid type enumeration
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum GridType {
25    /// Linear grid with evenly spaced lines
26    Linear,
27    /// Grid aligned with axis ticks
28    TickBased,
29    /// Custom grid with user-defined positions
30    Custom,
31}
32
33/// Linear grid with evenly spaced grid lines
34#[derive(Debug, Clone)]
35pub struct LinearGrid<C: PixelColor> {
36    /// Grid orientation
37    orientation: GridOrientation,
38    /// Spacing between grid lines
39    spacing: GridSpacing,
40    /// Grid style
41    style: GridStyle<C>,
42    /// Whether the grid is visible
43    visible: bool,
44    /// Grid renderer
45    renderer: DefaultGridRenderer,
46}
47
48impl<C: PixelColor> LinearGrid<C> {
49    /// Create a new linear grid
50    pub fn new(orientation: GridOrientation, spacing: GridSpacing) -> Self
51    where
52        C: From<embedded_graphics::pixelcolor::Rgb565>,
53    {
54        Self {
55            orientation,
56            spacing,
57            style: GridStyle::default(),
58            visible: true,
59            renderer: DefaultGridRenderer,
60        }
61    }
62
63    /// Create a horizontal linear grid
64    pub fn horizontal(spacing: GridSpacing) -> Self
65    where
66        C: From<embedded_graphics::pixelcolor::Rgb565>,
67    {
68        Self::new(GridOrientation::Horizontal, spacing)
69    }
70
71    /// Create a vertical linear grid
72    pub fn vertical(spacing: GridSpacing) -> Self
73    where
74        C: From<embedded_graphics::pixelcolor::Rgb565>,
75    {
76        Self::new(GridOrientation::Vertical, spacing)
77    }
78
79    /// Set the grid style
80    pub fn with_style(mut self, style: GridStyle<C>) -> Self {
81        self.style = style;
82        self
83    }
84
85    /// Set grid visibility
86    pub fn with_visibility(mut self, visible: bool) -> Self {
87        self.visible = visible;
88        self
89    }
90
91    /// Calculate spacing in pixels for the given viewport
92    fn calculate_pixel_spacing(&self, viewport: Rectangle) -> u32 {
93        match self.spacing {
94            GridSpacing::Pixels(pixels) => pixels,
95            GridSpacing::DataUnits(_) => {
96                // For linear grids, we need to estimate pixel spacing
97                // This is a simplified calculation
98                match self.orientation {
99                    GridOrientation::Horizontal => viewport.size.height / 10,
100                    GridOrientation::Vertical => viewport.size.width / 10,
101                }
102            }
103            GridSpacing::Auto => match self.orientation {
104                GridOrientation::Horizontal => (viewport.size.height / 8).max(20),
105                GridOrientation::Vertical => (viewport.size.width / 8).max(20),
106            },
107        }
108    }
109}
110
111impl<C: PixelColor + 'static> Grid<C> for LinearGrid<C> {
112    fn draw<D>(&self, viewport: Rectangle, target: &mut D) -> ChartResult<()>
113    where
114        D: DrawTarget<Color = C>,
115    {
116        if !self.visible || !self.style.visibility.any_visible() {
117            return Ok(());
118        }
119
120        let positions = self.calculate_positions(viewport);
121
122        for &pos in positions.iter() {
123            let (start, end) = match self.orientation {
124                GridOrientation::Horizontal => (
125                    Point::new(viewport.top_left.x, pos),
126                    Point::new(viewport.top_left.x + viewport.size.width as i32, pos),
127                ),
128                GridOrientation::Vertical => (
129                    Point::new(pos, viewport.top_left.y),
130                    Point::new(pos, viewport.top_left.y + viewport.size.height as i32),
131                ),
132            };
133
134            // Draw major grid lines
135            if self.style.major.enabled && self.style.visibility.major {
136                self.renderer.draw_major_line(
137                    start,
138                    end,
139                    &self.style.major.line.line_style,
140                    target,
141                )?;
142            }
143        }
144
145        Ok(())
146    }
147
148    fn orientation(&self) -> GridOrientation {
149        self.orientation
150    }
151
152    fn is_visible(&self) -> bool {
153        self.visible
154    }
155
156    fn set_visible(&mut self, visible: bool) {
157        self.visible = visible;
158    }
159
160    fn style(&self) -> &GridStyle<C> {
161        &self.style
162    }
163
164    fn set_style(&mut self, style: GridStyle<C>) {
165        self.style = style;
166    }
167
168    fn calculate_positions(&self, viewport: Rectangle) -> heapless::Vec<i32, 64> {
169        let mut positions = heapless::Vec::new();
170        let spacing = self.calculate_pixel_spacing(viewport);
171
172        match self.orientation {
173            GridOrientation::Horizontal => {
174                let mut y = viewport.top_left.y + spacing as i32;
175                while y < viewport.top_left.y + viewport.size.height as i32 {
176                    let _ = positions.push(y);
177                    y += spacing as i32;
178                }
179            }
180            GridOrientation::Vertical => {
181                let mut x = viewport.top_left.x + spacing as i32;
182                while x < viewport.top_left.x + viewport.size.width as i32 {
183                    let _ = positions.push(x);
184                    x += spacing as i32;
185                }
186            }
187        }
188
189        positions
190    }
191
192    fn spacing(&self) -> f32 {
193        match self.spacing {
194            GridSpacing::Pixels(pixels) => pixels as f32,
195            GridSpacing::DataUnits(units) => units,
196            GridSpacing::Auto => 1.0,
197        }
198    }
199
200    fn set_spacing(&mut self, spacing: f32) {
201        self.spacing = GridSpacing::DataUnits(spacing);
202    }
203
204    fn as_any(&self) -> &dyn core::any::Any {
205        self
206    }
207}
208
209/// Grid that aligns with axis ticks
210#[derive(Debug, Clone)]
211pub struct TickBasedGrid<T, C>
212where
213    T: Copy + PartialOrd + core::fmt::Display,
214    C: PixelColor,
215{
216    /// Grid orientation
217    orientation: GridOrientation,
218    /// Grid style
219    style: GridStyle<C>,
220    /// Whether the grid is visible
221    visible: bool,
222    /// Whether to show only major tick grid lines
223    major_ticks_only: bool,
224    /// Grid renderer
225    renderer: DefaultGridRenderer,
226    /// Phantom data for axis value type
227    _phantom: core::marker::PhantomData<T>,
228}
229
230impl<T, C> TickBasedGrid<T, C>
231where
232    T: Copy + PartialOrd + core::fmt::Display,
233    C: PixelColor,
234{
235    /// Create a new tick-based grid
236    pub fn new(orientation: GridOrientation) -> Self
237    where
238        C: From<embedded_graphics::pixelcolor::Rgb565>,
239    {
240        Self {
241            orientation,
242            style: GridStyle::default(),
243            visible: true,
244            major_ticks_only: false,
245            renderer: DefaultGridRenderer,
246            _phantom: core::marker::PhantomData,
247        }
248    }
249
250    /// Create a horizontal tick-based grid
251    pub fn horizontal() -> Self
252    where
253        C: From<embedded_graphics::pixelcolor::Rgb565>,
254    {
255        Self::new(GridOrientation::Horizontal)
256    }
257
258    /// Create a vertical tick-based grid
259    pub fn vertical() -> Self
260    where
261        C: From<embedded_graphics::pixelcolor::Rgb565>,
262    {
263        Self::new(GridOrientation::Vertical)
264    }
265
266    /// Set the grid style
267    pub fn with_style(mut self, style: GridStyle<C>) -> Self {
268        self.style = style;
269        self
270    }
271
272    /// Set whether to show only major tick grid lines
273    pub fn with_major_ticks_only(mut self, major_only: bool) -> Self {
274        self.major_ticks_only = major_only;
275        self
276    }
277
278    /// Check if only major tick grid lines are shown
279    pub fn is_major_ticks_only(&self) -> bool {
280        self.major_ticks_only
281    }
282
283    /// Set whether to show grid lines for major ticks only
284    pub fn set_major_ticks_only(&mut self, major_only: bool) {
285        self.major_ticks_only = major_only;
286    }
287}
288
289impl<T, C> Grid<C> for TickBasedGrid<T, C>
290where
291    T: Copy + PartialOrd + core::fmt::Display + 'static,
292    C: PixelColor + 'static,
293{
294    fn draw<D>(&self, viewport: Rectangle, target: &mut D) -> ChartResult<()>
295    where
296        D: DrawTarget<Color = C>,
297    {
298        if !self.visible || !self.style.visibility.any_visible() {
299            return Ok(());
300        }
301
302        // For basic grid drawing without axis, fall back to linear spacing
303        let positions = self.calculate_positions(viewport);
304
305        for &pos in positions.iter() {
306            let (start, end) = match self.orientation {
307                GridOrientation::Horizontal => (
308                    Point::new(viewport.top_left.x, pos),
309                    Point::new(viewport.top_left.x + viewport.size.width as i32, pos),
310                ),
311                GridOrientation::Vertical => (
312                    Point::new(pos, viewport.top_left.y),
313                    Point::new(pos, viewport.top_left.y + viewport.size.height as i32),
314                ),
315            };
316
317            // Draw major grid lines
318            if self.style.major.enabled && self.style.visibility.major {
319                self.renderer.draw_major_line(
320                    start,
321                    end,
322                    &self.style.major.line.line_style,
323                    target,
324                )?;
325            }
326        }
327
328        Ok(())
329    }
330
331    fn orientation(&self) -> GridOrientation {
332        self.orientation
333    }
334
335    fn is_visible(&self) -> bool {
336        self.visible
337    }
338
339    fn set_visible(&mut self, visible: bool) {
340        self.visible = visible;
341    }
342
343    fn style(&self) -> &GridStyle<C> {
344        &self.style
345    }
346
347    fn set_style(&mut self, style: GridStyle<C>) {
348        self.style = style;
349    }
350
351    fn calculate_positions(&self, viewport: Rectangle) -> heapless::Vec<i32, 64> {
352        let mut positions = heapless::Vec::new();
353        let spacing = match self.orientation {
354            GridOrientation::Horizontal => viewport.size.height / 8,
355            GridOrientation::Vertical => viewport.size.width / 8,
356        };
357
358        match self.orientation {
359            GridOrientation::Horizontal => {
360                let mut y = viewport.top_left.y + spacing as i32;
361                while y < viewport.top_left.y + viewport.size.height as i32 {
362                    let _ = positions.push(y);
363                    y += spacing as i32;
364                }
365            }
366            GridOrientation::Vertical => {
367                let mut x = viewport.top_left.x + spacing as i32;
368                while x < viewport.top_left.x + viewport.size.width as i32 {
369                    let _ = positions.push(x);
370                    x += spacing as i32;
371                }
372            }
373        }
374
375        positions
376    }
377
378    fn spacing(&self) -> f32 {
379        1.0 // Default spacing for tick-based grids
380    }
381
382    fn set_spacing(&mut self, _spacing: f32) {
383        // Spacing is determined by axis ticks, so this is a no-op
384    }
385
386    fn as_any(&self) -> &dyn core::any::Any {
387        self
388    }
389}
390
391impl<T, C> TickAlignedGrid<T, C> for TickBasedGrid<T, C>
392where
393    T: crate::axes::traits::AxisValue + 'static,
394    C: PixelColor + 'static,
395{
396    fn draw_with_axis<D, A>(&self, viewport: Rectangle, axis: &A, target: &mut D) -> ChartResult<()>
397    where
398        D: DrawTarget<Color = C>,
399        A: crate::axes::traits::Axis<T, C>,
400    {
401        if !self.visible || !self.style.visibility.any_visible() {
402            return Ok(());
403        }
404
405        let positions = self.calculate_tick_positions(viewport, axis);
406
407        for &pos in positions.iter() {
408            let (start, end) = match self.orientation {
409                GridOrientation::Horizontal => (
410                    Point::new(viewport.top_left.x, pos),
411                    Point::new(viewport.top_left.x + viewport.size.width as i32, pos),
412                ),
413                GridOrientation::Vertical => (
414                    Point::new(pos, viewport.top_left.y),
415                    Point::new(pos, viewport.top_left.y + viewport.size.height as i32),
416                ),
417            };
418
419            // Draw grid lines based on tick type
420            if self.style.major.enabled && self.style.visibility.major {
421                self.renderer.draw_major_line(
422                    start,
423                    end,
424                    &self.style.major.line.line_style,
425                    target,
426                )?;
427            }
428        }
429
430        Ok(())
431    }
432
433    fn calculate_tick_positions<A>(&self, viewport: Rectangle, axis: &A) -> heapless::Vec<i32, 64>
434    where
435        A: crate::axes::traits::Axis<T, C>,
436    {
437        let mut positions = heapless::Vec::new();
438
439        // Generate ticks for the axis range
440        let ticks = axis
441            .tick_generator()
442            .generate_ticks(axis.min(), axis.max(), 16);
443
444        for tick in ticks.iter() {
445            if self.major_ticks_only && !tick.is_major {
446                continue;
447            }
448
449            let screen_pos = axis.transform_value(tick.value, viewport);
450            let _ = positions.push(screen_pos);
451        }
452
453        positions
454    }
455
456    fn set_major_ticks_only(&mut self, major_only: bool) {
457        self.major_ticks_only = major_only;
458    }
459
460    fn is_major_ticks_only(&self) -> bool {
461        self.major_ticks_only
462    }
463}
464
465/// Custom grid with user-defined positions
466#[derive(Debug, Clone)]
467pub struct CustomGrid<C: PixelColor> {
468    /// Grid orientation
469    orientation: GridOrientation,
470    /// Custom grid line positions (in screen coordinates)
471    positions: heapless::Vec<i32, 64>,
472    /// Grid style
473    style: GridStyle<C>,
474    /// Whether the grid is visible
475    visible: bool,
476    /// Grid renderer
477    renderer: DefaultGridRenderer,
478}
479
480impl<C: PixelColor> CustomGrid<C> {
481    /// Create a new custom grid
482    pub fn new(orientation: GridOrientation) -> Self
483    where
484        C: From<embedded_graphics::pixelcolor::Rgb565>,
485    {
486        Self {
487            orientation,
488            positions: heapless::Vec::new(),
489            style: GridStyle::default(),
490            visible: true,
491            renderer: DefaultGridRenderer,
492        }
493    }
494
495    /// Create a horizontal custom grid
496    pub fn horizontal() -> Self
497    where
498        C: From<embedded_graphics::pixelcolor::Rgb565>,
499    {
500        Self::new(GridOrientation::Horizontal)
501    }
502
503    /// Create a vertical custom grid
504    pub fn vertical() -> Self
505    where
506        C: From<embedded_graphics::pixelcolor::Rgb565>,
507    {
508        Self::new(GridOrientation::Vertical)
509    }
510
511    /// Add a grid line at the specified position
512    pub fn add_line(&mut self, position: i32) -> Result<(), crate::error::DataError> {
513        self.positions
514            .push(position)
515            .map_err(|_| crate::error::DataError::buffer_full("add grid line", 32))
516    }
517
518    /// Add multiple grid lines
519    pub fn add_lines(&mut self, positions: &[i32]) {
520        for &pos in positions {
521            let _ = self.add_line(pos);
522        }
523    }
524
525    /// Clear all grid lines
526    pub fn clear_lines(&mut self) {
527        self.positions.clear();
528    }
529
530    /// Set the grid style
531    pub fn with_style(mut self, style: GridStyle<C>) -> Self {
532        self.style = style;
533        self
534    }
535
536    /// Add lines with builder pattern
537    pub fn with_lines(mut self, positions: &[i32]) -> Self {
538        self.add_lines(positions);
539        self
540    }
541}
542
543impl<C: PixelColor + 'static> Grid<C> for CustomGrid<C> {
544    fn draw<D>(&self, viewport: Rectangle, target: &mut D) -> ChartResult<()>
545    where
546        D: DrawTarget<Color = C>,
547    {
548        if !self.visible || !self.style.visibility.any_visible() {
549            return Ok(());
550        }
551
552        for &pos in self.positions.iter() {
553            let (start, end) = match self.orientation {
554                GridOrientation::Horizontal => (
555                    Point::new(viewport.top_left.x, pos),
556                    Point::new(viewport.top_left.x + viewport.size.width as i32, pos),
557                ),
558                GridOrientation::Vertical => (
559                    Point::new(pos, viewport.top_left.y),
560                    Point::new(pos, viewport.top_left.y + viewport.size.height as i32),
561                ),
562            };
563
564            // Draw grid lines
565            if self.style.major.enabled && self.style.visibility.major {
566                self.renderer.draw_major_line(
567                    start,
568                    end,
569                    &self.style.major.line.line_style,
570                    target,
571                )?;
572            }
573        }
574
575        Ok(())
576    }
577
578    fn orientation(&self) -> GridOrientation {
579        self.orientation
580    }
581
582    fn is_visible(&self) -> bool {
583        self.visible
584    }
585
586    fn set_visible(&mut self, visible: bool) {
587        self.visible = visible;
588    }
589
590    fn style(&self) -> &GridStyle<C> {
591        &self.style
592    }
593
594    fn set_style(&mut self, style: GridStyle<C>) {
595        self.style = style;
596    }
597
598    fn calculate_positions(&self, _viewport: Rectangle) -> heapless::Vec<i32, 64> {
599        self.positions.clone()
600    }
601
602    fn spacing(&self) -> f32 {
603        // Calculate average spacing
604        if self.positions.len() < 2 {
605            return 1.0;
606        }
607
608        let mut total_spacing = 0;
609        for window in self.positions.windows(2) {
610            if let [a, b] = window {
611                total_spacing += (b - a).abs();
612            }
613        }
614
615        total_spacing as f32 / (self.positions.len() - 1) as f32
616    }
617
618    fn set_spacing(&mut self, _spacing: f32) {
619        // Custom grids don't use automatic spacing
620    }
621
622    fn as_any(&self) -> &dyn core::any::Any {
623        self
624    }
625}
626
627#[cfg(test)]
628mod tests {
629    use super::*;
630    use embedded_graphics::pixelcolor::Rgb565;
631
632    #[test]
633    fn test_linear_grid_creation() {
634        let grid: LinearGrid<Rgb565> = LinearGrid::horizontal(GridSpacing::Pixels(20));
635        assert_eq!(grid.orientation(), GridOrientation::Horizontal);
636        assert!(grid.is_visible());
637    }
638
639    #[test]
640    fn test_tick_based_grid_creation() {
641        let grid: TickBasedGrid<f32, Rgb565> = TickBasedGrid::vertical();
642        assert_eq!(grid.orientation(), GridOrientation::Vertical);
643        assert!(!grid.is_major_ticks_only());
644    }
645
646    #[test]
647    fn test_custom_grid_creation() {
648        let mut grid: CustomGrid<Rgb565> = CustomGrid::horizontal();
649        assert_eq!(grid.orientation(), GridOrientation::Horizontal);
650
651        grid.add_line(100).unwrap();
652        grid.add_line(200).unwrap();
653
654        let positions =
655            grid.calculate_positions(Rectangle::new(Point::zero(), Size::new(400, 300)));
656        assert_eq!(positions.len(), 2);
657    }
658
659    #[test]
660    fn test_grid_spacing() {
661        assert_eq!(GridSpacing::Pixels(20), GridSpacing::Pixels(20));
662        assert_ne!(GridSpacing::Pixels(20), GridSpacing::Pixels(30));
663        assert_ne!(GridSpacing::Pixels(20), GridSpacing::Auto);
664    }
665
666    #[test]
667    fn test_grid_type() {
668        assert_eq!(GridType::Linear, GridType::Linear);
669        assert_ne!(GridType::Linear, GridType::TickBased);
670    }
671}