Skip to main content

ggplot_rs/scale/
scale_set.rs

1use crate::aes::Aesthetic;
2use crate::data::{DataFrame, Value};
3use crate::render::backend::{Linetype, PointShape};
4use crate::scale::alpha::ScaleAlphaContinuous;
5use crate::scale::color::{ScaleColorContinuous, ScaleColorDiscrete};
6use crate::scale::continuous::ScaleContinuous;
7use crate::scale::datetime::ScaleDateTime;
8use crate::scale::discrete::ScaleDiscrete;
9use crate::scale::linetype::ScaleLinetypeDiscrete;
10use crate::scale::shape::ScaleShapeDiscrete;
11use crate::scale::size::ScaleSizeContinuous;
12
13use super::Scale;
14
15/// Registry of all scales for a plot. Handles auto-detection and training.
16pub struct ScaleSet {
17    scales: Vec<Box<dyn Scale>>,
18}
19
20impl ScaleSet {
21    pub fn new() -> Self {
22        ScaleSet { scales: Vec::new() }
23    }
24
25    /// Add a user-specified scale.
26    pub fn add(&mut self, scale: Box<dyn Scale>) {
27        // Replace existing scale for same aesthetic
28        let aes = scale.aesthetic();
29        self.scales.retain(|s| s.aesthetic() != aes);
30        self.scales.push(scale);
31    }
32
33    /// Give every scale the plot-level default locale (scales with their own
34    /// locale keep it; scales without numeric/date labels ignore it).
35    pub fn set_default_locale(&mut self, locale: &crate::locale::Locale) {
36        for s in &mut self.scales {
37            s.set_default_locale(locale);
38        }
39    }
40
41    /// Get a scale for a specific aesthetic.
42    pub fn get(&self, aes: &Aesthetic) -> Option<&dyn Scale> {
43        self.scales
44            .iter()
45            .find(|s| s.aesthetic() == *aes)
46            .map(|s| s.as_ref())
47    }
48
49    /// Get mutable scale for a specific aesthetic.
50    pub fn get_mut(&mut self, aes: &Aesthetic) -> Option<&mut Box<dyn Scale>> {
51        self.scales.iter_mut().find(|s| s.aesthetic() == *aes)
52    }
53
54    /// Ensure a scale exists for a given aesthetic. Auto-detect type from data.
55    pub fn ensure_scale(&mut self, aes: &Aesthetic, data: &DataFrame) {
56        if self.get(aes).is_some() {
57            return;
58        }
59        // OHLC values live on the y scale (via the geom's ymin/ymax extents);
60        // they get no scale of their own.
61        if matches!(
62            aes,
63            Aesthetic::Open
64                | Aesthetic::High
65                | Aesthetic::Low
66                | Aesthetic::Close
67                | Aesthetic::Slope
68                | Aesthetic::Intercept
69                // Reference-line intercepts live on the x / y scale; the
70                // build creates that scale from them only if no layer did
71                // (so a `geom_vline(0)` added first can't force a
72                // continuous scale onto a discrete axis).
73                | Aesthetic::Xintercept
74                | Aesthetic::Yintercept
75        ) {
76            return;
77        }
78        let values = data.column(aes.col_name());
79
80        let is_discrete = match values {
81            Some(vals) => vals
82                .iter()
83                .any(|v| matches!(v, Value::Str(_) | Value::Bool(_))),
84            None => false,
85        };
86
87        let is_datetime = match values {
88            Some(vals) => vals.iter().any(|v| v.is_datetime()),
89            None => false,
90        };
91
92        match aes {
93            Aesthetic::Color | Aesthetic::Fill => {
94                if is_discrete {
95                    let scale = ScaleColorDiscrete::new(aes.clone());
96                    self.scales.push(Box::new(scale));
97                } else {
98                    let scale = ScaleColorContinuous::new(aes.clone());
99                    self.scales.push(Box::new(scale));
100                }
101            }
102            Aesthetic::Shape => {
103                let scale = ScaleShapeDiscrete::new();
104                self.scales.push(Box::new(scale));
105            }
106            Aesthetic::Linetype => {
107                let scale = ScaleLinetypeDiscrete::new();
108                self.scales.push(Box::new(scale));
109            }
110            Aesthetic::Size => {
111                let scale = ScaleSizeContinuous::new();
112                self.scales.push(Box::new(scale));
113            }
114            Aesthetic::Alpha => {
115                let scale = ScaleAlphaContinuous::new();
116                self.scales.push(Box::new(scale));
117            }
118            _ => {
119                if is_discrete {
120                    let scale = ScaleDiscrete::new().for_aesthetic(aes.clone());
121                    self.scales.push(Box::new(scale));
122                } else if is_datetime {
123                    let scale = ScaleDateTime::new().for_aesthetic(aes.clone());
124                    self.scales.push(Box::new(scale));
125                } else {
126                    let scale = ScaleContinuous::new().for_aesthetic(aes.clone());
127                    self.scales.push(Box::new(scale));
128                }
129            }
130        }
131    }
132
133    /// Train all scales on data from one layer.
134    pub fn train_layer(&mut self, data: &DataFrame) {
135        for scale in &mut self.scales {
136            let col_name = scale.aesthetic().col_name().to_string();
137            if let Some(values) = data.column(&col_name) {
138                scale.train(values);
139            }
140        }
141    }
142
143    /// Map a single value through the appropriate scale.
144    pub fn map_value(&self, aes: &Aesthetic, value: &Value) -> f64 {
145        self.get(aes).map(|s| s.map(value)).unwrap_or(0.0)
146    }
147
148    /// Map a value to an RGB color through the appropriate color/fill scale.
149    pub fn map_color(&self, aes: &Aesthetic, value: &Value) -> Option<(u8, u8, u8)> {
150        self.get(aes).and_then(|s| s.map_to_color(value))
151    }
152
153    /// Map a value to a point shape through the shape scale.
154    pub fn map_shape(&self, value: &Value) -> Option<PointShape> {
155        self.get(&Aesthetic::Shape)
156            .and_then(|s| s.map_to_shape(value))
157    }
158
159    /// Map a value to a linetype through the linetype scale.
160    pub fn map_linetype(&self, value: &Value) -> Option<Linetype> {
161        self.get(&Aesthetic::Linetype)
162            .and_then(|s| s.map_to_linetype(value))
163    }
164
165    /// Map a value to a point size through the size scale.
166    pub fn map_size(&self, value: &Value) -> Option<f64> {
167        self.get(&Aesthetic::Size)
168            .and_then(|s| s.map_to_size(value))
169    }
170
171    /// Map a value to an alpha (opacity) through the alpha scale.
172    pub fn map_alpha(&self, value: &Value) -> Option<f64> {
173        self.get(&Aesthetic::Alpha)
174            .and_then(|s| s.map_to_alpha(value))
175    }
176
177    /// Override the domain limits for a scale (used by coord_cartesian zoom).
178    pub fn set_limits(&mut self, aes: &Aesthetic, min: f64, max: f64) {
179        if let Some(scale) = self.get_mut(aes) {
180            scale.set_limits(min, max);
181        }
182    }
183
184    /// Get the secondary axis for an aesthetic, if one exists.
185    pub fn sec_axis(&self, aes: &Aesthetic) -> Option<&crate::scale::sec_axis::SecAxis> {
186        self.get(aes).and_then(|s| s.sec_axis())
187    }
188
189    /// Get all scales.
190    pub fn iter(&self) -> impl Iterator<Item = &dyn Scale> {
191        self.scales.iter().map(|s| s.as_ref())
192    }
193}
194
195impl Clone for ScaleSet {
196    fn clone(&self) -> Self {
197        ScaleSet {
198            scales: self.scales.iter().map(|s| s.clone_box()).collect(),
199        }
200    }
201}
202
203impl Default for ScaleSet {
204    fn default() -> Self {
205        Self::new()
206    }
207}