embedded_graphics_sparklines/
lib.rs

1//! # Embedded Graphics Sparklines
2//!
3//! `embedded-graphics-sparklines` is an implementation of sparkline graphs
4//! which can be used to effectively present data on small emedded displays.
5//!
6//!
7//! > Sparklines are "small, high resolution graphics embedded in a context
8//! > of words, numbers or images". Edward Tufte describes sparklines as
9//! > "data-intense, design-simple, word-sized graphics".
10//!
11
12use embedded_graphics::pixelcolor::PixelColor;
13use embedded_graphics::prelude::Primitive;
14use embedded_graphics::primitives::{PrimitiveStyle, StyledDrawable};
15
16use embedded_graphics::prelude::{DrawTarget, Point};
17use embedded_graphics::primitives::Rectangle;
18use embedded_graphics::Drawable;
19use std::collections::VecDeque;
20
21/// `Drawable` primitive (in sense of `embedded-graphics` lib) that is reponsible
22///  for performing normalization, sample storage and drawing of the accumulated
23/// data.
24///
25/// # Example
26/// ```
27/// use embedded_graphics::prelude::*;
28/// use embedded_graphics::pixelcolor::BinaryColor;
29/// use embedded_graphics::primitives::{Line, Rectangle};
30/// use embedded_graphics::mock_display::MockDisplay;
31/// use embedded_graphics_sparklines::Sparkline;
32///
33/// let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
34/// display.set_allow_overdraw(true);
35///
36/// let draw_fn = |lastp, p| Line::new(lastp, p);
37/// let mut sparkline = Sparkline::new(
38///         Rectangle::new(Point::new(0, 0), Size::new(16, 5)), // position and size of the sparkline
39///         12, // max samples to store in memory (and display on graph)
40///         BinaryColor::On,
41///         1, // stroke width
42///         draw_fn,
43///     );
44///
45///     for n in 0..11 {
46///         sparkline.add((f32::sin(n as f32) * 5_f32) as i32);
47///     }
48/// sparkline.draw(&mut display).unwrap();
49///
50/// ```
51pub struct Sparkline<C, F, P>
52where
53    C: PixelColor,
54    F: Fn(Point, Point) -> P,
55    P: Primitive + StyledDrawable<PrimitiveStyle<C>, Color = C>,
56{
57    /// stores max_samples number of values
58    pub values: VecDeque<i32>,
59    bbox: Rectangle,
60    /// defines the max number of values that sparkline will present
61    pub max_samples: usize,
62    color: C,
63    stroke_width: u32,
64    draw_fn: F,
65}
66
67impl<C, F, P> Sparkline<C, F, P>
68where
69    C: PixelColor,
70    F: Fn(Point, Point) -> P,
71    P: Primitive + StyledDrawable<PrimitiveStyle<C>, Color = C>,
72{
73    pub fn new(
74        bbox: Rectangle,
75        max_samples: usize,
76        color: C,
77        stroke_width: u32,
78        draw_fn: F,
79    ) -> Self {
80        Self {
81            values: VecDeque::with_capacity(max_samples),
82            bbox,
83            max_samples,
84            color,
85            stroke_width,
86            draw_fn,
87        }
88    }
89
90    pub fn add(&mut self, val: i32) {
91        if self.values.len() == self.max_samples {
92            self.values.pop_front();
93        }
94        self.values.push_back(val);
95    }
96}
97
98impl<C, F, P> Drawable for Sparkline<C, F, P>
99where
100    C: PixelColor,
101    F: Fn(Point, Point) -> P,
102    P: Primitive + StyledDrawable<PrimitiveStyle<C>, Color = C>,
103{
104    type Color = C;
105    type Output = ();
106
107    fn draw<D>(&self, target: &mut D) -> Result<Self::Output, D::Error>
108    where
109        D: DrawTarget<Color = Self::Color>,
110    {
111        let mut slope: f32 = self.bbox.size.height as f32 - self.stroke_width as f32;
112
113        // find min and max in a single pass
114        let (min, max): (&i32, &i32) =
115            self.values
116                .iter()
117                .fold((&i32::MAX, &i32::MIN), |mut acc, val| {
118                    if val < acc.0 {
119                        acc.0 = val;
120                    }
121                    if val > acc.1 {
122                        acc.1 = val;
123                    }
124                    acc
125                });
126
127        // slope mod
128        if max != min {
129            slope /= (max - min) as f32;
130        }
131
132        let px_per_seg = (self.bbox.size.width - 1) as f32 / (self.values.len() - 1) as f32;
133        let mut lastp = Point::new(0, 0);
134
135        for (i, val) in self.values.iter().enumerate() {
136            let scaled_val = self.bbox.top_left.y as f32 + self.bbox.size.height as f32
137                - ((val - min) as f32 * slope)
138                - self.stroke_width as f32 / 2f32;
139
140            let p = Point::new(
141                (i as f32 * px_per_seg) as i32 + self.bbox.top_left.x,
142                scaled_val as i32,
143            );
144
145            // skip first point as it goes from zero
146            if i > 0 {
147                // draw using supplied closure drawing function
148                (self.draw_fn)(lastp, p)
149                    .into_styled(PrimitiveStyle::with_stroke(self.color, self.stroke_width))
150                    .draw(target)?;
151            }
152            lastp = p;
153        }
154        Ok(())
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use embedded_graphics::mock_display::MockDisplay;
161    use embedded_graphics::pixelcolor::BinaryColor;
162    use embedded_graphics::prelude::Size;
163    use embedded_graphics::primitives::{Circle, Line};
164
165    use super::*;
166
167    enum DrawSignal {
168        Sin,
169        Lin,
170    }
171
172    fn generate_sparkline(
173        max_samples: usize,
174        stroke_width: u32,
175        draw_signal: DrawSignal,
176    ) -> Sparkline<BinaryColor, impl Fn(Point, Point) -> Line, Line> {
177        let draw_fn = |lastp, p| Line::new(lastp, p);
178        let mut sparkline = Sparkline::new(
179            Rectangle::new(Point::new(0, 0), Size::new(16, 5)), // position and size of the sparkline
180            max_samples, // max samples to store in memory (and display on graph)
181            BinaryColor::On,
182            stroke_width, // stroke size
183            draw_fn,
184        );
185
186        for n in 0..11 {
187            let val = match &draw_signal {
188                DrawSignal::Sin => (f32::sin(n as f32) * 5_f32) as i32,
189                DrawSignal::Lin => n,
190            };
191            sparkline.add(val);
192        }
193
194        sparkline
195    }
196
197    #[test]
198    fn draws_sin() {
199        let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
200        display.set_allow_overdraw(true);
201
202        let sparkline = generate_sparkline(32, 1, DrawSignal::Sin);
203        sparkline.draw(&mut display).unwrap();
204
205        display.assert_pattern(&[
206            " ###        #   ",
207            "#  #      ## #  ",
208            "#   #    #    # ",
209            "     #   #     #",
210            "      ###       ",
211        ]);
212    }
213
214    #[test]
215    fn draws_sin_10_samples() {
216        let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
217        display.set_allow_overdraw(true);
218
219        let sparkline = generate_sparkline(10, 1, DrawSignal::Sin);
220        sparkline.draw(&mut display).unwrap();
221
222        display.assert_pattern(&[
223            "##         ##   ",
224            "  #       #  #  ",
225            "   #     #    # ",
226            "    #   #      #",
227            "     ###        ",
228        ]);
229        assert_eq!(10, sparkline.values.len());
230    }
231
232    #[test]
233    fn draws_line_using_stroke_width() {
234        let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
235        display.set_allow_overdraw(true);
236
237        let sparkline_1 = generate_sparkline(20, 1, DrawSignal::Lin);
238        sparkline_1.draw(&mut display).unwrap();
239
240        display.assert_pattern(&[
241            "             ###",
242            "          ###   ",
243            "      ####      ",
244            "   ###          ",
245            "###             ",
246        ]);
247
248        display = MockDisplay::new();
249        display.set_allow_overdraw(true);
250        let sparkline_2 = generate_sparkline(20, 2, DrawSignal::Lin);
251        sparkline_2.draw(&mut display).unwrap();
252
253        display.assert_pattern(&[
254            "          ######",
255            "      ##########",
256            " #########      ",
257            "######          ",
258            "#               ",
259        ]);
260
261        display = MockDisplay::new();
262        display.set_allow_overdraw(true);
263        display.set_allow_out_of_bounds_drawing(true);
264        let sparkline_3 = generate_sparkline(20, 3, DrawSignal::Lin);
265        sparkline_3.draw(&mut display).unwrap();
266
267        display.assert_pattern(&[
268            "            ####",
269            "   #############",
270            "################",
271            "############    ",
272            "####            ",
273        ]);
274    }
275
276    #[test]
277    fn draws_in_bounding_box() {
278        let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
279        display.set_allow_overdraw(true);
280
281        let draw_fn = |lastp, p| Line::new(lastp, p);
282        let mut sparkline = Sparkline::new(
283            Rectangle::new(Point::new(5, 4), Size::new(16, 5)), // position and size of the sparkline
284            32, // max samples to store in memory (and display on graph)
285            BinaryColor::On,
286            1, // stroke size
287            draw_fn,
288        );
289
290        for n in 0..11 {
291            sparkline.add(n)
292        }
293
294        sparkline.draw(&mut display).unwrap();
295
296        display.assert_pattern(&[
297            "                     ",
298            "                     ",
299            "                     ",
300            "                     ",
301            "                  ###",
302            "               ###   ",
303            "           ####      ",
304            "        ###          ",
305            "     ###             ",
306        ]);
307    }
308
309    #[test]
310    fn uses_drawing_function() {
311        let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
312        // display.set_allow_overdraw(true);
313
314        let draw_fn = |lastp: Point, _p: Point| Circle::new(Point::new(lastp.x, lastp.y), 3);
315        let mut sparkline = Sparkline::new(
316            Rectangle::new(Point::new(0, 0), Size::new(26, 9)), // position and size of the sparkline
317            32, // max samples to store in memory (and display on graph)
318            BinaryColor::On,
319            1, // stroke size
320            draw_fn,
321        );
322
323        for n in 0..6 {
324            sparkline.add(n)
325        }
326
327        sparkline.draw(&mut display).unwrap();
328
329        display.assert_pattern(&[
330            "                       ",
331            "                       ",
332            "                     # ",
333            "                #   # #",
334            "               # #   # ",
335            "           #    #      ",
336            "      #   # #          ",
337            "     # #   #           ",
338            " #    #                ",
339            "# #                    ",
340            " #                     ",
341        ]);
342    }
343}