Skip to main content

ggplot_rs/guide/
key.rs

1//! Legend key glyphs (#410): ggplot2's `draw_key_*` — which picture a
2//! layer contributes to a legend key — chosen per geom (`Geom::key_glyph`)
3//! or per layer (`GGPlot::key_glyph`), and several layers drawn into one key.
4
5use crate::aes::Aesthetic;
6use crate::render::backend::{
7    DrawBackend, LineStyle, Linetype, PointShape, PointStyle, RectStyle, TextAnchor, TextStyle,
8};
9use crate::render::RenderError;
10
11/// The picture a layer draws in a legend key (ggplot2's `key_glyph =`).
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum KeyGlyph {
14    /// A point (`draw_key_point`).
15    Point,
16    /// A horizontal line (`draw_key_path`).
17    Path,
18    /// A filled square (`draw_key_rect`).
19    Rect,
20    /// A filled square with an outline (`draw_key_polygon`).
21    Polygon,
22    /// A small boxplot (`draw_key_boxplot`).
23    Boxplot,
24    /// A crossbar (`draw_key_crossbar`).
25    Crossbar,
26    /// A vertical line with a point (`draw_key_pointrange`).
27    Pointrange,
28    /// A vertical line (`draw_key_linerange`).
29    Linerange,
30    /// A line over a light ribbon (`draw_key_smooth`).
31    Smooth,
32    /// The letter "a" (`draw_key_text`).
33    Text,
34    /// "a" on a box (`draw_key_label`).
35    Label,
36    /// A vertical line (`draw_key_vline`).
37    Vline,
38    /// A vertical path (`draw_key_vpath`).
39    Vpath,
40    /// A diagonal line (`draw_key_abline`).
41    Abline,
42    /// A zig-zag line (`draw_key_timeseries`).
43    Timeseries,
44    /// A filled dot (`draw_key_dotplot`).
45    Dotplot,
46    /// Nothing (`draw_key_blank`).
47    Blank,
48}
49
50impl KeyGlyph {
51    /// Parse ggplot2's glyph names (`"point"`, `"path"`, `"boxplot"`, …).
52    pub fn parse(s: &str) -> Option<Self> {
53        Some(match s {
54            "point" => KeyGlyph::Point,
55            "path" => KeyGlyph::Path,
56            "rect" => KeyGlyph::Rect,
57            "polygon" => KeyGlyph::Polygon,
58            "boxplot" => KeyGlyph::Boxplot,
59            "crossbar" => KeyGlyph::Crossbar,
60            "pointrange" => KeyGlyph::Pointrange,
61            "linerange" => KeyGlyph::Linerange,
62            "smooth" => KeyGlyph::Smooth,
63            "text" => KeyGlyph::Text,
64            "label" => KeyGlyph::Label,
65            "vline" => KeyGlyph::Vline,
66            "vpath" => KeyGlyph::Vpath,
67            "abline" => KeyGlyph::Abline,
68            "timeseries" => KeyGlyph::Timeseries,
69            "dotplot" => KeyGlyph::Dotplot,
70            "blank" => KeyGlyph::Blank,
71            _ => return None,
72        })
73    }
74}
75
76/// One layer's contribution to the legends: its glyph (`None` = the
77/// pre-#410 per-aesthetic swatch) and the legend aesthetics it maps and shows.
78#[derive(Clone, Debug, PartialEq)]
79pub struct LayerKey {
80    pub glyph: Option<KeyGlyph>,
81    pub aesthetics: Vec<Aesthetic>,
82}
83
84/// The mapped values a key is drawn with (each `None` = not mapped in this
85/// legend → the glyph's default).
86#[derive(Clone, Debug, Default)]
87pub(crate) struct KeyProps {
88    pub color: Option<(u8, u8, u8)>,
89    pub fill: Option<(u8, u8, u8)>,
90    pub shape: Option<PointShape>,
91    pub linetype: Option<Linetype>,
92    pub size: Option<f64>,
93    pub alpha: Option<f64>,
94    pub linewidth: Option<f64>,
95}
96
97/// Draw `glyph` into the key square at `(x, y)` of side `s`.
98pub(crate) fn draw_key(
99    backend: &mut dyn DrawBackend,
100    glyph: KeyGlyph,
101    props: &KeyProps,
102    ink: (u8, u8, u8),
103    (x, y, s): (f64, f64, f64),
104) -> Result<(), RenderError> {
105    let color = props.color.unwrap_or(ink);
106    // A rect key is coloured by fill, else colour; a polygon key fills with
107    // its fill (default grey35, as ggplot2's bars) and outlines in colour.
108    let fill = props.fill.or(props.color).unwrap_or((89, 89, 89));
109    let poly_fill = props.fill.unwrap_or((89, 89, 89));
110    let alpha = props.alpha.unwrap_or(1.0);
111    let lw = props.linewidth.unwrap_or(1.0).min(s);
112    let lt = props.linetype.clone().unwrap_or(Linetype::Solid);
113    let line = |w: f64| LineStyle {
114        color,
115        width: w,
116        alpha,
117        linetype: lt.clone(),
118        ..Default::default()
119    };
120    let (cx, cy) = (x + s / 2.0, y + s / 2.0);
121    let at = |fx: f64, fy: f64| (x + fx * s, y + fy * s);
122    let rect = |fill: Option<(u8, u8, u8)>, stroke: Option<(u8, u8, u8)>| RectStyle {
123        fill,
124        stroke,
125        stroke_width: lw.max(0.5),
126        alpha,
127        clip: false,
128        corner_radius: 0.0,
129    };
130    let point = |backend: &mut dyn DrawBackend, r: f64| {
131        let shape = props.shape.unwrap_or(PointShape::Circle);
132        backend.draw_shape(
133            (cx, cy),
134            r,
135            &PointStyle {
136                color,
137                alpha,
138                filled: true,
139                shape,
140            },
141        )
142    };
143    match glyph {
144        KeyGlyph::Point => point(backend, props.size.unwrap_or(s / 5.0).min(s / 2.0))?,
145        KeyGlyph::Dotplot => point(backend, s / 3.0)?,
146        KeyGlyph::Path => backend.draw_line(&[at(0.1, 0.5), at(0.9, 0.5)], &line(lw))?,
147        KeyGlyph::Vline | KeyGlyph::Linerange | KeyGlyph::Vpath => {
148            backend.draw_line(&[at(0.5, 0.1), at(0.5, 0.9)], &line(lw))?
149        }
150        KeyGlyph::Abline => backend.draw_line(&[at(0.0, 1.0), at(1.0, 0.0)], &line(lw))?,
151        KeyGlyph::Timeseries => backend.draw_line(
152            &[at(0.0, 0.9), at(0.4, 0.1), at(0.6, 0.6), at(1.0, 0.1)],
153            &line(lw),
154        )?,
155        KeyGlyph::Rect => backend.draw_rect(at(0.0, 0.0), at(1.0, 1.0), &rect(Some(fill), None))?,
156        KeyGlyph::Polygon => backend.draw_rect(
157            at(0.0, 0.0),
158            at(1.0, 1.0),
159            &rect(Some(poly_fill), props.color),
160        )?,
161        KeyGlyph::Smooth => {
162            let band = props.fill.or(props.color).unwrap_or((153, 153, 153));
163            backend.draw_rect(
164                at(0.0, 0.0),
165                at(1.0, 1.0),
166                &RectStyle {
167                    alpha: 0.4,
168                    ..rect(Some(band), None)
169                },
170            )?;
171            backend.draw_line(&[at(0.1, 0.5), at(0.9, 0.5)], &line(lw.max(1.0)))?;
172        }
173        KeyGlyph::Boxplot => {
174            let fill = props.fill.unwrap_or((255, 255, 255));
175            backend.draw_line(&[at(0.5, 0.1), at(0.5, 0.25)], &line(0.5))?;
176            backend.draw_line(&[at(0.5, 0.75), at(0.5, 0.9)], &line(0.5))?;
177            backend.draw_rect(
178                at(0.125, 0.25),
179                at(0.875, 0.75),
180                &rect(Some(fill), Some(color)),
181            )?;
182            backend.draw_line(&[at(0.125, 0.5), at(0.875, 0.5)], &line(1.0))?;
183        }
184        KeyGlyph::Crossbar => {
185            let fill = props.fill.unwrap_or((255, 255, 255));
186            backend.draw_rect(
187                at(0.125, 0.25),
188                at(0.875, 0.75),
189                &rect(Some(fill), Some(color)),
190            )?;
191            backend.draw_line(&[at(0.125, 0.5), at(0.875, 0.5)], &line(1.0))?;
192        }
193        KeyGlyph::Pointrange => {
194            backend.draw_line(&[at(0.5, 0.1), at(0.5, 0.9)], &line(lw))?;
195            point(backend, props.size.unwrap_or(s / 6.0).min(s / 3.0))?;
196        }
197        KeyGlyph::Text | KeyGlyph::Label => {
198            if glyph == KeyGlyph::Label {
199                backend.draw_rect(
200                    at(0.1, 0.15),
201                    at(0.9, 0.85),
202                    &RectStyle {
203                        fill: Some((255, 255, 255)),
204                        stroke: Some(color),
205                        stroke_width: 0.5,
206                        alpha,
207                        clip: false,
208                        corner_radius: 1.5,
209                    },
210                )?;
211            }
212            backend.draw_text(
213                "a",
214                (cx, cy + s * 0.18),
215                &TextStyle {
216                    color,
217                    size: s * 0.6,
218                    anchor: TextAnchor::Middle,
219                    ..Default::default()
220                },
221            )?;
222        }
223        KeyGlyph::Blank => {}
224    }
225    Ok(())
226}