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