1use crate::chart::traits::{Chart, ChartBuilder, ChartConfig};
4use crate::data::{DataPoint, DataSeries};
5use crate::error::{ChartError, ChartResult};
6use crate::math::Math;
7use crate::math::NumericConversion;
8use crate::style::BorderStyle;
9use embedded_graphics::{
10 draw_target::DrawTarget,
11 prelude::*,
12 primitives::{Circle, PrimitiveStyle, Rectangle},
13};
14use heapless::Vec;
15
16#[derive(Debug, Clone)]
18pub struct PieChart<C: PixelColor> {
19 style: PieChartStyle<C>,
20 config: ChartConfig<C>,
21 center: Point,
22 radius: u32,
23}
24
25#[derive(Debug, Clone)]
27pub struct PieChartStyle<C: PixelColor> {
28 pub colors: Vec<C, 16>,
30 pub border: Option<BorderStyle<C>>,
32 pub labels: LabelStyle,
34 pub start_angle: f32,
36 pub donut_inner_radius: Option<u32>,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct LabelStyle {
43 pub visible: bool,
45 pub show_percentage: bool,
47 pub show_values: bool,
49 pub offset: u32,
51}
52
53#[derive(Debug, Clone, Copy)]
55pub struct PieSlice {
56 pub start_angle: f32,
58 pub end_angle: f32,
60 pub value: f32,
62 pub percentage: f32,
64}
65
66impl<C: PixelColor> PieChart<C>
67where
68 C: From<embedded_graphics::pixelcolor::Rgb565>,
69{
70 pub fn new(center: Point, radius: u32) -> Self {
72 Self {
73 style: PieChartStyle::default(),
74 config: ChartConfig::default(),
75 center,
76 radius,
77 }
78 }
79
80 pub fn builder() -> PieChartBuilder<C> {
82 PieChartBuilder::new()
83 }
84
85 pub fn set_style(&mut self, style: PieChartStyle<C>) {
87 self.style = style;
88 }
89
90 pub fn style(&self) -> &PieChartStyle<C> {
92 &self.style
93 }
94
95 pub fn set_config(&mut self, config: ChartConfig<C>) {
97 self.config = config;
98 }
99
100 pub fn config(&self) -> &ChartConfig<C> {
102 &self.config
103 }
104
105 pub fn set_center(&mut self, center: Point) {
107 self.center = center;
108 }
109
110 pub fn center(&self) -> Point {
112 self.center
113 }
114
115 pub fn set_radius(&mut self, radius: u32) {
117 self.radius = radius;
118 }
119
120 pub fn radius(&self) -> u32 {
122 self.radius
123 }
124
125 fn calculate_slices(
127 &self,
128 data: &crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>,
129 ) -> ChartResult<Vec<PieSlice, 16>> {
130 let mut slices = Vec::new();
131
132 let total: f32 = data
134 .iter()
135 .map(|point| point.y())
136 .filter(|&value: &f32| value >= 0.0) .sum();
138
139 if total <= 0.0 {
140 return Err(ChartError::InsufficientData);
141 }
142
143 let start_angle_rad = self.style.start_angle.to_radians();
145 let mut current_angle = start_angle_rad;
146
147 for point in data.iter() {
149 let value: f32 = point.y();
150 if value < 0.0 {
151 continue; }
153
154 let percentage = value / total;
155 let angle_span = percentage * 2.0 * core::f32::consts::PI;
156 let end_angle = current_angle + angle_span;
157
158 let slice = PieSlice {
159 start_angle: current_angle,
160 end_angle,
161 value,
162 percentage: percentage * 100.0,
163 };
164
165 slices.push(slice).map_err(|_| ChartError::MemoryFull)?;
166 current_angle = end_angle;
167 }
168
169 Ok(slices)
170 }
171
172 fn draw_slice<D>(&self, slice: &PieSlice, color_index: usize, target: &mut D) -> ChartResult<()>
174 where
175 D: DrawTarget<Color = C>,
176 {
177 let slice_color = if !self.style.colors.is_empty() {
179 self.style.colors[color_index % self.style.colors.len()]
180 } else {
181 return Err(ChartError::InvalidConfiguration);
182 };
183
184 self.draw_pie_slice_custom(slice, slice_color, target)?;
186
187 Ok(())
188 }
189
190 fn draw_pie_slice_custom<D>(
192 &self,
193 slice: &PieSlice,
194 color: C,
195 target: &mut D,
196 ) -> ChartResult<()>
197 where
198 D: DrawTarget<Color = C>,
199 {
200 use embedded_graphics::Drawable;
201 use embedded_graphics::Pixel;
202
203 let center_x = self.center.x;
204 let center_y = self.center.y;
205 let radius_num = (self.radius as i32).to_number();
206
207 let min_x = (center_x - self.radius as i32).max(0);
209 let max_x = center_x + self.radius as i32;
210 let min_y = (center_y - self.radius as i32).max(0);
211 let max_y = center_y + self.radius as i32;
212
213 let zero = 0i32.to_number();
215 let pi = core::f32::consts::PI.to_number();
216 let two_pi = pi + pi;
217
218 for y in min_y..=max_y {
219 for x in min_x..=max_x {
220 let dx_num = (x - center_x).to_number();
221 let dy_num = (y - center_y).to_number();
222 let distance_squared = dx_num * dx_num + dy_num * dy_num;
223 let distance = Math::sqrt(distance_squared);
224
225 let tolerance = 0.5f32.to_number();
228 if distance > radius_num + tolerance || distance < tolerance {
229 continue;
230 }
231
232 let angle = Math::atan2(-dy_num, dx_num);
235
236 let normalized_angle = {
238 let mut a = angle;
239 if a < zero {
240 a += two_pi;
241 }
242 while a >= two_pi {
244 a -= two_pi;
245 }
246 while a < zero {
247 a += two_pi;
248 }
249 a
250 };
251
252 let start_angle_num = slice.start_angle.to_number();
254 let end_angle_num = slice.end_angle.to_number();
255
256 let start_norm = {
258 let mut a = start_angle_num;
259 while a >= two_pi {
260 a -= two_pi;
261 }
262 while a < zero {
263 a += two_pi;
264 }
265 a
266 };
267 let end_norm = {
268 let mut a = end_angle_num;
269 while a >= two_pi {
270 a -= two_pi;
271 }
272 while a < zero {
273 a += two_pi;
274 }
275 a
276 };
277
278 let in_slice = if start_norm <= end_norm {
279 normalized_angle >= start_norm && normalized_angle <= end_norm
280 } else {
281 normalized_angle >= start_norm || normalized_angle <= end_norm
283 };
284
285 if in_slice {
286 let point = Point::new(x, y);
287 Pixel(point, color)
288 .draw(target)
289 .map_err(|_| ChartError::RenderingError)?;
290 }
291 }
292 }
293
294 Ok(())
295 }
296
297 fn draw_donut_center<D>(&self, target: &mut D) -> ChartResult<()>
299 where
300 D: DrawTarget<Color = C>,
301 {
302 if let Some(inner_radius) = self.style.donut_inner_radius {
303 let center_color = self
305 .config
306 .background_color
307 .unwrap_or_else(|| embedded_graphics::pixelcolor::Rgb565::WHITE.into());
308
309 let fill_style = PrimitiveStyle::with_fill(center_color);
310 Circle::new(
311 Point::new(
312 self.center.x - inner_radius as i32,
313 self.center.y - inner_radius as i32,
314 ),
315 inner_radius * 2,
316 )
317 .into_styled(fill_style)
318 .draw(target)
319 .map_err(|_| ChartError::RenderingError)?;
320 }
321
322 Ok(())
323 }
324}
325impl<C: PixelColor> Default for PieChart<C>
326where
327 C: From<embedded_graphics::pixelcolor::Rgb565>,
328{
329 fn default() -> Self {
330 Self::new(Point::new(50, 50), 40)
331 }
332}
333
334impl<C: PixelColor> Chart<C> for PieChart<C>
335where
336 C: From<embedded_graphics::pixelcolor::Rgb565>,
337{
338 type Data = crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>;
339 type Config = ChartConfig<C>;
340
341 fn draw<D>(
342 &self,
343 data: &Self::Data,
344 config: &Self::Config,
345 viewport: Rectangle,
346 target: &mut D,
347 ) -> ChartResult<()>
348 where
349 D: DrawTarget<Color = C>,
350 Self::Data: DataSeries,
351 <Self::Data as DataSeries>::Item: DataPoint,
352 <<Self::Data as DataSeries>::Item as DataPoint>::Y: Into<f32> + Copy + PartialOrd,
353 {
354 if data.is_empty() {
355 return Err(ChartError::InsufficientData);
356 }
357
358 if let Some(bg_color) = config.background_color {
360 Rectangle::new(viewport.top_left, viewport.size)
361 .into_styled(PrimitiveStyle::with_fill(bg_color))
362 .draw(target)
363 .map_err(|_| ChartError::RenderingError)?;
364 }
365
366 let title_height = if config.title.is_some() { 30 } else { 0 };
368 let available_height = viewport.size.height.saturating_sub(title_height);
369
370 let center_x = viewport.top_left.x + (viewport.size.width as i32) / 2;
372 let center_y = viewport.top_left.y + title_height as i32 + (available_height as i32) / 2;
373 let actual_center = Point::new(center_x, center_y);
374
375 let mut chart_for_drawing = self.clone();
377 chart_for_drawing.center = actual_center;
378
379 let slices = chart_for_drawing.calculate_slices(data)?;
381
382 for (index, slice) in slices.iter().enumerate() {
384 chart_for_drawing.draw_slice(slice, index, target)?;
385 }
386
387 chart_for_drawing.draw_donut_center(target)?;
389
390 if let Some(title) = &config.title {
392 use embedded_graphics::{
393 mono_font::{ascii::FONT_6X10, MonoTextStyle},
394 text::{Alignment, Text},
395 };
396
397 let text_color = embedded_graphics::pixelcolor::Rgb565::BLACK.into();
398 let text_style = MonoTextStyle::new(&FONT_6X10, text_color);
399
400 let title_x = viewport.top_left.x + (viewport.size.width as i32) / 2;
401 let title_y = viewport.top_left.y + 15;
402
403 Text::with_alignment(
404 title,
405 Point::new(title_x, title_y),
406 text_style,
407 Alignment::Center,
408 )
409 .draw(target)
410 .map_err(|_| ChartError::RenderingError)?;
411 }
412
413 Ok(())
414 }
415}
416
417impl<C: PixelColor> Default for PieChartStyle<C>
418where
419 C: From<embedded_graphics::pixelcolor::Rgb565>,
420{
421 fn default() -> Self {
422 let mut colors = Vec::new();
423 let _ = colors.push(embedded_graphics::pixelcolor::Rgb565::BLUE.into());
424 let _ = colors.push(embedded_graphics::pixelcolor::Rgb565::RED.into());
425 let _ = colors.push(embedded_graphics::pixelcolor::Rgb565::GREEN.into());
426 let _ = colors.push(embedded_graphics::pixelcolor::Rgb565::YELLOW.into());
427 let _ = colors.push(embedded_graphics::pixelcolor::Rgb565::MAGENTA.into());
428 let _ = colors.push(embedded_graphics::pixelcolor::Rgb565::CYAN.into());
429
430 Self {
431 colors,
432 border: None,
433 labels: LabelStyle::default(),
434 start_angle: 0.0,
435 donut_inner_radius: None,
436 }
437 }
438}
439
440impl Default for LabelStyle {
441 fn default() -> Self {
442 Self {
443 visible: false,
444 show_percentage: true,
445 show_values: false,
446 offset: 10,
447 }
448 }
449}
450
451#[derive(Debug)]
453pub struct PieChartBuilder<C: PixelColor> {
454 style: PieChartStyle<C>,
455 config: ChartConfig<C>,
456 center: Point,
457 radius: u32,
458}
459
460impl<C: PixelColor> PieChartBuilder<C>
461where
462 C: From<embedded_graphics::pixelcolor::Rgb565>,
463{
464 pub fn new() -> Self {
466 Self {
467 style: PieChartStyle::default(),
468 config: ChartConfig::default(),
469 center: Point::new(50, 50),
470 radius: 40,
471 }
472 }
473
474 pub fn center(mut self, center: Point) -> Self {
476 self.center = center;
477 self
478 }
479
480 pub fn radius(mut self, radius: u32) -> Self {
482 self.radius = radius;
483 self
484 }
485
486 pub fn colors(mut self, colors: &[C]) -> Self {
488 self.style.colors.clear();
489 for &color in colors {
490 if self.style.colors.push(color).is_err() {
491 break; }
493 }
494 self
495 }
496
497 pub fn start_angle(mut self, angle: f32) -> Self {
499 self.style.start_angle = angle;
500 self
501 }
502
503 pub fn donut(mut self, inner_radius: u32) -> Self {
505 self.style.donut_inner_radius = Some(inner_radius);
506 self
507 }
508
509 pub fn donut_percentage(mut self, percentage: u32) -> Self {
529 let percentage = percentage.min(100); let inner_radius = (self.radius as f32 * percentage as f32 / 100.0) as u32;
531 self.style.donut_inner_radius = Some(inner_radius);
532 self
533 }
534
535 pub fn balanced_donut(self) -> Self {
554 self.donut_percentage(50)
555 }
556
557 pub fn thin_donut(self) -> Self {
562 self.donut_percentage(25)
563 }
564
565 pub fn thick_donut(self) -> Self {
570 self.donut_percentage(75)
571 }
572
573 pub fn with_border(mut self, border: BorderStyle<C>) -> Self {
575 self.style.border = Some(border);
576 self
577 }
578
579 pub fn labels(mut self, labels: LabelStyle) -> Self {
581 self.style.labels = labels;
582 self
583 }
584
585 pub fn with_title(mut self, title: &str) -> Self {
587 if let Ok(title_string) = heapless::String::try_from(title) {
588 self.config.title = Some(title_string);
589 }
590 self
591 }
592
593 pub fn background_color(mut self, color: C) -> Self {
595 self.config.background_color = Some(color);
596 self
597 }
598}
599
600impl<C: PixelColor> ChartBuilder<C> for PieChartBuilder<C>
601where
602 C: From<embedded_graphics::pixelcolor::Rgb565>,
603{
604 type Chart = PieChart<C>;
605 type Error = ChartError;
606
607 fn build(self) -> Result<Self::Chart, Self::Error> {
608 Ok(PieChart {
609 style: self.style,
610 config: self.config,
611 center: self.center,
612 radius: self.radius,
613 })
614 }
615}
616
617impl<C: PixelColor> Default for PieChartBuilder<C>
618where
619 C: From<embedded_graphics::pixelcolor::Rgb565>,
620{
621 fn default() -> Self {
622 Self::new()
623 }
624}
625
626#[cfg(test)]
627mod tests {
628 use super::*;
629 use embedded_graphics::pixelcolor::Rgb565;
630
631 #[test]
632 fn test_pie_chart_creation() {
633 let chart: PieChart<Rgb565> = PieChart::new(Point::new(100, 100), 50);
634 assert_eq!(chart.center(), Point::new(100, 100));
635 assert_eq!(chart.radius(), 50);
636 assert!(chart.style().donut_inner_radius.is_none());
637 }
638
639 #[test]
640 fn test_pie_chart_builder() {
641 let chart: PieChart<Rgb565> = PieChart::builder()
642 .center(Point::new(150, 150))
643 .radius(60)
644 .colors(&[Rgb565::RED, Rgb565::BLUE, Rgb565::GREEN])
645 .start_angle(90.0)
646 .donut(20)
647 .with_title("Test Pie Chart")
648 .build()
649 .unwrap();
650
651 assert_eq!(chart.center(), Point::new(150, 150));
652 assert_eq!(chart.radius(), 60);
653 assert_eq!(chart.style().colors.len(), 3);
654 assert_eq!(chart.style().start_angle, 90.0);
655 assert_eq!(chart.style().donut_inner_radius, Some(20));
656 assert_eq!(
657 chart.config().title.as_ref().map(|s| s.as_str()),
658 Some("Test Pie Chart")
659 );
660 }
661
662 #[test]
663 fn test_label_style() {
664 let labels = LabelStyle {
665 visible: true,
666 show_percentage: true,
667 show_values: false,
668 offset: 15,
669 };
670
671 assert!(labels.visible);
672 assert!(labels.show_percentage);
673 assert!(!labels.show_values);
674 assert_eq!(labels.offset, 15);
675 }
676
677 #[test]
678 fn test_pie_slice() {
679 let slice = PieSlice {
680 start_angle: 0.0,
681 end_angle: core::f32::consts::PI / 2.0,
682 value: 25.0,
683 percentage: 25.0,
684 };
685
686 assert_eq!(slice.value, 25.0);
687 assert_eq!(slice.percentage, 25.0);
688 assert_eq!(slice.start_angle, 0.0);
689 }
690
691 #[test]
692 fn test_donut_percentage() {
693 let chart: PieChart<Rgb565> = PieChart::builder()
695 .radius(100)
696 .donut_percentage(50)
697 .build()
698 .unwrap();
699
700 assert_eq!(chart.style().donut_inner_radius, Some(50));
701
702 let chart: PieChart<Rgb565> = PieChart::builder()
704 .radius(80)
705 .donut_percentage(25)
706 .build()
707 .unwrap();
708
709 assert_eq!(chart.style().donut_inner_radius, Some(20));
710
711 let chart: PieChart<Rgb565> = PieChart::builder()
713 .radius(60)
714 .donut_percentage(150) .build()
716 .unwrap();
717
718 assert_eq!(chart.style().donut_inner_radius, Some(60));
719 }
720
721 #[test]
722 fn test_donut_convenience_methods() {
723 let chart: PieChart<Rgb565> = PieChart::builder()
725 .radius(100)
726 .balanced_donut()
727 .build()
728 .unwrap();
729
730 assert_eq!(chart.style().donut_inner_radius, Some(50));
731
732 let chart: PieChart<Rgb565> = PieChart::builder().radius(80).thin_donut().build().unwrap();
734
735 assert_eq!(chart.style().donut_inner_radius, Some(20));
736
737 let chart: PieChart<Rgb565> = PieChart::builder()
739 .radius(60)
740 .thick_donut()
741 .build()
742 .unwrap();
743
744 assert_eq!(chart.style().donut_inner_radius, Some(45));
745 }
746
747 #[test]
748 fn test_donut_vs_regular_pie() {
749 let pie: PieChart<Rgb565> = PieChart::builder().radius(50).build().unwrap();
751
752 assert_eq!(pie.style().donut_inner_radius, None);
753
754 let donut: PieChart<Rgb565> = PieChart::builder().radius(50).donut(20).build().unwrap();
756
757 assert_eq!(donut.style().donut_inner_radius, Some(20));
758 }
759}