1use crate::axes::{
4 style::AxisStyle,
5 ticks::LinearTickGenerator,
6 traits::{Axis, AxisRenderer, AxisValue, TickGenerator},
7 AxisConfig, AxisOrientation, AxisPosition,
8};
9use crate::error::ChartResult;
10use crate::style::LineStyle;
11use embedded_graphics::{
12 draw_target::DrawTarget,
13 prelude::*,
14 primitives::{Line, PrimitiveStyle, Rectangle},
15};
16
17#[derive(Debug, Clone)]
19pub struct LinearAxis<T, C: PixelColor> {
20 config: AxisConfig<T>,
22 tick_generator: LinearTickGenerator,
24 style: AxisStyle<C>,
26 renderer: DefaultAxisRenderer<C>,
28}
29
30#[derive(Debug, Clone)]
32pub struct DefaultAxisRenderer<C: PixelColor> {
33 _phantom: core::marker::PhantomData<C>,
34}
35
36impl<C: PixelColor> DefaultAxisRenderer<C> {
37 pub fn new() -> Self {
39 Self {
40 _phantom: core::marker::PhantomData,
41 }
42 }
43}
44
45impl<C: PixelColor> Default for DefaultAxisRenderer<C> {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl<T, C> LinearAxis<T, C>
52where
53 T: AxisValue,
54 C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>,
55{
56 pub fn new(min: T, max: T, orientation: AxisOrientation, position: AxisPosition) -> Self {
58 Self {
59 config: AxisConfig::new(min, max, orientation, position),
60 tick_generator: LinearTickGenerator::new(5),
61 style: AxisStyle::new(),
62 renderer: DefaultAxisRenderer::new(),
63 }
64 }
65
66 pub fn with_tick_generator(mut self, generator: LinearTickGenerator) -> Self {
68 self.tick_generator = generator;
69 self
70 }
71
72 pub fn with_style(mut self, style: AxisStyle<C>) -> Self {
74 self.style = style;
75 self
76 }
77
78 pub fn with_range(mut self, min: T, max: T) -> Self {
80 self.config.min = min;
81 self.config.max = max;
82 self
83 }
84
85 pub fn show_line(mut self, show: bool) -> Self {
87 self.config.show_line = show;
88 self
89 }
90
91 pub fn show_ticks(mut self, show: bool) -> Self {
93 self.config.show_ticks = show;
94 self
95 }
96
97 pub fn show_labels(mut self, show: bool) -> Self {
99 self.config.show_labels = show;
100 self
101 }
102
103 pub fn show_grid(mut self, show: bool) -> Self {
105 self.config.show_grid = show;
106 self
107 }
108
109 fn calculate_axis_line(&self, viewport: Rectangle) -> (Point, Point) {
111 match (self.config.orientation, self.config.position) {
112 (AxisOrientation::Horizontal, AxisPosition::Bottom) => {
113 let y = viewport.top_left.y + viewport.size.height as i32 - 1;
114 (
115 Point::new(viewport.top_left.x, y),
116 Point::new(viewport.top_left.x + viewport.size.width as i32 - 1, y),
117 )
118 }
119 (AxisOrientation::Horizontal, AxisPosition::Top) => {
120 let y = viewport.top_left.y;
121 (
122 Point::new(viewport.top_left.x, y),
123 Point::new(viewport.top_left.x + viewport.size.width as i32 - 1, y),
124 )
125 }
126 (AxisOrientation::Vertical, AxisPosition::Left) => {
127 let x = viewport.top_left.x;
128 (
129 Point::new(x, viewport.top_left.y),
130 Point::new(x, viewport.top_left.y + viewport.size.height as i32 - 1),
131 )
132 }
133 (AxisOrientation::Vertical, AxisPosition::Right) => {
134 let x = viewport.top_left.x + viewport.size.width as i32 - 1;
135 (
136 Point::new(x, viewport.top_left.y),
137 Point::new(x, viewport.top_left.y + viewport.size.height as i32 - 1),
138 )
139 }
140 (AxisOrientation::Horizontal, AxisPosition::Left)
142 | (AxisOrientation::Horizontal, AxisPosition::Right) => {
143 let y = viewport.top_left.y + viewport.size.height as i32 - 1;
145 (
146 Point::new(viewport.top_left.x, y),
147 Point::new(viewport.top_left.x + viewport.size.width as i32 - 1, y),
148 )
149 }
150 (AxisOrientation::Vertical, AxisPosition::Bottom)
151 | (AxisOrientation::Vertical, AxisPosition::Top) => {
152 let x = viewport.top_left.x;
154 (
155 Point::new(x, viewport.top_left.y),
156 Point::new(x, viewport.top_left.y + viewport.size.height as i32 - 1),
157 )
158 }
159 }
160 }
161
162 fn calculate_tick_position(&self, value: T, viewport: Rectangle) -> Point {
164 let screen_coord = self.transform_value(value, viewport);
165
166 match (self.config.orientation, self.config.position) {
167 (AxisOrientation::Horizontal, AxisPosition::Bottom) => Point::new(
168 screen_coord,
169 viewport.top_left.y + viewport.size.height as i32 - 1,
170 ),
171 (AxisOrientation::Horizontal, AxisPosition::Top) => {
172 Point::new(screen_coord, viewport.top_left.y)
173 }
174 (AxisOrientation::Vertical, AxisPosition::Left) => {
175 Point::new(viewport.top_left.x, screen_coord)
176 }
177 (AxisOrientation::Vertical, AxisPosition::Right) => Point::new(
178 viewport.top_left.x + viewport.size.width as i32 - 1,
179 screen_coord,
180 ),
181 (AxisOrientation::Horizontal, AxisPosition::Left)
183 | (AxisOrientation::Horizontal, AxisPosition::Right) => {
184 Point::new(
186 screen_coord,
187 viewport.top_left.y + viewport.size.height as i32 - 1,
188 )
189 }
190 (AxisOrientation::Vertical, AxisPosition::Bottom)
191 | (AxisOrientation::Vertical, AxisPosition::Top) => {
192 Point::new(viewport.top_left.x, screen_coord)
194 }
195 }
196 }
197
198 fn calculate_grid_line(
200 &self,
201 value: T,
202 viewport: Rectangle,
203 chart_area: Rectangle,
204 ) -> (Point, Point) {
205 let tick_pos = self.calculate_tick_position(value, viewport);
206
207 match self.config.orientation {
208 AxisOrientation::Horizontal => {
209 (
211 Point::new(tick_pos.x, chart_area.top_left.y),
212 Point::new(
213 tick_pos.x,
214 chart_area.top_left.y + chart_area.size.height as i32 - 1,
215 ),
216 )
217 }
218 AxisOrientation::Vertical => {
219 (
221 Point::new(chart_area.top_left.x, tick_pos.y),
222 Point::new(
223 chart_area.top_left.x + chart_area.size.width as i32 - 1,
224 tick_pos.y,
225 ),
226 )
227 }
228 }
229 }
230
231 pub fn draw_grid_lines<D>(
233 &self,
234 viewport: Rectangle,
235 chart_area: Rectangle,
236 target: &mut D,
237 ) -> ChartResult<()>
238 where
239 D: DrawTarget<Color = C>,
240 {
241 if !self.config.show_grid || self.style.grid_lines.is_none() {
242 return Ok(());
243 }
244
245 let grid_style = self.style.grid_lines.as_ref().unwrap();
246 let ticks = self
247 .tick_generator
248 .generate_ticks(self.config.min, self.config.max, 20);
249
250 for tick in &ticks {
251 if tick.is_major {
252 let (start, end) = self.calculate_grid_line(tick.value, viewport, chart_area);
253 self.renderer
254 .draw_grid_line(start, end, grid_style, target)?;
255 }
256 }
257
258 Ok(())
259 }
260
261 pub fn draw_axis_only<D>(&self, viewport: Rectangle, target: &mut D) -> ChartResult<()>
263 where
264 D: DrawTarget<Color = C>,
265 {
266 if self.config.show_line {
268 let (start, end) = self.calculate_axis_line(viewport);
269 self.renderer
270 .draw_axis_line(start, end, &self.style.axis_line, target)?;
271 }
272
273 let ticks = self
275 .tick_generator
276 .generate_ticks(self.config.min, self.config.max, 50);
277
278 if self.config.show_ticks {
280 for tick in &ticks {
281 let tick_pos = self.calculate_tick_position(tick.value, viewport);
282 let tick_style = if tick.is_major {
283 &self.style.major_ticks
284 } else {
285 &self.style.minor_ticks
286 };
287
288 if tick_style.visible {
289 self.renderer.draw_tick(
290 tick_pos,
291 tick_style.length,
292 self.config.orientation,
293 &tick_style.line,
294 target,
295 )?;
296 }
297 }
298 }
299
300 if self.config.show_labels && self.style.labels.visible {
302 for tick in &ticks {
303 if tick.is_major && tick.label.is_some() {
304 let tick_pos = self.calculate_tick_position(tick.value, viewport);
305 let label_pos = self.calculate_label_position(tick_pos);
306 self.renderer.draw_label(
307 tick.label.as_ref().unwrap().as_str(),
308 label_pos,
309 target,
310 )?;
311 }
312 }
313 }
314
315 Ok(())
316 }
317}
318
319impl<T, C> Axis<T, C> for LinearAxis<T, C>
320where
321 T: AxisValue,
322 C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>,
323{
324 type TickGenerator = LinearTickGenerator;
325 type Style = AxisStyle<C>;
326
327 fn min(&self) -> T {
328 self.config.min
329 }
330
331 fn max(&self) -> T {
332 self.config.max
333 }
334
335 fn orientation(&self) -> AxisOrientation {
336 self.config.orientation
337 }
338
339 fn position(&self) -> AxisPosition {
340 self.config.position
341 }
342
343 fn transform_value(&self, value: T, viewport: Rectangle) -> i32 {
344 let min_f32 = self.config.min.to_f32();
345 let max_f32 = self.config.max.to_f32();
346 let value_f32 = value.to_f32();
347
348 if max_f32 <= min_f32 {
349 return match self.config.orientation {
350 AxisOrientation::Horizontal => viewport.top_left.x + viewport.size.width as i32 / 2,
351 AxisOrientation::Vertical => viewport.top_left.y + viewport.size.height as i32 / 2,
352 };
353 }
354
355 let normalized = (value_f32 - min_f32) / (max_f32 - min_f32);
356
357 match self.config.orientation {
358 AxisOrientation::Horizontal => {
359 viewport.top_left.x + (normalized * (viewport.size.width as f32 - 1.0)) as i32
360 }
361 AxisOrientation::Vertical => {
362 viewport.top_left.y + viewport.size.height as i32
364 - 1
365 - (normalized * (viewport.size.height as f32 - 1.0)) as i32
366 }
367 }
368 }
369
370 fn inverse_transform(&self, coordinate: i32, viewport: Rectangle) -> T {
371 let min_f32 = self.config.min.to_f32();
372 let max_f32 = self.config.max.to_f32();
373
374 let normalized = match self.config.orientation {
375 AxisOrientation::Horizontal => {
376 (coordinate - viewport.top_left.x) as f32 / (viewport.size.width as f32 - 1.0)
377 }
378 AxisOrientation::Vertical => {
379 1.0 - ((coordinate - viewport.top_left.y) as f32
381 / (viewport.size.height as f32 - 1.0))
382 }
383 };
384
385 let value_f32 = min_f32 + normalized * (max_f32 - min_f32);
386 T::from_f32(value_f32)
387 }
388
389 fn tick_generator(&self) -> &Self::TickGenerator {
390 &self.tick_generator
391 }
392
393 fn style(&self) -> &Self::Style {
394 &self.style
395 }
396
397 fn draw<D>(&self, viewport: Rectangle, target: &mut D) -> ChartResult<()>
398 where
399 D: DrawTarget<Color = C>,
400 {
401 if self.config.show_line {
403 let (start, end) = self.calculate_axis_line(viewport);
404 self.renderer
405 .draw_axis_line(start, end, &self.style.axis_line, target)?;
406 }
407
408 let ticks = self
410 .tick_generator
411 .generate_ticks(self.config.min, self.config.max, 50);
412
413 if self.config.show_ticks {
415 for tick in &ticks {
416 let tick_pos = self.calculate_tick_position(tick.value, viewport);
417 let tick_style = if tick.is_major {
418 &self.style.major_ticks
419 } else {
420 &self.style.minor_ticks
421 };
422
423 if tick_style.visible {
424 self.renderer.draw_tick(
425 tick_pos,
426 tick_style.length,
427 self.config.orientation,
428 &tick_style.line,
429 target,
430 )?;
431 }
432 }
433 }
434
435 if self.config.show_labels && self.style.labels.visible {
439 for tick in &ticks {
440 if tick.is_major && tick.label.is_some() {
441 let tick_pos = self.calculate_tick_position(tick.value, viewport);
442 let label_pos = self.calculate_label_position(tick_pos);
443 self.renderer.draw_label(
444 tick.label.as_ref().unwrap().as_str(),
445 label_pos,
446 target,
447 )?;
448 }
449 }
450 }
451
452 Ok(())
453 }
454
455 fn required_space(&self) -> u32 {
456 let mut space = 0;
457
458 if self.config.show_line {
460 space += self.style.axis_line.width;
461 }
462
463 if self.config.show_ticks {
465 let major_tick_space = if self.style.major_ticks.visible {
466 self.style.major_ticks.length
467 } else {
468 0
469 };
470 let minor_tick_space = if self.style.minor_ticks.visible {
471 self.style.minor_ticks.length
472 } else {
473 0
474 };
475 space += major_tick_space.max(minor_tick_space);
476 }
477
478 if self.config.show_labels && self.style.labels.visible {
480 space += self.style.label_offset + self.style.labels.font_size;
481 }
482
483 space
484 }
485}
486
487impl<T, C> LinearAxis<T, C>
488where
489 T: AxisValue,
490 C: PixelColor,
491{
492 fn calculate_label_position(&self, tick_pos: Point) -> Point {
494 match (self.config.orientation, self.config.position) {
495 (AxisOrientation::Horizontal, AxisPosition::Bottom) => {
496 Point::new(tick_pos.x, tick_pos.y + self.style.label_offset as i32)
497 }
498 (AxisOrientation::Horizontal, AxisPosition::Top) => {
499 Point::new(tick_pos.x, tick_pos.y - self.style.label_offset as i32)
500 }
501 (AxisOrientation::Vertical, AxisPosition::Left) => {
502 Point::new(tick_pos.x - self.style.label_offset as i32, tick_pos.y)
503 }
504 (AxisOrientation::Vertical, AxisPosition::Right) => {
505 Point::new(tick_pos.x + self.style.label_offset as i32, tick_pos.y)
506 }
507 (AxisOrientation::Horizontal, AxisPosition::Left)
509 | (AxisOrientation::Horizontal, AxisPosition::Right) => {
510 Point::new(tick_pos.x, tick_pos.y + self.style.label_offset as i32)
512 }
513 (AxisOrientation::Vertical, AxisPosition::Bottom)
514 | (AxisOrientation::Vertical, AxisPosition::Top) => {
515 Point::new(tick_pos.x - self.style.label_offset as i32, tick_pos.y)
517 }
518 }
519 }
520}
521
522impl<C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>> AxisRenderer<C>
523 for DefaultAxisRenderer<C>
524{
525 fn draw_axis_line<D>(
526 &self,
527 start: Point,
528 end: Point,
529 style: &LineStyle<C>,
530 target: &mut D,
531 ) -> ChartResult<()>
532 where
533 D: DrawTarget<Color = C>,
534 {
535 Line::new(start, end)
536 .into_styled(PrimitiveStyle::with_stroke(style.color, style.width))
537 .draw(target)
538 .map_err(|_| crate::error::ChartError::RenderingError)?;
539 Ok(())
540 }
541
542 fn draw_tick<D>(
543 &self,
544 position: Point,
545 length: u32,
546 orientation: AxisOrientation,
547 style: &LineStyle<C>,
548 target: &mut D,
549 ) -> ChartResult<()>
550 where
551 D: DrawTarget<Color = C>,
552 {
553 let (start, end) = match orientation {
554 AxisOrientation::Horizontal => {
555 (
557 Point::new(position.x, position.y),
558 Point::new(position.x, position.y + length as i32),
559 )
560 }
561 AxisOrientation::Vertical => {
562 (
564 Point::new(position.x, position.y),
565 Point::new(position.x - length as i32, position.y),
566 )
567 }
568 };
569
570 Line::new(start, end)
571 .into_styled(PrimitiveStyle::with_stroke(style.color, style.width))
572 .draw(target)
573 .map_err(|_| crate::error::ChartError::RenderingError)?;
574 Ok(())
575 }
576
577 fn draw_grid_line<D>(
578 &self,
579 start: Point,
580 end: Point,
581 style: &LineStyle<C>,
582 target: &mut D,
583 ) -> ChartResult<()>
584 where
585 D: DrawTarget<Color = C>,
586 {
587 Line::new(start, end)
588 .into_styled(PrimitiveStyle::with_stroke(style.color, style.width))
589 .draw(target)
590 .map_err(|_| crate::error::ChartError::RenderingError)?;
591 Ok(())
592 }
593
594 fn draw_label<D>(&self, text: &str, position: Point, target: &mut D) -> ChartResult<()>
595 where
596 D: DrawTarget<Color = C>,
597 {
598 use embedded_graphics::{
600 mono_font::{ascii::FONT_6X10, MonoTextStyle},
601 text::{Alignment, Text},
602 };
603
604 let text_color = embedded_graphics::pixelcolor::Rgb565::BLACK.into();
606
607 let text_style = MonoTextStyle::new(&FONT_6X10, text_color);
608
609 Text::with_alignment(text, position, text_style, Alignment::Center)
611 .draw(target)
612 .map_err(|_| crate::error::ChartError::RenderingError)?;
613
614 Ok(())
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621 use embedded_graphics::pixelcolor::Rgb565;
622
623 #[test]
624 fn test_linear_axis_creation() {
625 let axis: LinearAxis<f32, Rgb565> =
626 LinearAxis::new(0.0, 10.0, AxisOrientation::Horizontal, AxisPosition::Bottom);
627
628 assert_eq!(axis.min(), 0.0);
629 assert_eq!(axis.max(), 10.0);
630 assert_eq!(axis.orientation(), AxisOrientation::Horizontal);
631 assert_eq!(axis.position(), AxisPosition::Bottom);
632 }
633
634 #[test]
635 fn test_value_transformation() {
636 let axis: LinearAxis<f32, Rgb565> =
637 LinearAxis::new(0.0, 10.0, AxisOrientation::Horizontal, AxisPosition::Bottom);
638
639 let viewport = Rectangle::new(Point::new(0, 0), Size::new(100, 50));
640
641 assert_eq!(axis.transform_value(0.0, viewport), 0);
643 assert_eq!(axis.transform_value(10.0, viewport), 99);
644 assert_eq!(axis.transform_value(5.0, viewport), 49);
645
646 assert!((axis.inverse_transform(0, viewport) - 0.0).abs() < 0.1);
648 assert!((axis.inverse_transform(99, viewport) - 10.0).abs() < 0.1);
649 }
650
651 #[test]
652 fn test_axis_builder_pattern() {
653 let axis: LinearAxis<f32, Rgb565> =
654 LinearAxis::new(0.0, 10.0, AxisOrientation::Vertical, AxisPosition::Left)
655 .show_grid(true)
656 .show_labels(false)
657 .with_tick_generator(LinearTickGenerator::new(8));
658
659 assert!(axis.config.show_grid);
660 assert!(!axis.config.show_labels);
661 }
664}