use crate::error::ChartResult;
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;
pub trait Legend<C: PixelColor> {
type Entry: LegendEntry<C>;
fn entries(&self) -> &[Self::Entry];
fn entries_mut(&mut self) -> &mut [Self::Entry];
fn add_entry(&mut self, entry: Self::Entry) -> ChartResult<()>;
fn remove_entry(&mut self, index: usize) -> ChartResult<()>;
fn clear_entries(&mut self);
fn position(&self) -> crate::legend::position::LegendPosition;
fn set_position(&mut self, position: crate::legend::position::LegendPosition);
fn orientation(&self) -> crate::legend::types::LegendOrientation;
fn set_orientation(&mut self, orientation: crate::legend::types::LegendOrientation);
fn calculate_size(&self) -> Size;
fn is_empty(&self) -> bool {
self.entries().is_empty()
}
fn visible_entry_count(&self) -> usize {
self.entries().iter().filter(|e| e.is_visible()).count()
}
}
pub trait LegendRenderer<C: PixelColor> {
type Legend: Legend<C>;
fn render<D>(
&self,
legend: &Self::Legend,
viewport: Rectangle,
target: &mut D,
) -> ChartResult<()>
where
D: DrawTarget<Color = C>;
fn calculate_layout(
&self,
legend: &Self::Legend,
viewport: Rectangle,
) -> ChartResult<heapless::Vec<Rectangle, 8>>;
fn render_entry<D>(
&self,
entry: &<Self::Legend as Legend<C>>::Entry,
bounds: Rectangle,
target: &mut D,
) -> ChartResult<()>
where
D: DrawTarget<Color = C>;
}
pub trait LegendEntry<C: PixelColor> {
fn label(&self) -> &str;
fn set_label(&mut self, label: &str) -> ChartResult<()>;
fn entry_type(&self) -> &crate::legend::types::LegendEntryType<C>;
fn set_entry_type(&mut self, entry_type: crate::legend::types::LegendEntryType<C>);
fn is_visible(&self) -> bool;
fn set_visible(&mut self, visible: bool);
fn calculate_size(&self, style: &crate::legend::style::LegendStyle<C>) -> Size;
fn render_symbol<D>(
&self,
bounds: Rectangle,
style: &crate::legend::style::SymbolStyle<C>,
target: &mut D,
) -> ChartResult<()>
where
D: DrawTarget<Color = C>;
}
pub trait AutoLegend<C: PixelColor>: Legend<C> {
type DataSeries;
fn generate_from_series(&mut self, series: &[Self::DataSeries]) -> ChartResult<()>;
fn generate_entry_from_series(
&self,
series: &Self::DataSeries,
index: usize,
) -> ChartResult<Self::Entry>;
fn update_from_series(&mut self, series: &[Self::DataSeries]) -> ChartResult<()>;
}
pub trait InteractiveLegend<C: PixelColor>: Legend<C> {
type Event;
type Response;
fn handle_event(
&mut self,
event: Self::Event,
viewport: Rectangle,
) -> ChartResult<Self::Response>;
fn hit_test(&self, point: Point, viewport: Rectangle) -> Option<usize>;
fn toggle_entry(&mut self, index: usize) -> ChartResult<()>;
fn selected_entry(&self) -> Option<usize>;
fn set_selected_entry(&mut self, index: Option<usize>);
}
#[derive(Debug, Clone)]
pub struct DefaultLegendRenderer<C: PixelColor> {
_phantom: core::marker::PhantomData<C>,
}
impl<C: PixelColor> DefaultLegendRenderer<C> {
pub fn new() -> Self {
Self {
_phantom: core::marker::PhantomData,
}
}
}
impl<C: PixelColor> Default for DefaultLegendRenderer<C> {
fn default() -> Self {
Self::new()
}
}
impl<C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>> LegendRenderer<C>
for DefaultLegendRenderer<C>
{
type Legend = crate::legend::DefaultLegend<C>;
fn render<D>(
&self,
legend: &Self::Legend,
viewport: Rectangle,
target: &mut D,
) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
{
if legend.entries.is_empty() {
return Ok(());
}
let entry_bounds = self.calculate_layout(legend, viewport)?;
if let Some(bg_color) = legend.style.background.color {
use embedded_graphics::primitives::PrimitiveStyle;
use embedded_graphics::primitives::Rectangle as EgRectangle;
EgRectangle::new(viewport.top_left, viewport.size)
.into_styled(PrimitiveStyle::with_fill(bg_color))
.draw(target)
.map_err(|_| crate::error::ChartError::RenderingError)?;
}
for (entry, bounds) in legend
.entries
.iter()
.filter(|e| e.visible)
.zip(entry_bounds.iter())
{
self.render_entry(entry, *bounds, target)?;
}
Ok(())
}
fn calculate_layout(
&self,
legend: &Self::Legend,
viewport: Rectangle,
) -> ChartResult<heapless::Vec<Rectangle, 8>> {
let mut layouts = heapless::Vec::new();
let visible_entries: Vec<_> = legend.entries.iter().filter(|e| e.visible).collect();
if visible_entries.is_empty() {
return Ok(layouts);
}
match legend.orientation {
crate::legend::types::LegendOrientation::Vertical => {
let entry_height = legend.style.text.line_height;
let spacing = legend.style.spacing.entry_spacing;
for (i, _) in visible_entries.iter().enumerate() {
let y_offset = i as u32 * (entry_height + spacing);
let bounds = Rectangle::new(
Point::new(viewport.top_left.x, viewport.top_left.y + y_offset as i32),
Size::new(viewport.size.width, entry_height),
);
if layouts.push(bounds).is_err() {
return Err(crate::error::ChartError::ConfigurationError);
}
}
}
crate::legend::types::LegendOrientation::Horizontal => {
let mut x_offset = 0u32;
let entry_height = legend.style.text.line_height;
for entry in visible_entries.iter() {
let entry_width = legend.style.spacing.symbol_width
+ legend.style.spacing.symbol_text_gap
+ entry.label.len() as u32 * legend.style.text.char_width;
let bounds = Rectangle::new(
Point::new(viewport.top_left.x + x_offset as i32, viewport.top_left.y),
Size::new(entry_width, entry_height),
);
if layouts.push(bounds).is_err() {
return Err(crate::error::ChartError::ConfigurationError);
}
x_offset += entry_width + legend.style.spacing.entry_spacing;
}
}
}
#[derive(Debug, Clone)]
pub struct StandardLegendRenderer<C: PixelColor> {
_phantom: core::marker::PhantomData<C>,
}
impl<C: PixelColor> StandardLegendRenderer<C> {
pub fn new() -> Self {
Self {
_phantom: core::marker::PhantomData,
}
}
}
impl<C: PixelColor> Default for StandardLegendRenderer<C> {
fn default() -> Self {
Self::new()
}
}
impl<C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>> LegendRenderer<C>
for StandardLegendRenderer<C>
{
type Legend = crate::legend::types::StandardLegend<C>;
fn render<D>(
&self,
legend: &Self::Legend,
viewport: Rectangle,
target: &mut D,
) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
{
if legend.entries().is_empty() {
return Ok(());
}
let entry_bounds = self.calculate_layout(legend, viewport)?;
if let Some(bg_color) = legend.style().background.color {
use embedded_graphics::primitives::PrimitiveStyle;
use embedded_graphics::primitives::Rectangle as EgRectangle;
EgRectangle::new(viewport.top_left, viewport.size)
.into_styled(PrimitiveStyle::with_fill(bg_color))
.draw(target)
.map_err(|_| crate::error::ChartError::RenderingError)?;
}
for (entry, bounds) in legend
.entries()
.iter()
.filter(|e| e.is_visible())
.zip(entry_bounds.iter())
{
self.render_entry(entry, *bounds, target)?;
}
Ok(())
}
fn calculate_layout(
&self,
legend: &Self::Legend,
viewport: Rectangle,
) -> ChartResult<heapless::Vec<Rectangle, 8>> {
let mut layouts = heapless::Vec::new();
let visible_entries: Vec<_> =
legend.entries().iter().filter(|e| e.is_visible()).collect();
if visible_entries.is_empty() {
return Ok(layouts);
}
match legend.orientation() {
crate::legend::types::LegendOrientation::Vertical => {
let entry_height = legend.style().text.line_height;
let spacing = legend.style().spacing.entry_spacing;
for (i, _) in visible_entries.iter().enumerate() {
let y_offset = i as u32 * (entry_height + spacing);
let bounds = Rectangle::new(
Point::new(
viewport.top_left.x,
viewport.top_left.y + y_offset as i32,
),
Size::new(viewport.size.width, entry_height),
);
if layouts.push(bounds).is_err() {
return Err(crate::error::ChartError::ConfigurationError);
}
}
}
crate::legend::types::LegendOrientation::Horizontal => {
let mut x_offset = 0u32;
let entry_height = legend.style().text.line_height;
for entry in visible_entries.iter() {
let entry_width = legend.style().spacing.symbol_width
+ legend.style().spacing.symbol_text_gap
+ entry.label().len() as u32 * legend.style().text.char_width;
let bounds = Rectangle::new(
Point::new(
viewport.top_left.x + x_offset as i32,
viewport.top_left.y,
),
Size::new(entry_width, entry_height),
);
if layouts.push(bounds).is_err() {
return Err(crate::error::ChartError::ConfigurationError);
}
x_offset += entry_width + legend.style().spacing.entry_spacing;
}
}
}
Ok(layouts)
}
fn render_entry<D>(
&self,
entry: &crate::legend::types::StandardLegendEntry<C>,
bounds: Rectangle,
target: &mut D,
) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
{
let symbol_bounds = Rectangle::new(
bounds.top_left,
Size::new(bounds.size.width.min(20), bounds.size.height),
);
entry.render_symbol(
symbol_bounds,
&crate::legend::style::SymbolStyle::default(),
target,
)?;
Ok(())
}
}
Ok(layouts)
}
fn render_entry<D>(
&self,
entry: &crate::legend::DefaultLegendEntry<C>,
bounds: Rectangle,
target: &mut D,
) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
{
let symbol_bounds = Rectangle::new(
bounds.top_left,
Size::new(bounds.size.width.min(20), bounds.size.height),
);
entry.render_symbol(
symbol_bounds,
&crate::legend::style::SymbolStyle::default(),
target,
)?;
let text_x = bounds.top_left.x + 25; let text_y = bounds.top_left.y + (bounds.size.height as i32 / 2);
use embedded_graphics::{
mono_font::{ascii::FONT_6X10, MonoTextStyle},
text::{Baseline, Text},
};
let text_style = MonoTextStyle::new(
&FONT_6X10,
C::from(embedded_graphics::pixelcolor::Rgb565::BLACK),
);
Text::with_baseline(
entry.label(),
Point::new(text_x, text_y),
text_style,
Baseline::Middle,
)
.draw(target)
.map_err(|_| crate::error::ChartError::RenderingError)?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct StandardLegendRenderer<C: PixelColor> {
_phantom: core::marker::PhantomData<C>,
}
impl<C: PixelColor> StandardLegendRenderer<C> {
pub fn new() -> Self {
Self {
_phantom: core::marker::PhantomData,
}
}
}
impl<C: PixelColor> Default for StandardLegendRenderer<C> {
fn default() -> Self {
Self::new()
}
}
impl<C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>> LegendRenderer<C>
for StandardLegendRenderer<C>
{
type Legend = crate::legend::types::StandardLegend<C>;
fn render<D>(
&self,
legend: &Self::Legend,
viewport: Rectangle,
target: &mut D,
) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
{
if legend.entries().is_empty() {
return Ok(());
}
let entry_bounds = self.calculate_layout(legend, viewport)?;
if let Some(bg_color) = legend.style().background.color {
use embedded_graphics::primitives::PrimitiveStyle;
use embedded_graphics::primitives::Rectangle as EgRectangle;
EgRectangle::new(viewport.top_left, viewport.size)
.into_styled(PrimitiveStyle::with_fill(bg_color))
.draw(target)
.map_err(|_| crate::error::ChartError::RenderingError)?;
}
for (entry, bounds) in legend
.entries()
.iter()
.filter(|e| e.is_visible())
.zip(entry_bounds.iter())
{
self.render_entry(entry, *bounds, target)?;
}
Ok(())
}
fn calculate_layout(
&self,
legend: &Self::Legend,
viewport: Rectangle,
) -> ChartResult<heapless::Vec<Rectangle, 8>> {
let mut layouts = heapless::Vec::new();
let visible_entries: Vec<_> = legend.entries().iter().filter(|e| e.is_visible()).collect();
if visible_entries.is_empty() {
return Ok(layouts);
}
match legend.orientation() {
crate::legend::types::LegendOrientation::Vertical => {
let entry_height = legend.style().text.line_height;
let spacing = legend.style().spacing.entry_spacing;
for (i, _) in visible_entries.iter().enumerate() {
let y_offset = i as u32 * (entry_height + spacing);
let bounds = Rectangle::new(
Point::new(viewport.top_left.x, viewport.top_left.y + y_offset as i32),
Size::new(viewport.size.width, entry_height),
);
if layouts.push(bounds).is_err() {
return Err(crate::error::ChartError::ConfigurationError);
}
}
}
crate::legend::types::LegendOrientation::Horizontal => {
let mut x_offset = 0u32;
let entry_height = legend.style().text.line_height;
for entry in visible_entries.iter() {
let entry_width = legend.style().spacing.symbol_width
+ legend.style().spacing.symbol_text_gap
+ entry.label().len() as u32 * legend.style().text.char_width;
let bounds = Rectangle::new(
Point::new(viewport.top_left.x + x_offset as i32, viewport.top_left.y),
Size::new(entry_width, entry_height),
);
if layouts.push(bounds).is_err() {
return Err(crate::error::ChartError::ConfigurationError);
}
x_offset += entry_width + legend.style().spacing.entry_spacing;
}
}
}
Ok(layouts)
}
fn render_entry<D>(
&self,
entry: &crate::legend::types::StandardLegendEntry<C>,
bounds: Rectangle,
target: &mut D,
) -> ChartResult<()>
where
D: DrawTarget<Color = C>,
{
let symbol_bounds = Rectangle::new(
bounds.top_left,
Size::new(bounds.size.width.min(20), bounds.size.height),
);
entry.render_symbol(
symbol_bounds,
&crate::legend::style::SymbolStyle::default(),
target,
)?;
let text_x = bounds.top_left.x + 25; let text_y = bounds.top_left.y + (bounds.size.height as i32 / 2);
use embedded_graphics::{
mono_font::{ascii::FONT_6X10, MonoTextStyle},
text::{Baseline, Text},
};
let text_style = MonoTextStyle::new(
&FONT_6X10,
C::from(embedded_graphics::pixelcolor::Rgb565::BLACK),
);
Text::with_baseline(
entry.label(),
Point::new(text_x, text_y),
text_style,
Baseline::Middle,
)
.draw(target)
.map_err(|_| crate::error::ChartError::RenderingError)?;
Ok(())
}
}