embedded_plots/
axis.rs

1use core::{fmt::Write, ops::Range};
2use heapless::String;
3
4use embedded_graphics::{
5    prelude::*,
6    primitives::{Line, PrimitiveStyle},
7    text::Text,
8    text::TextStyle,
9};
10
11use crate::range_conv::Scalable;
12use embedded_graphics::mono_font::ascii::FONT_5X8;
13use embedded_graphics::mono_font::MonoTextStyle;
14use embedded_graphics::text::{Alignment, Baseline, TextStyleBuilder};
15
16/// Used to provide alignment of an axis, it will be dsizerown exactly on the line marked by the points
17pub enum Placement {
18    X { x1: i32, x2: i32, y: i32 },
19    Y { y1: i32, y2: i32, x: i32 },
20}
21
22/// Used to describe how densely ticks should be drawn
23#[derive(Clone, Copy)]
24pub enum Scale {
25    /// Fixed scale means that ticks will be drawn between each increment of absolute distance provided.
26    /// for example, on range 0..30 and Fixed(10), ticks will be drawn for 0, 10 and 20
27    Fixed(usize),
28    /// RangeFraction means that provided number of ticks ticks will be drawn on entire range
29    /// for example, on range 0..60 and RangeFraction(3), ticks will be drawn for 0, 20 and 40
30    RangeFraction(usize),
31}
32
33impl Default for Scale {
34    fn default() -> Self {
35        Scale::RangeFraction(5)
36    }
37}
38
39/// Display-agnostic axis object, only contains scale range and title, can be converted to drawable axis for specific display
40pub struct Axis<'a> {
41    /// range that the scale will be drawn for
42    range: Range<i32>,
43    /// axis title displayed right next to it
44    title: Option<&'a str>,
45    /// Definition on how scale ticks should be drawn
46    scale: Option<Scale>,
47}
48
49/// builder methods to modify axis decoration
50impl<'a> Axis<'a> {
51    /// create new axis data
52    pub fn new(range: Range<i32>) -> Axis<'a> {
53        Axis {
54            range,
55            title: None,
56            scale: None,
57        }
58    }
59
60    /// define how scale ticks should be drawn
61    pub fn set_scale(mut self, scale: Scale) -> Axis<'a> {
62        self.scale = Some(scale);
63        self
64    }
65
66    /// set axis title
67    pub fn set_title(mut self, title: &'a str) -> Axis<'a> {
68        self.title = Some(title);
69        self
70    }
71
72    /// turn axis data into drawable object suitable for specific display
73    pub fn into_drawable_axis<C>(self, placement: Placement) -> DrawableAxis<'a, C>
74    where
75        C: PixelColor + Default,
76        TextStyle: Clone + Default,
77    {
78        DrawableAxis {
79            axis: self,
80            placement,
81            color: None,
82            text_style: None,
83            tick_size: None,
84            thickness: None,
85        }
86    }
87}
88
89/// Drawable axis object, constructed for specific display
90pub struct DrawableAxis<'a, C>
91where
92    C: PixelColor,
93    TextStyle: Clone + Default,
94{
95    axis: Axis<'a>,
96    placement: Placement,
97    color: Option<C>,
98    text_style: Option<MonoTextStyle<'a, C>>,
99    tick_size: Option<usize>,
100    thickness: Option<usize>,
101}
102
103impl<'a, C> DrawableAxis<'a, C>
104where
105    C: PixelColor + Default,
106    TextStyle: Clone + Default,
107{
108    pub fn set_color(mut self, val: C) -> DrawableAxis<'a, C> {
109        self.color = Some(val);
110        self
111    }
112    pub fn set_text_style(mut self, val: MonoTextStyle<'a, C>) -> DrawableAxis<'a, C> {
113        self.text_style = Some(val);
114        self
115    }
116
117    /// set how wide tick should be drawn on the axis
118    pub fn set_tick_size(mut self, val: usize) -> DrawableAxis<'a, C> {
119        self.tick_size = Some(val);
120        self
121    }
122
123    /// set thickness of the main line of the axis
124    pub fn set_thickness(mut self, val: usize) -> DrawableAxis<'a, C> {
125        self.thickness = Some(val);
126        self
127    }
128}
129
130impl<'a, C> Drawable for DrawableAxis<'a, C>
131where
132    C: PixelColor + Default,
133    TextStyle: Clone + Default,
134{
135    type Color = C;
136    type Output = ();
137
138    /// most important function - draw the axis on the display
139    fn draw<D: DrawTarget<Color = C>>(&self, display: &mut D) -> Result<(), D::Error> {
140        let color = self.color.unwrap_or_default();
141        let thickness = self.thickness.unwrap_or(1);
142        let tick_size = self.tick_size.unwrap_or(2);
143
144        let character_style = MonoTextStyle::new(&FONT_5X8, color);
145
146        let scale_marks = match self.axis.scale.unwrap_or_default() {
147            Scale::Fixed(interval) => self.axis.range.clone().into_iter().step_by(interval),
148            Scale::RangeFraction(fraction) => {
149                let len = self.axis.range.len();
150                self.axis.range.clone().into_iter().step_by(len / fraction)
151            }
152        };
153        match self.placement {
154            Placement::X { x1, x2, y } => {
155                let title_text_style = TextStyleBuilder::new()
156                    .alignment(Alignment::Center)
157                    .baseline(Baseline::Top)
158                    .build();
159                let tick_text_style = TextStyleBuilder::new()
160                    .alignment(Alignment::Left)
161                    .baseline(Baseline::Top)
162                    .build();
163                Line {
164                    start: Point { x: x1, y },
165                    end: Point { x: x2, y },
166                }
167                .into_styled(PrimitiveStyle::with_stroke(color, thickness as u32))
168                .draw(display)?;
169                if let Some(title) = self.axis.title {
170                    Text::with_text_style(
171                        title,
172                        Point {
173                            x: x1 + (x2 - x1) / 2,
174                            y: y + 10,
175                        },
176                        character_style,
177                        title_text_style,
178                    )
179                    .draw(display)?;
180                }
181                for mark in scale_marks {
182                    let x = mark.scale_between_ranges(&self.axis.range, &(x1..x2));
183                    Line {
184                        start: Point {
185                            x,
186                            y: y - tick_size as i32,
187                        },
188                        end: Point {
189                            x,
190                            y: y + tick_size as i32,
191                        },
192                    }
193                    .into_styled(PrimitiveStyle::with_stroke(color, thickness as u32))
194                    .draw(display)?;
195                    let mut buf: String<8> = String::new();
196                    write!(buf, "{}", mark).unwrap();
197                    Text::with_text_style(
198                        &buf,
199                        Point { x: x + 2, y: y + 2 },
200                        character_style,
201                        tick_text_style,
202                    )
203                    .draw(display)?;
204                }
205            }
206            Placement::Y { y1, y2, x } => {
207                let title_text_style = TextStyleBuilder::new()
208                    .alignment(Alignment::Right)
209                    .baseline(Baseline::Middle)
210                    .build();
211                let tick_text_style = TextStyleBuilder::new()
212                    .alignment(Alignment::Right)
213                    .baseline(Baseline::Top)
214                    .build();
215                Line {
216                    start: Point { x, y: y1 },
217                    end: Point { x, y: y2 },
218                }
219                .into_styled(PrimitiveStyle::with_stroke(color, thickness as u32))
220                .draw(display)?;
221
222                let mut tick_text_left_pos_bound = i32::MAX;
223                for mark in scale_marks {
224                    let y = mark.scale_between_ranges(&self.axis.range, &(y2..y1));
225                    Line {
226                        start: Point {
227                            x: x - tick_size as i32,
228                            y,
229                        },
230                        end: Point {
231                            x: x + tick_size as i32,
232                            y,
233                        },
234                    }
235                    .into_styled(PrimitiveStyle::with_stroke(color, thickness as u32))
236                    .draw(display)?;
237                    let mut buf: String<8> = String::new();
238                    write!(buf, "{}", mark).unwrap();
239                    let tick_val = Text::with_text_style(
240                        &buf,
241                        Point { x, y },
242                        character_style,
243                        tick_text_style,
244                    );
245                    if tick_val.bounding_box().top_left.x < tick_text_left_pos_bound {
246                        tick_text_left_pos_bound = tick_val.bounding_box().top_left.x
247                    };
248                    tick_val.draw(display)?;
249                }
250                if let Some(title) = self.axis.title {
251                    Text::with_text_style(
252                        title,
253                        Point {
254                            x: tick_text_left_pos_bound - 1,
255                            y: y1 + (y2 - y1) / 2,
256                        },
257                        character_style,
258                        title_text_style,
259                    )
260                    .draw(display)?;
261                }
262            }
263        }
264        Ok(())
265    }
266}