pub mod builder;
pub mod position;
pub mod style;
pub mod traits;
pub mod types;
pub use builder::{
CompactLegendBuilder, CustomLegendBuilder, LegendBuilder, StandardLegendBuilder,
};
pub use position::{LegendAlignment, LegendMargins, LegendPosition, PositionCalculator};
pub use style::{BackgroundStyle, LegendStyle, SpacingStyle, SymbolStyle, TextStyle};
pub use traits::{
DefaultLegendRenderer, Legend, LegendEntry, LegendRenderer, StandardLegendRenderer,
};
pub use types::{CompactLegend, CustomLegend, LegendEntryType, LegendOrientation, StandardLegend};
use crate::error::ChartResult;
use embedded_graphics::{prelude::*, primitives::Rectangle};
#[derive(Debug, Clone)]
pub struct DefaultLegend<C: PixelColor> {
pub entries: heapless::Vec<DefaultLegendEntry<C>, 8>,
pub position: LegendPosition,
pub orientation: LegendOrientation,
pub style: LegendStyle<C>,
}
#[derive(Debug, Clone)]
pub struct DefaultLegendEntry<C: PixelColor> {
pub label: heapless::String<32>,
pub entry_type: LegendEntryType<C>,
pub visible: bool,
}
impl<C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>> DefaultLegend<C> {
pub fn new(position: LegendPosition) -> Self {
Self {
entries: heapless::Vec::new(),
position,
orientation: LegendOrientation::Vertical,
style: LegendStyle::default(),
}
}
pub fn add_entry(&mut self, label: &str, entry_type: LegendEntryType<C>) -> ChartResult<()> {
let label_string = heapless::String::try_from(label)
.map_err(|_| crate::error::ChartError::ConfigurationError)?;
let entry = DefaultLegendEntry {
label: label_string,
entry_type,
visible: true,
};
self.entries
.push(entry)
.map_err(|_| crate::error::ChartError::ConfigurationError)?;
Ok(())
}
pub fn set_orientation(&mut self, orientation: LegendOrientation) {
self.orientation = orientation;
}
pub fn set_style(&mut self, style: LegendStyle<C>) {
self.style = style;
}
pub fn calculate_size(&self) -> Size {
if self.entries.is_empty() {
return Size::zero();
}
let entry_count = self.entries.iter().filter(|e| e.visible).count();
if entry_count == 0 {
return Size::zero();
}
match self.orientation {
LegendOrientation::Vertical => {
let width = self.style.spacing.symbol_width
+ self.style.spacing.symbol_text_gap
+ self.style.text.max_text_width;
let height = entry_count as u32 * self.style.text.line_height
+ (entry_count.saturating_sub(1)) as u32 * self.style.spacing.entry_spacing;
Size::new(width, height)
}
LegendOrientation::Horizontal => {
let height = self.style.text.line_height;
let total_width: u32 = self
.entries
.iter()
.filter(|e| e.visible)
.map(|e| {
self.style.spacing.symbol_width
+ self.style.spacing.symbol_text_gap
+ e.label.len() as u32 * self.style.text.char_width
})
.sum();
let spacing_width =
(entry_count.saturating_sub(1)) as u32 * self.style.spacing.entry_spacing;
Size::new(total_width + spacing_width, height)
}
}
}
}
impl<C: PixelColor> LegendEntry<C> for DefaultLegendEntry<C> {
fn label(&self) -> &str {
&self.label
}
fn set_label(&mut self, label: &str) -> ChartResult<()> {
self.label = heapless::String::try_from(label)
.map_err(|_| crate::error::ChartError::ConfigurationError)?;
Ok(())
}
fn entry_type(&self) -> &LegendEntryType<C> {
&self.entry_type
}
fn set_entry_type(&mut self, entry_type: LegendEntryType<C>) {
self.entry_type = entry_type;
}
fn is_visible(&self) -> bool {
self.visible
}
fn set_visible(&mut self, visible: bool) {
self.visible = visible;
}
fn calculate_size(&self, style: &LegendStyle<C>) -> Size {
let text_width = self.label.len() as u32 * style.text.char_width;
let total_width = style.spacing.symbol_width + style.spacing.symbol_text_gap + text_width;
Size::new(total_width, style.text.line_height)
}
fn render_symbol<D>(
&self,
bounds: Rectangle,
_style: &SymbolStyle<C>,
target: &mut D,
) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
{
use embedded_graphics::primitives::{
Circle, Line, PrimitiveStyle, Rectangle as EgRectangle,
};
match &self.entry_type {
LegendEntryType::Line { color, .. } => {
let line_y = bounds.top_left.y + bounds.size.height as i32 / 2;
let line_start = Point::new(bounds.top_left.x + 2, line_y);
let line_end = Point::new(bounds.top_left.x + bounds.size.width as i32 - 2, line_y);
Line::new(line_start, line_end)
.into_styled(PrimitiveStyle::with_stroke(*color, 1))
.draw(target)
.map_err(|_| crate::error::ChartError::RenderingError)?;
}
LegendEntryType::Bar { color, .. } | LegendEntryType::Pie { color, .. } => {
let rect_size = Size::new(bounds.size.width.min(16), bounds.size.height.min(12));
let rect_pos = Point::new(
bounds.top_left.x + (bounds.size.width as i32 - rect_size.width as i32) / 2,
bounds.top_left.y + (bounds.size.height as i32 - rect_size.height as i32) / 2,
);
EgRectangle::new(rect_pos, rect_size)
.into_styled(PrimitiveStyle::with_fill(*color))
.draw(target)
.map_err(|_| crate::error::ChartError::RenderingError)?;
}
LegendEntryType::Custom {
color,
shape: _,
size,
} => {
let symbol_size = (*size).min(bounds.size.width).min(bounds.size.height);
let center = Point::new(
bounds.top_left.x + bounds.size.width as i32 / 2,
bounds.top_left.y + bounds.size.height as i32 / 2,
);
Circle::with_center(center, symbol_size)
.into_styled(PrimitiveStyle::with_fill(*color))
.draw(target)
.map_err(|_| crate::error::ChartError::RenderingError)?;
}
}
Ok(())
}
}
impl<C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>> Legend<C> for DefaultLegend<C> {
type Entry = DefaultLegendEntry<C>;
fn entries(&self) -> &[Self::Entry] {
&self.entries
}
fn entries_mut(&mut self) -> &mut [Self::Entry] {
&mut self.entries
}
fn add_entry(&mut self, entry: Self::Entry) -> ChartResult<()> {
self.entries
.push(entry)
.map_err(|_| crate::error::ChartError::ConfigurationError)
}
fn remove_entry(&mut self, index: usize) -> ChartResult<()> {
if index < self.entries.len() {
self.entries.remove(index);
Ok(())
} else {
Err(crate::error::ChartError::ConfigurationError)
}
}
fn clear_entries(&mut self) {
self.entries.clear();
}
fn position(&self) -> LegendPosition {
self.position
}
fn set_position(&mut self, position: LegendPosition) {
self.position = position;
}
fn orientation(&self) -> LegendOrientation {
self.orientation
}
fn set_orientation(&mut self, orientation: LegendOrientation) {
self.orientation = orientation;
}
fn calculate_size(&self) -> Size {
DefaultLegend::calculate_size(self)
}
}