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::{format_number, nice_step};
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    expand: (f64, f64), // multiplicative and additive expansion
20    pub(crate) scale_transform: ScaleTransform,
21    custom_breaks: Option<Vec<f64>>,
22    custom_labels: Option<Vec<String>>,
23    pub(crate) sec_axis: Option<SecAxis>,
24    label_formatter: Option<LabelFormatter>,
25}
26
27impl ScaleContinuous {
28    pub fn new() -> Self {
29        ScaleContinuous {
30            aesthetic: Aesthetic::X,
31            name: String::new(),
32            min: f64::INFINITY,
33            max: f64::NEG_INFINITY,
34            trained: false,
35            filter_oob: false,
36            expand: (0.05, 0.0),
37            scale_transform: ScaleTransform::Identity,
38            custom_breaks: None,
39            custom_labels: None,
40            sec_axis: None,
41            label_formatter: None,
42        }
43    }
44
45    pub fn for_aesthetic(mut self, aes: Aesthetic) -> Self {
46        self.aesthetic = aes;
47        self
48    }
49
50    pub fn with_name(mut self, name: &str) -> Self {
51        self.name = name.to_string();
52        self
53    }
54
55    pub fn with_limits(mut self, min: f64, max: f64) -> Self {
56        self.min = min;
57        self.max = max;
58        self.trained = true;
59        self.filter_oob = true;
60        self
61    }
62
63    pub fn with_transform(mut self, transform: ScaleTransform) -> Self {
64        self.scale_transform = transform;
65        self
66    }
67
68    /// Set custom break positions (data values where ticks appear).
69    pub fn with_breaks(mut self, breaks: Vec<f64>) -> Self {
70        self.custom_breaks = Some(breaks);
71        self
72    }
73
74    /// Set custom labels for breaks. Must match the number of breaks.
75    pub fn with_labels(mut self, labels: Vec<String>) -> Self {
76        self.custom_labels = Some(labels);
77        self
78    }
79
80    /// Set the expansion multiplier and additive constant.
81    /// Like R's `expand = c(mult, add)`. Default is `(0.05, 0.0)`.
82    pub fn with_expand(mut self, mult: f64, add: f64) -> Self {
83        self.expand = (mult, add);
84        self
85    }
86
87    /// Set a label formatter. Accepts a plain `fn` (e.g. `label_comma`) or a
88    /// configurable formatter such as `label_si()` / `label_number(...)`.
89    pub fn with_label_formatter<F>(mut self, f: F) -> Self
90    where
91        F: Fn(f64) -> String + Send + Sync + 'static,
92    {
93        self.label_formatter = Some(std::sync::Arc::new(f));
94        self
95    }
96
97    /// Add a secondary axis with a transformation function.
98    pub fn with_sec_axis(mut self, sec: SecAxis) -> Self {
99        self.sec_axis = Some(sec);
100        self
101    }
102
103    /// Get the secondary axis, if any.
104    pub fn sec_axis(&self) -> Option<&SecAxis> {
105        self.sec_axis.as_ref()
106    }
107
108    fn format_label(&self, v: f64) -> String {
109        if let Some(f) = &self.label_formatter {
110            f(v)
111        } else {
112            format_number(v)
113        }
114    }
115
116    fn expanded_range(&self) -> (f64, f64) {
117        let range = self.max - self.min;
118        let mult = self.expand.0;
119        let add = self.expand.1;
120        (self.min - range * mult - add, self.max + range * mult + add)
121    }
122}
123
124impl Default for ScaleContinuous {
125    fn default() -> Self {
126        Self::new()
127    }
128}
129
130impl Scale for ScaleContinuous {
131    fn aesthetic(&self) -> Aesthetic {
132        self.aesthetic.clone()
133    }
134
135    fn train(&mut self, values: &[Value]) {
136        for v in values {
137            if let Some(f) = v.as_f64() {
138                if f.is_finite() {
139                    if f < self.min {
140                        self.min = f;
141                    }
142                    if f > self.max {
143                        self.max = f;
144                    }
145                }
146            }
147        }
148        self.trained = true;
149    }
150
151    fn map(&self, value: &Value) -> f64 {
152        let f = match value.as_f64() {
153            Some(f) => f,
154            None => return 0.0,
155        };
156        let (emin, emax) = self.expanded_range();
157        let range = emax - emin;
158        if range.abs() < f64::EPSILON {
159            0.5
160        } else {
161            (f - emin) / range
162        }
163    }
164
165    fn breaks(&self) -> Vec<(f64, String)> {
166        if !self.trained || self.min > self.max {
167            return vec![];
168        }
169
170        // Use custom breaks if provided
171        if let Some(ref custom) = self.custom_breaks {
172            return custom
173                .iter()
174                .enumerate()
175                .map(|(i, &v)| {
176                    let pos = self.map(&Value::Float(v));
177                    let label = if let Some(ref labels) = self.custom_labels {
178                        labels
179                            .get(i)
180                            .cloned()
181                            .unwrap_or_else(|| self.format_label(v))
182                    } else {
183                        self.format_label(self.scale_transform.inverse(v))
184                    };
185                    (pos, label)
186                })
187                .collect();
188        }
189
190        let range = self.max - self.min;
191        if range.abs() < f64::EPSILON {
192            let label = self.format_label(self.scale_transform.inverse(self.min));
193            return vec![(0.5, label)];
194        }
195
196        // Generate nice breaks across the expanded (visible) range
197        let (emin, emax) = self.expanded_range();
198        let n_breaks = 5;
199        let raw_step = range / n_breaks as f64;
200        let step = nice_step(raw_step);
201
202        let start = (emin / step).ceil() * step;
203        let mut breaks = Vec::new();
204        let mut v = start;
205        while v <= emax + step * 0.001 {
206            let pos = self.map(&Value::Float(v));
207            // Labels show the original (inverse-transformed) value
208            let label = self.format_label(self.scale_transform.inverse(v));
209            breaks.push((pos, label));
210            v += step;
211        }
212        breaks
213    }
214
215    fn name(&self) -> &str {
216        &self.name
217    }
218
219    fn set_name(&mut self, name: &str) {
220        self.name = name.to_string();
221    }
222
223    fn transform(&self, value: &Value) -> Value {
224        self.scale_transform.transform_value(value)
225    }
226
227    fn sec_axis(&self) -> Option<&SecAxis> {
228        self.sec_axis.as_ref()
229    }
230
231    fn set_limits(&mut self, min: f64, max: f64) {
232        self.min = min;
233        self.max = max;
234        self.trained = true;
235    }
236
237    fn filter_limits(&self) -> Option<(f64, f64)> {
238        if self.filter_oob && self.trained {
239            Some((self.min, self.max))
240        } else {
241            None
242        }
243    }
244
245    fn domain(&self) -> Option<(f64, f64)> {
246        if self.trained {
247            Some((self.min, self.max))
248        } else {
249            None
250        }
251    }
252
253    fn clone_box(&self) -> Box<dyn Scale> {
254        Box::new(self.clone())
255    }
256
257    fn reset_training(&mut self) {
258        self.min = f64::INFINITY;
259        self.max = f64::NEG_INFINITY;
260        self.trained = false;
261    }
262}