embedded_charts/grid/
traits.rs

1//! Core traits for grid implementations.
2
3use crate::error::ChartResult;
4use embedded_graphics::{prelude::*, primitives::Rectangle};
5
6use crate::math::{Math, NumericConversion};
7
8/// Core trait for all grid types
9pub trait Grid<C: PixelColor> {
10    /// Draw the grid lines to the target
11    ///
12    /// # Arguments
13    /// * `viewport` - The area to draw the grid in
14    /// * `target` - The display target to draw to
15    fn draw<D>(&self, viewport: Rectangle, target: &mut D) -> ChartResult<()>
16    where
17        D: DrawTarget<Color = C>;
18
19    /// Get the grid orientation (horizontal or vertical)
20    fn orientation(&self) -> GridOrientation;
21
22    /// Check if the grid is visible
23    fn is_visible(&self) -> bool;
24
25    /// Set grid visibility
26    fn set_visible(&mut self, visible: bool);
27
28    /// Get the grid style
29    fn style(&self) -> &crate::grid::style::GridStyle<C>;
30
31    /// Set the grid style
32    fn set_style(&mut self, style: crate::grid::style::GridStyle<C>);
33
34    /// Calculate the positions where grid lines should be drawn
35    /// Returns a vector of positions in screen coordinates
36    fn calculate_positions(&self, viewport: Rectangle) -> heapless::Vec<i32, 64>;
37
38    /// Get the spacing between grid lines
39    fn spacing(&self) -> f32;
40
41    /// Set the spacing between grid lines
42    fn set_spacing(&mut self, spacing: f32);
43
44    /// Support for downcasting to concrete types
45    fn as_any(&self) -> &dyn core::any::Any;
46}
47
48/// Trait for rendering grid lines
49pub trait GridRenderer<C: PixelColor> {
50    /// Draw a major grid line
51    ///
52    /// # Arguments
53    /// * `start` - Start point of the grid line
54    /// * `end` - End point of the grid line
55    /// * `style` - Line style to use
56    /// * `target` - The display target to draw to
57    fn draw_major_line<D>(
58        &self,
59        start: Point,
60        end: Point,
61        style: &crate::style::LineStyle<C>,
62        target: &mut D,
63    ) -> ChartResult<()>
64    where
65        D: DrawTarget<Color = C>;
66
67    /// Draw a minor grid line
68    ///
69    /// # Arguments
70    /// * `start` - Start point of the grid line
71    /// * `end` - End point of the grid line
72    /// * `style` - Line style to use
73    /// * `target` - The display target to draw to
74    fn draw_minor_line<D>(
75        &self,
76        start: Point,
77        end: Point,
78        style: &crate::style::LineStyle<C>,
79        target: &mut D,
80    ) -> ChartResult<()>
81    where
82        D: DrawTarget<Color = C>;
83
84    /// Draw a grid line with custom style
85    ///
86    /// # Arguments
87    /// * `start` - Start point of the grid line
88    /// * `end` - End point of the grid line
89    /// * `style` - Line style to use
90    /// * `target` - The display target to draw to
91    fn draw_grid_line<D>(
92        &self,
93        start: Point,
94        end: Point,
95        style: &crate::style::LineStyle<C>,
96        target: &mut D,
97    ) -> ChartResult<()>
98    where
99        D: DrawTarget<Color = C>;
100}
101
102/// Trait for configuring grid systems
103pub trait GridConfiguration<C: PixelColor> {
104    /// Configure major grid lines
105    ///
106    /// # Arguments
107    /// * `enabled` - Whether major grid lines are enabled
108    /// * `spacing` - Spacing between major grid lines
109    /// * `style` - Style for major grid lines
110    fn configure_major_grid(
111        &mut self,
112        enabled: bool,
113        spacing: f32,
114        style: crate::grid::style::MajorGridStyle<C>,
115    );
116
117    /// Configure minor grid lines
118    ///
119    /// # Arguments
120    /// * `enabled` - Whether minor grid lines are enabled
121    /// * `spacing` - Spacing between minor grid lines
122    /// * `style` - Style for minor grid lines
123    fn configure_minor_grid(
124        &mut self,
125        enabled: bool,
126        spacing: f32,
127        style: crate::grid::style::MinorGridStyle<C>,
128    );
129
130    /// Set the overall grid visibility
131    ///
132    /// # Arguments
133    /// * `visible` - Whether the grid is visible
134    fn set_grid_visible(&mut self, visible: bool);
135
136    /// Get the current grid configuration
137    fn grid_config(&self) -> &crate::grid::style::GridStyle<C>;
138}
139
140/// Trait for grids that align with axis ticks
141pub trait TickAlignedGrid<T, C>: Grid<C>
142where
143    T: crate::axes::traits::AxisValue,
144    C: PixelColor,
145{
146    /// Draw grid lines aligned with axis ticks
147    ///
148    /// # Arguments
149    /// * `viewport` - The area to draw the grid in
150    /// * `axis` - The axis to align with
151    /// * `target` - The display target to draw to
152    fn draw_with_axis<D, A>(
153        &self,
154        viewport: Rectangle,
155        axis: &A,
156        target: &mut D,
157    ) -> ChartResult<()>
158    where
159        D: DrawTarget<Color = C>,
160        A: crate::axes::traits::Axis<T, C>;
161
162    /// Calculate grid positions based on axis ticks
163    ///
164    /// # Arguments
165    /// * `viewport` - The viewport area
166    /// * `axis` - The axis to get ticks from
167    fn calculate_tick_positions<A>(&self, viewport: Rectangle, axis: &A) -> heapless::Vec<i32, 64>
168    where
169        A: crate::axes::traits::Axis<T, C>;
170
171    /// Set whether to show grid lines for major ticks only
172    ///
173    /// # Arguments
174    /// * `major_only` - If true, only show grid lines for major ticks
175    fn set_major_ticks_only(&mut self, major_only: bool);
176
177    /// Check if only major tick grid lines are shown
178    fn is_major_ticks_only(&self) -> bool;
179}
180
181/// Grid orientation
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183pub enum GridOrientation {
184    /// Horizontal grid lines (parallel to X-axis)
185    Horizontal,
186    /// Vertical grid lines (parallel to Y-axis)
187    Vertical,
188}
189
190/// Default grid renderer implementation
191#[derive(Debug, Clone)]
192pub struct DefaultGridRenderer;
193
194impl<C: PixelColor> GridRenderer<C> for DefaultGridRenderer {
195    fn draw_major_line<D>(
196        &self,
197        start: Point,
198        end: Point,
199        style: &crate::style::LineStyle<C>,
200        target: &mut D,
201    ) -> ChartResult<()>
202    where
203        D: DrawTarget<Color = C>,
204    {
205        self.draw_grid_line(start, end, style, target)
206    }
207
208    fn draw_minor_line<D>(
209        &self,
210        start: Point,
211        end: Point,
212        style: &crate::style::LineStyle<C>,
213        target: &mut D,
214    ) -> ChartResult<()>
215    where
216        D: DrawTarget<Color = C>,
217    {
218        self.draw_grid_line(start, end, style, target)
219    }
220
221    fn draw_grid_line<D>(
222        &self,
223        start: Point,
224        end: Point,
225        style: &crate::style::LineStyle<C>,
226        target: &mut D,
227    ) -> ChartResult<()>
228    where
229        D: DrawTarget<Color = C>,
230    {
231        use crate::error::ChartError;
232        use embedded_graphics::primitives::{Line, PrimitiveStyle};
233
234        let primitive_style = PrimitiveStyle::with_stroke(style.color, style.width);
235
236        // For dashed/dotted lines, we need to implement pattern drawing
237        match style.pattern {
238            crate::style::LinePattern::Solid => {
239                Line::new(start, end)
240                    .into_styled(primitive_style)
241                    .draw(target)
242                    .map_err(|_| ChartError::RenderingError)?;
243            }
244            crate::style::LinePattern::Dashed => {
245                self.draw_dashed_line(start, end, style, target)?;
246            }
247            crate::style::LinePattern::Dotted => {
248                self.draw_dotted_line(start, end, style, target)?;
249            }
250            crate::style::LinePattern::DashDot => {
251                self.draw_dash_dot_line(start, end, style, target)?;
252            }
253            crate::style::LinePattern::Custom => {
254                // Fall back to solid for custom patterns
255                Line::new(start, end)
256                    .into_styled(primitive_style)
257                    .draw(target)
258                    .map_err(|_| ChartError::RenderingError)?;
259            }
260        }
261
262        Ok(())
263    }
264}
265
266impl DefaultGridRenderer {
267    /// Draw a dashed line
268    fn draw_dashed_line<C, D>(
269        &self,
270        start: Point,
271        end: Point,
272        style: &crate::style::LineStyle<C>,
273        target: &mut D,
274    ) -> ChartResult<()>
275    where
276        C: PixelColor,
277        D: DrawTarget<Color = C>,
278    {
279        use crate::error::ChartError;
280        use embedded_graphics::primitives::{Line, PrimitiveStyle};
281
282        let primitive_style = PrimitiveStyle::with_stroke(style.color, style.width);
283        let dash_length = 8;
284        let gap_length = 4;
285
286        let dx = end.x - start.x;
287        let dy = end.y - start.y;
288        let dx_f32 = dx as f32;
289        let dy_f32 = dy as f32;
290        let dx_num = dx_f32.to_number();
291        let dy_num = dy_f32.to_number();
292        let length_squared = dx_num * dx_num + dy_num * dy_num;
293        let length_num = Math::sqrt(length_squared);
294        let length = f32::from_number(length_num);
295
296        let one = 1.0f32.to_number();
297        if length_num < one {
298            return Ok(());
299        }
300
301        let unit_x = dx as f32 / length;
302        let unit_y = dy as f32 / length;
303
304        let mut current_pos = 0.0;
305        let mut drawing = true;
306
307        while current_pos < length {
308            let segment_length = if drawing {
309                dash_length as f32
310            } else {
311                gap_length as f32
312            };
313            let next_pos = (current_pos + segment_length).min(length);
314
315            if drawing {
316                let seg_start = Point::new(
317                    start.x + (current_pos * unit_x) as i32,
318                    start.y + (current_pos * unit_y) as i32,
319                );
320                let seg_end = Point::new(
321                    start.x + (next_pos * unit_x) as i32,
322                    start.y + (next_pos * unit_y) as i32,
323                );
324
325                Line::new(seg_start, seg_end)
326                    .into_styled(primitive_style)
327                    .draw(target)
328                    .map_err(|_| ChartError::RenderingError)?;
329            }
330
331            current_pos = next_pos;
332            drawing = !drawing;
333        }
334
335        Ok(())
336    }
337
338    /// Draw a dotted line
339    fn draw_dotted_line<C, D>(
340        &self,
341        start: Point,
342        end: Point,
343        style: &crate::style::LineStyle<C>,
344        target: &mut D,
345    ) -> ChartResult<()>
346    where
347        C: PixelColor,
348        D: DrawTarget<Color = C>,
349    {
350        use crate::error::ChartError;
351        use embedded_graphics::primitives::{Circle, PrimitiveStyle};
352
353        let primitive_style = PrimitiveStyle::with_fill(style.color);
354        let dot_spacing = 6;
355
356        let dx = end.x - start.x;
357        let dy = end.y - start.y;
358        let dx_f32 = dx as f32;
359        let dy_f32 = dy as f32;
360        let dx_num = dx_f32.to_number();
361        let dy_num = dy_f32.to_number();
362        let length_squared = dx_num * dx_num + dy_num * dy_num;
363        let length_num = Math::sqrt(length_squared);
364        let length = f32::from_number(length_num);
365
366        let one = 1.0f32.to_number();
367        if length_num < one {
368            return Ok(());
369        }
370
371        let unit_x = dx as f32 / length;
372        let unit_y = dy as f32 / length;
373
374        let mut current_pos = 0.0;
375
376        while current_pos <= length {
377            let dot_center = Point::new(
378                start.x + (current_pos * unit_x) as i32,
379                start.y + (current_pos * unit_y) as i32,
380            );
381
382            Circle::new(Point::new(dot_center.x - 1, dot_center.y - 1), 2)
383                .into_styled(primitive_style)
384                .draw(target)
385                .map_err(|_| ChartError::RenderingError)?;
386
387            current_pos += dot_spacing as f32;
388        }
389
390        Ok(())
391    }
392
393    /// Draw a dash-dot line
394    fn draw_dash_dot_line<C, D>(
395        &self,
396        start: Point,
397        end: Point,
398        style: &crate::style::LineStyle<C>,
399        target: &mut D,
400    ) -> ChartResult<()>
401    where
402        C: PixelColor,
403        D: DrawTarget<Color = C>,
404    {
405        use crate::error::ChartError;
406        use embedded_graphics::primitives::{Circle, Line, PrimitiveStyle};
407
408        let line_style = PrimitiveStyle::with_stroke(style.color, style.width);
409        let dot_style = PrimitiveStyle::with_fill(style.color);
410        let dash_length = 8;
411        let gap_length = 3;
412        let dot_gap = 3;
413
414        let dx = end.x - start.x;
415        let dy = end.y - start.y;
416        let dx_f32 = dx as f32;
417        let dy_f32 = dy as f32;
418        let dx_num = dx_f32.to_number();
419        let dy_num = dy_f32.to_number();
420        let length_squared = dx_num * dx_num + dy_num * dy_num;
421        let length_num = Math::sqrt(length_squared);
422        let length = f32::from_number(length_num);
423
424        let one = 1.0f32.to_number();
425        if length_num < one {
426            return Ok(());
427        }
428
429        let unit_x = dx as f32 / length;
430        let unit_y = dy as f32 / length;
431
432        let mut current_pos = 0.0;
433        let pattern = [dash_length as f32, gap_length as f32, 2.0, dot_gap as f32]; // dash, gap, dot, gap
434        let mut pattern_index = 0;
435
436        while current_pos < length {
437            let segment_length = pattern[pattern_index % pattern.len()];
438            let next_pos = (current_pos + segment_length).min(length);
439
440            match pattern_index % 4 {
441                0 => {
442                    // Draw dash
443                    let seg_start = Point::new(
444                        start.x + (current_pos * unit_x) as i32,
445                        start.y + (current_pos * unit_y) as i32,
446                    );
447                    let seg_end = Point::new(
448                        start.x + (next_pos * unit_x) as i32,
449                        start.y + (next_pos * unit_y) as i32,
450                    );
451
452                    Line::new(seg_start, seg_end)
453                        .into_styled(line_style)
454                        .draw(target)
455                        .map_err(|_| ChartError::RenderingError)?;
456                }
457                2 => {
458                    // Draw dot
459                    let dot_center = Point::new(
460                        start.x + (current_pos * unit_x) as i32,
461                        start.y + (current_pos * unit_y) as i32,
462                    );
463
464                    Circle::new(Point::new(dot_center.x - 1, dot_center.y - 1), 2)
465                        .into_styled(dot_style)
466                        .draw(target)
467                        .map_err(|_| ChartError::RenderingError)?;
468                }
469                _ => {
470                    // Gap - do nothing
471                }
472            }
473
474            current_pos = next_pos;
475            pattern_index += 1;
476        }
477
478        Ok(())
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485
486    #[test]
487    fn test_grid_orientation() {
488        assert_eq!(GridOrientation::Horizontal, GridOrientation::Horizontal);
489        assert_ne!(GridOrientation::Horizontal, GridOrientation::Vertical);
490    }
491
492    #[test]
493    fn test_default_grid_renderer() {
494        let renderer = DefaultGridRenderer;
495        // Basic instantiation test
496        assert_eq!(core::mem::size_of_val(&renderer), 0); // Zero-sized type
497    }
498}