embedded_charts/legend/
builder.rs

1//! Builder pattern for legend configuration.
2
3use crate::error::{ChartError, ChartResult};
4use crate::legend::{
5    position::{LegendAlignment, LegendMargins, LegendPosition},
6    style::LegendStyle,
7    traits::Legend,
8    types::{
9        CompactLegend, CompactLegendEntry, CustomLegend, CustomLegendEntry, LegendEntryType,
10        LegendOrientation, StandardLegend, StandardLegendEntry,
11    },
12};
13use embedded_graphics::prelude::*;
14
15/// Builder for creating legends with fluent configuration
16pub trait LegendBuilder<C: PixelColor> {
17    /// The legend type this builder creates
18    type Legend: Legend<C>;
19    /// Error type for building operations
20    type Error;
21
22    /// Build the legend with current configuration
23    fn build(self) -> Result<Self::Legend, Self::Error>;
24}
25
26/// Builder for standard legends
27#[derive(Debug)]
28pub struct StandardLegendBuilder<C: PixelColor> {
29    position: LegendPosition,
30    orientation: LegendOrientation,
31    style: LegendStyle<C>,
32    alignment: LegendAlignment,
33    margins: LegendMargins,
34    entries: heapless::Vec<StandardLegendEntry<C>, 16>,
35}
36
37/// Builder for compact legends
38#[derive(Debug)]
39pub struct CompactLegendBuilder<C: PixelColor> {
40    position: LegendPosition,
41    orientation: LegendOrientation,
42    style: LegendStyle<C>,
43    #[allow(dead_code)]
44    alignment: LegendAlignment,
45    #[allow(dead_code)]
46    margins: LegendMargins,
47    entries: heapless::Vec<CompactLegendEntry<C>, 8>,
48}
49
50/// Builder for custom legends
51#[derive(Debug)]
52pub struct CustomLegendBuilder<C: PixelColor> {
53    position: LegendPosition,
54    orientation: LegendOrientation,
55    style: LegendStyle<C>,
56    #[allow(dead_code)]
57    alignment: LegendAlignment,
58    #[allow(dead_code)]
59    margins: LegendMargins,
60    entries: heapless::Vec<CustomLegendEntry<C>, 12>,
61    layout_params: crate::legend::types::CustomLayoutParams,
62}
63
64impl<C: PixelColor> StandardLegendBuilder<C>
65where
66    C: From<embedded_graphics::pixelcolor::Rgb565>,
67{
68    /// Create a new standard legend builder
69    pub fn new() -> Self {
70        Self {
71            position: LegendPosition::Right,
72            orientation: LegendOrientation::Vertical,
73            style: LegendStyle::new(),
74            alignment: LegendAlignment::Start,
75            margins: LegendMargins::default(),
76            entries: heapless::Vec::new(),
77        }
78    }
79
80    /// Set the legend position
81    pub fn position(mut self, position: LegendPosition) -> Self {
82        self.position = position;
83        self
84    }
85
86    /// Set the legend orientation
87    pub fn orientation(mut self, orientation: LegendOrientation) -> Self {
88        self.orientation = orientation;
89        self
90    }
91
92    /// Set the legend style
93    pub fn style(mut self, style: LegendStyle<C>) -> Self {
94        self.style = style;
95        self
96    }
97
98    /// Use minimal styling for small displays
99    pub fn minimal_style(mut self) -> Self {
100        self.style = LegendStyle::minimal();
101        self
102    }
103
104    /// Use professional styling
105    pub fn professional_style(mut self) -> Self {
106        self.style = LegendStyle::professional();
107        self
108    }
109
110    /// Use compact styling
111    pub fn compact_style(mut self) -> Self {
112        self.style = LegendStyle::compact();
113        self
114    }
115
116    /// Set the legend alignment
117    pub fn alignment(mut self, alignment: LegendAlignment) -> Self {
118        self.alignment = alignment;
119        self
120    }
121
122    /// Set the legend margins
123    pub fn margins(mut self, margins: LegendMargins) -> Self {
124        self.margins = margins;
125        self
126    }
127
128    /// Add a line entry to the legend
129    pub fn add_line_entry(mut self, label: &str, color: C) -> ChartResult<Self> {
130        let entry_type = LegendEntryType::Line {
131            color,
132            width: 2,
133            pattern: crate::style::LinePattern::Solid,
134            marker: None,
135        };
136        let entry = StandardLegendEntry::new(label, entry_type)?;
137        self.entries
138            .push(entry)
139            .map_err(|_| ChartError::ConfigurationError)?;
140        Ok(self)
141    }
142
143    /// Add a line entry with marker to the legend
144    pub fn add_line_entry_with_marker(
145        mut self,
146        label: &str,
147        color: C,
148        marker: crate::legend::types::MarkerStyle<C>,
149    ) -> ChartResult<Self> {
150        let entry_type = LegendEntryType::Line {
151            color,
152            width: 2,
153            pattern: crate::style::LinePattern::Solid,
154            marker: Some(marker),
155        };
156        let entry = StandardLegendEntry::new(label, entry_type)?;
157        self.entries
158            .push(entry)
159            .map_err(|_| ChartError::ConfigurationError)?;
160        Ok(self)
161    }
162
163    /// Add a bar entry to the legend
164    pub fn add_bar_entry(mut self, label: &str, color: C) -> ChartResult<Self> {
165        let entry_type = LegendEntryType::Bar {
166            color,
167            border_color: None,
168            border_width: 0,
169        };
170        let entry = StandardLegendEntry::new(label, entry_type)?;
171        self.entries
172            .push(entry)
173            .map_err(|_| ChartError::ConfigurationError)?;
174        Ok(self)
175    }
176
177    /// Add a bar entry with border to the legend
178    pub fn add_bar_entry_with_border(
179        mut self,
180        label: &str,
181        color: C,
182        border_color: C,
183        border_width: u32,
184    ) -> ChartResult<Self> {
185        let entry_type = LegendEntryType::Bar {
186            color,
187            border_color: Some(border_color),
188            border_width,
189        };
190        let entry = StandardLegendEntry::new(label, entry_type)?;
191        self.entries
192            .push(entry)
193            .map_err(|_| ChartError::ConfigurationError)?;
194        Ok(self)
195    }
196
197    /// Add a pie entry to the legend
198    pub fn add_pie_entry(mut self, label: &str, color: C) -> ChartResult<Self> {
199        let entry_type = LegendEntryType::Pie {
200            color,
201            border_color: None,
202            border_width: 0,
203        };
204        let entry = StandardLegendEntry::new(label, entry_type)?;
205        self.entries
206            .push(entry)
207            .map_err(|_| ChartError::ConfigurationError)?;
208        Ok(self)
209    }
210
211    /// Add a custom symbol entry to the legend
212    pub fn add_custom_entry(
213        mut self,
214        label: &str,
215        color: C,
216        shape: crate::legend::types::SymbolShape,
217        size: u32,
218    ) -> ChartResult<Self> {
219        let entry_type = LegendEntryType::Custom { color, shape, size };
220        let entry = StandardLegendEntry::new(label, entry_type)?;
221        self.entries
222            .push(entry)
223            .map_err(|_| ChartError::ConfigurationError)?;
224        Ok(self)
225    }
226
227    /// Add a generic entry to the legend
228    pub fn add_entry(mut self, label: &str, entry_type: LegendEntryType<C>) -> ChartResult<Self> {
229        let entry = StandardLegendEntry::new(label, entry_type)?;
230        self.entries
231            .push(entry)
232            .map_err(|_| ChartError::ConfigurationError)?;
233        Ok(self)
234    }
235
236    /// Clear all entries
237    pub fn clear_entries(mut self) -> Self {
238        self.entries.clear();
239        self
240    }
241}
242
243impl<C: PixelColor> LegendBuilder<C> for StandardLegendBuilder<C>
244where
245    C: From<embedded_graphics::pixelcolor::Rgb565>,
246{
247    type Legend = StandardLegend<C>;
248    type Error = ChartError;
249
250    fn build(self) -> Result<Self::Legend, Self::Error> {
251        let mut legend = StandardLegend::new(self.position);
252        legend.set_orientation(self.orientation);
253        legend.set_style(self.style);
254
255        // Add all entries
256        for entry in self.entries {
257            legend.add_entry(entry)?;
258        }
259
260        Ok(legend)
261    }
262}
263
264impl<C: PixelColor> Default for StandardLegendBuilder<C>
265where
266    C: From<embedded_graphics::pixelcolor::Rgb565>,
267{
268    fn default() -> Self {
269        Self::new()
270    }
271}
272
273impl<C: PixelColor> CompactLegendBuilder<C>
274where
275    C: From<embedded_graphics::pixelcolor::Rgb565>,
276{
277    /// Create a new compact legend builder
278    pub fn new() -> Self {
279        Self {
280            position: LegendPosition::Right,
281            orientation: LegendOrientation::Vertical,
282            style: LegendStyle::compact(),
283            alignment: LegendAlignment::Start,
284            margins: LegendMargins::all(4),
285            entries: heapless::Vec::new(),
286        }
287    }
288
289    /// Set the legend position
290    pub fn position(mut self, position: LegendPosition) -> Self {
291        self.position = position;
292        self
293    }
294
295    /// Set the legend orientation
296    pub fn orientation(mut self, orientation: LegendOrientation) -> Self {
297        self.orientation = orientation;
298        self
299    }
300
301    /// Add a simple entry with just color
302    pub fn add_simple_entry(mut self, label: &str, color: C) -> ChartResult<Self> {
303        let entry_type = LegendEntryType::Custom {
304            color,
305            shape: crate::legend::types::SymbolShape::Circle,
306            size: 8,
307        };
308        let entry = CompactLegendEntry::new(label, entry_type)?;
309        self.entries
310            .push(entry)
311            .map_err(|_| ChartError::ConfigurationError)?;
312        Ok(self)
313    }
314}
315
316impl<C: PixelColor> LegendBuilder<C> for CompactLegendBuilder<C>
317where
318    C: From<embedded_graphics::pixelcolor::Rgb565>,
319{
320    type Legend = CompactLegend<C>;
321    type Error = ChartError;
322
323    fn build(self) -> Result<Self::Legend, Self::Error> {
324        let mut legend = CompactLegend::new(self.position);
325        legend.set_orientation(self.orientation);
326        legend.set_style(self.style);
327
328        // Add all entries
329        for entry in self.entries {
330            legend.add_entry(entry)?;
331        }
332
333        Ok(legend)
334    }
335}
336
337impl<C: PixelColor> Default for CompactLegendBuilder<C>
338where
339    C: From<embedded_graphics::pixelcolor::Rgb565>,
340{
341    fn default() -> Self {
342        Self::new()
343    }
344}
345
346impl<C: PixelColor> CustomLegendBuilder<C>
347where
348    C: From<embedded_graphics::pixelcolor::Rgb565>,
349{
350    /// Create a new custom legend builder
351    pub fn new() -> Self {
352        Self {
353            position: LegendPosition::Right,
354            orientation: LegendOrientation::Vertical,
355            style: LegendStyle::new(),
356            alignment: LegendAlignment::Start,
357            margins: LegendMargins::default(),
358            entries: heapless::Vec::new(),
359            layout_params: crate::legend::types::CustomLayoutParams::default(),
360        }
361    }
362
363    /// Set the legend position
364    pub fn position(mut self, position: LegendPosition) -> Self {
365        self.position = position;
366        self
367    }
368
369    /// Set the legend orientation
370    pub fn orientation(mut self, orientation: LegendOrientation) -> Self {
371        self.orientation = orientation;
372        self
373    }
374
375    /// Set custom layout parameters
376    pub fn layout_params(mut self, params: crate::legend::types::CustomLayoutParams) -> Self {
377        self.layout_params = params;
378        self
379    }
380
381    /// Set custom entry spacing
382    pub fn entry_spacing(mut self, spacing: u32) -> Self {
383        self.layout_params.entry_spacing = spacing;
384        self
385    }
386
387    /// Set custom symbol size
388    pub fn symbol_size(mut self, size: u32) -> Self {
389        self.layout_params.symbol_size = size;
390        self
391    }
392
393    /// Enable or disable automatic layout
394    pub fn auto_layout(mut self, enabled: bool) -> Self {
395        self.layout_params.auto_layout = enabled;
396        self
397    }
398}
399
400impl<C: PixelColor> LegendBuilder<C> for CustomLegendBuilder<C>
401where
402    C: From<embedded_graphics::pixelcolor::Rgb565>,
403{
404    type Legend = CustomLegend<C>;
405    type Error = ChartError;
406
407    fn build(self) -> Result<Self::Legend, Self::Error> {
408        let mut legend = CustomLegend::new(self.position);
409        legend.set_orientation(self.orientation);
410        legend.set_style(self.style);
411        legend.set_layout_params(self.layout_params);
412
413        // Add all entries
414        for entry in self.entries {
415            legend.add_entry(entry)?;
416        }
417
418        Ok(legend)
419    }
420}
421
422impl<C: PixelColor> Default for CustomLegendBuilder<C>
423where
424    C: From<embedded_graphics::pixelcolor::Rgb565>,
425{
426    fn default() -> Self {
427        Self::new()
428    }
429}
430
431// Convenience functions for creating common legend configurations
432/// Preset legend configurations for common use cases
433pub mod presets {
434    use super::*;
435    use embedded_graphics::pixelcolor::Rgb565;
436
437    /// Create a standard legend on the right side
438    pub fn right_legend<C: PixelColor + From<Rgb565>>() -> StandardLegendBuilder<C> {
439        StandardLegendBuilder::new()
440            .position(LegendPosition::Right)
441            .orientation(LegendOrientation::Vertical)
442    }
443
444    /// Create a standard legend at the bottom
445    pub fn bottom_legend<C: PixelColor + From<Rgb565>>() -> StandardLegendBuilder<C> {
446        StandardLegendBuilder::new()
447            .position(LegendPosition::Bottom)
448            .orientation(LegendOrientation::Horizontal)
449    }
450
451    /// Create a minimal legend for small displays
452    pub fn minimal_legend<C: PixelColor + From<Rgb565>>() -> CompactLegendBuilder<C> {
453        CompactLegendBuilder::new()
454            .position(LegendPosition::TopRight)
455            .orientation(LegendOrientation::Vertical)
456    }
457
458    /// Create a professional legend with styling
459    pub fn professional_legend<C: PixelColor + From<Rgb565>>() -> StandardLegendBuilder<C> {
460        StandardLegendBuilder::new()
461            .position(LegendPosition::Right)
462            .professional_style()
463            .margins(LegendMargins::all(12))
464    }
465
466    /// Create a floating legend that overlays the chart
467    pub fn floating_legend<C: PixelColor + From<Rgb565>>(
468        position: Point,
469    ) -> StandardLegendBuilder<C> {
470        StandardLegendBuilder::new()
471            .position(LegendPosition::Floating(position))
472            .orientation(LegendOrientation::Vertical)
473    }
474}
475
476// CompactLegendEntry implementation is in types.rs
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481    use embedded_graphics::pixelcolor::Rgb565;
482
483    #[test]
484    fn test_standard_legend_builder() {
485        let legend = StandardLegendBuilder::new()
486            .position(LegendPosition::Bottom)
487            .orientation(LegendOrientation::Horizontal)
488            .add_line_entry("Series 1", Rgb565::RED)
489            .unwrap()
490            .add_bar_entry("Series 2", Rgb565::BLUE)
491            .unwrap()
492            .build()
493            .unwrap();
494
495        assert_eq!(legend.position(), LegendPosition::Bottom);
496        assert_eq!(legend.orientation(), LegendOrientation::Horizontal);
497        assert_eq!(legend.entries().len(), 2);
498    }
499
500    #[test]
501    fn test_preset_legends() {
502        let legend = presets::right_legend::<Rgb565>()
503            .add_line_entry("Test", Rgb565::GREEN)
504            .unwrap()
505            .build()
506            .unwrap();
507
508        assert_eq!(legend.position(), LegendPosition::Right);
509        assert_eq!(legend.orientation(), LegendOrientation::Vertical);
510    }
511}