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