Skip to main content

ggplot_rs/guide/
config.rs

1/// Configuration for legend guides.
2/// Analogous to R's `guide_legend()` and `guides()`.
3#[derive(Clone, Debug, Default)]
4pub struct GuideLegend {
5    /// Override the legend title. None = use scale name.
6    pub title: Option<String>,
7    /// Number of columns in the legend layout.
8    pub ncol: Option<usize>,
9    /// Number of rows in the legend layout.
10    pub nrow: Option<usize>,
11    /// Reverse the order of legend keys.
12    pub reverse: bool,
13    /// Fill an explicit `nrow`/`ncol` grid row-major instead of ggplot2's
14    /// default column-major (B3-2; R's `guide_legend(byrow = TRUE)`).
15    pub byrow: bool,
16    /// Cap on the number of rows an auto-wrapped horizontal legend may grow
17    /// to (B3-2); entries beyond the cap collapse into a trailing "+N more"
18    /// label. `None` (the default) uses `1 + floor(total_height / 3 /
19    /// key_height)`. `Some(0)` is invalid (see `validated`, internal) and
20    /// is treated as `Some(1)`.
21    pub max_rows: Option<usize>,
22    /// Aesthetics whose legend is dropped (ggplot2's `guides(fill = "none")`, #409).
23    pub hidden: Vec<crate::aes::Aesthetic>,
24    /// Per-aesthetic legend titles (`guides(colour = guide_legend(title =))`, #409).
25    pub titles: Vec<(crate::aes::Aesthetic, String)>,
26    /// Fixed key appearance per legend (ggplot2's `guide_legend(override.aes
27    /// = list(...))`, #409).
28    pub overrides: Vec<(crate::aes::Aesthetic, KeyOverride)>,
29    /// Aesthetics whose legend keys run in reverse (`guides(colour =
30    /// guide_legend(reverse = TRUE))`, #409) — `reverse` above flips all.
31    pub reversed: Vec<crate::aes::Aesthetic>,
32    /// Per-aesthetic colourbar options (`guide_colourbar(..)` /
33    /// `guide_coloursteps(..)`, #409) for a continuous colour / fill.
34    pub colourbars: Vec<(crate::aes::Aesthetic, GuideColourbar)>,
35    /// Per-aesthetic `guide_bins()` options (#409) for a binned scale.
36    pub bins: Vec<(crate::aes::Aesthetic, GuideBins)>,
37    /// Per-aesthetic legend positions (ggplot2 3.5's `guide_legend(position
38    /// =)`, #409): those legends draw at their own side instead of the
39    /// theme's `legend_position`.
40    pub positions: Vec<(crate::aes::Aesthetic, crate::theme::LegendPosition)>,
41    /// Custom legend entries (`guide_custom()`, #409), drawn after the
42    /// scales' own legends.
43    pub custom: Vec<GuideCustom>,
44    /// Binned-scale aesthetics drawn as a plain key-per-bin legend
45    /// (`guides(size = "legend")`) instead of their default `guide_bins()`.
46    pub plain_keys: Vec<crate::aes::Aesthetic>,
47    /// Filled by the build (#410): every layer's legend glyph and the legend
48    /// aesthetics it shows, so keys draw each layer's own picture.
49    #[doc(hidden)]
50    pub layer_keys: Vec<crate::guide::key::LayerKey>,
51}
52
53impl GuideLegend {
54    pub fn new() -> Self {
55        Self::default()
56    }
57
58    /// The colourbar options for `aes` (the defaults when none were set).
59    pub(crate) fn colourbar_for(&self, aes: &crate::aes::Aesthetic) -> GuideColourbar {
60        self.colourbars
61            .iter()
62            .find(|(a, _)| a == aes)
63            .map(|(_, c)| c.clone())
64            .unwrap_or_default()
65    }
66
67    /// The `guide_bins()` options `aes`'s legend draws with: the ones set
68    /// for it, or the defaults for a binned scale (ggplot2's default guide
69    /// for `scale_*_binned()`) — `None` for a plain legend.
70    pub(crate) fn bins_for(
71        &self,
72        aes: &crate::aes::Aesthetic,
73        scale: &dyn crate::scale::Scale,
74    ) -> Option<GuideBins> {
75        scale.bin_edge_labels()?;
76        if self.plain_keys.contains(aes) {
77            return None;
78        }
79        Some(
80            self.bins
81                .iter()
82                .find(|(a, _)| a == aes)
83                .map(|(_, b)| b.clone())
84                .unwrap_or_default(),
85        )
86    }
87
88    /// Where `aes`'s legend draws: its own position, else `main`.
89    pub(crate) fn position_for<'a>(
90        &'a self,
91        aes: &crate::aes::Aesthetic,
92        main: &'a crate::theme::LegendPosition,
93    ) -> &'a crate::theme::LegendPosition {
94        self.positions
95            .iter()
96            .find(|(a, _)| a == aes)
97            .map_or(main, |(_, p)| p)
98    }
99
100    /// Are `aes`'s legend keys reversed (globally or for `aes` alone)?
101    pub(crate) fn is_reversed(&self, aes: &crate::aes::Aesthetic) -> bool {
102        self.reverse != self.reversed.contains(aes)
103    }
104
105    pub fn with_title(mut self, title: &str) -> Self {
106        self.title = Some(title.to_string());
107        self
108    }
109
110    pub fn with_ncol(mut self, ncol: usize) -> Self {
111        self.ncol = Some(ncol);
112        self
113    }
114
115    pub fn with_nrow(mut self, nrow: usize) -> Self {
116        self.nrow = Some(nrow);
117        self
118    }
119
120    pub fn reverse(mut self) -> Self {
121        self.reverse = true;
122        self
123    }
124
125    /// Fill an explicit grid row-major (left-to-right, top-to-bottom)
126    /// instead of ggplot2's default column-major.
127    pub fn with_byrow(mut self, byrow: bool) -> Self {
128        self.byrow = byrow;
129        self
130    }
131
132    /// Cap the number of rows an auto-wrapped horizontal legend may grow to.
133    pub fn with_max_rows(mut self, max_rows: usize) -> Self {
134        self.max_rows = Some(max_rows);
135        self
136    }
137
138    /// Sanitize `nrow`/`ncol`/`max_rows` against `n` breaks (B3-2, D2):
139    /// `nrow`/`ncol` of `0` is treated as unset; an explicit grid smaller
140    /// than `n` drops `ncol` (falls back to an `nrow`-only grid, which
141    /// grows columns to fit); `max_rows` of `0` is treated as `1`. Each
142    /// correction pushes one warning into `warnings` and never panics.
143    pub(crate) fn validated(&self, n: usize, warnings: &mut Vec<String>) -> GuideLegend {
144        let mut out = self.clone();
145
146        if out.nrow == Some(0) {
147            warnings.push("guide_legend: nrow(0) is invalid; using the default".to_string());
148            out.nrow = None;
149        }
150        if out.ncol == Some(0) {
151            warnings.push("guide_legend: ncol(0) is invalid; using the default".to_string());
152            out.ncol = None;
153        }
154        if let (Some(nrow), Some(ncol)) = (out.nrow, out.ncol) {
155            if nrow.saturating_mul(ncol) < n {
156                warnings.push(format!(
157                    "guide_legend: nrow({nrow}) x ncol({ncol}) = {} is smaller than {n} keys; dropping ncol",
158                    nrow.saturating_mul(ncol)
159                ));
160                out.ncol = None;
161            }
162        }
163        if out.max_rows == Some(0) {
164            warnings.push("guide_legend: max_rows(0) is invalid; using 1".to_string());
165            out.max_rows = Some(1);
166        }
167        out
168    }
169}
170
171/// The drawing callback of a [`GuideCustom`]: paints into the given box.
172pub type CustomGuideDraw = std::sync::Arc<
173    dyn Fn(
174            &mut dyn crate::render::backend::DrawBackend,
175            &crate::render::Rect,
176        ) -> Result<(), crate::render::RenderError>
177        + Send
178        + Sync,
179>;
180
181/// A legend entry drawn by your own code — ggplot2's `guide_custom(grob,
182/// width, height, title)` (#409): a `width` × `height` px box in the legend
183/// stack (under `title`, if any), handed to `draw` to paint with the usual
184/// [`DrawBackend`](crate::render::backend::DrawBackend) calls. Add with
185/// [`GGPlot::guide_custom`](crate::GGPlot::guide_custom).
186#[derive(Clone)]
187pub struct GuideCustom {
188    pub title: Option<String>,
189    pub width: f64,
190    pub height: f64,
191    pub draw: CustomGuideDraw,
192}
193
194impl GuideCustom {
195    /// A `width` × `height` px box painted by `draw`.
196    pub fn new<F>(width: f64, height: f64, draw: F) -> Self
197    where
198        F: Fn(
199                &mut dyn crate::render::backend::DrawBackend,
200                &crate::render::Rect,
201            ) -> Result<(), crate::render::RenderError>
202            + Send
203            + Sync
204            + 'static,
205    {
206        GuideCustom {
207            title: None,
208            width: width.max(0.0),
209            height: height.max(0.0),
210            draw: std::sync::Arc::new(draw),
211        }
212    }
213
214    /// A title above the box, styled like the other legend titles.
215    pub fn with_title(mut self, title: &str) -> Self {
216        self.title = Some(title.to_string());
217        self
218    }
219
220    /// Height of the title line above the box (0 without a title).
221    pub(crate) fn title_offset(&self, theme: &crate::theme::Theme) -> f64 {
222        match &self.title {
223            Some(t) if !t.is_empty() => theme.legend_title.size + 4.0,
224            _ => 0.0,
225        }
226    }
227}
228
229impl std::fmt::Debug for GuideCustom {
230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231        f.debug_struct("GuideCustom")
232            .field("title", &self.title)
233            .field("width", &self.width)
234            .field("height", &self.height)
235            .finish_non_exhaustive()
236    }
237}
238
239/// Options of a binned scale's legend — ggplot2's `guide_bins()` (#409):
240/// keys stacked edge to edge, labelled at the bin boundaries along a small
241/// axis. Apply with [`GGPlot::guide_bins`](crate::GGPlot::guide_bins).
242#[derive(Clone, Debug, PartialEq)]
243pub struct GuideBins {
244    /// Label the two outer limits too (`show.limits`, #408).
245    pub show_limits: bool,
246    /// Draw the axis line and ticks beside the keys.
247    pub axis: bool,
248}
249
250impl Default for GuideBins {
251    fn default() -> Self {
252        GuideBins {
253            show_limits: false,
254            axis: true,
255        }
256    }
257}
258
259impl GuideBins {
260    pub fn new() -> Self {
261        Self::default()
262    }
263
264    /// Label the outer limits (`show.limits = TRUE`).
265    pub fn with_show_limits(mut self, on: bool) -> Self {
266        self.show_limits = on;
267        self
268    }
269
270    /// Draw the axis line and ticks (`TRUE` by default).
271    pub fn with_axis(mut self, on: bool) -> Self {
272        self.axis = on;
273        self
274    }
275}
276
277/// How a colourbar paints its colours (ggplot2's `guide_colourbar(display =)`).
278#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
279pub enum ColourbarDisplay {
280    /// A smooth ramp (`"gradient"`, the default).
281    #[default]
282    Gradient,
283    /// `nbin` solid, visibly separate blocks (`"rectangles"`).
284    Rectangles,
285}
286
287/// Options of a continuous colour / fill legend — ggplot2's
288/// `guide_colourbar()`, or `guide_coloursteps()` when `steps` is set
289/// (#409). Apply with [`GGPlot::guide_colourbar`](crate::GGPlot::guide_colourbar).
290#[derive(Clone, Debug, PartialEq)]
291pub struct GuideColourbar {
292    /// The bar's horizontal extent in px (`barwidth`); `None` = the theme's
293    /// key size (long side 8 keys).
294    pub bar_width: Option<f64>,
295    /// The bar's vertical extent in px (`barheight`).
296    pub bar_height: Option<f64>,
297    /// Colour samples along the bar (`nbin`).
298    pub nbin: usize,
299    /// Gradient or separate rectangles (`display`).
300    pub display: ColourbarDisplay,
301    /// Draw the outline round the bar (`frame`); `None` = the theme's
302    /// `legend.frame` (`legend_bar_border`).
303    pub frame: Option<bool>,
304    /// Draw tick marks at the breaks (`ticks`).
305    pub ticks: bool,
306    /// `guide_coloursteps()`: one solid block per interval between breaks.
307    pub steps: bool,
308    /// Coloursteps: every block the same length (`even.steps`, default
309    /// `true`), instead of proportional to its value range.
310    pub even_steps: bool,
311    /// Coloursteps: label the two outer limits too (`show.limits`).
312    pub show_limits: bool,
313}
314
315impl Default for GuideColourbar {
316    fn default() -> Self {
317        GuideColourbar {
318            bar_width: None,
319            bar_height: None,
320            nbin: 50,
321            display: ColourbarDisplay::Gradient,
322            frame: None,
323            ticks: true,
324            steps: false,
325            even_steps: true,
326            show_limits: false,
327        }
328    }
329}
330
331impl GuideColourbar {
332    /// `guide_colourbar()` with its defaults.
333    pub fn new() -> Self {
334        Self::default()
335    }
336
337    /// `guide_coloursteps()`: stepped blocks, even steps, no ticks.
338    pub fn steps() -> Self {
339        GuideColourbar {
340            steps: true,
341            ticks: false,
342            ..Self::default()
343        }
344    }
345
346    pub fn with_bar_width(mut self, px: f64) -> Self {
347        self.bar_width = Some(px);
348        self
349    }
350
351    pub fn with_bar_height(mut self, px: f64) -> Self {
352        self.bar_height = Some(px);
353        self
354    }
355
356    pub fn with_nbin(mut self, nbin: usize) -> Self {
357        self.nbin = nbin.max(1);
358        self
359    }
360
361    pub fn with_display(mut self, display: ColourbarDisplay) -> Self {
362        self.display = display;
363        self
364    }
365
366    pub fn with_frame(mut self, frame: bool) -> Self {
367        self.frame = Some(frame);
368        self
369    }
370
371    pub fn with_ticks(mut self, ticks: bool) -> Self {
372        self.ticks = ticks;
373        self
374    }
375
376    pub fn with_even_steps(mut self, even: bool) -> Self {
377        self.even_steps = even;
378        self
379    }
380
381    pub fn with_show_limits(mut self, show: bool) -> Self {
382        self.show_limits = show;
383        self
384    }
385}
386
387/// Fixed values a legend's keys are drawn with, whatever the data maps
388/// (ggplot2's `override.aes`, #409) — e.g. opaque, larger keys for a
389/// scatter plot drawn with small, translucent points. `None` keeps the key's
390/// own value.
391#[derive(Clone, Debug, Default, PartialEq)]
392pub struct KeyOverride {
393    pub color: Option<(u8, u8, u8)>,
394    pub fill: Option<(u8, u8, u8)>,
395    pub shape: Option<crate::render::backend::PointShape>,
396    pub linetype: Option<crate::render::backend::Linetype>,
397    pub size: Option<f64>,
398    pub alpha: Option<f64>,
399    pub linewidth: Option<f64>,
400}
401
402impl KeyOverride {
403    pub fn new() -> Self {
404        Self::default()
405    }
406    pub fn color(mut self, c: (u8, u8, u8)) -> Self {
407        self.color = Some(c);
408        self
409    }
410    pub fn fill(mut self, c: (u8, u8, u8)) -> Self {
411        self.fill = Some(c);
412        self
413    }
414    pub fn shape(mut self, s: crate::render::backend::PointShape) -> Self {
415        self.shape = Some(s);
416        self
417    }
418    pub fn linetype(mut self, l: crate::render::backend::Linetype) -> Self {
419        self.linetype = Some(l);
420        self
421    }
422    pub fn size(mut self, v: f64) -> Self {
423        self.size = Some(v);
424        self
425    }
426    pub fn alpha(mut self, v: f64) -> Self {
427        self.alpha = Some(v.clamp(0.0, 1.0));
428        self
429    }
430    pub fn linewidth(mut self, v: f64) -> Self {
431        self.linewidth = Some(v);
432        self
433    }
434}