Skip to main content

ggplot_rs/scale/
continuous.rs

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