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}
39
40impl ScaleContinuous {
41    pub fn new() -> Self {
42        ScaleContinuous {
43            aesthetic: Aesthetic::X,
44            name: String::new(),
45            min: f64::INFINITY,
46            max: f64::NEG_INFINITY,
47            trained: false,
48            filter_oob: false,
49            expand: (0.05, 0.0, 0.05, 0.0),
50            position_opposite: false,
51            scale_transform: ScaleTransform::Identity,
52            custom_breaks: None,
53            custom_labels: None,
54            localized_labels: None,
55            sec_axis: None,
56            label_formatter: None,
57            locale: None,
58        }
59    }
60
61    pub fn for_aesthetic(mut self, aes: Aesthetic) -> Self {
62        self.aesthetic = aes;
63        self
64    }
65
66    pub fn with_name(mut self, name: &str) -> Self {
67        self.name = name.to_string();
68        self
69    }
70
71    pub fn with_limits(mut self, min: f64, max: f64) -> Self {
72        self.min = min;
73        self.max = max;
74        self.trained = true;
75        self.filter_oob = true;
76        self
77    }
78
79    pub fn with_transform(mut self, transform: ScaleTransform) -> Self {
80        self.scale_transform = transform;
81        self
82    }
83
84    /// Set custom break positions (data values where ticks appear).
85    pub fn with_breaks(mut self, breaks: Vec<f64>) -> Self {
86        self.custom_breaks = Some(breaks);
87        self
88    }
89
90    /// Set custom labels for breaks. Must match the number of breaks.
91    pub fn with_labels(mut self, labels: Vec<String>) -> Self {
92        self.custom_labels = Some(labels);
93        self
94    }
95
96    /// Labels for the custom breaks that depend on the locale (month or
97    /// weekday names): `f` gets the scale's locale — the plot's
98    /// [`GGPlot::locale`](crate::plot::GGPlot::locale) unless set on the
99    /// scale — or `None`. Takes precedence over [`with_labels`](Self::with_labels).
100    pub fn with_localized_labels<F>(mut self, f: F) -> Self
101    where
102        F: Fn(Option<&crate::locale::Locale>) -> Vec<String> + Send + Sync + 'static,
103    {
104        self.localized_labels = Some(Arc::new(f));
105        self
106    }
107
108    /// Set the expansion multiplier and additive constant.
109    /// Like R's `expand = c(mult, add)`, applied symmetrically. Default `(0.05, 0.0)`.
110    pub fn with_expand(mut self, mult: f64, add: f64) -> Self {
111        self.expand = (mult, add, mult, add);
112        self
113    }
114
115    /// Per-side expansion (R's `expansion(mult = c(l, u), add = c(l, u))`):
116    /// separate multiplicative/additive expansion for the lower and upper ends.
117    pub fn with_expand_sides(
118        mut self,
119        mult_lower: f64,
120        add_lower: f64,
121        mult_upper: f64,
122        add_upper: f64,
123    ) -> Self {
124        self.expand = (mult_lower, add_lower, mult_upper, add_upper);
125        self
126    }
127
128    /// Place this axis on the opposite side (x → top, y → right).
129    pub fn with_position_opposite(mut self) -> Self {
130        self.position_opposite = true;
131        self
132    }
133
134    /// Set a label formatter. Accepts a plain `fn` (e.g. `label_comma`) or a
135    /// configurable formatter such as `label_si()` / `label_number(...)`.
136    pub fn with_label_formatter<F>(mut self, f: F) -> Self
137    where
138        F: Fn(f64) -> String + Send + Sync + 'static,
139    {
140        self.label_formatter = Some(std::sync::Arc::new(f));
141        self
142    }
143
144    /// Render the default break labels with `locale`'s decimal and thousands
145    /// marks (`1.000`, `2,5` in de-DE). Overrides the plot locale
146    /// ([`GGPlot::locale`](crate::GGPlot::locale)); a custom
147    /// [`with_label_formatter`](Self::with_label_formatter) still wins.
148    pub fn with_locale(mut self, locale: crate::locale::Locale) -> Self {
149        self.locale = Some(locale);
150        self
151    }
152
153    /// Add a secondary axis with a transformation function.
154    pub fn with_sec_axis(mut self, sec: SecAxis) -> Self {
155        self.sec_axis = Some(sec);
156        self
157    }
158
159    /// Get the secondary axis, if any.
160    pub fn sec_axis(&self) -> Option<&SecAxis> {
161        self.sec_axis.as_ref()
162    }
163
164    fn format_label(&self, v: f64) -> String {
165        if let Some(f) = &self.label_formatter {
166            f(v)
167        } else {
168            super::util::format_number_opt(v, self.locale.as_ref())
169        }
170    }
171
172    fn expanded_range(&self) -> (f64, f64) {
173        let range = self.max - self.min;
174        let (ml, al, mu, au) = self.expand;
175        (self.min - range * ml - al, self.max + range * mu + au)
176    }
177
178    /// Whether this axis should be drawn on the opposite side (x → top, y → right).
179    pub fn is_opposite(&self) -> bool {
180        self.position_opposite
181    }
182}
183
184impl Default for ScaleContinuous {
185    fn default() -> Self {
186        Self::new()
187    }
188}
189
190impl Scale for ScaleContinuous {
191    fn set_default_locale(&mut self, locale: &crate::locale::Locale) {
192        if self.locale.is_none() {
193            self.locale = Some(locale.clone());
194        }
195    }
196
197    fn locale(&self) -> Option<&crate::locale::Locale> {
198        self.locale.as_ref()
199    }
200
201    fn aesthetic(&self) -> Aesthetic {
202        self.aesthetic.clone()
203    }
204
205    fn train(&mut self, values: &[Value]) {
206        for v in values {
207            if let Some(f) = v.as_f64() {
208                if f.is_finite() {
209                    if f < self.min {
210                        self.min = f;
211                    }
212                    if f > self.max {
213                        self.max = f;
214                    }
215                }
216            }
217        }
218        self.trained = true;
219    }
220
221    fn map(&self, value: &Value) -> f64 {
222        let f = match value.as_f64() {
223            Some(f) => f,
224            None => return 0.0,
225        };
226        // ±Inf means "panel edge" (ggplot2): -Inf → lower edge, +Inf → upper.
227        if f == f64::INFINITY {
228            return 1.0;
229        }
230        if f == f64::NEG_INFINITY {
231            return 0.0;
232        }
233        // An untrained scale (empty data) has no domain — centre everything
234        // rather than producing NaN.
235        if !self.trained || !self.min.is_finite() || !self.max.is_finite() {
236            return 0.5;
237        }
238        let (emin, emax) = self.expanded_range();
239        let range = emax - emin;
240        if range.abs() < f64::EPSILON {
241            0.5
242        } else {
243            (f - emin) / range
244        }
245    }
246
247    fn expanded_domain(&self) -> Option<(f64, f64)> {
248        if !self.trained || !self.min.is_finite() || !self.max.is_finite() {
249            return None;
250        }
251        let (a, b) = self.expanded_range();
252        if !(a.is_finite() && b.is_finite()) {
253            return None;
254        }
255        if (b - a).abs() < f64::EPSILON {
256            // A degenerate domain maps everything to the panel centre.
257            return Some((a - 0.5, b + 0.5));
258        }
259        Some((a, b))
260    }
261
262    fn breaks(&self) -> Vec<(f64, String)> {
263        if !self.trained || self.min > self.max {
264            return vec![];
265        }
266
267        // Use custom breaks if provided
268        if let Some(ref custom) = self.custom_breaks {
269            let localized = self
270                .localized_labels
271                .as_ref()
272                .map(|f| f(self.locale.as_ref()));
273            let custom_labels = localized.as_ref().or(self.custom_labels.as_ref());
274            return custom
275                .iter()
276                .enumerate()
277                .map(|(i, &v)| {
278                    let pos = self.map(&Value::Float(v));
279                    let label = if let Some(labels) = custom_labels {
280                        labels
281                            .get(i)
282                            .cloned()
283                            .unwrap_or_else(|| self.format_label(v))
284                    } else {
285                        self.format_label(self.scale_transform.inverse(v))
286                    };
287                    (pos, label)
288                })
289                .collect();
290        }
291
292        if super::util::is_degenerate_range(self.min, self.max) {
293            let label = self.format_label(self.scale_transform.inverse(self.min));
294            return vec![(0.5, label)];
295        }
296
297        // Extended-Wilkinson breaks over the data range (matching ggplot2's
298        // scales::extended_breaks), keeping those within the expanded (visible)
299        // range. Labels show the original (inverse-transformed) value.
300        let (emin, emax) = self.expanded_range();
301        let tol = (emax - emin).abs() * 1e-9;
302        let breaks: Vec<f64> = extended_breaks(self.min, self.max, 5)
303            .into_iter()
304            .filter(|&v| v >= emin - tol && v <= emax + tol)
305            .collect();
306        let shown: Vec<f64> = breaks
307            .iter()
308            .map(|&v| self.scale_transform.inverse(v))
309            .collect();
310        // Without a custom formatter, label the breaks jointly so neighbours
311        // stay distinguishable on narrow ranges.
312        let labels: Vec<String> = match &self.label_formatter {
313            Some(f) => shown.iter().map(|&v| f(v)).collect(),
314            None => super::util::format_numbers_opt(&shown, self.locale.as_ref()),
315        };
316        breaks
317            .iter()
318            .zip(labels)
319            .map(|(&v, label)| (self.map(&Value::Float(v)), label))
320            .collect()
321    }
322
323    fn name(&self) -> &str {
324        &self.name
325    }
326
327    fn set_name(&mut self, name: &str) {
328        self.name = name.to_string();
329    }
330
331    fn transform(&self, value: &Value) -> Value {
332        self.scale_transform.transform_value(value)
333    }
334
335    fn sec_axis(&self) -> Option<&SecAxis> {
336        self.sec_axis.as_ref()
337    }
338
339    fn set_limits(&mut self, min: f64, max: f64) {
340        self.min = min;
341        self.max = max;
342        self.trained = true;
343    }
344
345    fn set_expand(&mut self, mult: f64, add: f64) {
346        self.expand = (mult, add, mult, add);
347    }
348
349    fn filter_limits(&self) -> Option<(f64, f64)> {
350        if self.filter_oob && self.trained {
351            Some((self.min, self.max))
352        } else {
353            None
354        }
355    }
356
357    fn domain(&self) -> Option<(f64, f64)> {
358        if self.trained {
359            Some((self.min, self.max))
360        } else {
361            None
362        }
363    }
364
365    fn axis_position_opposite(&self) -> bool {
366        self.position_opposite
367    }
368
369    fn clone_box(&self) -> Box<dyn Scale> {
370        Box::new(self.clone())
371    }
372
373    fn reset_training(&mut self) {
374        self.min = f64::INFINITY;
375        self.max = f64::NEG_INFINITY;
376        self.trained = false;
377    }
378}