Skip to main content

ggplot_rs/scale/
mod.rs

1pub mod alpha;
2pub mod binned;
3pub mod bivariate;
4pub mod color;
5pub mod continuous;
6pub mod datetime;
7pub mod discrete;
8pub mod format;
9pub mod gradient;
10pub mod gradient_n;
11pub mod grey;
12pub mod identity;
13pub mod linetype;
14pub mod linetype_manual;
15pub mod linewidth;
16pub mod manual;
17pub mod modified;
18pub mod palettes;
19pub mod scale_set;
20pub mod sec_axis;
21pub mod shape;
22pub mod shape_manual;
23pub mod size;
24pub mod steps;
25pub mod transform;
26#[rustfmt::skip]
27pub(crate) mod viridis_lut;
28pub mod util;
29pub mod values;
30
31pub use scale_set::ScaleSet;
32
33use crate::aes::Aesthetic;
34use crate::data::Value;
35use crate::render::backend::{Linetype, PointShape};
36
37/// ggplot2's "out of bounds" handling for a continuous colour scale's fixed
38/// [`with_limits`](color::ScaleColorContinuous::with_limits) domain — e.g.
39/// fixing `[-1, 1]` for a correlation matrix so the *colours* (not just the
40/// legend) reflect that fixed range rather than whatever the data happens
41/// to span.
42#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
43pub enum Oob {
44    /// A value outside the limits maps to the scale's `na_value` (default
45    /// grey, ggplot2's `"grey50"`/`(127, 127, 127)`) — its own
46    /// `with_na_value`, not the geom's default colour, the same way a
47    /// genuinely missing (`NA`) value is rendered.
48    #[default]
49    Censor,
50    /// A value outside the limits is clamped to the nearest limit before
51    /// mapping, so it still gets the end colour instead of `na_value`.
52    Squish,
53}
54
55/// Default `na_value` for the gradient colour scales — ggplot2's
56/// `"grey50"`. Used for a genuinely missing (`NA`) or non-finite value, and
57/// for a value censored by [`Oob::Censor`] under `with_limits`.
58pub(crate) const DEFAULT_NA_VALUE: (u8, u8, u8) = (127, 127, 127);
59
60/// Resolve one value against a scale's optional fixed `(lo, hi)` limits and
61/// [`Oob`] policy, for `map_to_color` on the gradient scales. `limits: None`
62/// (the default — domain from trained data) never rejects a value: this
63/// only has an effect once `with_limits` is set. Returns `None` exactly
64/// when the value is outside fixed limits under [`Oob::Censor`] (the
65/// caller then uses `na_value`) — including when `lo > hi` (inverted
66/// limits, e.g. a `with_limits` typo): there is no value that satisfies
67/// `lo <= f <= hi` then, so every value is "outside", and treating it as
68/// unconditionally censored — regardless of the configured `Oob` — avoids
69/// `f64::clamp`'s `min > max` panic that `Oob::Squish` would otherwise hit.
70/// `Scale::validate` warns about this separately.
71pub(crate) fn resolve_oob(f: f64, limits: Option<(f64, f64)>, oob: Oob) -> Option<f64> {
72    let Some((lo, hi)) = limits else {
73        return Some(f);
74    };
75    if lo > hi {
76        return None;
77    }
78    if f < lo || f > hi {
79        match oob {
80            Oob::Squish => Some(f.clamp(lo, hi)),
81            Oob::Censor => None,
82        }
83    } else {
84        Some(f)
85    }
86}
87
88/// Trait for scales that map data values to visual properties.
89pub trait Scale: Send + Sync {
90    /// Which aesthetic this scale is for.
91    fn aesthetic(&self) -> Aesthetic;
92
93    /// Incorporate data values to determine domain.
94    fn train(&mut self, values: &[Value]);
95
96    /// Map a data value to a [0, 1] normalized position (position scales)
97    /// or to a concrete visual value index (color/size scales).
98    fn map(&self, value: &Value) -> f64;
99
100    /// Generate break positions and labels for axis/legend.
101    fn breaks(&self) -> Vec<(f64, String)>;
102
103    /// Human-readable name (axis title).
104    fn name(&self) -> &str;
105
106    /// Set the scale name.
107    fn set_name(&mut self, name: &str);
108
109    /// Apply transformation to raw data (e.g., log10).
110    fn transform(&self, value: &Value) -> Value {
111        value.clone()
112    }
113
114    /// Whether this is a discrete scale.
115    fn is_discrete(&self) -> bool {
116        false
117    }
118
119    /// Map a data value to an RGB color. Default returns None.
120    fn map_to_color(&self, _value: &Value) -> Option<(u8, u8, u8)> {
121        None
122    }
123
124    /// A pattern fill for a value this scale draws with its flat `na`
125    /// colour (`Value::Na`, or whatever else a scale treats that way) —
126    /// `ScaleFillBinned::na_pattern` (#206) is the only implementor so
127    /// far. Default `None`: the flat colour from
128    /// [`map_to_color`](Self::map_to_color) wins, unchanged. A geom that
129    /// draws filled areas (`geom_sf`/`geom_polygon`/`geom_rect`/
130    /// `geom_tile`) checks this *before* falling back to `map_to_color`'s
131    /// na-colour branch, and the legend's "no data" key checks it too.
132    fn na_fill_pattern(&self) -> Option<crate::render::backend::FillPattern> {
133        None
134    }
135
136    /// Whether `label` (a [`breaks`](Self::breaks) label) is this scale's
137    /// "no data" legend key, so the legend draws that one key with
138    /// [`na_fill_pattern`](Self::na_fill_pattern) instead of its ordinary
139    /// flat swatch colour. Default `false` (no scale without an na key
140    /// needs this). `ScaleFillBinned` overrides it.
141    fn is_na_break(&self, _label: &str) -> bool {
142        false
143    }
144
145    /// Labels to *measure* when sizing a legend's auto-wrap layout
146    /// (`guide::legend::plan_legend`'s per-key pitch sums) — never what's
147    /// actually drawn, which stays [`breaks`](Self::breaks). Default:
148    /// identical to `breaks()`, so every scale but one is unaffected.
149    /// `ScaleFillBinned::count_reserve` (sipemu/ggplot-rs#271) overrides
150    /// this: a legend whose key labels carry a trained count
151    /// (`"10–50 (12)"`) measures as if every count had the widest
152    /// expected digit width, so the legend's row count — and so the
153    /// panel — stays stable across renders whose counts differ only in
154    /// digit width (an animated map's frames, sharing one `GGPlot` built
155    /// once per frame with the same breaks/colours).
156    fn breaks_for_layout(&self) -> Vec<(f64, String)> {
157        self.breaks()
158    }
159
160    /// A binned scale's labels for its bin *edges* — one more than its
161    /// `breaks()` (one per bin) — which `guide_bins()` (#409) draws at the
162    /// boundaries between keys. `None` (the default) for every other scale.
163    fn bin_edge_labels(&self) -> Option<Vec<String>> {
164        None
165    }
166
167    /// Map a data value to a point shape. Default returns None.
168    fn map_to_shape(&self, _value: &Value) -> Option<PointShape> {
169        None
170    }
171
172    /// Map a data value to a linetype. Default returns None.
173    fn map_to_linetype(&self, _value: &Value) -> Option<Linetype> {
174        None
175    }
176
177    /// Map a data value to a point size (radius in pixels). Default returns None.
178    fn map_to_size(&self, _value: &Value) -> Option<f64> {
179        None
180    }
181
182    /// Map a data value to an alpha (opacity) value. Default returns None.
183    fn map_to_alpha(&self, _value: &Value) -> Option<f64> {
184        None
185    }
186
187    /// Get the secondary axis specification, if any.
188    fn sec_axis(&self) -> Option<&sec_axis::SecAxis> {
189        None
190    }
191
192    /// Adopt a plot-level default [`Locale`](crate::locale::Locale) for the
193    /// default break labels (number marks, month names, date formats), unless
194    /// the scale already has its own. Called by the plot builder when
195    /// [`GGPlot::locale`](crate::GGPlot::locale) is set. Default: ignored
196    /// (scales without numeric/date labels).
197    fn set_default_locale(&mut self, _locale: &crate::locale::Locale) {}
198
199    /// The locale this scale's default labels are rendered with, if any.
200    fn locale(&self) -> Option<&crate::locale::Locale> {
201        None
202    }
203
204    /// Format a single raw (pre-scale-transform) data `value` for a
205    /// per-mark attribute (`data-fvalue`/`data-fx`, B2-4) or a tooltip
206    /// template placeholder — e.g. a de-DE currency string, or a datetime
207    /// x's ISO/locale date, independent of how the axis itself abbreviates
208    /// its break labels. `None` for a missing/non-finite value, or a scale
209    /// with nothing meaningful to format (default: every scale except
210    /// continuous position/size/linewidth and datetime/colour-continuous,
211    /// which override this).
212    fn format_value(&self, _value: &Value) -> Option<String> {
213        None
214    }
215
216    /// Override the trained domain limits (used by coord_cartesian zoom).
217    fn set_limits(&mut self, _min: f64, _max: f64) {
218        // Default no-op. Continuous scales override this.
219    }
220
221    /// Return OOB filter limits if this scale was created with explicit limits
222    /// (e.g., via xlim/ylim). Data outside these limits should be removed before stats.
223    fn filter_limits(&self) -> Option<(f64, f64)> {
224        None
225    }
226
227    /// Return the trained data domain (min, max) for continuous scales.
228    /// Used by the colorbar legend to pass data-space values to map_to_color().
229    fn domain(&self) -> Option<(f64, f64)> {
230        None
231    }
232
233    /// The data values at the panel edges (normalized 0 and 1) for a trained
234    /// continuous position scale — the limits after expansion, in the scale's
235    /// transformed space. `None` for discrete / untrained / non-position scales.
236    fn expanded_domain(&self) -> Option<(f64, f64)> {
237        None
238    }
239
240    /// Whether this axis is drawn on the opposite side (x → top, y → right).
241    fn axis_position_opposite(&self) -> bool {
242        false
243    }
244
245    /// Set the expansion multiplier/additive constant (R's `expand =
246    /// c(mult, add)`), applied symmetrically to both ends. Used by
247    /// `CoordSf::train_scales` (`expand(false)` passes `(0.0, 0.0)`) to pin
248    /// an explicit `xlim`/`ylim` to the panel exactly. Default: no-op
249    /// (discrete / non-position scales have no expansion to set).
250    fn set_expand(&mut self, _mult: f64, _add: f64) {}
251
252    /// Drop the expansion at the lower and / or upper end (a coord's
253    /// `expand = FALSE` for one side, #411). Default: no-op.
254    fn clear_expand(&mut self, _lower: bool, _upper: bool) {}
255
256    /// A binned position scale (`scale_x_binned`, #408) replaces each value
257    /// with its bin's midpoint before the stat runs; `None` (the default)
258    /// leaves positions alone.
259    fn bin_positions(&mut self, _values: &[Value]) -> Option<Vec<Value>> {
260        None
261    }
262
263    /// `guide_axis_logticks()` (#409): `(position, kind)` of the log10
264    /// ticks to draw on this axis — kind 0 at each decade (long), 1 at the
265    /// 5s (mid), 2 at the other integers (short). Empty by default.
266    fn log_ticks(&self) -> Vec<(f64, u8)> {
267        Vec::new()
268    }
269
270    /// Switch [`log_ticks`](Self::log_ticks) on or off. Default: no-op.
271    fn set_log_ticks(&mut self, _on: bool) {}
272
273    /// Map an already-computed `[0, 1]` scale fraction (as returned
274    /// alongside each label by [`breaks`](Self::breaks)) directly to an
275    /// output size, bypassing the round trip through [`map`](Self::map) — a
276    /// discrete-style legend key only has the break's rendered *label*, not
277    /// its original data value, so a continuous scale can't recover the
278    /// fraction by re-mapping the label text. Default: `None`.
279    fn map_fraction_to_size(&self, _fraction: f64) -> Option<f64> {
280        None
281    }
282
283    /// Map an already-computed `[0, 1]` scale fraction directly to an output
284    /// alpha (opacity), the [`map_fraction_to_size`](Self::map_fraction_to_size)
285    /// equivalent for a continuous alpha scale's discrete-style legend keys —
286    /// see that method's doc for why the fraction, not the label, is the
287    /// input. Default: `None`.
288    fn map_fraction_to_alpha(&self, _fraction: f64) -> Option<f64> {
289        None
290    }
291
292    /// Clone this scale into a boxed trait object.
293    fn clone_box(&self) -> Box<dyn Scale>;
294
295    /// Reset training state so the scale can be retrained on new data.
296    fn reset_training(&mut self) {}
297
298    /// Validate this scale's configuration once training is complete (e.g.
299    /// a transform undefined at the domain's own limits) and push a
300    /// human-readable warning for each problem found. Called once per scale
301    /// after the final training pass, with the plot's warnings sink —
302    /// `map`/`map_to_size` run per mark at draw time and have no such
303    /// access, so a scale-level problem must be reported here instead.
304    /// Default: no-op.
305    fn validate(&self, _warnings: &mut Vec<String>) {}
306}
307
308#[cfg(test)]
309mod default_trait_method_tests {
310    use super::Scale;
311    use crate::aes::Aesthetic;
312    use crate::data::Value;
313
314    /// Scales that don't override `set_expand`/`map_fraction_to_size`
315    /// (e.g. an identity scale, which has no expansion or numeric output
316    /// at all) get the documented no-op/`None` defaults rather than a
317    /// missing-method compile error.
318    #[test]
319    fn set_expand_and_map_fraction_to_size_defaults_are_harmless() {
320        let mut scale = crate::scale::identity::ScaleColorIdentity::new(Aesthetic::Color);
321        // No panic, no effect — there's no expansion concept for a
322        // colour-identity scale.
323        scale.set_expand(0.1, 5.0);
324        assert_eq!(scale.map_fraction_to_size(0.5), None);
325        assert_eq!(scale.map_fraction_to_alpha(0.5), None);
326        // validate() is also a no-op default; an identity scale never has
327        // anything to warn about.
328        let mut warnings = Vec::new();
329        scale.validate(&mut warnings);
330        assert!(warnings.is_empty());
331        // The scale is otherwise unaffected.
332        assert_eq!(
333            scale.map_to_color(&Value::Str("#FFFFFF".into())),
334            Some((255, 255, 255))
335        );
336        // format_value's default (B2-4): a scale with nothing meaningful to
337        // format (an identity colour scale has no numeric formatter) gets
338        // `None` rather than a missing-method compile error.
339        assert_eq!(scale.format_value(&Value::Str("#FFFFFF".into())), None);
340    }
341
342    /// `resolve_oob` (B2-7.5's gradient `with_limits`): `limits: None` never
343    /// rejects a value — the no-op path every scale takes before
344    /// `with_limits` is ever called.
345    #[test]
346    fn resolve_oob_without_limits_always_passes_through() {
347        assert_eq!(
348            super::resolve_oob(42.0, None, super::Oob::Censor),
349            Some(42.0)
350        );
351        assert_eq!(
352            super::resolve_oob(-5.0, None, super::Oob::Squish),
353            Some(-5.0)
354        );
355    }
356}