embedded_charts/legend/
mod.rs

1//! Legend system for charts.
2//!
3//! This module provides a comprehensive legend system for embedded graphics charts,
4//! supporting multiple legend types, flexible positioning, and customizable styling.
5
6pub mod builder;
7pub mod position;
8pub mod style;
9pub mod traits;
10pub mod types;
11
12// Re-export main types
13pub use builder::{
14    CompactLegendBuilder, CustomLegendBuilder, LegendBuilder, StandardLegendBuilder,
15};
16pub use position::{LegendAlignment, LegendMargins, LegendPosition, PositionCalculator};
17pub use style::{BackgroundStyle, LegendStyle, SpacingStyle, SymbolStyle, TextStyle};
18pub use traits::{
19    DefaultLegendRenderer, Legend, LegendEntry, LegendRenderer, StandardLegendRenderer,
20};
21pub use types::{CompactLegend, CustomLegend, LegendEntryType, LegendOrientation, StandardLegend};
22
23use crate::error::ChartResult;
24use embedded_graphics::{prelude::*, primitives::Rectangle};
25
26/// Default legend configuration
27#[derive(Debug, Clone)]
28pub struct DefaultLegend<C: PixelColor> {
29    /// Legend entries
30    pub entries: heapless::Vec<DefaultLegendEntry<C>, 8>,
31    /// Legend position
32    pub position: LegendPosition,
33    /// Legend orientation
34    pub orientation: LegendOrientation,
35    /// Legend style
36    pub style: LegendStyle<C>,
37}
38
39/// Default legend entry implementation
40#[derive(Debug, Clone)]
41pub struct DefaultLegendEntry<C: PixelColor> {
42    /// Label text
43    pub label: heapless::String<32>,
44    /// Entry type (determines symbol)
45    pub entry_type: LegendEntryType<C>,
46    /// Whether this entry is visible
47    pub visible: bool,
48}
49
50impl<C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>> DefaultLegend<C> {
51    /// Create a new default legend
52    pub fn new(position: LegendPosition) -> Self {
53        Self {
54            entries: heapless::Vec::new(),
55            position,
56            orientation: LegendOrientation::Vertical,
57            style: LegendStyle::default(),
58        }
59    }
60
61    /// Add an entry to the legend
62    pub fn add_entry(&mut self, label: &str, entry_type: LegendEntryType<C>) -> ChartResult<()> {
63        let label_string = heapless::String::try_from(label)
64            .map_err(|_| crate::error::ChartError::ConfigurationError)?;
65
66        let entry = DefaultLegendEntry {
67            label: label_string,
68            entry_type,
69            visible: true,
70        };
71
72        self.entries
73            .push(entry)
74            .map_err(|_| crate::error::ChartError::ConfigurationError)?;
75
76        Ok(())
77    }
78
79    /// Set the legend orientation
80    pub fn set_orientation(&mut self, orientation: LegendOrientation) {
81        self.orientation = orientation;
82    }
83
84    /// Set the legend style
85    pub fn set_style(&mut self, style: LegendStyle<C>) {
86        self.style = style;
87    }
88
89    /// Calculate the required size for this legend
90    pub fn calculate_size(&self) -> Size {
91        if self.entries.is_empty() {
92            return Size::zero();
93        }
94
95        let entry_count = self.entries.iter().filter(|e| e.visible).count();
96        if entry_count == 0 {
97            return Size::zero();
98        }
99
100        match self.orientation {
101            LegendOrientation::Vertical => {
102                let width = self.style.spacing.symbol_width
103                    + self.style.spacing.symbol_text_gap
104                    + self.style.text.max_text_width;
105                let height = entry_count as u32 * self.style.text.line_height
106                    + (entry_count.saturating_sub(1)) as u32 * self.style.spacing.entry_spacing;
107                Size::new(width, height)
108            }
109            LegendOrientation::Horizontal => {
110                let height = self.style.text.line_height;
111                let total_width: u32 = self
112                    .entries
113                    .iter()
114                    .filter(|e| e.visible)
115                    .map(|e| {
116                        self.style.spacing.symbol_width
117                            + self.style.spacing.symbol_text_gap
118                            + e.label.len() as u32 * self.style.text.char_width
119                    })
120                    .sum();
121                let spacing_width =
122                    (entry_count.saturating_sub(1)) as u32 * self.style.spacing.entry_spacing;
123                Size::new(total_width + spacing_width, height)
124            }
125        }
126    }
127}
128
129impl<C: PixelColor> LegendEntry<C> for DefaultLegendEntry<C> {
130    fn label(&self) -> &str {
131        &self.label
132    }
133
134    fn set_label(&mut self, label: &str) -> ChartResult<()> {
135        self.label = heapless::String::try_from(label)
136            .map_err(|_| crate::error::ChartError::ConfigurationError)?;
137        Ok(())
138    }
139
140    fn entry_type(&self) -> &LegendEntryType<C> {
141        &self.entry_type
142    }
143
144    fn set_entry_type(&mut self, entry_type: LegendEntryType<C>) {
145        self.entry_type = entry_type;
146    }
147
148    fn is_visible(&self) -> bool {
149        self.visible
150    }
151
152    fn set_visible(&mut self, visible: bool) {
153        self.visible = visible;
154    }
155
156    fn calculate_size(&self, style: &LegendStyle<C>) -> Size {
157        let text_width = self.label.len() as u32 * style.text.char_width;
158        let total_width = style.spacing.symbol_width + style.spacing.symbol_text_gap + text_width;
159        Size::new(total_width, style.text.line_height)
160    }
161
162    fn render_symbol<D>(
163        &self,
164        bounds: Rectangle,
165        _style: &SymbolStyle<C>,
166        target: &mut D,
167    ) -> ChartResult<()>
168    where
169        D: DrawTarget<Color = C>,
170    {
171        use embedded_graphics::primitives::{
172            Circle, Line, PrimitiveStyle, Rectangle as EgRectangle,
173        };
174
175        match &self.entry_type {
176            LegendEntryType::Line { color, .. } => {
177                let line_y = bounds.top_left.y + bounds.size.height as i32 / 2;
178                let line_start = Point::new(bounds.top_left.x + 2, line_y);
179                let line_end = Point::new(bounds.top_left.x + bounds.size.width as i32 - 2, line_y);
180
181                Line::new(line_start, line_end)
182                    .into_styled(PrimitiveStyle::with_stroke(*color, 1))
183                    .draw(target)
184                    .map_err(|_| crate::error::ChartError::RenderingError)?;
185            }
186            LegendEntryType::Bar { color, .. } | LegendEntryType::Pie { color, .. } => {
187                let rect_size = Size::new(bounds.size.width.min(16), bounds.size.height.min(12));
188                let rect_pos = Point::new(
189                    bounds.top_left.x + (bounds.size.width as i32 - rect_size.width as i32) / 2,
190                    bounds.top_left.y + (bounds.size.height as i32 - rect_size.height as i32) / 2,
191                );
192
193                EgRectangle::new(rect_pos, rect_size)
194                    .into_styled(PrimitiveStyle::with_fill(*color))
195                    .draw(target)
196                    .map_err(|_| crate::error::ChartError::RenderingError)?;
197            }
198            LegendEntryType::Custom {
199                color,
200                shape: _,
201                size,
202            } => {
203                let symbol_size = (*size).min(bounds.size.width).min(bounds.size.height);
204                let center = Point::new(
205                    bounds.top_left.x + bounds.size.width as i32 / 2,
206                    bounds.top_left.y + bounds.size.height as i32 / 2,
207                );
208
209                Circle::with_center(center, symbol_size)
210                    .into_styled(PrimitiveStyle::with_fill(*color))
211                    .draw(target)
212                    .map_err(|_| crate::error::ChartError::RenderingError)?;
213            }
214        }
215
216        Ok(())
217    }
218}
219
220impl<C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>> Legend<C> for DefaultLegend<C> {
221    type Entry = DefaultLegendEntry<C>;
222
223    fn entries(&self) -> &[Self::Entry] {
224        &self.entries
225    }
226
227    fn entries_mut(&mut self) -> &mut [Self::Entry] {
228        &mut self.entries
229    }
230
231    fn add_entry(&mut self, entry: Self::Entry) -> ChartResult<()> {
232        self.entries
233            .push(entry)
234            .map_err(|_| crate::error::ChartError::ConfigurationError)
235    }
236
237    fn remove_entry(&mut self, index: usize) -> ChartResult<()> {
238        if index < self.entries.len() {
239            self.entries.remove(index);
240            Ok(())
241        } else {
242            Err(crate::error::ChartError::ConfigurationError)
243        }
244    }
245
246    fn clear_entries(&mut self) {
247        self.entries.clear();
248    }
249
250    fn position(&self) -> LegendPosition {
251        self.position
252    }
253
254    fn set_position(&mut self, position: LegendPosition) {
255        self.position = position;
256    }
257
258    fn orientation(&self) -> LegendOrientation {
259        self.orientation
260    }
261
262    fn set_orientation(&mut self, orientation: LegendOrientation) {
263        self.orientation = orientation;
264    }
265
266    fn calculate_size(&self) -> Size {
267        DefaultLegend::calculate_size(self)
268    }
269}