embedded_graphics_sparklines/
lib.rs1use 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
21pub 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 pub values: VecDeque<i32>,
59 bbox: Rectangle,
60 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 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 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 if i > 0 {
147 (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)), max_samples, BinaryColor::On,
182 stroke_width, 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)), 32, BinaryColor::On,
286 1, 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 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)), 32, BinaryColor::On,
319 1, 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}