use crate::error::{ChartError, ChartResult};
use crate::legend::position::LegendPosition;
use crate::legend::style::{LegendStyle, SymbolStyle};
use crate::legend::traits::{Legend, LegendEntry};
use embedded_graphics::{prelude::*, primitives::Rectangle};
#[cfg(feature = "std")]
use std::vec::Vec;
#[cfg(all(feature = "no_std", not(feature = "std")))]
extern crate alloc;
#[cfg(all(feature = "no_std", not(feature = "std")))]
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LegendOrientation {
Vertical,
Horizontal,
}
#[derive(Debug, Clone)]
pub enum LegendEntryType<C: PixelColor> {
Line {
color: C,
width: u32,
pattern: crate::style::LinePattern,
marker: Option<MarkerStyle<C>>,
},
Bar {
color: C,
border_color: Option<C>,
border_width: u32,
},
Pie {
color: C,
border_color: Option<C>,
border_width: u32,
},
Custom {
color: C,
shape: SymbolShape,
size: u32,
},
}
#[derive(Debug, Clone)]
pub struct MarkerStyle<C: PixelColor> {
pub shape: MarkerShape,
pub color: C,
pub size: u32,
pub filled: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MarkerShape {
Circle,
Square,
Triangle,
Diamond,
Cross,
Plus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SymbolShape {
Circle,
Square,
Triangle,
Diamond,
Star,
Cross,
}
#[derive(Debug, Clone)]
pub struct StandardLegend<C: PixelColor> {
entries: heapless::Vec<StandardLegendEntry<C>, 16>,
position: LegendPosition,
orientation: LegendOrientation,
style: LegendStyle<C>,
}
#[derive(Debug, Clone)]
pub struct StandardLegendEntry<C: PixelColor> {
label: heapless::String<64>,
entry_type: LegendEntryType<C>,
visible: bool,
}
#[derive(Debug, Clone)]
pub struct CompactLegend<C: PixelColor> {
entries: heapless::Vec<CompactLegendEntry<C>, 8>,
position: LegendPosition,
orientation: LegendOrientation,
style: LegendStyle<C>,
}
impl<C: PixelColor> CompactLegend<C>
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
pub fn new(position: LegendPosition) -> Self {
Self {
entries: heapless::Vec::new(),
position,
orientation: LegendOrientation::Vertical,
style: LegendStyle::compact(),
}
}
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 add_entry(&mut self, entry: CompactLegendEntry<C>) -> ChartResult<()> {
self.entries
.push(entry)
.map_err(|_| ChartError::ConfigurationError)?;
Ok(())
}
}
impl<C: PixelColor> crate::legend::traits::Legend<C> for CompactLegend<C>
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
type Entry = CompactLegendEntry<C>;
fn position(&self) -> LegendPosition {
self.position
}
fn orientation(&self) -> LegendOrientation {
self.orientation
}
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(|_| ChartError::ConfigurationError)?;
Ok(())
}
fn remove_entry(&mut self, index: usize) -> ChartResult<()> {
if index < self.entries.len() {
self.entries.swap_remove(index);
Ok(())
} else {
Err(ChartError::InvalidConfiguration)
}
}
fn clear_entries(&mut self) {
self.entries.clear();
}
fn set_position(&mut self, position: LegendPosition) {
self.position = position;
}
fn set_orientation(&mut self, orientation: LegendOrientation) {
self.orientation = orientation;
}
fn calculate_size(&self) -> embedded_graphics::prelude::Size {
let entry_count = self.entries.len() as u32;
match self.orientation {
LegendOrientation::Vertical => {
embedded_graphics::prelude::Size::new(80, entry_count * 16 + 8)
}
LegendOrientation::Horizontal => {
embedded_graphics::prelude::Size::new(entry_count * 60 + 8, 20)
}
}
}
}
#[derive(Debug, Clone)]
pub struct CompactLegendEntry<C: PixelColor> {
pub label: heapless::String<16>,
pub entry_type: LegendEntryType<C>,
pub visible: bool,
}
impl<C: PixelColor> CompactLegendEntry<C> {
pub fn new(label: &str, entry_type: LegendEntryType<C>) -> ChartResult<Self> {
let mut label_string = heapless::String::new();
label_string
.push_str(label)
.map_err(|_| ChartError::ConfigurationError)?;
Ok(Self {
label: label_string,
entry_type,
visible: true,
})
}
}
#[derive(Debug, Clone)]
pub struct CustomLegend<C: PixelColor> {
entries: heapless::Vec<CustomLegendEntry<C>, 12>,
position: LegendPosition,
orientation: LegendOrientation,
style: LegendStyle<C>,
layout_params: CustomLayoutParams,
}
impl<C: PixelColor> CustomLegend<C>
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
pub fn new(position: LegendPosition) -> Self {
Self {
entries: heapless::Vec::new(),
position,
orientation: LegendOrientation::Vertical,
style: LegendStyle::new(),
layout_params: CustomLayoutParams::default(),
}
}
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 set_layout_params(&mut self, params: CustomLayoutParams) {
self.layout_params = params;
}
pub fn add_entry(&mut self, entry: CustomLegendEntry<C>) -> ChartResult<()> {
self.entries
.push(entry)
.map_err(|_| ChartError::ConfigurationError)?;
Ok(())
}
}
impl<C: PixelColor> crate::legend::traits::Legend<C> for CustomLegend<C>
where
C: From<embedded_graphics::pixelcolor::Rgb565>,
{
type Entry = CustomLegendEntry<C>;
fn position(&self) -> LegendPosition {
self.position
}
fn orientation(&self) -> LegendOrientation {
self.orientation
}
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(|_| ChartError::ConfigurationError)?;
Ok(())
}
fn remove_entry(&mut self, index: usize) -> ChartResult<()> {
if index < self.entries.len() {
self.entries.swap_remove(index);
Ok(())
} else {
Err(ChartError::InvalidConfiguration)
}
}
fn clear_entries(&mut self) {
self.entries.clear();
}
fn set_position(&mut self, position: LegendPosition) {
self.position = position;
}
fn set_orientation(&mut self, orientation: LegendOrientation) {
self.orientation = orientation;
}
fn calculate_size(&self) -> embedded_graphics::prelude::Size {
let entry_count = self.entries.len() as u32;
let symbol_size = self.layout_params.symbol_size;
let entry_spacing = self.layout_params.entry_spacing;
match self.orientation {
LegendOrientation::Vertical => {
let width = symbol_size + 100; let height = entry_count * (symbol_size + entry_spacing) + 16;
embedded_graphics::prelude::Size::new(width, height)
}
LegendOrientation::Horizontal => {
let width = entry_count * (symbol_size + 80 + entry_spacing) + 16;
let height = symbol_size + 16;
embedded_graphics::prelude::Size::new(width, height)
}
}
}
}
#[derive(Debug, Clone)]
pub struct CustomLegendEntry<C: PixelColor> {
label: heapless::String<32>,
entry_type: LegendEntryType<C>,
visible: bool,
#[allow(dead_code)]
offset: Point,
size_override: Option<Size>,
}
impl<C: PixelColor> CustomLegendEntry<C> {
pub fn new(label: &str, entry_type: LegendEntryType<C>) -> ChartResult<Self> {
let mut label_string = heapless::String::new();
label_string
.push_str(label)
.map_err(|_| ChartError::ConfigurationError)?;
Ok(Self {
label: label_string,
entry_type,
visible: true,
offset: embedded_graphics::prelude::Point::zero(),
size_override: None,
})
}
}
#[derive(Debug, Clone)]
pub struct CustomLayoutParams {
pub entry_spacing: u32,
pub symbol_size: u32,
pub text_offset: Point,
pub auto_layout: bool,
}
impl<C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>> StandardLegend<C> {
pub fn new(position: LegendPosition) -> Self {
Self {
entries: heapless::Vec::new(),
position,
orientation: LegendOrientation::Vertical,
style: LegendStyle::default(),
}
}
pub fn set_style(&mut self, style: LegendStyle<C>) {
self.style = style;
}
pub fn style(&self) -> &LegendStyle<C> {
&self.style
}
}
impl<C: PixelColor> Legend<C> for StandardLegend<C> {
type Entry = StandardLegendEntry<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 {
if self.entries.is_empty() {
return Size::zero();
}
let visible_entries: Vec<_> = self.entries.iter().filter(|e| e.visible).collect();
if visible_entries.is_empty() {
return Size::zero();
}
match self.orientation {
LegendOrientation::Vertical => {
let max_width = visible_entries
.iter()
.map(|e| e.calculate_size(&self.style).width)
.max()
.unwrap_or(0);
let total_height = visible_entries
.iter()
.map(|e| e.calculate_size(&self.style).height)
.sum::<u32>()
+ (visible_entries.len().saturating_sub(1) as u32
* self.style.spacing.entry_spacing);
Size::new(max_width, total_height)
}
LegendOrientation::Horizontal => {
let total_width = visible_entries
.iter()
.map(|e| e.calculate_size(&self.style).width)
.sum::<u32>()
+ (visible_entries.len().saturating_sub(1) as u32
* self.style.spacing.entry_spacing);
let max_height = visible_entries
.iter()
.map(|e| e.calculate_size(&self.style).height)
.max()
.unwrap_or(0);
Size::new(total_width, max_height)
}
}
}
}
impl<C: PixelColor> StandardLegendEntry<C> {
pub fn new(label: &str, entry_type: LegendEntryType<C>) -> ChartResult<Self> {
let label_string = heapless::String::try_from(label)
.map_err(|_| crate::error::ChartError::ConfigurationError)?;
Ok(Self {
label: label_string,
entry_type,
visible: true,
})
}
}
impl<C: PixelColor> LegendEntry<C> for StandardLegendEntry<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, 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);
use embedded_graphics::primitives::Line;
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,
);
match shape {
SymbolShape::Circle => {
Circle::with_center(center, symbol_size)
.into_styled(PrimitiveStyle::with_fill(*color))
.draw(target)
.map_err(|_| crate::error::ChartError::RenderingError)?;
}
SymbolShape::Square => {
let rect_size = Size::new(symbol_size, symbol_size);
let rect_pos = Point::new(
center.x - symbol_size as i32 / 2,
center.y - symbol_size as i32 / 2,
);
EgRectangle::new(rect_pos, rect_size)
.into_styled(PrimitiveStyle::with_fill(*color))
.draw(target)
.map_err(|_| crate::error::ChartError::RenderingError)?;
}
_ => {
Circle::with_center(center, symbol_size)
.into_styled(PrimitiveStyle::with_fill(*color))
.draw(target)
.map_err(|_| crate::error::ChartError::RenderingError)?;
}
}
}
}
Ok(())
}
}
impl<C: PixelColor> LegendEntry<C> for CompactLegendEntry<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, 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);
use embedded_graphics::primitives::Line;
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,
);
match shape {
SymbolShape::Circle => {
Circle::with_center(center, symbol_size)
.into_styled(PrimitiveStyle::with_fill(*color))
.draw(target)
.map_err(|_| crate::error::ChartError::RenderingError)?;
}
SymbolShape::Square => {
let rect_size = Size::new(symbol_size, symbol_size);
let rect_pos = Point::new(
center.x - symbol_size as i32 / 2,
center.y - symbol_size as i32 / 2,
);
EgRectangle::new(rect_pos, rect_size)
.into_styled(PrimitiveStyle::with_fill(*color))
.draw(target)
.map_err(|_| crate::error::ChartError::RenderingError)?;
}
_ => {
Circle::with_center(center, symbol_size)
.into_styled(PrimitiveStyle::with_fill(*color))
.draw(target)
.map_err(|_| crate::error::ChartError::RenderingError)?;
}
}
}
}
Ok(())
}
}
impl Default for LegendOrientation {
fn default() -> Self {
Self::Vertical
}
}
impl Default for CustomLayoutParams {
fn default() -> Self {
Self {
entry_spacing: 8,
symbol_size: 16,
text_offset: Point::new(20, 0),
auto_layout: true,
}
}
}
impl<C: PixelColor> LegendEntry<C> for CustomLegendEntry<C> {
fn label(&self) -> &str {
&self.label
}
fn set_label(&mut self, label: &str) -> ChartResult<()> {
self.label =
heapless::String::try_from(label).map_err(|_| 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;
if let Some(size_override) = self.size_override {
size_override
} else {
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(|_| 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(|_| 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,
);
match shape {
SymbolShape::Circle => {
Circle::with_center(center, symbol_size)
.into_styled(PrimitiveStyle::with_fill(*color))
.draw(target)
.map_err(|_| ChartError::RenderingError)?;
}
SymbolShape::Square => {
let half_size = symbol_size / 2;
let rect_pos =
Point::new(center.x - half_size as i32, center.y - half_size as i32);
EgRectangle::new(rect_pos, Size::new(symbol_size, symbol_size))
.into_styled(PrimitiveStyle::with_fill(*color))
.draw(target)
.map_err(|_| ChartError::RenderingError)?;
}
_ => {
Circle::with_center(center, symbol_size)
.into_styled(PrimitiveStyle::with_fill(*color))
.draw(target)
.map_err(|_| ChartError::RenderingError)?;
}
}
}
}
Ok(())
}
}