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