1use crate::error::ChartResult;
4use embedded_graphics::{prelude::*, primitives::Rectangle};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum LegendPosition {
9 Top,
11 Bottom,
13 Left,
15 Right,
17 TopLeft,
19 TopRight,
21 BottomLeft,
23 BottomRight,
25 Custom(Point),
27 Floating(Point),
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum LegendAlignment {
34 Start,
36 Center,
38 End,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct LegendMargins {
45 pub top: u32,
47 pub right: u32,
49 pub bottom: u32,
51 pub left: u32,
53}
54
55#[derive(Debug, Clone)]
57pub struct PositionCalculator {
58 chart_area: Rectangle,
60 plot_area: Rectangle,
62 margins: LegendMargins,
64 alignment: LegendAlignment,
66}
67
68impl PositionCalculator {
69 pub fn new(chart_area: Rectangle, plot_area: Rectangle) -> Self {
71 Self {
72 chart_area,
73 plot_area,
74 margins: LegendMargins::default(),
75 alignment: LegendAlignment::Start,
76 }
77 }
78
79 pub fn with_margins(mut self, margins: LegendMargins) -> Self {
81 self.margins = margins;
82 self
83 }
84
85 pub fn with_alignment(mut self, alignment: LegendAlignment) -> Self {
87 self.alignment = alignment;
88 self
89 }
90
91 pub fn calculate_legend_rect(
93 &self,
94 position: LegendPosition,
95 legend_size: Size,
96 ) -> ChartResult<Rectangle> {
97 match position {
98 LegendPosition::Top => self.calculate_top_position(legend_size),
99 LegendPosition::Bottom => self.calculate_bottom_position(legend_size),
100 LegendPosition::Left => self.calculate_left_position(legend_size),
101 LegendPosition::Right => self.calculate_right_position(legend_size),
102 LegendPosition::TopLeft => self.calculate_corner_position(legend_size, true, true),
103 LegendPosition::TopRight => self.calculate_corner_position(legend_size, true, false),
104 LegendPosition::BottomLeft => self.calculate_corner_position(legend_size, false, true),
105 LegendPosition::BottomRight => {
106 self.calculate_corner_position(legend_size, false, false)
107 }
108 LegendPosition::Custom(point) => Ok(Rectangle::new(point, legend_size)),
109 LegendPosition::Floating(point) => Ok(Rectangle::new(point, legend_size)),
110 }
111 }
112
113 pub fn calculate_adjusted_plot_area(
115 &self,
116 position: LegendPosition,
117 legend_size: Size,
118 ) -> ChartResult<Rectangle> {
119 match position {
120 LegendPosition::Top => {
121 let height_reduction = legend_size.height + self.margins.vertical();
122 Ok(Rectangle::new(
123 Point::new(
124 self.plot_area.top_left.x,
125 self.plot_area.top_left.y + height_reduction as i32,
126 ),
127 Size::new(
128 self.plot_area.size.width,
129 self.plot_area.size.height.saturating_sub(height_reduction),
130 ),
131 ))
132 }
133 LegendPosition::Bottom => {
134 let height_reduction = legend_size.height + self.margins.vertical();
135 Ok(Rectangle::new(
136 self.plot_area.top_left,
137 Size::new(
138 self.plot_area.size.width,
139 self.plot_area.size.height.saturating_sub(height_reduction),
140 ),
141 ))
142 }
143 LegendPosition::Left => {
144 let width_reduction = legend_size.width + self.margins.horizontal();
145 Ok(Rectangle::new(
146 Point::new(
147 self.plot_area.top_left.x + width_reduction as i32,
148 self.plot_area.top_left.y,
149 ),
150 Size::new(
151 self.plot_area.size.width.saturating_sub(width_reduction),
152 self.plot_area.size.height,
153 ),
154 ))
155 }
156 LegendPosition::Right => {
157 let width_reduction = legend_size.width + self.margins.horizontal();
158 Ok(Rectangle::new(
159 self.plot_area.top_left,
160 Size::new(
161 self.plot_area.size.width.saturating_sub(width_reduction),
162 self.plot_area.size.height,
163 ),
164 ))
165 }
166 _ => Ok(self.plot_area),
168 }
169 }
170
171 pub fn validate_legend_fit(
173 &self,
174 position: LegendPosition,
175 legend_size: Size,
176 ) -> ChartResult<bool> {
177 let legend_rect = self.calculate_legend_rect(position, legend_size)?;
178
179 let fits_horizontally = legend_rect.top_left.x >= self.chart_area.top_left.x
183 && legend_rect.top_left.x + legend_size.width as i32
184 <= self.chart_area.top_left.x + self.chart_area.size.width as i32;
185
186 let fits_vertically = legend_rect.top_left.y >= self.chart_area.top_left.y
187 && legend_rect.top_left.y + legend_size.height as i32
188 <= self.chart_area.top_left.y + self.chart_area.size.height as i32;
189
190 Ok(fits_horizontally && fits_vertically)
191 }
192
193 fn calculate_top_position(&self, legend_size: Size) -> ChartResult<Rectangle> {
196 let x = match self.alignment {
197 LegendAlignment::Start => self.chart_area.top_left.x + self.margins.left as i32,
198 LegendAlignment::Center => {
199 self.chart_area.top_left.x
200 + (self.chart_area.size.width as i32 - legend_size.width as i32) / 2
201 }
202 LegendAlignment::End => {
203 self.chart_area.top_left.x + self.chart_area.size.width as i32
204 - legend_size.width as i32
205 - self.margins.right as i32
206 }
207 };
208
209 let y = self.chart_area.top_left.y + self.margins.top as i32;
210
211 Ok(Rectangle::new(Point::new(x, y), legend_size))
212 }
213
214 fn calculate_bottom_position(&self, legend_size: Size) -> ChartResult<Rectangle> {
215 let x = match self.alignment {
216 LegendAlignment::Start => self.chart_area.top_left.x + self.margins.left as i32,
217 LegendAlignment::Center => {
218 self.chart_area.top_left.x
219 + (self.chart_area.size.width as i32 - legend_size.width as i32) / 2
220 }
221 LegendAlignment::End => {
222 self.chart_area.top_left.x + self.chart_area.size.width as i32
223 - legend_size.width as i32
224 - self.margins.right as i32
225 }
226 };
227
228 let y = self.chart_area.top_left.y + self.chart_area.size.height as i32
229 - legend_size.height as i32
230 - self.margins.bottom as i32;
231
232 Ok(Rectangle::new(Point::new(x, y), legend_size))
233 }
234
235 fn calculate_left_position(&self, legend_size: Size) -> ChartResult<Rectangle> {
236 let x = self.chart_area.top_left.x + self.margins.left as i32;
237
238 let y = match self.alignment {
239 LegendAlignment::Start => self.chart_area.top_left.y + self.margins.top as i32,
240 LegendAlignment::Center => {
241 self.chart_area.top_left.y
242 + (self.chart_area.size.height as i32 - legend_size.height as i32) / 2
243 }
244 LegendAlignment::End => {
245 self.chart_area.top_left.y + self.chart_area.size.height as i32
246 - legend_size.height as i32
247 - self.margins.bottom as i32
248 }
249 };
250
251 Ok(Rectangle::new(Point::new(x, y), legend_size))
252 }
253
254 fn calculate_right_position(&self, legend_size: Size) -> ChartResult<Rectangle> {
255 let x = self.chart_area.top_left.x + self.chart_area.size.width as i32
257 - legend_size.width as i32
258 - self.margins.right as i32;
259
260 let y = match self.alignment {
261 LegendAlignment::Start => self.chart_area.top_left.y + self.margins.top as i32,
262 LegendAlignment::Center => {
263 self.chart_area.top_left.y
264 + (self.chart_area.size.height as i32 - legend_size.height as i32) / 2
265 }
266 LegendAlignment::End => {
267 self.chart_area.top_left.y + self.chart_area.size.height as i32
268 - legend_size.height as i32
269 - self.margins.bottom as i32
270 }
271 };
272
273 Ok(Rectangle::new(Point::new(x, y), legend_size))
274 }
275
276 fn calculate_corner_position(
277 &self,
278 legend_size: Size,
279 top: bool,
280 left: bool,
281 ) -> ChartResult<Rectangle> {
282 let x = if left {
283 self.chart_area.top_left.x + self.margins.left as i32
284 } else {
285 self.chart_area.top_left.x + self.chart_area.size.width as i32
286 - legend_size.width as i32
287 - self.margins.right as i32
288 };
289
290 let y = if top {
291 self.chart_area.top_left.y + self.margins.top as i32
292 } else {
293 self.chart_area.top_left.y + self.chart_area.size.height as i32
294 - legend_size.height as i32
295 - self.margins.bottom as i32
296 };
297
298 Ok(Rectangle::new(Point::new(x, y), legend_size))
299 }
300}
301
302impl LegendMargins {
303 pub const fn all(value: u32) -> Self {
305 Self {
306 top: value,
307 right: value,
308 bottom: value,
309 left: value,
310 }
311 }
312
313 pub const fn symmetric(horizontal: u32, vertical: u32) -> Self {
315 Self {
316 top: vertical,
317 right: horizontal,
318 bottom: vertical,
319 left: horizontal,
320 }
321 }
322
323 pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
325 Self {
326 top,
327 right,
328 bottom,
329 left,
330 }
331 }
332
333 pub const fn horizontal(&self) -> u32 {
335 self.left + self.right
336 }
337
338 pub const fn vertical(&self) -> u32 {
340 self.top + self.bottom
341 }
342}
343
344impl Default for LegendPosition {
345 fn default() -> Self {
346 Self::Right
347 }
348}
349
350impl Default for LegendAlignment {
351 fn default() -> Self {
352 Self::Start
353 }
354}
355
356impl Default for LegendMargins {
357 fn default() -> Self {
358 Self::all(8)
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn test_legend_margins() {
368 let margins = LegendMargins::all(10);
369 assert_eq!(margins.horizontal(), 20);
370 assert_eq!(margins.vertical(), 20);
371
372 let margins = LegendMargins::symmetric(5, 8);
373 assert_eq!(margins.horizontal(), 10);
374 assert_eq!(margins.vertical(), 16);
375 }
376
377 #[test]
378 fn test_position_calculator() {
379 let chart_area = Rectangle::new(Point::zero(), Size::new(200, 150));
380 let plot_area = Rectangle::new(Point::new(20, 20), Size::new(160, 110));
381 let calculator = PositionCalculator::new(chart_area, plot_area);
382
383 let legend_size = Size::new(60, 40);
384
385 let legend_rect = calculator
387 .calculate_legend_rect(LegendPosition::Right, legend_size)
388 .unwrap();
389 assert!(
391 legend_rect.top_left.x + legend_size.width as i32
392 <= chart_area.top_left.x + chart_area.size.width as i32
393 );
394 assert!(legend_rect.top_left.x >= chart_area.top_left.x);
395
396 assert!(calculator
398 .validate_legend_fit(LegendPosition::Right, legend_size)
399 .unwrap());
400 }
401
402 #[test]
403 fn test_adjusted_plot_area() {
404 let chart_area = Rectangle::new(Point::zero(), Size::new(200, 150));
405 let plot_area = Rectangle::new(Point::new(20, 20), Size::new(160, 110));
406 let calculator = PositionCalculator::new(chart_area, plot_area);
407
408 let legend_size = Size::new(60, 40);
409
410 let adjusted = calculator
412 .calculate_adjusted_plot_area(LegendPosition::Right, legend_size)
413 .unwrap();
414 assert!(adjusted.size.width < plot_area.size.width);
415
416 let adjusted = calculator
418 .calculate_adjusted_plot_area(LegendPosition::Bottom, legend_size)
419 .unwrap();
420 assert!(adjusted.size.height < plot_area.size.height);
421 }
422}