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 LegendPosition::Inside(fx, fy) => (
134 plot_area.x + fx * plot_area.width,
135 plot_area.y + (1.0 - fy) * plot_area.height,
136 false,
137 ),
138 }
139}
140
141#[allow(clippy::too_many_arguments)]
143fn draw_discrete_legend_at(
144 scales: &ScaleSet,
145 aes: &Aesthetic,
146 scale: &dyn crate::scale::Scale,
147 theme: &Theme,
148 legend_x: f64,
149 legend_y: f64,
150 backend: &mut dyn DrawBackend,
151 guide: &GuideLegend,
152) -> Result<f64, RenderError> {
153 let mut breaks = scale.breaks();
154 if breaks.is_empty() {
155 return Ok(0.0);
156 }
157
158 if guide.reverse {
160 breaks.reverse();
161 }
162
163 let item_height = theme.legend_key_height;
164 let swatch_size = theme.legend_key_width;
165
166 let title = guide.title.as_deref().unwrap_or_else(|| scale.name());
168 let legend_family = if theme.legend_title.family.is_empty() {
169 None
170 } else {
171 Some(theme.legend_title.family.clone())
172 };
173 let title_offset = if !title.is_empty() {
174 backend.draw_text(
175 title,
176 (legend_x, legend_y),
177 &TextStyle {
178 color: theme.legend_title.color,
179 size: theme.legend_title.size,
180 anchor: TextAnchor::Start,
181 angle: 0.0,
182 family: legend_family,
183 },
184 )?;
185 theme.legend_title.size + 4.0
186 } else {
187 0.0
188 };
189
190 let items_y = legend_y + title_offset;
191
192 if theme.legend_background.visible {
194 let total_height = breaks.len() as f64 * item_height;
195 let total_width = swatch_size + theme.legend_spacing + theme.legend_text.size * 6.0;
196 if let Some(fill) = theme.legend_background.fill {
197 backend.draw_rect(
198 (legend_x - 2.0, items_y - 2.0),
199 (legend_x + total_width + 2.0, items_y + total_height + 2.0),
200 &RectStyle {
201 fill: Some(fill),
202 stroke: theme.legend_background.color,
203 stroke_width: theme.legend_background.width,
204 alpha: 1.0,
205 clip: false,
206 },
207 )?;
208 }
209 }
210
211 for (i, (_, label)) in breaks.iter().enumerate() {
212 let y = items_y + i as f64 * item_height;
213 let center_x = legend_x + swatch_size / 2.0;
214 let center_y = y + swatch_size / 2.0;
215
216 if theme.legend_key.visible {
218 if let Some(fill) = theme.legend_key.fill {
219 backend.draw_rect(
220 (legend_x, y),
221 (legend_x + swatch_size, y + swatch_size),
222 &RectStyle {
223 fill: Some(fill),
224 stroke: theme.legend_key.color,
225 stroke_width: theme.legend_key.width,
226 alpha: 1.0,
227 clip: false,
228 },
229 )?;
230 }
231 }
232
233 let value = Value::Str(label.clone());
235 match aes {
236 Aesthetic::Color | Aesthetic::Fill => {
237 let color = scales.map_color(aes, &value).unwrap_or((127, 127, 127));
238 backend.draw_rect(
239 (legend_x, y),
240 (legend_x + swatch_size, y + swatch_size),
241 &RectStyle {
242 fill: Some(color),
243 stroke: None,
244 stroke_width: 0.0,
245 alpha: 1.0,
246 clip: false,
247 },
248 )?;
249 }
250 Aesthetic::Shape => {
251 let shape = scales
252 .map_shape(&value)
253 .unwrap_or(crate::render::backend::PointShape::Circle);
254 backend.draw_shape(
255 (center_x, center_y),
256 swatch_size / 3.0,
257 &PointStyle {
258 color: (50, 50, 50),
259 alpha: 1.0,
260 filled: true,
261 shape,
262 },
263 )?;
264 }
265 Aesthetic::Linetype => {
266 let lt = scales.map_linetype(&value).unwrap_or(Linetype::Solid);
267 backend.draw_line(
268 &[
269 (legend_x + 2.0, center_y),
270 (legend_x + swatch_size - 2.0, center_y),
271 ],
272 &LineStyle {
273 color: (50, 50, 50),
274 width: 1.5,
275 alpha: 1.0,
276 linetype: lt,
277 },
278 )?;
279 }
280 Aesthetic::Size => {
281 let size = scales.map_size(&value).unwrap_or(3.0);
283 backend.draw_shape(
284 (center_x, center_y),
285 size.min(swatch_size / 2.0),
286 &PointStyle {
287 color: (50, 50, 50),
288 alpha: 1.0,
289 filled: true,
290 shape: crate::render::backend::PointShape::Circle,
291 },
292 )?;
293 }
294 Aesthetic::Alpha => {
295 let alpha = scales.map_alpha(&value).unwrap_or(1.0);
296 backend.draw_rect(
297 (legend_x, y),
298 (legend_x + swatch_size, y + swatch_size),
299 &RectStyle {
300 fill: Some((50, 50, 50)),
301 stroke: None,
302 stroke_width: 0.0,
303 alpha,
304 clip: false,
305 },
306 )?;
307 }
308 _ => {}
309 }
310
311 let label_family = if theme.legend_text.family.is_empty() {
313 None
314 } else {
315 Some(theme.legend_text.family.clone())
316 };
317 backend.draw_text(
318 label,
319 (legend_x + swatch_size + theme.legend_spacing, center_y),
320 &TextStyle {
321 color: theme.legend_text.color,
322 size: theme.legend_text.size,
323 anchor: TextAnchor::Start,
324 angle: 0.0,
325 family: label_family,
326 },
327 )?;
328 }
329
330 Ok(title_offset + breaks.len() as f64 * item_height)
331}
332
333fn draw_continuous_legend_at(
335 scale: &dyn crate::scale::Scale,
336 theme: &Theme,
337 legend_x: f64,
338 legend_y: f64,
339 backend: &mut dyn DrawBackend,
340 guide: &GuideLegend,
341) -> Result<f64, RenderError> {
342 let breaks = scale.breaks();
343 if breaks.is_empty() {
344 return Ok(0.0);
345 }
346
347 let bar_width = theme.legend_key_width;
348 let bar_height = theme.legend_key_height * 8.0;
349
350 let title = guide.title.as_deref().unwrap_or_else(|| scale.name());
352 let cont_family = if theme.legend_title.family.is_empty() {
353 None
354 } else {
355 Some(theme.legend_title.family.clone())
356 };
357 let title_offset = if !title.is_empty() {
358 backend.draw_text(
359 title,
360 (legend_x, legend_y),
361 &TextStyle {
362 color: theme.legend_title.color,
363 size: theme.legend_title.size,
364 anchor: TextAnchor::Start,
365 angle: 0.0,
366 family: cont_family,
367 },
368 )?;
369 theme.legend_title.size + 4.0
370 } else {
371 0.0
372 };
373
374 let bar_top = legend_y + title_offset;
375
376 if theme.legend_background.visible {
378 let total_width = bar_width + theme.legend_spacing + theme.legend_text.size * 6.0;
379 if let Some(fill) = theme.legend_background.fill {
380 backend.draw_rect(
381 (legend_x - 2.0, bar_top - 2.0),
382 (legend_x + total_width + 2.0, bar_top + bar_height + 2.0),
383 &RectStyle {
384 fill: Some(fill),
385 stroke: theme.legend_background.color,
386 stroke_width: theme.legend_background.width,
387 alpha: 1.0,
388 clip: false,
389 },
390 )?;
391 }
392 }
393
394 let (data_min, data_max) = scale.domain().unwrap_or((0.0, 1.0));
397 let n_slices = 50;
398 let slice_height = bar_height / n_slices as f64;
399 for i in 0..n_slices {
400 let t = 1.0 - i as f64 / n_slices as f64;
401 let data_val = data_min + t * (data_max - data_min);
402 let color = scale
403 .map_to_color(&Value::Float(data_val))
404 .unwrap_or((127, 127, 127));
405 let sy = bar_top + i as f64 * slice_height;
406 backend.draw_rect(
407 (legend_x, sy),
408 (legend_x + bar_width, sy + slice_height + 0.5),
409 &RectStyle {
410 fill: Some(color),
411 stroke: None,
412 stroke_width: 0.0,
413 alpha: 1.0,
414 clip: false,
415 },
416 )?;
417 }
418
419 let border_style = LineStyle {
421 color: theme.legend_key.color.unwrap_or((50, 50, 50)),
422 width: 0.5,
423 alpha: 1.0,
424 linetype: Linetype::Solid,
425 };
426 backend.draw_line(
427 &[
428 (legend_x, bar_top),
429 (legend_x + bar_width, bar_top),
430 (legend_x + bar_width, bar_top + bar_height),
431 (legend_x, bar_top + bar_height),
432 (legend_x, bar_top),
433 ],
434 &border_style,
435 )?;
436
437 let tick_len = 3.0;
439 for (pos, label) in &breaks {
440 let tick_y = bar_top + bar_height * (1.0 - pos);
441 backend.draw_line(
442 &[
443 (legend_x + bar_width, tick_y),
444 (legend_x + bar_width + tick_len, tick_y),
445 ],
446 &border_style,
447 )?;
448 let tick_family = if theme.legend_text.family.is_empty() {
449 None
450 } else {
451 Some(theme.legend_text.family.clone())
452 };
453 backend.draw_text(
454 label,
455 (
456 legend_x + bar_width + tick_len + theme.legend_spacing,
457 tick_y,
458 ),
459 &TextStyle {
460 color: theme.legend_text.color,
461 size: theme.legend_text.size,
462 anchor: TextAnchor::Start,
463 angle: 0.0,
464 family: tick_family,
465 },
466 )?;
467 }
468
469 Ok(title_offset + bar_height)
470}