1use crate::aes::Aesthetic;
2use crate::data::Value;
3use crate::guide::config::GuideLegend;
4use crate::render::backend::{
5 DrawBackend, LineStyle, Linetype, PointStyle, RectStyle, TextAnchor, TextStyle,
6};
7use crate::render::{Rect, RenderError};
8use crate::scale::ScaleSet;
9use crate::theme::{LegendPosition, Theme};
10
11const LEGEND_AESTHETICS: &[Aesthetic] = &[
13 Aesthetic::Color,
14 Aesthetic::Fill,
15 Aesthetic::Shape,
16 Aesthetic::Linetype,
17 Aesthetic::Size,
18 Aesthetic::Alpha,
19];
20
21pub fn draw_legend(
23 scales: &ScaleSet,
24 theme: &Theme,
25 plot_area: &Rect,
26 backend: &mut dyn DrawBackend,
27 guide: &GuideLegend,
28 suppressed: &std::collections::HashSet<Aesthetic>,
29) -> Result<(), RenderError> {
30 if matches!(theme.legend_position, LegendPosition::None) {
31 return Ok(());
32 }
33
34 let mut legend_scales: Vec<&Aesthetic> = Vec::new();
36 for aes in LEGEND_AESTHETICS {
37 if suppressed.contains(aes) {
39 continue;
40 }
41 if let Some(scale) = scales.get(aes) {
42 if !scale.breaks().is_empty() {
43 if *aes == Aesthetic::Fill && legend_scales.contains(&&Aesthetic::Color) {
45 continue;
46 }
47 legend_scales.push(aes);
48 }
49 }
50 }
51
52 if legend_scales.is_empty() {
53 return Ok(());
54 }
55
56 let (legend_x, legend_y, mut is_horizontal) = legend_position(theme, plot_area);
58 if let Some(dir) = theme.legend_direction {
60 is_horizontal = matches!(dir, crate::theme::LegendDirection::Horizontal);
61 }
62
63 let mut offset_y = legend_y;
64 let mut offset_x = legend_x;
65
66 for aes in &legend_scales {
67 let scale = scales.get(aes).unwrap();
68
69 if scale.is_discrete() {
70 if is_horizontal {
71 let width = draw_discrete_legend_at(
72 scales, aes, scale, theme, offset_x, offset_y, backend, guide,
73 )?;
74 offset_x += width + theme.legend_spacing * 2.0;
75 } else {
76 let height = draw_discrete_legend_at(
77 scales, aes, scale, theme, offset_x, offset_y, backend, guide,
78 )?;
79 offset_y += height + theme.legend_spacing * 2.0;
80 }
81 } else {
82 if matches!(aes, Aesthetic::Color | Aesthetic::Fill) {
84 let height =
85 draw_continuous_legend_at(scale, theme, offset_x, offset_y, backend, guide)?;
86 if is_horizontal {
87 offset_x += theme.legend_key_width
88 + theme.legend_text.size * 6.0
89 + theme.legend_spacing * 2.0;
90 } else {
91 offset_y += height + theme.legend_spacing * 2.0;
92 }
93 } else {
94 let height = draw_discrete_legend_at(
96 scales, aes, scale, theme, offset_x, offset_y, backend, guide,
97 )?;
98 if is_horizontal {
99 offset_x += theme.legend_key_width
100 + theme.legend_text.size * 6.0
101 + theme.legend_spacing * 2.0;
102 } else {
103 offset_y += height + theme.legend_spacing * 2.0;
104 }
105 }
106 }
107 }
108
109 Ok(())
110}
111
112fn legend_position(theme: &Theme, plot_area: &Rect) -> (f64, f64, bool) {
115 match theme.legend_position {
116 LegendPosition::Right => (
117 plot_area.x + plot_area.width + theme.legend_margin.left,
118 plot_area.y + theme.legend_margin.top,
119 false,
120 ),
121 LegendPosition::Left => (
122 theme.legend_margin.left,
123 plot_area.y + theme.legend_margin.top,
124 false,
125 ),
126 LegendPosition::Top => (
127 plot_area.x + theme.legend_margin.left,
128 theme.legend_margin.top,
129 true,
130 ),
131 LegendPosition::Bottom => (
132 plot_area.x + theme.legend_margin.left,
133 plot_area.y + plot_area.height + theme.legend_margin.top + 30.0,
134 true,
135 ),
136 LegendPosition::None => (0.0, 0.0, false),
137 LegendPosition::Inside(fx, fy) => (
138 plot_area.x + fx * plot_area.width,
139 plot_area.y + (1.0 - fy) * plot_area.height,
140 false,
141 ),
142 }
143}
144
145#[allow(clippy::too_many_arguments)]
147fn draw_discrete_legend_at(
148 scales: &ScaleSet,
149 aes: &Aesthetic,
150 scale: &dyn crate::scale::Scale,
151 theme: &Theme,
152 legend_x: f64,
153 legend_y: f64,
154 backend: &mut dyn DrawBackend,
155 guide: &GuideLegend,
156) -> Result<f64, RenderError> {
157 let mut breaks = scale.breaks();
158 if breaks.is_empty() {
159 return Ok(0.0);
160 }
161
162 if guide.reverse {
164 breaks.reverse();
165 }
166
167 let item_height = theme.legend_key_height;
168 let swatch_size = theme.legend_key_width;
169
170 let title = guide.title.as_deref().unwrap_or_else(|| scale.name());
172 let legend_family = if theme.legend_title.family.is_empty() {
173 None
174 } else {
175 Some(theme.legend_title.family.clone())
176 };
177 let title_offset = if !title.is_empty() {
178 backend.draw_text(
179 title,
180 (legend_x, legend_y),
181 &TextStyle {
182 color: theme.legend_title.color,
183 size: theme.legend_title.size,
184 anchor: TextAnchor::Start,
185 angle: 0.0,
186 family: legend_family,
187 face: theme.legend_title.face,
188 },
189 )?;
190 theme.legend_title.size + 4.0
191 } else {
192 0.0
193 };
194
195 let items_y = legend_y + title_offset;
196
197 if theme.legend_background.visible {
199 let total_height = breaks.len() as f64 * item_height;
200 let total_width = swatch_size + theme.legend_spacing + theme.legend_text.size * 6.0;
201 if let Some(fill) = theme.legend_background.fill {
202 backend.draw_rect(
203 (legend_x - 2.0, items_y - 2.0),
204 (legend_x + total_width + 2.0, items_y + total_height + 2.0),
205 &RectStyle {
206 fill: Some(fill),
207 stroke: theme.legend_background.color,
208 stroke_width: theme.legend_background.width,
209 alpha: 1.0,
210 clip: false,
211 },
212 )?;
213 }
214 }
215
216 for (i, (_, label)) in breaks.iter().enumerate() {
217 let y = items_y + i as f64 * item_height;
218 let center_x = legend_x + swatch_size / 2.0;
219 let center_y = y + swatch_size / 2.0;
220
221 if theme.legend_key.visible {
223 if let Some(fill) = theme.legend_key.fill {
224 backend.draw_rect(
225 (legend_x, y),
226 (legend_x + swatch_size, y + swatch_size),
227 &RectStyle {
228 fill: Some(fill),
229 stroke: theme.legend_key.color,
230 stroke_width: theme.legend_key.width,
231 alpha: 1.0,
232 clip: false,
233 },
234 )?;
235 }
236 }
237
238 let value = Value::Str(label.clone());
240 match aes {
241 Aesthetic::Color | Aesthetic::Fill => {
242 let color = scales.map_color(aes, &value).unwrap_or((127, 127, 127));
243 backend.draw_rect(
244 (legend_x, y),
245 (legend_x + swatch_size, y + swatch_size),
246 &RectStyle {
247 fill: Some(color),
248 stroke: None,
249 stroke_width: 0.0,
250 alpha: 1.0,
251 clip: false,
252 },
253 )?;
254 }
255 Aesthetic::Shape => {
256 let shape = scales
257 .map_shape(&value)
258 .unwrap_or(crate::render::backend::PointShape::Circle);
259 backend.draw_shape(
260 (center_x, center_y),
261 swatch_size / 3.0,
262 &PointStyle {
263 color: (50, 50, 50),
264 alpha: 1.0,
265 filled: true,
266 shape,
267 },
268 )?;
269 }
270 Aesthetic::Linetype => {
271 let lt = scales.map_linetype(&value).unwrap_or(Linetype::Solid);
272 backend.draw_line(
273 &[
274 (legend_x + 2.0, center_y),
275 (legend_x + swatch_size - 2.0, center_y),
276 ],
277 &LineStyle {
278 color: (50, 50, 50),
279 width: 1.5,
280 alpha: 1.0,
281 linetype: lt,
282 },
283 )?;
284 }
285 Aesthetic::Size => {
286 let size = scales.map_size(&value).unwrap_or(3.0);
288 backend.draw_shape(
289 (center_x, center_y),
290 size.min(swatch_size / 2.0),
291 &PointStyle {
292 color: (50, 50, 50),
293 alpha: 1.0,
294 filled: true,
295 shape: crate::render::backend::PointShape::Circle,
296 },
297 )?;
298 }
299 Aesthetic::Alpha => {
300 let alpha = scales.map_alpha(&value).unwrap_or(1.0);
301 backend.draw_rect(
302 (legend_x, y),
303 (legend_x + swatch_size, y + swatch_size),
304 &RectStyle {
305 fill: Some((50, 50, 50)),
306 stroke: None,
307 stroke_width: 0.0,
308 alpha,
309 clip: false,
310 },
311 )?;
312 }
313 _ => {}
314 }
315
316 let label_family = if theme.legend_text.family.is_empty() {
318 None
319 } else {
320 Some(theme.legend_text.family.clone())
321 };
322 backend.draw_text(
323 label,
324 (legend_x + swatch_size + theme.legend_spacing, center_y),
325 &TextStyle {
326 color: theme.legend_text.color,
327 size: theme.legend_text.size,
328 anchor: TextAnchor::Start,
329 angle: 0.0,
330 family: label_family,
331 face: theme.legend_text.face,
332 },
333 )?;
334 }
335
336 Ok(title_offset + breaks.len() as f64 * item_height)
337}
338
339fn draw_continuous_legend_at(
341 scale: &dyn crate::scale::Scale,
342 theme: &Theme,
343 legend_x: f64,
344 legend_y: f64,
345 backend: &mut dyn DrawBackend,
346 guide: &GuideLegend,
347) -> Result<f64, RenderError> {
348 let breaks = scale.breaks();
349 if breaks.is_empty() {
350 return Ok(0.0);
351 }
352
353 let bar_width = theme.legend_key_width;
354 let bar_height = theme.legend_key_height * 8.0;
355
356 let title = guide.title.as_deref().unwrap_or_else(|| scale.name());
358 let cont_family = if theme.legend_title.family.is_empty() {
359 None
360 } else {
361 Some(theme.legend_title.family.clone())
362 };
363 let title_offset = if !title.is_empty() {
364 backend.draw_text(
365 title,
366 (legend_x, legend_y),
367 &TextStyle {
368 color: theme.legend_title.color,
369 size: theme.legend_title.size,
370 anchor: TextAnchor::Start,
371 angle: 0.0,
372 family: cont_family,
373 face: theme.legend_title.face,
374 },
375 )?;
376 theme.legend_title.size + 4.0
377 } else {
378 0.0
379 };
380
381 let bar_top = legend_y + title_offset;
382
383 if theme.legend_background.visible {
385 let total_width = bar_width + theme.legend_spacing + theme.legend_text.size * 6.0;
386 if let Some(fill) = theme.legend_background.fill {
387 backend.draw_rect(
388 (legend_x - 2.0, bar_top - 2.0),
389 (legend_x + total_width + 2.0, bar_top + bar_height + 2.0),
390 &RectStyle {
391 fill: Some(fill),
392 stroke: theme.legend_background.color,
393 stroke_width: theme.legend_background.width,
394 alpha: 1.0,
395 clip: false,
396 },
397 )?;
398 }
399 }
400
401 let (data_min, data_max) = scale.domain().unwrap_or((0.0, 1.0));
404 let n_slices = 50;
405 let slice_height = bar_height / n_slices as f64;
406 for i in 0..n_slices {
407 let t = 1.0 - i as f64 / n_slices as f64;
408 let data_val = data_min + t * (data_max - data_min);
409 let color = scale
410 .map_to_color(&Value::Float(data_val))
411 .unwrap_or((127, 127, 127));
412 let sy = bar_top + i as f64 * slice_height;
413 backend.draw_rect(
414 (legend_x, sy),
415 (legend_x + bar_width, sy + slice_height + 0.5),
416 &RectStyle {
417 fill: Some(color),
418 stroke: None,
419 stroke_width: 0.0,
420 alpha: 1.0,
421 clip: false,
422 },
423 )?;
424 }
425
426 let border_style = LineStyle {
428 color: theme.legend_key.color.unwrap_or((50, 50, 50)),
429 width: 0.5,
430 alpha: 1.0,
431 linetype: Linetype::Solid,
432 };
433 backend.draw_line(
434 &[
435 (legend_x, bar_top),
436 (legend_x + bar_width, bar_top),
437 (legend_x + bar_width, bar_top + bar_height),
438 (legend_x, bar_top + bar_height),
439 (legend_x, bar_top),
440 ],
441 &border_style,
442 )?;
443
444 let tick_len = 3.0;
446 for (pos, label) in &breaks {
447 let tick_y = bar_top + bar_height * (1.0 - pos);
448 backend.draw_line(
449 &[
450 (legend_x + bar_width, tick_y),
451 (legend_x + bar_width + tick_len, tick_y),
452 ],
453 &border_style,
454 )?;
455 let tick_family = if theme.legend_text.family.is_empty() {
456 None
457 } else {
458 Some(theme.legend_text.family.clone())
459 };
460 backend.draw_text(
461 label,
462 (
463 legend_x + bar_width + tick_len + theme.legend_spacing,
464 tick_y,
465 ),
466 &TextStyle {
467 color: theme.legend_text.color,
468 size: theme.legend_text.size,
469 anchor: TextAnchor::Start,
470 angle: 0.0,
471 family: tick_family,
472 face: theme.legend_text.face,
473 },
474 )?;
475 }
476
477 Ok(title_offset + bar_height)
478}