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