1use crate::error::ChartResult;
4use embedded_graphics::{prelude::*, primitives::Rectangle};
5
6#[cfg(feature = "std")]
7use std::vec::Vec;
8
9#[cfg(all(feature = "no_std", not(feature = "std")))]
10extern crate alloc;
11
12#[cfg(all(feature = "no_std", not(feature = "std")))]
13use alloc::vec::Vec;
14
15pub trait Legend<C: PixelColor> {
17 type Entry: LegendEntry<C>;
19
20 fn entries(&self) -> &[Self::Entry];
22
23 fn entries_mut(&mut self) -> &mut [Self::Entry];
25
26 fn add_entry(&mut self, entry: Self::Entry) -> ChartResult<()>;
28
29 fn remove_entry(&mut self, index: usize) -> ChartResult<()>;
31
32 fn clear_entries(&mut self);
34
35 fn position(&self) -> crate::legend::position::LegendPosition;
37
38 fn set_position(&mut self, position: crate::legend::position::LegendPosition);
40
41 fn orientation(&self) -> crate::legend::types::LegendOrientation;
43
44 fn set_orientation(&mut self, orientation: crate::legend::types::LegendOrientation);
46
47 fn calculate_size(&self) -> Size;
49
50 fn is_empty(&self) -> bool {
52 self.entries().is_empty()
53 }
54
55 fn visible_entry_count(&self) -> usize {
57 self.entries().iter().filter(|e| e.is_visible()).count()
58 }
59}
60
61pub trait LegendRenderer<C: PixelColor> {
63 type Legend: Legend<C>;
65
66 fn render<D>(
73 &self,
74 legend: &Self::Legend,
75 viewport: Rectangle,
76 target: &mut D,
77 ) -> ChartResult<()>
78 where
79 D: DrawTarget<Color = C>;
80
81 fn calculate_layout(
87 &self,
88 legend: &Self::Legend,
89 viewport: Rectangle,
90 ) -> ChartResult<heapless::Vec<Rectangle, 8>>;
91
92 fn render_entry<D>(
99 &self,
100 entry: &<Self::Legend as Legend<C>>::Entry,
101 bounds: Rectangle,
102 target: &mut D,
103 ) -> ChartResult<()>
104 where
105 D: DrawTarget<Color = C>;
106}
107
108pub trait LegendEntry<C: PixelColor> {
110 fn label(&self) -> &str;
112
113 fn set_label(&mut self, label: &str) -> ChartResult<()>;
115
116 fn entry_type(&self) -> &crate::legend::types::LegendEntryType<C>;
118
119 fn set_entry_type(&mut self, entry_type: crate::legend::types::LegendEntryType<C>);
121
122 fn is_visible(&self) -> bool;
124
125 fn set_visible(&mut self, visible: bool);
127
128 fn calculate_size(&self, style: &crate::legend::style::LegendStyle<C>) -> Size;
130
131 fn render_symbol<D>(
133 &self,
134 bounds: Rectangle,
135 style: &crate::legend::style::SymbolStyle<C>,
136 target: &mut D,
137 ) -> ChartResult<()>
138 where
139 D: DrawTarget<Color = C>;
140}
141
142pub trait AutoLegend<C: PixelColor>: Legend<C> {
144 type DataSeries;
146
147 fn generate_from_series(&mut self, series: &[Self::DataSeries]) -> ChartResult<()>;
149
150 fn generate_entry_from_series(
152 &self,
153 series: &Self::DataSeries,
154 index: usize,
155 ) -> ChartResult<Self::Entry>;
156
157 fn update_from_series(&mut self, series: &[Self::DataSeries]) -> ChartResult<()>;
159}
160
161pub trait InteractiveLegend<C: PixelColor>: Legend<C> {
163 type Event;
165 type Response;
167
168 fn handle_event(
174 &mut self,
175 event: Self::Event,
176 viewport: Rectangle,
177 ) -> ChartResult<Self::Response>;
178
179 fn hit_test(&self, point: Point, viewport: Rectangle) -> Option<usize>;
185
186 fn toggle_entry(&mut self, index: usize) -> ChartResult<()>;
188
189 fn selected_entry(&self) -> Option<usize>;
191
192 fn set_selected_entry(&mut self, index: Option<usize>);
194}
195
196#[derive(Debug, Clone)]
198pub struct DefaultLegendRenderer<C: PixelColor> {
199 _phantom: core::marker::PhantomData<C>,
200}
201
202impl<C: PixelColor> DefaultLegendRenderer<C> {
203 pub fn new() -> Self {
205 Self {
206 _phantom: core::marker::PhantomData,
207 }
208 }
209}
210
211impl<C: PixelColor> Default for DefaultLegendRenderer<C> {
212 fn default() -> Self {
213 Self::new()
214 }
215}
216
217impl<C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>> LegendRenderer<C>
218 for DefaultLegendRenderer<C>
219{
220 type Legend = crate::legend::DefaultLegend<C>;
221
222 fn render<D>(
223 &self,
224 legend: &Self::Legend,
225 viewport: Rectangle,
226 target: &mut D,
227 ) -> ChartResult<()>
228 where
229 D: DrawTarget<Color = C>,
230 {
231 if legend.entries.is_empty() {
232 return Ok(());
233 }
234
235 let entry_bounds = self.calculate_layout(legend, viewport)?;
236
237 if let Some(bg_color) = legend.style.background.color {
239 use embedded_graphics::primitives::PrimitiveStyle;
240 use embedded_graphics::primitives::Rectangle as EgRectangle;
241
242 EgRectangle::new(viewport.top_left, viewport.size)
243 .into_styled(PrimitiveStyle::with_fill(bg_color))
244 .draw(target)
245 .map_err(|_| crate::error::ChartError::RenderingError)?;
246 }
247
248 for (entry, bounds) in legend
250 .entries
251 .iter()
252 .filter(|e| e.visible)
253 .zip(entry_bounds.iter())
254 {
255 self.render_entry(entry, *bounds, target)?;
256 }
257
258 Ok(())
259 }
260
261 fn calculate_layout(
262 &self,
263 legend: &Self::Legend,
264 viewport: Rectangle,
265 ) -> ChartResult<heapless::Vec<Rectangle, 8>> {
266 let mut layouts = heapless::Vec::new();
267 let visible_entries: Vec<_> = legend.entries.iter().filter(|e| e.visible).collect();
268
269 if visible_entries.is_empty() {
270 return Ok(layouts);
271 }
272
273 match legend.orientation {
274 crate::legend::types::LegendOrientation::Vertical => {
275 let entry_height = legend.style.text.line_height;
276 let spacing = legend.style.spacing.entry_spacing;
277
278 for (i, _) in visible_entries.iter().enumerate() {
279 let y_offset = i as u32 * (entry_height + spacing);
280 let bounds = Rectangle::new(
281 Point::new(viewport.top_left.x, viewport.top_left.y + y_offset as i32),
282 Size::new(viewport.size.width, entry_height),
283 );
284 if layouts.push(bounds).is_err() {
285 return Err(crate::error::ChartError::ConfigurationError);
286 }
287 }
288 }
289 crate::legend::types::LegendOrientation::Horizontal => {
290 let mut x_offset = 0u32;
291 let entry_height = legend.style.text.line_height;
292
293 for entry in visible_entries.iter() {
294 let entry_width = legend.style.spacing.symbol_width
295 + legend.style.spacing.symbol_text_gap
296 + entry.label.len() as u32 * legend.style.text.char_width;
297
298 let bounds = Rectangle::new(
299 Point::new(viewport.top_left.x + x_offset as i32, viewport.top_left.y),
300 Size::new(entry_width, entry_height),
301 );
302 if layouts.push(bounds).is_err() {
303 return Err(crate::error::ChartError::ConfigurationError);
304 }
305
306 x_offset += entry_width + legend.style.spacing.entry_spacing;
307 }
308 }
309 }
310
311 #[derive(Debug, Clone)]
313 pub struct StandardLegendRenderer<C: PixelColor> {
314 _phantom: core::marker::PhantomData<C>,
315 }
316
317 impl<C: PixelColor> StandardLegendRenderer<C> {
318 pub fn new() -> Self {
320 Self {
321 _phantom: core::marker::PhantomData,
322 }
323 }
324 }
325
326 impl<C: PixelColor> Default for StandardLegendRenderer<C> {
327 fn default() -> Self {
328 Self::new()
329 }
330 }
331
332 impl<C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>> LegendRenderer<C>
333 for StandardLegendRenderer<C>
334 {
335 type Legend = crate::legend::types::StandardLegend<C>;
336
337 fn render<D>(
338 &self,
339 legend: &Self::Legend,
340 viewport: Rectangle,
341 target: &mut D,
342 ) -> ChartResult<()>
343 where
344 D: DrawTarget<Color = C>,
345 {
346 if legend.entries().is_empty() {
347 return Ok(());
348 }
349
350 let entry_bounds = self.calculate_layout(legend, viewport)?;
351
352 if let Some(bg_color) = legend.style().background.color {
354 use embedded_graphics::primitives::PrimitiveStyle;
355 use embedded_graphics::primitives::Rectangle as EgRectangle;
356
357 EgRectangle::new(viewport.top_left, viewport.size)
358 .into_styled(PrimitiveStyle::with_fill(bg_color))
359 .draw(target)
360 .map_err(|_| crate::error::ChartError::RenderingError)?;
361 }
362
363 for (entry, bounds) in legend
365 .entries()
366 .iter()
367 .filter(|e| e.is_visible())
368 .zip(entry_bounds.iter())
369 {
370 self.render_entry(entry, *bounds, target)?;
371 }
372
373 Ok(())
374 }
375
376 fn calculate_layout(
377 &self,
378 legend: &Self::Legend,
379 viewport: Rectangle,
380 ) -> ChartResult<heapless::Vec<Rectangle, 8>> {
381 let mut layouts = heapless::Vec::new();
382 let visible_entries: Vec<_> =
383 legend.entries().iter().filter(|e| e.is_visible()).collect();
384
385 if visible_entries.is_empty() {
386 return Ok(layouts);
387 }
388
389 match legend.orientation() {
390 crate::legend::types::LegendOrientation::Vertical => {
391 let entry_height = legend.style().text.line_height;
392 let spacing = legend.style().spacing.entry_spacing;
393
394 for (i, _) in visible_entries.iter().enumerate() {
395 let y_offset = i as u32 * (entry_height + spacing);
396 let bounds = Rectangle::new(
397 Point::new(
398 viewport.top_left.x,
399 viewport.top_left.y + y_offset as i32,
400 ),
401 Size::new(viewport.size.width, entry_height),
402 );
403 if layouts.push(bounds).is_err() {
404 return Err(crate::error::ChartError::ConfigurationError);
405 }
406 }
407 }
408 crate::legend::types::LegendOrientation::Horizontal => {
409 let mut x_offset = 0u32;
410 let entry_height = legend.style().text.line_height;
411
412 for entry in visible_entries.iter() {
413 let entry_width = legend.style().spacing.symbol_width
414 + legend.style().spacing.symbol_text_gap
415 + entry.label().len() as u32 * legend.style().text.char_width;
416
417 let bounds = Rectangle::new(
418 Point::new(
419 viewport.top_left.x + x_offset as i32,
420 viewport.top_left.y,
421 ),
422 Size::new(entry_width, entry_height),
423 );
424 if layouts.push(bounds).is_err() {
425 return Err(crate::error::ChartError::ConfigurationError);
426 }
427
428 x_offset += entry_width + legend.style().spacing.entry_spacing;
429 }
430 }
431 }
432
433 Ok(layouts)
434 }
435
436 fn render_entry<D>(
437 &self,
438 entry: &crate::legend::types::StandardLegendEntry<C>,
439 bounds: Rectangle,
440 target: &mut D,
441 ) -> ChartResult<()>
442 where
443 D: DrawTarget<Color = C>,
444 {
445 let symbol_bounds = Rectangle::new(
447 bounds.top_left,
448 Size::new(bounds.size.width.min(20), bounds.size.height),
449 );
450 entry.render_symbol(
451 symbol_bounds,
452 &crate::legend::style::SymbolStyle::default(),
453 target,
454 )?;
455
456 Ok(())
460 }
461 }
462
463 Ok(layouts)
464 }
465
466 fn render_entry<D>(
467 &self,
468 entry: &crate::legend::DefaultLegendEntry<C>,
469 bounds: Rectangle,
470 target: &mut D,
471 ) -> ChartResult<()>
472 where
473 D: DrawTarget<Color = C>,
474 {
475 let symbol_bounds = Rectangle::new(
477 bounds.top_left,
478 Size::new(bounds.size.width.min(20), bounds.size.height),
479 );
480 entry.render_symbol(
481 symbol_bounds,
482 &crate::legend::style::SymbolStyle::default(),
483 target,
484 )?;
485
486 let text_x = bounds.top_left.x + 25; let text_y = bounds.top_left.y + (bounds.size.height as i32 / 2);
489
490 use embedded_graphics::{
492 mono_font::{ascii::FONT_6X10, MonoTextStyle},
493 text::{Baseline, Text},
494 };
495
496 let text_style = MonoTextStyle::new(
497 &FONT_6X10,
498 C::from(embedded_graphics::pixelcolor::Rgb565::BLACK),
499 );
500
501 Text::with_baseline(
502 entry.label(),
503 Point::new(text_x, text_y),
504 text_style,
505 Baseline::Middle,
506 )
507 .draw(target)
508 .map_err(|_| crate::error::ChartError::RenderingError)?;
509
510 Ok(())
511 }
512}
513
514#[derive(Debug, Clone)]
516pub struct StandardLegendRenderer<C: PixelColor> {
517 _phantom: core::marker::PhantomData<C>,
518}
519
520impl<C: PixelColor> StandardLegendRenderer<C> {
521 pub fn new() -> Self {
523 Self {
524 _phantom: core::marker::PhantomData,
525 }
526 }
527}
528
529impl<C: PixelColor> Default for StandardLegendRenderer<C> {
530 fn default() -> Self {
531 Self::new()
532 }
533}
534
535impl<C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>> LegendRenderer<C>
536 for StandardLegendRenderer<C>
537{
538 type Legend = crate::legend::types::StandardLegend<C>;
539
540 fn render<D>(
541 &self,
542 legend: &Self::Legend,
543 viewport: Rectangle,
544 target: &mut D,
545 ) -> ChartResult<()>
546 where
547 D: DrawTarget<Color = C>,
548 {
549 if legend.entries().is_empty() {
550 return Ok(());
551 }
552
553 let entry_bounds = self.calculate_layout(legend, viewport)?;
554
555 if let Some(bg_color) = legend.style().background.color {
557 use embedded_graphics::primitives::PrimitiveStyle;
558 use embedded_graphics::primitives::Rectangle as EgRectangle;
559
560 EgRectangle::new(viewport.top_left, viewport.size)
561 .into_styled(PrimitiveStyle::with_fill(bg_color))
562 .draw(target)
563 .map_err(|_| crate::error::ChartError::RenderingError)?;
564 }
565
566 for (entry, bounds) in legend
568 .entries()
569 .iter()
570 .filter(|e| e.is_visible())
571 .zip(entry_bounds.iter())
572 {
573 self.render_entry(entry, *bounds, target)?;
574 }
575
576 Ok(())
577 }
578
579 fn calculate_layout(
580 &self,
581 legend: &Self::Legend,
582 viewport: Rectangle,
583 ) -> ChartResult<heapless::Vec<Rectangle, 8>> {
584 let mut layouts = heapless::Vec::new();
585 let visible_entries: Vec<_> = legend.entries().iter().filter(|e| e.is_visible()).collect();
586
587 if visible_entries.is_empty() {
588 return Ok(layouts);
589 }
590
591 match legend.orientation() {
592 crate::legend::types::LegendOrientation::Vertical => {
593 let entry_height = legend.style().text.line_height;
594 let spacing = legend.style().spacing.entry_spacing;
595
596 for (i, _) in visible_entries.iter().enumerate() {
597 let y_offset = i as u32 * (entry_height + spacing);
598 let bounds = Rectangle::new(
599 Point::new(viewport.top_left.x, viewport.top_left.y + y_offset as i32),
600 Size::new(viewport.size.width, entry_height),
601 );
602 if layouts.push(bounds).is_err() {
603 return Err(crate::error::ChartError::ConfigurationError);
604 }
605 }
606 }
607 crate::legend::types::LegendOrientation::Horizontal => {
608 let mut x_offset = 0u32;
609 let entry_height = legend.style().text.line_height;
610
611 for entry in visible_entries.iter() {
612 let entry_width = legend.style().spacing.symbol_width
613 + legend.style().spacing.symbol_text_gap
614 + entry.label().len() as u32 * legend.style().text.char_width;
615
616 let bounds = Rectangle::new(
617 Point::new(viewport.top_left.x + x_offset as i32, viewport.top_left.y),
618 Size::new(entry_width, entry_height),
619 );
620 if layouts.push(bounds).is_err() {
621 return Err(crate::error::ChartError::ConfigurationError);
622 }
623
624 x_offset += entry_width + legend.style().spacing.entry_spacing;
625 }
626 }
627 }
628
629 Ok(layouts)
630 }
631
632 fn render_entry<D>(
633 &self,
634 entry: &crate::legend::types::StandardLegendEntry<C>,
635 bounds: Rectangle,
636 target: &mut D,
637 ) -> ChartResult<()>
638 where
639 D: DrawTarget<Color = C>,
640 {
641 let symbol_bounds = Rectangle::new(
643 bounds.top_left,
644 Size::new(bounds.size.width.min(20), bounds.size.height),
645 );
646 entry.render_symbol(
647 symbol_bounds,
648 &crate::legend::style::SymbolStyle::default(),
649 target,
650 )?;
651
652 let text_x = bounds.top_left.x + 25; let text_y = bounds.top_left.y + (bounds.size.height as i32 / 2);
655
656 use embedded_graphics::{
658 mono_font::{ascii::FONT_6X10, MonoTextStyle},
659 text::{Baseline, Text},
660 };
661
662 let text_style = MonoTextStyle::new(
663 &FONT_6X10,
664 C::from(embedded_graphics::pixelcolor::Rgb565::BLACK),
665 );
666
667 Text::with_baseline(
668 entry.label(),
669 Point::new(text_x, text_y),
670 text_style,
671 Baseline::Middle,
672 )
673 .draw(target)
674 .map_err(|_| crate::error::ChartError::RenderingError)?;
675
676 Ok(())
677 }
678}