embedded_charts/chart/bar.rs
1//! Bar chart implementation with no_std compatibility.
2//!
3//! This module provides a comprehensive bar chart implementation optimized for embedded systems.
4//! It supports both vertical and horizontal orientations, multiple styling options, and efficient
5//! rendering while maintaining memory efficiency suitable for resource-constrained environments.
6//!
7//! # Features
8//!
9//! - **Dual orientation**: Support for both vertical and horizontal bar charts
10//! - **Flexible bar width**: Fixed width, proportional width, or automatic sizing
11//! - **Multi-color support**: Up to 16 different bar colors with automatic cycling
12//! - **Customizable spacing**: Configurable spacing between bars
13//! - **Border styling**: Optional borders with customizable styles
14//! - **Stacked bars**: Support for stacked bar charts (feature-gated)
15//! - **Memory efficient**: Static allocation with compile-time bounds
16//!
17//! # Basic Usage
18//!
19//! ```rust
20//! use embedded_charts::prelude::*;
21//! use embedded_graphics::pixelcolor::Rgb565;
22//!
23//! // Create sample data
24//! let mut data: StaticDataSeries<Point2D, 256> = StaticDataSeries::new();
25//! data.push(Point2D::new(0.0, 10.0))?;
26//! data.push(Point2D::new(1.0, 20.0))?;
27//! data.push(Point2D::new(2.0, 15.0))?;
28//!
29//! // Create a basic bar chart
30//! let chart = BarChart::builder()
31//! .orientation(BarOrientation::Vertical)
32//! .bar_width(BarWidth::Fixed(20))
33//! .colors(&[Rgb565::BLUE])
34//! .build()?;
35//!
36//! // Configure the chart
37//! let config: ChartConfig<Rgb565> = ChartConfig::default();
38//! let viewport = Rectangle::new(Point::zero(), Size::new(320, 240));
39//!
40//! // Render to display (display would be provided by your embedded target)
41//! // chart.draw(&data, &config, viewport, &mut display)?;
42//! # Ok::<(), embedded_charts::error::ChartError>(())
43//! ```
44//!
45//! # Advanced Styling
46//!
47//! ```rust
48//! use embedded_charts::prelude::*;
49//! use embedded_graphics::pixelcolor::Rgb565;
50//!
51//! let line_style = LineStyle::solid(Rgb565::BLACK);
52//! let border_style = BorderStyle::new(line_style);
53//! let chart = BarChart::builder()
54//! .orientation(BarOrientation::Horizontal)
55//! .bar_width(BarWidth::Percentage(0.8)) // 80% of available space
56//! .spacing(5)
57//! .with_border(border_style)
58//! .colors(&[Rgb565::BLUE, Rgb565::RED, Rgb565::GREEN])
59//! .build()?;
60//! # Ok::<(), embedded_charts::error::ChartError>(())
61//! ```
62//!
63//! # Multi-Series Bar Charts
64//!
65//! ```rust
66//! use embedded_charts::prelude::*;
67//! use embedded_graphics::pixelcolor::Rgb565;
68//!
69//! // Create multi-series data
70//! let mut multi_series: MultiSeries<Point2D, 8, 256> = MultiSeries::new();
71//! let series1 = data_points![(0.0, 10.0), (1.0, 15.0), (2.0, 12.0)];
72//! let series2 = data_points![(0.0, 8.0), (1.0, 18.0), (2.0, 14.0)];
73//!
74//! multi_series.add_series(series1)?;
75//! multi_series.add_series(series2)?;
76//!
77//! // Create chart with multiple colors
78//! let chart = BarChart::builder()
79//! .bar_width(BarWidth::Auto)
80//! .spacing(2)
81//! .colors(&[Rgb565::BLUE, Rgb565::RED])
82//! .build()?;
83//! # Ok::<(), embedded_charts::error::ChartError>(())
84//! ```
85
86use crate::chart::traits::{Chart, ChartBuilder, ChartConfig};
87use crate::data::{DataBounds, DataPoint, DataSeries};
88use crate::error::{ChartError, ChartResult};
89use crate::style::BorderStyle;
90use embedded_graphics::{
91 draw_target::DrawTarget,
92 prelude::*,
93 primitives::{PrimitiveStyle, Rectangle},
94};
95use heapless::Vec;
96
97/// Bar chart implementation for displaying categorical data.
98///
99/// A bar chart displays data using rectangular bars with lengths proportional to the values
100/// they represent. This implementation supports both vertical and horizontal orientations,
101/// making it suitable for various data visualization needs on embedded displays.
102///
103/// # Features
104///
105/// - Configurable bar orientation (vertical/horizontal)
106/// - Flexible bar width options (fixed, proportional, automatic)
107/// - Multi-color support with automatic color cycling
108/// - Customizable spacing and borders
109/// - Support for stacked bars (feature-gated)
110/// - Memory-efficient static allocation
111///
112/// # Memory Usage
113///
114/// The bar chart uses static allocation with:
115/// - Up to 16 bar colors stored in a heapless vector
116/// - Screen coordinate calculations for bar positioning
117/// - Efficient rendering with minimal temporary storage
118///
119/// # Examples
120///
121/// Basic vertical bar chart:
122/// ```rust
123/// use embedded_charts::prelude::*;
124/// use embedded_graphics::pixelcolor::Rgb565;
125///
126/// let chart: BarChart<Rgb565> = BarChart::new();
127/// let mut data: StaticDataSeries<Point2D, 256> = StaticDataSeries::new();
128/// data.push(Point2D::new(0.0, 10.0))?;
129/// data.push(Point2D::new(1.0, 20.0))?;
130/// # Ok::<(), embedded_charts::error::DataError>(())
131/// ```
132///
133/// Horizontal bar chart with custom styling:
134/// ```rust
135/// use embedded_charts::prelude::*;
136/// use embedded_graphics::pixelcolor::Rgb565;
137///
138/// let chart = BarChart::builder()
139/// .orientation(BarOrientation::Horizontal)
140/// .bar_width(BarWidth::Fixed(25))
141/// .spacing(3)
142/// .colors(&[Rgb565::GREEN])
143/// .build()?;
144/// # Ok::<(), embedded_charts::error::ChartError>(())
145/// ```
146#[derive(Debug, Clone)]
147pub struct BarChart<C: PixelColor> {
148 style: BarChartStyle<C>,
149 config: ChartConfig<C>,
150 orientation: BarOrientation,
151}
152
153/// Style configuration for bar charts.
154///
155/// This structure contains all visual styling options for bar charts,
156/// including colors, dimensions, spacing, and border styles.
157///
158/// # Examples
159///
160/// ```rust
161/// use embedded_charts::prelude::*;
162/// use embedded_graphics::pixelcolor::Rgb565;
163///
164/// let mut colors = heapless::Vec::new();
165/// colors.push(Rgb565::BLUE).unwrap();
166/// colors.push(Rgb565::RED).unwrap();
167///
168/// let style = BarChartStyle {
169/// bar_colors: colors,
170/// bar_width: BarWidth::Fixed(20),
171/// spacing: 5,
172/// border: None,
173/// stacked: false,
174/// };
175/// ```
176#[derive(Debug, Clone)]
177pub struct BarChartStyle<C: PixelColor> {
178 /// Colors for the bars.
179 ///
180 /// The chart cycles through these colors for multiple data series.
181 /// Maximum of 16 colors supported for memory efficiency.
182 pub bar_colors: Vec<C, 16>,
183 /// Width configuration for bars.
184 ///
185 /// Determines how bar widths are calculated based on available space.
186 pub bar_width: BarWidth,
187 /// Spacing between bars in pixels.
188 ///
189 /// This spacing is applied between individual bars and between groups
190 /// in multi-series charts.
191 pub spacing: u32,
192 /// Optional border style for bars.
193 ///
194 /// When `Some`, draws borders around each bar with the specified style.
195 /// When `None`, bars are drawn without borders.
196 pub border: Option<BorderStyle<C>>,
197 /// Whether bars should be stacked.
198 ///
199 /// When `true`, multiple data series are stacked on top of each other.
200 /// When `false`, series are displayed side by side.
201 pub stacked: bool,
202}
203
204/// Bar orientation options.
205///
206/// Determines whether bars extend vertically (from bottom to top) or
207/// horizontally (from left to right).
208///
209/// # Examples
210///
211/// ```rust
212/// use embedded_charts::prelude::*;
213///
214/// // Vertical bars (traditional bar chart)
215/// let vertical = BarOrientation::Vertical;
216///
217/// // Horizontal bars (useful for long category names)
218/// let horizontal = BarOrientation::Horizontal;
219/// ```
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub enum BarOrientation {
222 /// Vertical bars extending from bottom to top.
223 ///
224 /// This is the traditional bar chart orientation where bars grow upward
225 /// from a baseline. Best for time-series data or when category names are short.
226 Vertical,
227 /// Horizontal bars extending from left to right.
228 ///
229 /// Useful when category names are long or when you want to emphasize
230 /// the comparison between values. The baseline is on the left side.
231 Horizontal,
232}
233
234/// Bar width configuration options.
235///
236/// Determines how the width of bars is calculated based on the available
237/// chart space and the number of data points.
238///
239/// # Examples
240///
241/// ```rust
242/// use embedded_charts::prelude::*;
243///
244/// // Fixed width bars
245/// let fixed = BarWidth::Fixed(20); // 20 pixels wide
246///
247/// // Percentage width (80% of available space per bar)
248/// let percentage = BarWidth::Percentage(0.8);
249///
250/// // Automatic width calculation
251/// let auto = BarWidth::Auto;
252/// ```
253#[derive(Debug, Clone, Copy, PartialEq)]
254pub enum BarWidth {
255 /// Fixed width in pixels.
256 ///
257 /// Each bar will have exactly the specified width regardless of the
258 /// available space or number of data points. This provides consistent
259 /// bar appearance but may cause bars to overlap or leave unused space.
260 ///
261 /// # Examples
262 ///
263 /// ```rust
264 /// use embedded_charts::prelude::*;
265 ///
266 /// let width = BarWidth::Fixed(25); // 25 pixels wide
267 /// ```
268 Fixed(u32),
269 /// Percentage of available space per bar (0.0 to 1.0).
270 ///
271 /// Each bar will occupy the specified percentage of the space allocated
272 /// to it. For example, 0.8 means each bar uses 80% of its allocated space,
273 /// leaving 20% for spacing.
274 ///
275 /// # Examples
276 ///
277 /// ```rust
278 /// use embedded_charts::prelude::*;
279 ///
280 /// let width = BarWidth::Percentage(0.7); // 70% of allocated space
281 /// ```
282 Percentage(f32),
283 /// Automatic width calculation based on available space and data count.
284 ///
285 /// The chart automatically calculates optimal bar width based on the
286 /// viewport size, number of data points, and spacing requirements.
287 /// This provides the best balance between bar visibility and spacing.
288 ///
289 /// # Examples
290 ///
291 /// ```rust
292 /// use embedded_charts::prelude::*;
293 ///
294 /// let width = BarWidth::Auto; // Automatic calculation
295 /// ```
296 Auto,
297}
298
299impl<C: PixelColor> BarChart<C>
300where
301 C: From<embedded_graphics::pixelcolor::Rgb565>,
302{
303 /// Create a new bar chart with default styling.
304 ///
305 /// This creates a bar chart with:
306 /// - Vertical orientation
307 /// - Single blue bar color
308 /// - Automatic bar width
309 /// - 5-pixel spacing between bars
310 /// - No borders
311 /// - Default margins (10 pixels on all sides)
312 ///
313 /// # Examples
314 ///
315 /// ```rust
316 /// use embedded_charts::prelude::*;
317 /// use embedded_graphics::pixelcolor::Rgb565;
318 ///
319 /// let chart: BarChart<Rgb565> = BarChart::new();
320 /// ```
321 pub fn new() -> Self {
322 Self {
323 style: BarChartStyle::default(),
324 config: ChartConfig::default(),
325 orientation: BarOrientation::Vertical,
326 }
327 }
328
329 /// Create a builder for configuring the bar chart.
330 ///
331 /// The builder pattern provides a fluent interface for configuring
332 /// all aspects of the bar chart before creation.
333 ///
334 /// # Examples
335 ///
336 /// ```rust
337 /// use embedded_charts::prelude::*;
338 /// use embedded_graphics::pixelcolor::Rgb565;
339 ///
340 /// let chart = BarChart::builder()
341 /// .orientation(BarOrientation::Horizontal)
342 /// .bar_width(BarWidth::Fixed(30))
343 /// .spacing(8)
344 /// .colors(&[Rgb565::GREEN])
345 /// .build()?;
346 /// # Ok::<(), embedded_charts::error::ChartError>(())
347 /// ```
348 pub fn builder() -> BarChartBuilder<C> {
349 BarChartBuilder::new()
350 }
351
352 /// Set the bar chart style configuration.
353 ///
354 /// This replaces the entire style configuration with the provided one.
355 /// Use the builder pattern for more granular control.
356 ///
357 /// # Arguments
358 ///
359 /// * `style` - The new bar chart style configuration
360 ///
361 /// # Examples
362 ///
363 /// ```rust
364 /// use embedded_charts::prelude::*;
365 /// use embedded_graphics::pixelcolor::Rgb565;
366 ///
367 /// let mut chart: BarChart<Rgb565> = BarChart::new();
368 /// let mut colors = heapless::Vec::new();
369 /// colors.push(Rgb565::BLUE).unwrap();
370 /// colors.push(Rgb565::RED).unwrap();
371 ///
372 /// let style = BarChartStyle {
373 /// bar_colors: colors,
374 /// bar_width: BarWidth::Fixed(25),
375 /// spacing: 3,
376 /// border: None,
377 /// stacked: false,
378 /// };
379 /// chart.set_style(style);
380 /// ```
381 pub fn set_style(&mut self, style: BarChartStyle<C>) {
382 self.style = style;
383 }
384
385 /// Get the current bar chart style configuration.
386 ///
387 /// Returns a reference to the current style configuration,
388 /// allowing inspection of current settings.
389 ///
390 /// # Returns
391 ///
392 /// A reference to the current `BarChartStyle`
393 pub fn style(&self) -> &BarChartStyle<C> {
394 &self.style
395 }
396
397 /// Set the chart configuration.
398 ///
399 /// This includes general chart settings like title, background color,
400 /// margins, and grid visibility.
401 ///
402 /// # Arguments
403 ///
404 /// * `config` - The new chart configuration
405 pub fn set_config(&mut self, config: ChartConfig<C>) {
406 self.config = config;
407 }
408
409 /// Get the chart configuration
410 pub fn config(&self) -> &ChartConfig<C> {
411 &self.config
412 }
413
414 /// Set the bar orientation
415 pub fn set_orientation(&mut self, orientation: BarOrientation) {
416 self.orientation = orientation;
417 }
418
419 /// Get the bar orientation
420 pub fn orientation(&self) -> BarOrientation {
421 self.orientation
422 }
423
424 /// Calculate bar dimensions and positions
425 fn calculate_bar_layout(
426 &self,
427 data: &crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>,
428 data_bounds: &DataBounds<f32, f32>,
429 viewport: Rectangle,
430 ) -> ChartResult<Vec<Rectangle, 256>> {
431 let mut bars = Vec::new();
432 let draw_area = self.config.margins.apply_to(viewport);
433
434 let data_count = data.len();
435 if data_count == 0 {
436 return Ok(bars);
437 }
438
439 // Calculate bar width
440 let bar_width = match self.style.bar_width {
441 BarWidth::Fixed(width) => width,
442 BarWidth::Percentage(pct) => {
443 let available_width = match self.orientation {
444 BarOrientation::Vertical => draw_area.size.width,
445 BarOrientation::Horizontal => draw_area.size.height,
446 };
447 (available_width as f32 * pct.clamp(0.0, 1.0)) as u32
448 }
449 BarWidth::Auto => {
450 let available_width = match self.orientation {
451 BarOrientation::Vertical => draw_area.size.width,
452 BarOrientation::Horizontal => draw_area.size.height,
453 };
454 let total_spacing = self.style.spacing * (data_count as u32).saturating_sub(1);
455 let calculated_width =
456 (available_width.saturating_sub(total_spacing)) / data_count as u32;
457 // Ensure minimum bar width for visibility
458 calculated_width.max(5)
459 }
460 };
461
462 // Calculate positions and sizes for each bar
463 let mut current_pos = 0;
464 for point in data.iter() {
465 let bar_rect = match self.orientation {
466 BarOrientation::Vertical => {
467 let x = draw_area.top_left.x + current_pos as i32;
468 let data_y: f32 = point.y();
469 let min_y: f32 = data_bounds.min_y;
470 let max_y: f32 = data_bounds.max_y;
471
472 // Normalize Y value (0.0 to 1.0)
473 let norm_y = if max_y > min_y {
474 (data_y - min_y) / (max_y - min_y)
475 } else {
476 0.5
477 };
478
479 // Ensure minimum bar height for visibility
480 let bar_height = ((norm_y * draw_area.size.height as f32) as u32).max(1);
481 let y = draw_area.top_left.y + draw_area.size.height as i32 - bar_height as i32;
482
483 Rectangle::new(Point::new(x, y), Size::new(bar_width, bar_height))
484 }
485 BarOrientation::Horizontal => {
486 let y = draw_area.top_left.y + current_pos as i32;
487 let data_y: f32 = point.y();
488 let min_y: f32 = data_bounds.min_y;
489 let max_y: f32 = data_bounds.max_y;
490
491 // Normalize Y value (0.0 to 1.0)
492 let norm_y = if max_y > min_y {
493 (data_y - min_y) / (max_y - min_y)
494 } else {
495 0.5
496 };
497
498 // Ensure minimum bar width for visibility
499 let bar_width_horizontal =
500 ((norm_y * draw_area.size.width as f32) as u32).max(1);
501 let x = draw_area.top_left.x;
502
503 Rectangle::new(Point::new(x, y), Size::new(bar_width_horizontal, bar_width))
504 }
505 };
506
507 bars.push(bar_rect).map_err(|_| ChartError::MemoryFull)?;
508 current_pos += bar_width + self.style.spacing;
509 }
510
511 Ok(bars)
512 }
513
514 /// Draw a single bar
515 fn draw_bar<D>(
516 &self,
517 bar_rect: Rectangle,
518 color_index: usize,
519 target: &mut D,
520 ) -> ChartResult<()>
521 where
522 D: DrawTarget<Color = C>,
523 {
524 // Get bar color (cycle through available colors)
525 let bar_color = if !self.style.bar_colors.is_empty() {
526 self.style.bar_colors[color_index % self.style.bar_colors.len()]
527 } else {
528 return Err(ChartError::InvalidConfiguration);
529 };
530
531 // Draw filled bar directly
532 bar_rect
533 .into_styled(PrimitiveStyle::with_fill(bar_color))
534 .draw(target)
535 .map_err(|_| ChartError::RenderingError)?;
536
537 // Draw border if specified
538 if let Some(border) = &self.style.border {
539 if border.visible {
540 bar_rect
541 .into_styled(PrimitiveStyle::with_stroke(
542 border.line.color,
543 border.line.width,
544 ))
545 .draw(target)
546 .map_err(|_| ChartError::RenderingError)?;
547 }
548 }
549
550 Ok(())
551 }
552}
553
554impl<C: PixelColor> Default for BarChart<C>
555where
556 C: From<embedded_graphics::pixelcolor::Rgb565>,
557{
558 fn default() -> Self {
559 Self::new()
560 }
561}
562
563impl<C: PixelColor> Chart<C> for BarChart<C>
564where
565 C: From<embedded_graphics::pixelcolor::Rgb565>,
566{
567 type Data = crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>;
568 type Config = ChartConfig<C>;
569
570 fn draw<D>(
571 &self,
572 data: &Self::Data,
573 config: &Self::Config,
574 viewport: Rectangle,
575 target: &mut D,
576 ) -> ChartResult<()>
577 where
578 D: DrawTarget<Color = C>,
579 Self::Data: DataSeries,
580 <Self::Data as DataSeries>::Item: DataPoint,
581 <<Self::Data as DataSeries>::Item as DataPoint>::X: Into<f32> + Copy + PartialOrd,
582 <<Self::Data as DataSeries>::Item as DataPoint>::Y: Into<f32> + Copy + PartialOrd,
583 {
584 if data.is_empty() {
585 return Err(ChartError::InsufficientData);
586 }
587
588 // Draw background if specified
589 if let Some(bg_color) = config.background_color {
590 Rectangle::new(viewport.top_left, viewport.size)
591 .into_styled(PrimitiveStyle::with_fill(bg_color))
592 .draw(target)
593 .map_err(|_| ChartError::RenderingError)?;
594 }
595
596 // Calculate data bounds
597 let data_bounds = data.bounds()?;
598
599 // Calculate bar layout
600 let bars = self.calculate_bar_layout(data, &data_bounds, viewport)?;
601
602 // Draw each bar
603 for (index, bar_rect) in bars.iter().enumerate() {
604 self.draw_bar(*bar_rect, index, target)?;
605 }
606
607 Ok(())
608 }
609}
610
611impl<C: PixelColor> Default for BarChartStyle<C>
612where
613 C: From<embedded_graphics::pixelcolor::Rgb565>,
614{
615 fn default() -> Self {
616 let mut colors = Vec::new();
617 let _ = colors.push(embedded_graphics::pixelcolor::Rgb565::BLUE.into());
618 let _ = colors.push(embedded_graphics::pixelcolor::Rgb565::RED.into());
619 let _ = colors.push(embedded_graphics::pixelcolor::Rgb565::GREEN.into());
620 let _ = colors.push(embedded_graphics::pixelcolor::Rgb565::YELLOW.into());
621
622 Self {
623 bar_colors: colors,
624 bar_width: BarWidth::Auto,
625 spacing: 2,
626 border: None,
627 stacked: false,
628 }
629 }
630}
631
632/// Builder for bar charts
633#[derive(Debug)]
634pub struct BarChartBuilder<C: PixelColor> {
635 style: BarChartStyle<C>,
636 config: ChartConfig<C>,
637 orientation: BarOrientation,
638}
639
640impl<C: PixelColor> BarChartBuilder<C>
641where
642 C: From<embedded_graphics::pixelcolor::Rgb565>,
643{
644 /// Create a new bar chart builder
645 pub fn new() -> Self {
646 Self {
647 style: BarChartStyle::default(),
648 config: ChartConfig::default(),
649 orientation: BarOrientation::Vertical,
650 }
651 }
652
653 /// Set the bar orientation
654 pub fn orientation(mut self, orientation: BarOrientation) -> Self {
655 self.orientation = orientation;
656 self
657 }
658
659 /// Set bar colors
660 pub fn colors(mut self, colors: &[C]) -> Self {
661 self.style.bar_colors.clear();
662 for &color in colors {
663 if self.style.bar_colors.push(color).is_err() {
664 break; // Reached capacity
665 }
666 }
667 self
668 }
669
670 /// Set bar width
671 pub fn bar_width(mut self, width: BarWidth) -> Self {
672 self.style.bar_width = width;
673 self
674 }
675
676 /// Set spacing between bars
677 pub fn spacing(mut self, spacing: u32) -> Self {
678 self.style.spacing = spacing;
679 self
680 }
681
682 /// Add a border to bars
683 pub fn with_border(mut self, border: BorderStyle<C>) -> Self {
684 self.style.border = Some(border);
685 self
686 }
687
688 /// Enable stacked bars
689 pub fn stacked(mut self, stacked: bool) -> Self {
690 self.style.stacked = stacked;
691 self
692 }
693
694 /// Set the chart title
695 pub fn with_title(mut self, title: &str) -> Self {
696 if let Ok(title_string) = heapless::String::try_from(title) {
697 self.config.title = Some(title_string);
698 }
699 self
700 }
701
702 /// Set the background color
703 pub fn background_color(mut self, color: C) -> Self {
704 self.config.background_color = Some(color);
705 self
706 }
707}
708
709impl<C: PixelColor> ChartBuilder<C> for BarChartBuilder<C>
710where
711 C: From<embedded_graphics::pixelcolor::Rgb565>,
712{
713 type Chart = BarChart<C>;
714 type Error = ChartError;
715
716 fn build(self) -> Result<Self::Chart, Self::Error> {
717 Ok(BarChart {
718 style: self.style,
719 config: self.config,
720 orientation: self.orientation,
721 })
722 }
723}
724
725impl<C: PixelColor> Default for BarChartBuilder<C>
726where
727 C: From<embedded_graphics::pixelcolor::Rgb565>,
728{
729 fn default() -> Self {
730 Self::new()
731 }
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737 use embedded_graphics::pixelcolor::Rgb565;
738
739 #[test]
740 fn test_bar_chart_creation() {
741 let chart: BarChart<Rgb565> = BarChart::new();
742 assert_eq!(chart.orientation(), BarOrientation::Vertical);
743 assert!(!chart.style().stacked);
744 }
745
746 #[test]
747 fn test_bar_chart_builder() {
748 let chart: BarChart<Rgb565> = BarChart::builder()
749 .orientation(BarOrientation::Horizontal)
750 .colors(&[Rgb565::RED, Rgb565::BLUE])
751 .bar_width(BarWidth::Fixed(30))
752 .spacing(5)
753 .with_title("Test Bar Chart")
754 .build()
755 .unwrap();
756
757 assert_eq!(chart.orientation(), BarOrientation::Horizontal);
758 assert_eq!(chart.style().bar_colors.len(), 2);
759 assert_eq!(chart.style().spacing, 5);
760 assert_eq!(
761 chart.config().title.as_ref().map(|s| s.as_str()),
762 Some("Test Bar Chart")
763 );
764 }
765
766 #[test]
767 fn test_bar_width_types() {
768 assert_eq!(BarWidth::Fixed(20), BarWidth::Fixed(20));
769
770 let percentage = BarWidth::Percentage(0.8);
771 if let BarWidth::Percentage(pct) = percentage {
772 assert_eq!(pct, 0.8);
773 }
774
775 assert_eq!(BarWidth::Auto, BarWidth::Auto);
776 }
777}
778
779/// Animated bar chart that extends BarChart with animation capabilities
780#[cfg(feature = "animations")]
781#[derive(Debug, Clone)]
782pub struct AnimatedBarChart<C: PixelColor> {
783 /// Base bar chart
784 base_chart: BarChart<C>,
785 /// Current animated data (interpolated values)
786 current_data: Option<crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>>,
787}
788
789#[cfg(feature = "animations")]
790impl<C: PixelColor> AnimatedBarChart<C>
791where
792 C: From<embedded_graphics::pixelcolor::Rgb565>,
793{
794 /// Create a new animated bar chart
795 pub fn new() -> Self {
796 Self {
797 base_chart: BarChart::new(),
798 current_data: None,
799 }
800 }
801
802 /// Create a builder for configuring the animated bar chart
803 pub fn builder() -> AnimatedBarChartBuilder<C> {
804 AnimatedBarChartBuilder::new()
805 }
806
807 /// Set the bar chart style
808 pub fn set_style(&mut self, style: BarChartStyle<C>) {
809 self.base_chart.set_style(style);
810 }
811
812 /// Get the current bar chart style
813 pub fn style(&self) -> &BarChartStyle<C> {
814 self.base_chart.style()
815 }
816
817 /// Set the chart configuration
818 pub fn set_config(&mut self, config: ChartConfig<C>) {
819 self.base_chart.set_config(config);
820 }
821
822 /// Get the chart configuration
823 pub fn config(&self) -> &ChartConfig<C> {
824 self.base_chart.config()
825 }
826
827 /// Set the bar orientation
828 pub fn set_orientation(&mut self, orientation: BarOrientation) {
829 self.base_chart.set_orientation(orientation);
830 }
831
832 /// Get the bar orientation
833 pub fn orientation(&self) -> BarOrientation {
834 self.base_chart.orientation()
835 }
836
837 /// Get the current animated data or fallback to empty series
838 fn get_render_data(
839 &self,
840 ) -> crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256> {
841 self.current_data.clone().unwrap_or_default()
842 }
843
844 /// Interpolate between two data series based on animation progress
845 #[allow(dead_code)]
846 fn interpolate_data(
847 &self,
848 from_data: &crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>,
849 to_data: &crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>,
850 progress: f32,
851 ) -> ChartResult<crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>> {
852 let mut result = crate::data::series::StaticDataSeries::new();
853
854 // Handle different data sizes by taking the minimum
855 let min_len = from_data.len().min(to_data.len());
856
857 for i in 0..min_len {
858 if let (Some(from_point), Some(to_point)) = (from_data.get(i), to_data.get(i)) {
859 let interpolated_x = from_point.x() + (to_point.x() - from_point.x()) * progress;
860 let interpolated_y = from_point.y() + (to_point.y() - from_point.y()) * progress;
861
862 result
863 .push(crate::data::point::Point2D::new(
864 interpolated_x,
865 interpolated_y,
866 ))
867 .map_err(|_| crate::error::ChartError::MemoryFull)?;
868 }
869 }
870
871 Ok(result)
872 }
873}
874
875#[cfg(feature = "animations")]
876impl<C: PixelColor> Default for AnimatedBarChart<C>
877where
878 C: From<embedded_graphics::pixelcolor::Rgb565>,
879{
880 fn default() -> Self {
881 Self::new()
882 }
883}
884
885#[cfg(feature = "animations")]
886impl<C: PixelColor> Chart<C> for AnimatedBarChart<C>
887where
888 C: From<embedded_graphics::pixelcolor::Rgb565>,
889{
890 type Data = crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>;
891 type Config = ChartConfig<C>;
892
893 fn draw<D>(
894 &self,
895 data: &Self::Data,
896 config: &Self::Config,
897 viewport: Rectangle,
898 target: &mut D,
899 ) -> ChartResult<()>
900 where
901 D: DrawTarget<Color = C>,
902 Self::Data: crate::data::DataSeries,
903 <Self::Data as crate::data::DataSeries>::Item: crate::data::DataPoint,
904 <<Self::Data as crate::data::DataSeries>::Item as crate::data::DataPoint>::X:
905 Into<f32> + Copy + PartialOrd,
906 <<Self::Data as crate::data::DataSeries>::Item as crate::data::DataPoint>::Y:
907 Into<f32> + Copy + PartialOrd,
908 {
909 // Use animated data if available, otherwise use provided data
910 if self.current_data.is_some() {
911 let render_data = self.get_render_data();
912 self.base_chart.draw(&render_data, config, viewport, target)
913 } else {
914 self.base_chart.draw(data, config, viewport, target)
915 }
916 }
917}
918
919#[cfg(feature = "animations")]
920impl<C: PixelColor> crate::chart::traits::AnimatedChart<C> for AnimatedBarChart<C>
921where
922 C: From<embedded_graphics::pixelcolor::Rgb565>,
923{
924 type AnimatedData = crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>;
925
926 fn draw_animated<D>(
927 &self,
928 data: &Self::Data,
929 config: &Self::Config,
930 viewport: embedded_graphics::primitives::Rectangle,
931 target: &mut D,
932 _progress: crate::animation::Progress,
933 ) -> ChartResult<()>
934 where
935 D: embedded_graphics::draw_target::DrawTarget<Color = C>,
936 {
937 // Use the provided data which should already be interpolated by the caller
938 self.base_chart.draw(data, config, viewport, target)
939 }
940
941 fn create_transition_animator(
942 &self,
943 from_data: Self::AnimatedData,
944 to_data: Self::AnimatedData,
945 easing: crate::animation::EasingFunction,
946 ) -> crate::animation::ChartAnimator<Self::AnimatedData> {
947 crate::animation::ChartAnimator::new(from_data, to_data, easing)
948 }
949
950 fn extract_animated_data(&self, data: &Self::Data) -> ChartResult<Self::AnimatedData> {
951 // Clone the data series for animation
952 Ok(data.clone())
953 }
954}
955
956/// Builder for animated bar charts
957#[cfg(feature = "animations")]
958#[derive(Debug)]
959pub struct AnimatedBarChartBuilder<C: PixelColor> {
960 base_builder: BarChartBuilder<C>,
961 frame_rate: u32,
962}
963
964#[cfg(feature = "animations")]
965impl<C: PixelColor> AnimatedBarChartBuilder<C>
966where
967 C: From<embedded_graphics::pixelcolor::Rgb565>,
968{
969 /// Create a new animated bar chart builder
970 pub fn new() -> Self {
971 Self {
972 base_builder: BarChartBuilder::new(),
973 frame_rate: 60,
974 }
975 }
976
977 /// Set the target frame rate
978 pub fn frame_rate(mut self, fps: u32) -> Self {
979 self.frame_rate = fps.clamp(1, 120);
980 self
981 }
982
983 /// Set the bar orientation
984 pub fn orientation(mut self, orientation: BarOrientation) -> Self {
985 self.base_builder = self.base_builder.orientation(orientation);
986 self
987 }
988
989 /// Set bar colors
990 pub fn colors(mut self, colors: &[C]) -> Self {
991 self.base_builder = self.base_builder.colors(colors);
992 self
993 }
994
995 /// Set bar width
996 pub fn bar_width(mut self, width: BarWidth) -> Self {
997 self.base_builder = self.base_builder.bar_width(width);
998 self
999 }
1000
1001 /// Set spacing between bars
1002 pub fn spacing(mut self, spacing: u32) -> Self {
1003 self.base_builder = self.base_builder.spacing(spacing);
1004 self
1005 }
1006
1007 /// Add a border to bars
1008 pub fn with_border(mut self, border: BorderStyle<C>) -> Self {
1009 self.base_builder = self.base_builder.with_border(border);
1010 self
1011 }
1012
1013 /// Enable stacked bars
1014 pub fn stacked(mut self, stacked: bool) -> Self {
1015 self.base_builder = self.base_builder.stacked(stacked);
1016 self
1017 }
1018
1019 /// Set the chart title
1020 pub fn with_title(mut self, title: &str) -> Self {
1021 self.base_builder = self.base_builder.with_title(title);
1022 self
1023 }
1024
1025 /// Set the background color
1026 pub fn background_color(mut self, color: C) -> Self {
1027 self.base_builder = self.base_builder.background_color(color);
1028 self
1029 }
1030
1031 /// Build the animated bar chart
1032 pub fn build(self) -> ChartResult<AnimatedBarChart<C>> {
1033 let base_chart = self.base_builder.build()?;
1034
1035 Ok(AnimatedBarChart {
1036 base_chart,
1037 current_data: None,
1038 })
1039 }
1040}
1041
1042#[cfg(feature = "animations")]
1043impl<C: PixelColor> Default for AnimatedBarChartBuilder<C>
1044where
1045 C: From<embedded_graphics::pixelcolor::Rgb565>,
1046{
1047 fn default() -> Self {
1048 Self::new()
1049 }
1050}