1use 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#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum GridSpacing {
14 Pixels(u32),
16 DataUnits(f32),
18 Auto,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum GridType {
25 Linear,
27 TickBased,
29 Custom,
31}
32
33#[derive(Debug, Clone)]
35pub struct LinearGrid<C: PixelColor> {
36 orientation: GridOrientation,
38 spacing: GridSpacing,
40 style: GridStyle<C>,
42 visible: bool,
44 renderer: DefaultGridRenderer,
46}
47
48impl<C: PixelColor> LinearGrid<C> {
49 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 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 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 pub fn with_style(mut self, style: GridStyle<C>) -> Self {
81 self.style = style;
82 self
83 }
84
85 pub fn with_visibility(mut self, visible: bool) -> Self {
87 self.visible = visible;
88 self
89 }
90
91 fn calculate_pixel_spacing(&self, viewport: Rectangle) -> u32 {
93 match self.spacing {
94 GridSpacing::Pixels(pixels) => pixels,
95 GridSpacing::DataUnits(_) => {
96 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 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#[derive(Debug, Clone)]
211pub struct TickBasedGrid<T, C>
212where
213 T: Copy + PartialOrd + core::fmt::Display,
214 C: PixelColor,
215{
216 orientation: GridOrientation,
218 style: GridStyle<C>,
220 visible: bool,
222 major_ticks_only: bool,
224 renderer: DefaultGridRenderer,
226 _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 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 pub fn horizontal() -> Self
252 where
253 C: From<embedded_graphics::pixelcolor::Rgb565>,
254 {
255 Self::new(GridOrientation::Horizontal)
256 }
257
258 pub fn vertical() -> Self
260 where
261 C: From<embedded_graphics::pixelcolor::Rgb565>,
262 {
263 Self::new(GridOrientation::Vertical)
264 }
265
266 pub fn with_style(mut self, style: GridStyle<C>) -> Self {
268 self.style = style;
269 self
270 }
271
272 pub fn with_major_ticks_only(mut self, major_only: bool) -> Self {
274 self.major_ticks_only = major_only;
275 self
276 }
277
278 pub fn is_major_ticks_only(&self) -> bool {
280 self.major_ticks_only
281 }
282
283 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 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 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 }
381
382 fn set_spacing(&mut self, _spacing: f32) {
383 }
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 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 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#[derive(Debug, Clone)]
467pub struct CustomGrid<C: PixelColor> {
468 orientation: GridOrientation,
470 positions: heapless::Vec<i32, 64>,
472 style: GridStyle<C>,
474 visible: bool,
476 renderer: DefaultGridRenderer,
478}
479
480impl<C: PixelColor> CustomGrid<C> {
481 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 pub fn horizontal() -> Self
497 where
498 C: From<embedded_graphics::pixelcolor::Rgb565>,
499 {
500 Self::new(GridOrientation::Horizontal)
501 }
502
503 pub fn vertical() -> Self
505 where
506 C: From<embedded_graphics::pixelcolor::Rgb565>,
507 {
508 Self::new(GridOrientation::Vertical)
509 }
510
511 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 pub fn add_lines(&mut self, positions: &[i32]) {
520 for &pos in positions {
521 let _ = self.add_line(pos);
522 }
523 }
524
525 pub fn clear_lines(&mut self) {
527 self.positions.clear();
528 }
529
530 pub fn with_style(mut self, style: GridStyle<C>) -> Self {
532 self.style = style;
533 self
534 }
535
536 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 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 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 }
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}