embedded_charts/legend/
mod.rs1pub mod builder;
7pub mod position;
8pub mod style;
9pub mod traits;
10pub mod types;
11
12pub 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#[derive(Debug, Clone)]
28pub struct DefaultLegend<C: PixelColor> {
29 pub entries: heapless::Vec<DefaultLegendEntry<C>, 8>,
31 pub position: LegendPosition,
33 pub orientation: LegendOrientation,
35 pub style: LegendStyle<C>,
37}
38
39#[derive(Debug, Clone)]
41pub struct DefaultLegendEntry<C: PixelColor> {
42 pub label: heapless::String<32>,
44 pub entry_type: LegendEntryType<C>,
46 pub visible: bool,
48}
49
50impl<C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>> DefaultLegend<C> {
51 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 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 pub fn set_orientation(&mut self, orientation: LegendOrientation) {
81 self.orientation = orientation;
82 }
83
84 pub fn set_style(&mut self, style: LegendStyle<C>) {
86 self.style = style;
87 }
88
89 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}