1use super::Widget;
24use alloc::vec::Vec;
25use core::marker::PhantomData;
26use embedded_graphics::{pixelcolor::PixelColor, prelude::*, primitives::Rectangle};
27use zest_core::{Constraints, Length, RenderError, Renderer, TouchPhase};
28use zest_theme::Theme;
29
30#[derive(Copy, Clone, Debug, PartialEq, Eq)]
32enum SeriesKind {
33 Line,
35 Bar,
37}
38
39struct Series<'a, C> {
41 kind: SeriesKind,
42 data: &'a [i32],
43 color: Option<C>,
45}
46
47pub struct Chart<'a, C: PixelColor, M: Clone> {
49 rect: Rectangle,
50 series: Vec<Series<'a, C>>,
51 axes: bool,
52 gridlines: u32,
53 points: bool,
54 width: Length,
55 height: Length,
56 _phantom: PhantomData<M>,
57}
58
59impl<'a, C: PixelColor + 'a, M: Clone> Chart<'a, C, M> {
60 pub fn new() -> Self {
64 Self {
65 rect: Rectangle::zero(),
66 series: Vec::new(),
67 axes: false,
68 gridlines: 0,
69 points: false,
70 width: Length::Fill,
71 height: Length::Fill,
72 _phantom: PhantomData,
73 }
74 }
75
76 #[must_use]
78 pub fn line_series(mut self, data: &'a [i32]) -> Self {
79 self.series.push(Series {
80 kind: SeriesKind::Line,
81 data,
82 color: None,
83 });
84 self
85 }
86
87 #[must_use]
89 pub fn line_series_colored(mut self, data: &'a [i32], color: C) -> Self {
90 self.series.push(Series {
91 kind: SeriesKind::Line,
92 data,
93 color: Some(color),
94 });
95 self
96 }
97
98 #[must_use]
100 pub fn bar_series(mut self, data: &'a [i32]) -> Self {
101 self.series.push(Series {
102 kind: SeriesKind::Bar,
103 data,
104 color: None,
105 });
106 self
107 }
108
109 #[must_use]
111 pub fn bar_series_colored(mut self, data: &'a [i32], color: C) -> Self {
112 self.series.push(Series {
113 kind: SeriesKind::Bar,
114 data,
115 color: Some(color),
116 });
117 self
118 }
119
120 #[must_use]
122 pub fn axes(mut self, axes: bool) -> Self {
123 self.axes = axes;
124 self
125 }
126
127 #[must_use]
129 pub fn gridlines(mut self, count: u32) -> Self {
130 self.gridlines = count;
131 self
132 }
133
134 #[must_use]
137 pub fn points(mut self, points: bool) -> Self {
138 self.points = points;
139 self
140 }
141
142 #[must_use]
144 pub fn width(mut self, width: impl Into<Length>) -> Self {
145 self.width = width.into();
146 self
147 }
148
149 #[must_use]
151 pub fn height(mut self, height: impl Into<Length>) -> Self {
152 self.height = height.into();
153 self
154 }
155
156 fn data_range(&self) -> Option<(i32, i32)> {
159 let mut min = i32::MAX;
160 let mut max = i32::MIN;
161 let mut any = false;
162 for s in &self.series {
163 for &v in s.data {
164 any = true;
165 min = min.min(v);
166 max = max.max(v);
167 }
168 }
169 if !any {
170 return None;
171 }
172 if min == max {
173 max = min + 1;
174 }
175 Some((min, max))
176 }
177
178 fn map_y(value: i32, min: i32, max: i32, top: i32, bottom: i32) -> i32 {
181 let span = (max - min) as i64;
182 let frac = ((value - min) as i64 * (bottom - top) as i64) / span;
183 bottom - frac as i32
184 }
185
186 fn map_x(index: usize, count: usize, left: i32, right: i32) -> i32 {
189 if count <= 1 {
190 return left;
191 }
192 left + ((index as i64 * (right - left) as i64) / (count as i64 - 1)) as i32
193 }
194}
195
196impl<'a, C: PixelColor + 'a, M: Clone> Default for Chart<'a, C, M> {
197 fn default() -> Self {
198 Self::new()
199 }
200}
201
202impl<'a, C: PixelColor + 'a, M: Clone> Widget<C, M> for Chart<'a, C, M> {
203 fn measure(&mut self, constraints: Constraints) -> Size {
204 let w = self
205 .width
206 .resolve(constraints.max.width, constraints.max.width);
207 let h = self
208 .height
209 .resolve(constraints.max.height, constraints.max.height);
210 constraints.clamp(Size::new(w, h))
211 }
212
213 fn preferred_size(&self) -> (Length, Length) {
214 (self.width, self.height)
215 }
216
217 fn arrange(&mut self, rect: Rectangle) {
218 self.rect = rect;
219 }
220
221 fn rect(&self) -> Rectangle {
222 self.rect
223 }
224
225 fn handle_touch(&mut self, _point: Point, _phase: TouchPhase) -> Option<M> {
226 None
227 }
228
229 fn draw<'t>(
230 &self,
231 renderer: &mut dyn Renderer<C>,
232 theme: &Theme<'t, C>,
233 ) -> Result<(), RenderError> {
234 let left = self.rect.top_left.x + 1;
236 let right = self.rect.top_left.x + self.rect.size.width as i32 - 2;
237 let top = self.rect.top_left.y + 1;
238 let bottom = self.rect.top_left.y + self.rect.size.height as i32 - 2;
239 if right <= left || bottom <= top {
240 return Ok(());
241 }
242
243 let grid = theme.background.divider;
244
245 if self.gridlines > 0 {
247 let n = self.gridlines;
248 for i in 0..=n {
249 let y = top + ((i as i64 * (bottom - top) as i64) / n as i64) as i32;
250 renderer.stroke_line(Point::new(left, y), Point::new(right, y), grid, 1)?;
251 }
252 }
253
254 if self.axes {
256 let axis = theme.background.on_base;
257 renderer.stroke_line(Point::new(left, top), Point::new(left, bottom), axis, 1)?;
258 renderer.stroke_line(Point::new(left, bottom), Point::new(right, bottom), axis, 1)?;
259 }
260
261 let Some((min, max)) = self.data_range() else {
262 return Ok(());
263 };
264
265 let bar_series_count = self
267 .series
268 .iter()
269 .filter(|s| s.kind == SeriesKind::Bar)
270 .count()
271 .max(1);
272 let mut bar_index = 0usize;
273
274 for s in &self.series {
275 if s.data.is_empty() {
276 continue;
277 }
278 let color = s.color.unwrap_or(theme.accent.base);
279 let count = s.data.len();
280 match s.kind {
281 SeriesKind::Line => {
282 let mut prev: Option<Point> = None;
283 for (i, &v) in s.data.iter().enumerate() {
284 let x = Self::map_x(i, count, left, right);
285 let y = Self::map_y(v, min, max, top, bottom);
286 let p = Point::new(x, y);
287 if let Some(prev) = prev {
288 renderer.stroke_line(prev, p, color, 1)?;
289 }
290 if self.points {
291 renderer.fill_circle(p, 2, color)?;
292 }
293 prev = Some(p);
294 }
295 }
296 SeriesKind::Bar => {
297 let slot = ((right - left) / count.max(1) as i32).max(1);
299 let group_w = (slot * 4 / 5).max(1);
300 let bar_w = (group_w / bar_series_count as i32).max(1);
301 for (i, &v) in s.data.iter().enumerate() {
302 let slot_left = left + i as i32 * slot;
303 let bx = slot_left + bar_index as i32 * bar_w;
304 let y = Self::map_y(v, min, max, top, bottom);
305 let h = (bottom - y).max(0) as u32;
306 if h == 0 {
307 continue;
308 }
309 renderer.fill_rect(
310 Rectangle::new(Point::new(bx, y), Size::new(bar_w as u32, h)),
311 color,
312 )?;
313 }
314 bar_index += 1;
315 }
316 }
317 }
318 Ok(())
319 }
320}