use crate::chart::traits::{Chart, ChartBuilder, ChartConfig};
use crate::data::{DataBounds, DataPoint, DataSeries};
use crate::error::{ChartError, ChartResult};
use crate::style::BorderStyle;
use embedded_graphics::{
draw_target::DrawTarget,
prelude::*,
primitives::{PrimitiveStyle, Rectangle},
};
use heapless::Vec;
#[derive(Debug, Clone)]
pub struct BarChart<C: PixelColor> {
style: BarChartStyle<C>,
config: ChartConfig<C>,
orientation: BarOrientation,
}
#[derive(Debug, Clone)]
pub struct BarChartStyle<C: PixelColor> {
pub bar_colors: Vec<C, 16>,
pub bar_width: BarWidth,
pub spacing: u32,
pub border: Option<BorderStyle<C>>,
pub stacked: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BarOrientation {
Vertical,
Horizontal,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BarWidth {
Fixed(u32),
Percentage(f32),
Auto,
}
impl<C: PixelColor> BarChart<C>
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
pub fn new() -> Self {
Self {
style: BarChartStyle::default(),
config: ChartConfig::default(),
orientation: BarOrientation::Vertical,
}
}
pub fn builder() -> BarChartBuilder<C> {
BarChartBuilder::new()
}
pub fn set_style(&mut self, style: BarChartStyle<C>) {
self.style = style;
}
pub fn style(&self) -> &BarChartStyle<C> {
&self.style
}
pub fn set_config(&mut self, config: ChartConfig<C>) {
self.config = config;
}
pub fn config(&self) -> &ChartConfig<C> {
&self.config
}
pub fn set_orientation(&mut self, orientation: BarOrientation) {
self.orientation = orientation;
}
pub fn orientation(&self) -> BarOrientation {
self.orientation
}
fn calculate_bar_layout(
&self,
data: &crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>,
data_bounds: &DataBounds<f32, f32>,
viewport: Rectangle,
) -> ChartResult<Vec<Rectangle, 256>> {
let mut bars = Vec::new();
let draw_area = self.config.margins.apply_to(viewport);
let data_count = data.len();
if data_count == 0 {
return Ok(bars);
}
let bar_width = match self.style.bar_width {
BarWidth::Fixed(width) => width,
BarWidth::Percentage(pct) => {
let available_width = match self.orientation {
BarOrientation::Vertical => draw_area.size.width,
BarOrientation::Horizontal => draw_area.size.height,
};
(available_width as f32 * pct.clamp(0.0, 1.0)) as u32
}
BarWidth::Auto => {
let available_width = match self.orientation {
BarOrientation::Vertical => draw_area.size.width,
BarOrientation::Horizontal => draw_area.size.height,
};
let total_spacing = self.style.spacing * (data_count as u32).saturating_sub(1);
let calculated_width =
(available_width.saturating_sub(total_spacing)) / data_count as u32;
calculated_width.max(5)
}
};
let mut current_pos = 0;
for point in data.iter() {
let bar_rect = match self.orientation {
BarOrientation::Vertical => {
let x = draw_area.top_left.x + current_pos as i32;
let data_y: f32 = point.y();
let min_y: f32 = data_bounds.min_y;
let max_y: f32 = data_bounds.max_y;
let norm_y = if max_y > min_y {
(data_y - min_y) / (max_y - min_y)
} else {
0.5
};
let bar_height = ((norm_y * draw_area.size.height as f32) as u32).max(1);
let y = draw_area.top_left.y + draw_area.size.height as i32 - bar_height as i32;
Rectangle::new(Point::new(x, y), Size::new(bar_width, bar_height))
}
BarOrientation::Horizontal => {
let y = draw_area.top_left.y + current_pos as i32;
let data_y: f32 = point.y();
let min_y: f32 = data_bounds.min_y;
let max_y: f32 = data_bounds.max_y;
let norm_y = if max_y > min_y {
(data_y - min_y) / (max_y - min_y)
} else {
0.5
};
let bar_width_horizontal =
((norm_y * draw_area.size.width as f32) as u32).max(1);
let x = draw_area.top_left.x;
Rectangle::new(Point::new(x, y), Size::new(bar_width_horizontal, bar_width))
}
};
bars.push(bar_rect).map_err(|_| ChartError::MemoryFull)?;
current_pos += bar_width + self.style.spacing;
}
Ok(bars)
}
fn draw_bar<D>(
&self,
bar_rect: Rectangle,
color_index: usize,
target: &mut D,
) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
{
let bar_color = if !self.style.bar_colors.is_empty() {
self.style.bar_colors[color_index % self.style.bar_colors.len()]
} else {
return Err(ChartError::InvalidConfiguration);
};
bar_rect
.into_styled(PrimitiveStyle::with_fill(bar_color))
.draw(target)
.map_err(|_| ChartError::RenderingError)?;
if let Some(border) = &self.style.border {
if border.visible {
bar_rect
.into_styled(PrimitiveStyle::with_stroke(
border.line.color,
border.line.width,
))
.draw(target)
.map_err(|_| ChartError::RenderingError)?;
}
}
Ok(())
}
}
impl<C: PixelColor> Default for BarChart<C>
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
fn default() -> Self {
Self::new()
}
}
impl<C: PixelColor> Chart<C> for BarChart<C>
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
type Data = crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>;
type Config = ChartConfig<C>;
fn draw<D>(
&self,
data: &Self::Data,
config: &Self::Config,
viewport: Rectangle,
target: &mut D,
) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
Self::Data: DataSeries,
<Self::Data as DataSeries>::Item: DataPoint,
<<Self::Data as DataSeries>::Item as DataPoint>::X: Into<f32> + Copy + PartialOrd,
<<Self::Data as DataSeries>::Item as DataPoint>::Y: Into<f32> + Copy + PartialOrd,
{
if data.is_empty() {
return Err(ChartError::InsufficientData);
}
if let Some(bg_color) = config.background_color {
Rectangle::new(viewport.top_left, viewport.size)
.into_styled(PrimitiveStyle::with_fill(bg_color))
.draw(target)
.map_err(|_| ChartError::RenderingError)?;
}
let data_bounds = data.bounds()?;
let bars = self.calculate_bar_layout(data, &data_bounds, viewport)?;
for (index, bar_rect) in bars.iter().enumerate() {
self.draw_bar(*bar_rect, index, target)?;
}
Ok(())
}
}
impl<C: PixelColor> Default for BarChartStyle<C>
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
fn default() -> Self {
let mut colors = Vec::new();
let _ = colors.push(embedded_graphics::pixelcolor::Rgb565::BLUE.into());
let _ = colors.push(embedded_graphics::pixelcolor::Rgb565::RED.into());
let _ = colors.push(embedded_graphics::pixelcolor::Rgb565::GREEN.into());
let _ = colors.push(embedded_graphics::pixelcolor::Rgb565::YELLOW.into());
Self {
bar_colors: colors,
bar_width: BarWidth::Auto,
spacing: 2,
border: None,
stacked: false,
}
}
}
#[derive(Debug)]
pub struct BarChartBuilder<C: PixelColor> {
style: BarChartStyle<C>,
config: ChartConfig<C>,
orientation: BarOrientation,
}
impl<C: PixelColor> BarChartBuilder<C>
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
pub fn new() -> Self {
Self {
style: BarChartStyle::default(),
config: ChartConfig::default(),
orientation: BarOrientation::Vertical,
}
}
pub fn orientation(mut self, orientation: BarOrientation) -> Self {
self.orientation = orientation;
self
}
pub fn colors(mut self, colors: &[C]) -> Self {
self.style.bar_colors.clear();
for &color in colors {
if self.style.bar_colors.push(color).is_err() {
break; }
}
self
}
pub fn bar_width(mut self, width: BarWidth) -> Self {
self.style.bar_width = width;
self
}
pub fn spacing(mut self, spacing: u32) -> Self {
self.style.spacing = spacing;
self
}
pub fn with_border(mut self, border: BorderStyle<C>) -> Self {
self.style.border = Some(border);
self
}
pub fn stacked(mut self, stacked: bool) -> Self {
self.style.stacked = stacked;
self
}
pub fn with_title(mut self, title: &str) -> Self {
if let Ok(title_string) = heapless::String::try_from(title) {
self.config.title = Some(title_string);
}
self
}
pub fn background_color(mut self, color: C) -> Self {
self.config.background_color = Some(color);
self
}
}
impl<C: PixelColor> ChartBuilder<C> for BarChartBuilder<C>
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
type Chart = BarChart<C>;
type Error = ChartError;
fn build(self) -> Result<Self::Chart, Self::Error> {
Ok(BarChart {
style: self.style,
config: self.config,
orientation: self.orientation,
})
}
}
impl<C: PixelColor> Default for BarChartBuilder<C>
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use embedded_graphics::pixelcolor::Rgb565;
#[test]
fn test_bar_chart_creation() {
let chart: BarChart<Rgb565> = BarChart::new();
assert_eq!(chart.orientation(), BarOrientation::Vertical);
assert!(!chart.style().stacked);
}
#[test]
fn test_bar_chart_builder() {
let chart: BarChart<Rgb565> = BarChart::builder()
.orientation(BarOrientation::Horizontal)
.colors(&[Rgb565::RED, Rgb565::BLUE])
.bar_width(BarWidth::Fixed(30))
.spacing(5)
.with_title("Test Bar Chart")
.build()
.unwrap();
assert_eq!(chart.orientation(), BarOrientation::Horizontal);
assert_eq!(chart.style().bar_colors.len(), 2);
assert_eq!(chart.style().spacing, 5);
assert_eq!(
chart.config().title.as_ref().map(|s| s.as_str()),
Some("Test Bar Chart")
);
}
#[test]
fn test_bar_width_types() {
assert_eq!(BarWidth::Fixed(20), BarWidth::Fixed(20));
let percentage = BarWidth::Percentage(0.8);
if let BarWidth::Percentage(pct) = percentage {
assert_eq!(pct, 0.8);
}
assert_eq!(BarWidth::Auto, BarWidth::Auto);
}
}
#[cfg(feature = "animations")]
#[derive(Debug, Clone)]
pub struct AnimatedBarChart<C: PixelColor> {
base_chart: BarChart<C>,
current_data: Option<crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>>,
}
#[cfg(feature = "animations")]
impl<C: PixelColor> AnimatedBarChart<C>
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
pub fn new() -> Self {
Self {
base_chart: BarChart::new(),
current_data: None,
}
}
pub fn builder() -> AnimatedBarChartBuilder<C> {
AnimatedBarChartBuilder::new()
}
pub fn set_style(&mut self, style: BarChartStyle<C>) {
self.base_chart.set_style(style);
}
pub fn style(&self) -> &BarChartStyle<C> {
self.base_chart.style()
}
pub fn set_config(&mut self, config: ChartConfig<C>) {
self.base_chart.set_config(config);
}
pub fn config(&self) -> &ChartConfig<C> {
self.base_chart.config()
}
pub fn set_orientation(&mut self, orientation: BarOrientation) {
self.base_chart.set_orientation(orientation);
}
pub fn orientation(&self) -> BarOrientation {
self.base_chart.orientation()
}
fn get_render_data(
&self,
) -> crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256> {
self.current_data.clone().unwrap_or_default()
}
#[allow(dead_code)]
fn interpolate_data(
&self,
from_data: &crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>,
to_data: &crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>,
progress: f32,
) -> ChartResult<crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>> {
let mut result = crate::data::series::StaticDataSeries::new();
let min_len = from_data.len().min(to_data.len());
for i in 0..min_len {
if let (Some(from_point), Some(to_point)) = (from_data.get(i), to_data.get(i)) {
let interpolated_x = from_point.x() + (to_point.x() - from_point.x()) * progress;
let interpolated_y = from_point.y() + (to_point.y() - from_point.y()) * progress;
result
.push(crate::data::point::Point2D::new(
interpolated_x,
interpolated_y,
))
.map_err(|_| crate::error::ChartError::MemoryFull)?;
}
}
Ok(result)
}
}
#[cfg(feature = "animations")]
impl<C: PixelColor> Default for AnimatedBarChart<C>
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "animations")]
impl<C: PixelColor> Chart<C> for AnimatedBarChart<C>
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
type Data = crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>;
type Config = ChartConfig<C>;
fn draw<D>(
&self,
data: &Self::Data,
config: &Self::Config,
viewport: Rectangle,
target: &mut D,
) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
Self::Data: crate::data::DataSeries,
<Self::Data as crate::data::DataSeries>::Item: crate::data::DataPoint,
<<Self::Data as crate::data::DataSeries>::Item as crate::data::DataPoint>::X:
Into<f32> + Copy + PartialOrd,
<<Self::Data as crate::data::DataSeries>::Item as crate::data::DataPoint>::Y:
Into<f32> + Copy + PartialOrd,
{
if self.current_data.is_some() {
let render_data = self.get_render_data();
self.base_chart.draw(&render_data, config, viewport, target)
} else {
self.base_chart.draw(data, config, viewport, target)
}
}
}
#[cfg(feature = "animations")]
impl<C: PixelColor> crate::chart::traits::AnimatedChart<C> for AnimatedBarChart<C>
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
type AnimatedData = crate::data::series::StaticDataSeries<crate::data::point::Point2D, 256>;
fn draw_animated<D>(
&self,
data: &Self::Data,
config: &Self::Config,
viewport: embedded_graphics::primitives::Rectangle,
target: &mut D,
_progress: crate::animation::Progress,
) -> ChartResult<()>
where
D: embedded_graphics::draw_target::DrawTarget<Color = C>,
{
self.base_chart.draw(data, config, viewport, target)
}
fn create_transition_animator(
&self,
from_data: Self::AnimatedData,
to_data: Self::AnimatedData,
easing: crate::animation::EasingFunction,
) -> crate::animation::ChartAnimator<Self::AnimatedData> {
crate::animation::ChartAnimator::new(from_data, to_data, easing)
}
fn extract_animated_data(&self, data: &Self::Data) -> ChartResult<Self::AnimatedData> {
Ok(data.clone())
}
}
#[cfg(feature = "animations")]
#[derive(Debug)]
pub struct AnimatedBarChartBuilder<C: PixelColor> {
base_builder: BarChartBuilder<C>,
frame_rate: u32,
}
#[cfg(feature = "animations")]
impl<C: PixelColor> AnimatedBarChartBuilder<C>
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
pub fn new() -> Self {
Self {
base_builder: BarChartBuilder::new(),
frame_rate: 60,
}
}
pub fn frame_rate(mut self, fps: u32) -> Self {
self.frame_rate = fps.clamp(1, 120);
self
}
pub fn orientation(mut self, orientation: BarOrientation) -> Self {
self.base_builder = self.base_builder.orientation(orientation);
self
}
pub fn colors(mut self, colors: &[C]) -> Self {
self.base_builder = self.base_builder.colors(colors);
self
}
pub fn bar_width(mut self, width: BarWidth) -> Self {
self.base_builder = self.base_builder.bar_width(width);
self
}
pub fn spacing(mut self, spacing: u32) -> Self {
self.base_builder = self.base_builder.spacing(spacing);
self
}
pub fn with_border(mut self, border: BorderStyle<C>) -> Self {
self.base_builder = self.base_builder.with_border(border);
self
}
pub fn stacked(mut self, stacked: bool) -> Self {
self.base_builder = self.base_builder.stacked(stacked);
self
}
pub fn with_title(mut self, title: &str) -> Self {
self.base_builder = self.base_builder.with_title(title);
self
}
pub fn background_color(mut self, color: C) -> Self {
self.base_builder = self.base_builder.background_color(color);
self
}
pub fn build(self) -> ChartResult<AnimatedBarChart<C>> {
let base_chart = self.base_builder.build()?;
Ok(AnimatedBarChart {
base_chart,
current_data: None,
})
}
}
#[cfg(feature = "animations")]
impl<C: PixelColor> Default for AnimatedBarChartBuilder<C>
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
fn default() -> Self {
Self::new()
}
}