Skip to main content

ggplot_rs/scale/
mod.rs

1pub mod alpha;
2pub mod color;
3pub mod continuous;
4pub mod datetime;
5pub mod discrete;
6pub mod format;
7pub mod gradient;
8pub mod gradient_n;
9pub mod grey;
10pub mod identity;
11pub mod linetype;
12pub mod linetype_manual;
13pub mod linewidth;
14pub mod manual;
15pub mod modified;
16pub mod palettes;
17pub mod scale_set;
18pub mod sec_axis;
19pub mod shape;
20pub mod shape_manual;
21pub mod size;
22pub mod steps;
23pub mod transform;
24pub mod util;
25
26pub use scale_set::ScaleSet;
27
28use crate::aes::Aesthetic;
29use crate::data::Value;
30use crate::render::backend::{Linetype, PointShape};
31
32/// ggplot2's "out of bounds" handling for a continuous colour scale's fixed
33/// [`with_limits`](color::ScaleColorContinuous::with_limits) domain — e.g.
34/// fixing `[-1, 1]` for a correlation matrix so the *colours* (not just the
35/// legend) reflect that fixed range rather than whatever the data happens
36/// to span.
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub enum Oob {
39    /// A value outside the limits maps to the scale's `na_value` (default
40    /// grey, ggplot2's `"grey50"`/`(127, 127, 127)`) — its own
41    /// `with_na_value`, not the geom's default colour, the same way a
42    /// genuinely missing (`NA`) value is rendered.
43    #[default]
44    Censor,
45    /// A value outside the limits is clamped to the nearest limit before
46    /// mapping, so it still gets the end colour instead of `na_value`.
47    Squish,
48}
49
50/// Default `na_value` for the gradient colour scales — ggplot2's
51/// `"grey50"`. Used for a genuinely missing (`NA`) or non-finite value, and
52/// for a value censored by [`Oob::Censor`] under `with_limits`.
53pub(crate) const DEFAULT_NA_VALUE: (u8, u8, u8) = (127, 127, 127);
54
55/// Resolve one value against a scale's optional fixed `(lo, hi)` limits and
56/// [`Oob`] policy, for `map_to_color` on the gradient scales. `limits: None`
57/// (the default — domain from trained data) never rejects a value: this
58/// only has an effect once `with_limits` is set. Returns `None` exactly
59/// when the value is outside fixed limits under [`Oob::Censor`] (the
60/// caller then uses `na_value`) — including when `lo > hi` (inverted
61/// limits, e.g. a `with_limits` typo): there is no value that satisfies
62/// `lo <= f <= hi` then, so every value is "outside", and treating it as
63/// unconditionally censored — regardless of the configured `Oob` — avoids
64/// `f64::clamp`'s `min > max` panic that `Oob::Squish` would otherwise hit.
65/// `Scale::validate` warns about this separately.
66pub(crate) fn resolve_oob(f: f64, limits: Option<(f64, f64)>, oob: Oob) -> Option<f64> {
67    let Some((lo, hi)) = limits else {
68        return Some(f);
69    };
70    if lo > hi {
71        return None;
72    }
73    if f < lo || f > hi {
74        match oob {
75            Oob::Squish => Some(f.clamp(lo, hi)),
76            Oob::Censor => None,
77        }
78    } else {
79        Some(f)
80    }
81}
82
83/// Trait for scales that map data values to visual properties.
84pub trait Scale: Send + Sync {
85    /// Which aesthetic this scale is for.
86    fn aesthetic(&self) -> Aesthetic;
87
88    /// Incorporate data values to determine domain.
89    fn train(&mut self, values: &[Value]);
90
91    /// Map a data value to a [0, 1] normalized position (position scales)
92    /// or to a concrete visual value index (color/size scales).
93    fn map(&self, value: &Value) -> f64;
94
95    /// Generate break positions and labels for axis/legend.
96    fn breaks(&self) -> Vec<(f64, String)>;
97
98    /// Human-readable name (axis title).
99    fn name(&self) -> &str;
100
101    /// Set the scale name.
102    fn set_name(&mut self, name: &str);
103
104    /// Apply transformation to raw data (e.g., log10).
105    fn transform(&self, value: &Value) -> Value {
106        value.clone()
107    }
108
109    /// Whether this is a discrete scale.
110    fn is_discrete(&self) -> bool {
111        false
112    }
113
114    /// Map a data value to an RGB color. Default returns None.
115    fn map_to_color(&self, _value: &Value) -> Option<(u8, u8, u8)> {
116        None
117    }
118
119    /// Map a data value to a point shape. Default returns None.
120    fn map_to_shape(&self, _value: &Value) -> Option<PointShape> {
121        None
122    }
123
124    /// Map a data value to a linetype. Default returns None.
125    fn map_to_linetype(&self, _value: &Value) -> Option<Linetype> {
126        None
127    }
128
129    /// Map a data value to a point size (radius in pixels). Default returns None.
130    fn map_to_size(&self, _value: &Value) -> Option<f64> {
131        None
132    }
133
134    /// Map a data value to an alpha (opacity) value. Default returns None.
135    fn map_to_alpha(&self, _value: &Value) -> Option<f64> {
136        None
137    }
138
139    /// Get the secondary axis specification, if any.
140    fn sec_axis(&self) -> Option<&sec_axis::SecAxis> {
141        None
142    }
143
144    /// Adopt a plot-level default [`Locale`](crate::locale::Locale) for the
145    /// default break labels (number marks, month names, date formats), unless
146    /// the scale already has its own. Called by the plot builder when
147    /// [`GGPlot::locale`](crate::GGPlot::locale) is set. Default: ignored
148    /// (scales without numeric/date labels).
149    fn set_default_locale(&mut self, _locale: &crate::locale::Locale) {}
150
151    /// The locale this scale's default labels are rendered with, if any.
152    fn locale(&self) -> Option<&crate::locale::Locale> {
153        None
154    }
155
156    /// Format a single raw (pre-scale-transform) data `value` for a
157    /// per-mark attribute (`data-fvalue`/`data-fx`, B2-4) or a tooltip
158    /// template placeholder — e.g. a de-DE currency string, or a datetime
159    /// x's ISO/locale date, independent of how the axis itself abbreviates
160    /// its break labels. `None` for a missing/non-finite value, or a scale
161    /// with nothing meaningful to format (default: every scale except
162    /// continuous position/size/linewidth and datetime/colour-continuous,
163    /// which override this).
164    fn format_value(&self, _value: &Value) -> Option<String> {
165        None
166    }
167
168    /// Override the trained domain limits (used by coord_cartesian zoom).
169    fn set_limits(&mut self, _min: f64, _max: f64) {
170        // Default no-op. Continuous scales override this.
171    }
172
173    /// Return OOB filter limits if this scale was created with explicit limits
174    /// (e.g., via xlim/ylim). Data outside these limits should be removed before stats.
175    fn filter_limits(&self) -> Option<(f64, f64)> {
176        None
177    }
178
179    /// Return the trained data domain (min, max) for continuous scales.
180    /// Used by the colorbar legend to pass data-space values to map_to_color().
181    fn domain(&self) -> Option<(f64, f64)> {
182        None
183    }
184
185    /// The data values at the panel edges (normalized 0 and 1) for a trained
186    /// continuous position scale — the limits after expansion, in the scale's
187    /// transformed space. `None` for discrete / untrained / non-position scales.
188    fn expanded_domain(&self) -> Option<(f64, f64)> {
189        None
190    }
191
192    /// Whether this axis is drawn on the opposite side (x → top, y → right).
193    fn axis_position_opposite(&self) -> bool {
194        false
195    }
196
197    /// Set the expansion multiplier/additive constant (R's `expand =
198    /// c(mult, add)`), applied symmetrically to both ends. Used by
199    /// `CoordSf::train_scales` (`expand(false)` passes `(0.0, 0.0)`) to pin
200    /// an explicit `xlim`/`ylim` to the panel exactly. Default: no-op
201    /// (discrete / non-position scales have no expansion to set).
202    fn set_expand(&mut self, _mult: f64, _add: f64) {}
203
204    /// Map an already-computed `[0, 1]` scale fraction (as returned
205    /// alongside each label by [`breaks`](Self::breaks)) directly to an
206    /// output size, bypassing the round trip through [`map`](Self::map) — a
207    /// discrete-style legend key only has the break's rendered *label*, not
208    /// its original data value, so a continuous scale can't recover the
209    /// fraction by re-mapping the label text. Default: `None`.
210    fn map_fraction_to_size(&self, _fraction: f64) -> Option<f64> {
211        None
212    }
213
214    /// Clone this scale into a boxed trait object.
215    fn clone_box(&self) -> Box<dyn Scale>;
216
217    /// Reset training state so the scale can be retrained on new data.
218    fn reset_training(&mut self) {}
219
220    /// Validate this scale's configuration once training is complete (e.g.
221    /// a transform undefined at the domain's own limits) and push a
222    /// human-readable warning for each problem found. Called once per scale
223    /// after the final training pass, with the plot's warnings sink —
224    /// `map`/`map_to_size` run per mark at draw time and have no such
225    /// access, so a scale-level problem must be reported here instead.
226    /// Default: no-op.
227    fn validate(&self, _warnings: &mut Vec<String>) {}
228}
229
230#[cfg(test)]
231mod default_trait_method_tests {
232    use super::Scale;
233    use crate::aes::Aesthetic;
234    use crate::data::Value;
235
236    /// Scales that don't override `set_expand`/`map_fraction_to_size`
237    /// (e.g. an identity scale, which has no expansion or numeric output
238    /// at all) get the documented no-op/`None` defaults rather than a
239    /// missing-method compile error.
240    #[test]
241    fn set_expand_and_map_fraction_to_size_defaults_are_harmless() {
242        let mut scale = crate::scale::identity::ScaleColorIdentity::new(Aesthetic::Color);
243        // No panic, no effect — there's no expansion concept for a
244        // colour-identity scale.
245        scale.set_expand(0.1, 5.0);
246        assert_eq!(scale.map_fraction_to_size(0.5), None);
247        // validate() is also a no-op default; an identity scale never has
248        // anything to warn about.
249        let mut warnings = Vec::new();
250        scale.validate(&mut warnings);
251        assert!(warnings.is_empty());
252        // The scale is otherwise unaffected.
253        assert_eq!(
254            scale.map_to_color(&Value::Str("#FFFFFF".into())),
255            Some((255, 255, 255))
256        );
257        // format_value's default (B2-4): a scale with nothing meaningful to
258        // format (an identity colour scale has no numeric formatter) gets
259        // `None` rather than a missing-method compile error.
260        assert_eq!(scale.format_value(&Value::Str("#FFFFFF".into())), None);
261    }
262
263    /// `resolve_oob` (B2-7.5's gradient `with_limits`): `limits: None` never
264    /// rejects a value — the no-op path every scale takes before
265    /// `with_limits` is ever called.
266    #[test]
267    fn resolve_oob_without_limits_always_passes_through() {
268        assert_eq!(
269            super::resolve_oob(42.0, None, super::Oob::Censor),
270            Some(42.0)
271        );
272        assert_eq!(
273            super::resolve_oob(-5.0, None, super::Oob::Squish),
274            Some(-5.0)
275        );
276    }
277}