Skip to main content

ggplot_rs/scale/
continuous.rs

1use std::sync::Arc;
2
3use crate::aes::Aesthetic;
4use crate::data::Value;
5
6use super::format::LabelFormatter;
7use super::sec_axis::SecAxis;
8use super::transform::ScaleTransform;
9use super::util::extended_breaks;
10use super::Scale;
11
12/// Locale-dependent break labels ([`ScaleContinuous::with_localized_labels`]).
13pub type LocalizedLabels = Arc<dyn Fn(Option<&crate::locale::Locale>) -> Vec<String> + Send + Sync>;
14
15/// Continuous linear scale.
16#[derive(Clone)]
17pub struct ScaleContinuous {
18    aesthetic: Aesthetic,
19    name: String,
20    min: f64,
21    max: f64,
22    trained: bool,
23    filter_oob: bool,
24    // Per-side expansion: (mult_lower, add_lower, mult_upper, add_upper).
25    expand: (f64, f64, f64, f64),
26    // Axis position: false = default (bottom for x, left for y), true = opposite
27    // side (top for x, right for y).
28    position_opposite: bool,
29    pub(crate) scale_transform: ScaleTransform,
30    custom_breaks: Option<Vec<f64>>,
31    custom_labels: Option<Vec<String>>,
32    /// Labels for `custom_breaks` resolved against the scale's locale.
33    localized_labels: Option<LocalizedLabels>,
34    pub(crate) sec_axis: Option<SecAxis>,
35    label_formatter: Option<LabelFormatter>,
36    /// Locale for the default labels (`None` = default marks).
37    locale: Option<crate::locale::Locale>,
38    /// Formatter for `format_value` (B2-4: `data-fvalue`/tooltip templates),
39    /// distinct from `label_formatter` — e.g. a full currency string where
40    /// the axis itself shows an abbreviated break.
41    value_formatter: Option<LabelFormatter>,
42}
43
44impl ScaleContinuous {
45    pub fn new() -> Self {
46        ScaleContinuous {
47            aesthetic: Aesthetic::X,
48            name: String::new(),
49            min: f64::INFINITY,
50            max: f64::NEG_INFINITY,
51            trained: false,
52            filter_oob: false,
53            expand: (0.05, 0.0, 0.05, 0.0),
54            position_opposite: false,
55            scale_transform: ScaleTransform::Identity,
56            custom_breaks: None,
57            custom_labels: None,
58            localized_labels: None,
59            sec_axis: None,
60            label_formatter: None,
61            locale: None,
62            value_formatter: None,
63        }
64    }
65
66    pub fn for_aesthetic(mut self, aes: Aesthetic) -> Self {
67        self.aesthetic = aes;
68        self
69    }
70
71    pub fn with_name(mut self, name: &str) -> Self {
72        self.name = name.to_string();
73        self
74    }
75
76    pub fn with_limits(mut self, min: f64, max: f64) -> Self {
77        self.min = min;
78        self.max = max;
79        self.trained = true;
80        self.filter_oob = true;
81        self
82    }
83
84    pub fn with_transform(mut self, transform: ScaleTransform) -> Self {
85        self.scale_transform = transform;
86        self
87    }
88
89    /// Set custom break positions (data values where ticks appear).
90    pub fn with_breaks(mut self, breaks: Vec<f64>) -> Self {
91        self.custom_breaks = Some(breaks);
92        self
93    }
94
95    /// Set custom labels for breaks. Must match the number of breaks.
96    pub fn with_labels(mut self, labels: Vec<String>) -> Self {
97        self.custom_labels = Some(labels);
98        self
99    }
100
101    /// Labels for the custom breaks that depend on the locale (month or
102    /// weekday names): `f` gets the scale's locale — the plot's
103    /// [`GGPlot::locale`](crate::plot::GGPlot::locale) unless set on the
104    /// scale — or `None`. Takes precedence over [`with_labels`](Self::with_labels).
105    pub fn with_localized_labels<F>(mut self, f: F) -> Self
106    where
107        F: Fn(Option<&crate::locale::Locale>) -> Vec<String> + Send + Sync + 'static,
108    {
109        self.localized_labels = Some(Arc::new(f));
110        self
111    }
112
113    /// Set the expansion multiplier and additive constant.
114    /// Like R's `expand = c(mult, add)`, applied symmetrically. Default `(0.05, 0.0)`.
115    pub fn with_expand(mut self, mult: f64, add: f64) -> Self {
116        self.expand = (mult, add, mult, add);
117        self
118    }
119
120    /// Per-side expansion (R's `expansion(mult = c(l, u), add = c(l, u))`):
121    /// separate multiplicative/additive expansion for the lower and upper ends.
122    pub fn with_expand_sides(
123        mut self,
124        mult_lower: f64,
125        add_lower: f64,
126        mult_upper: f64,
127        add_upper: f64,
128    ) -> Self {
129        self.expand = (mult_lower, add_lower, mult_upper, add_upper);
130        self
131    }
132
133    /// Place this axis on the opposite side (x → top, y → right).
134    pub fn with_position_opposite(mut self) -> Self {
135        self.position_opposite = true;
136        self
137    }
138
139    /// Set a label formatter. Accepts a plain `fn` (e.g. `label_comma`) or a
140    /// configurable formatter such as `label_si()` / `label_number(...)`.
141    pub fn with_label_formatter<F>(mut self, f: F) -> Self
142    where
143        F: Fn(f64) -> String + Send + Sync + 'static,
144    {
145        self.label_formatter = Some(std::sync::Arc::new(f));
146        self
147    }
148
149    /// Set a formatter for [`format_value`](Scale::format_value) — a
150    /// per-mark `data-fvalue` or tooltip-template value — distinct from
151    /// [`with_label_formatter`](Self::with_label_formatter), which only
152    /// affects axis break labels. Falls back to the label formatter, then
153    /// the locale default, when unset.
154    pub fn with_value_formatter<F>(mut self, f: F) -> Self
155    where
156        F: Fn(f64) -> String + Send + Sync + 'static,
157    {
158        self.value_formatter = Some(std::sync::Arc::new(f));
159        self
160    }
161
162    /// Render the default break labels with `locale`'s decimal and thousands
163    /// marks (`1.000`, `2,5` in de-DE). Overrides the plot locale
164    /// ([`GGPlot::locale`](crate::GGPlot::locale)); a custom
165    /// [`with_label_formatter`](Self::with_label_formatter) still wins.
166    pub fn with_locale(mut self, locale: crate::locale::Locale) -> Self {
167        self.locale = Some(locale);
168        self
169    }
170
171    /// Add a secondary axis with a transformation function.
172    pub fn with_sec_axis(mut self, sec: SecAxis) -> Self {
173        self.sec_axis = Some(sec);
174        self
175    }
176
177    /// Get the secondary axis, if any.
178    pub fn sec_axis(&self) -> Option<&SecAxis> {
179        self.sec_axis.as_ref()
180    }
181
182    fn format_label(&self, v: f64) -> String {
183        if let Some(f) = &self.label_formatter {
184            f(v)
185        } else {
186            super::util::format_number_opt(v, self.locale.as_ref())
187        }
188    }
189
190    fn expanded_range(&self) -> (f64, f64) {
191        let range = self.max - self.min;
192        let (ml, al, mu, au) = self.expand;
193        (self.min - range * ml - al, self.max + range * mu + au)
194    }
195
196    /// Whether this axis should be drawn on the opposite side (x → top, y → right).
197    pub fn is_opposite(&self) -> bool {
198        self.position_opposite
199    }
200}
201
202impl Default for ScaleContinuous {
203    fn default() -> Self {
204        Self::new()
205    }
206}
207
208impl Scale for ScaleContinuous {
209    fn set_default_locale(&mut self, locale: &crate::locale::Locale) {
210        if self.locale.is_none() {
211            self.locale = Some(locale.clone());
212        }
213    }
214
215    fn locale(&self) -> Option<&crate::locale::Locale> {
216        self.locale.as_ref()
217    }
218
219    fn format_value(&self, value: &Value) -> Option<String> {
220        let f = value.as_f64()?;
221        if !f.is_finite() {
222            return None;
223        }
224        // The built data holds the value in the scale's transformed space
225        // (e.g. `scale_y_log10` stores `log10(y)`), but `data-fvalue`/a
226        // tooltip template is meant to be the human-readable number —
227        // `y = 100` under log10, not `2` — the same inverse the break
228        // labels already apply (`breaks()` below calls `format_label`
229        // with `scale_transform.inverse(v)`, never raw `v`).
230        let f = self.scale_transform.inverse(f);
231        if !f.is_finite() {
232            return None;
233        }
234        Some(match &self.value_formatter {
235            Some(fmt) => fmt(f),
236            None => self.format_label(f),
237        })
238    }
239
240    fn aesthetic(&self) -> Aesthetic {
241        self.aesthetic.clone()
242    }
243
244    fn train(&mut self, values: &[Value]) {
245        for v in values {
246            if let Some(f) = v.as_f64() {
247                if f.is_finite() {
248                    if f < self.min {
249                        self.min = f;
250                    }
251                    if f > self.max {
252                        self.max = f;
253                    }
254                }
255            }
256        }
257        self.trained = true;
258    }
259
260    fn map(&self, value: &Value) -> f64 {
261        let f = match value.as_f64() {
262            Some(f) => f,
263            None => return 0.0,
264        };
265        // ±Inf means "panel edge" (ggplot2): -Inf → lower edge, +Inf → upper.
266        if f == f64::INFINITY {
267            return 1.0;
268        }
269        if f == f64::NEG_INFINITY {
270            return 0.0;
271        }
272        // An untrained scale (empty data) has no domain — centre everything
273        // rather than producing NaN.
274        if !self.trained || !self.min.is_finite() || !self.max.is_finite() {
275            return 0.5;
276        }
277        let (emin, emax) = self.expanded_range();
278        let range = emax - emin;
279        if range.abs() < f64::EPSILON {
280            0.5
281        } else {
282            (f - emin) / range
283        }
284    }
285
286    fn expanded_domain(&self) -> Option<(f64, f64)> {
287        if !self.trained || !self.min.is_finite() || !self.max.is_finite() {
288            return None;
289        }
290        let (a, b) = self.expanded_range();
291        if !(a.is_finite() && b.is_finite()) {
292            return None;
293        }
294        if (b - a).abs() < f64::EPSILON {
295            // A degenerate domain maps everything to the panel centre.
296            return Some((a - 0.5, b + 0.5));
297        }
298        Some((a, b))
299    }
300
301    fn breaks(&self) -> Vec<(f64, String)> {
302        if !self.trained || self.min > self.max {
303            return vec![];
304        }
305
306        // Use custom breaks if provided
307        if let Some(ref custom) = self.custom_breaks {
308            let localized = self
309                .localized_labels
310                .as_ref()
311                .map(|f| f(self.locale.as_ref()));
312            let custom_labels = localized.as_ref().or(self.custom_labels.as_ref());
313            return custom
314                .iter()
315                .enumerate()
316                .map(|(i, &v)| {
317                    let pos = self.map(&Value::Float(v));
318                    let label = if let Some(labels) = custom_labels {
319                        labels
320                            .get(i)
321                            .cloned()
322                            .unwrap_or_else(|| self.format_label(v))
323                    } else {
324                        self.format_label(self.scale_transform.inverse(v))
325                    };
326                    (pos, label)
327                })
328                .collect();
329        }
330
331        if super::util::is_degenerate_range(self.min, self.max) {
332            let label = self.format_label(self.scale_transform.inverse(self.min));
333            return vec![(0.5, label)];
334        }
335
336        // Extended-Wilkinson breaks over the data range (matching ggplot2's
337        // scales::extended_breaks), keeping those within the expanded (visible)
338        // range. Labels show the original (inverse-transformed) value.
339        let (emin, emax) = self.expanded_range();
340        let tol = (emax - emin).abs() * 1e-9;
341        let breaks: Vec<f64> = if self.scale_transform.uses_data_space_breaks() {
342            // Log/sqrt axes: pick round ticks in *data* space, then forward-
343            // transform them back onto the scale — not `extended_breaks`
344            // directly on the transformed range, which gives "nice" numbers
345            // in log/sqrt space that read as noise once inverse-transformed
346            // (half-decades like 316.23 under log10, squares like 625 under
347            // sqrt).
348            let data_min = self.scale_transform.inverse(self.min);
349            let data_max = self.scale_transform.inverse(self.max);
350            if data_min.is_finite() && data_max.is_finite() && data_max > data_min {
351                self.scale_transform
352                    .breaks_in_data_space(data_min, data_max, 5)
353                    .into_iter()
354                    .map(|d| self.scale_transform.apply(d))
355                    .filter(|&v| v.is_finite() && v >= emin - tol && v <= emax + tol)
356                    .collect()
357            } else {
358                vec![]
359            }
360        } else {
361            extended_breaks(self.min, self.max, 5)
362                .into_iter()
363                .filter(|&v| v >= emin - tol && v <= emax + tol)
364                .collect()
365        };
366        let shown: Vec<f64> = breaks
367            .iter()
368            .map(|&v| self.scale_transform.inverse(v))
369            .collect();
370        // Without a custom formatter, label the breaks jointly so neighbours
371        // stay distinguishable on narrow ranges.
372        let labels: Vec<String> = match &self.label_formatter {
373            Some(f) => shown.iter().map(|&v| f(v)).collect(),
374            None => super::util::format_numbers_opt(&shown, self.locale.as_ref()),
375        };
376        breaks
377            .iter()
378            .zip(labels)
379            .map(|(&v, label)| (self.map(&Value::Float(v)), label))
380            .collect()
381    }
382
383    fn name(&self) -> &str {
384        &self.name
385    }
386
387    fn set_name(&mut self, name: &str) {
388        self.name = name.to_string();
389    }
390
391    fn transform(&self, value: &Value) -> Value {
392        self.scale_transform.transform_value(value)
393    }
394
395    fn sec_axis(&self) -> Option<&SecAxis> {
396        self.sec_axis.as_ref()
397    }
398
399    fn set_limits(&mut self, min: f64, max: f64) {
400        self.min = min;
401        self.max = max;
402        self.trained = true;
403    }
404
405    fn set_expand(&mut self, mult: f64, add: f64) {
406        self.expand = (mult, add, mult, add);
407    }
408
409    fn filter_limits(&self) -> Option<(f64, f64)> {
410        if self.filter_oob && self.trained {
411            Some((self.min, self.max))
412        } else {
413            None
414        }
415    }
416
417    fn domain(&self) -> Option<(f64, f64)> {
418        if self.trained {
419            Some((self.min, self.max))
420        } else {
421            None
422        }
423    }
424
425    fn axis_position_opposite(&self) -> bool {
426        self.position_opposite
427    }
428
429    fn clone_box(&self) -> Box<dyn Scale> {
430        Box::new(self.clone())
431    }
432
433    fn reset_training(&mut self) {
434        self.min = f64::INFINITY;
435        self.max = f64::NEG_INFINITY;
436        self.trained = false;
437    }
438}