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 | Aesthetic::High | Aesthetic::Low | Aesthetic::Close
56        ) {
57            return;
58        }
59
60        let col_name = aes.col_name();
61        let values = data.column(col_name);
62
63        let is_discrete = match values {
64            Some(vals) => vals
65                .iter()
66                .any(|v| matches!(v, Value::Str(_) | Value::Bool(_))),
67            None => false,
68        };
69
70        let is_datetime = match values {
71            Some(vals) => vals.iter().any(|v| v.is_datetime()),
72            None => false,
73        };
74
75        match aes {
76            Aesthetic::Color | Aesthetic::Fill => {
77                if is_discrete {
78                    let scale = ScaleColorDiscrete::new(aes.clone());
79                    self.scales.push(Box::new(scale));
80                } else {
81                    let scale = ScaleColorContinuous::new(aes.clone());
82                    self.scales.push(Box::new(scale));
83                }
84            }
85            Aesthetic::Shape => {
86                let scale = ScaleShapeDiscrete::new();
87                self.scales.push(Box::new(scale));
88            }
89            Aesthetic::Linetype => {
90                let scale = ScaleLinetypeDiscrete::new();
91                self.scales.push(Box::new(scale));
92            }
93            Aesthetic::Size => {
94                let scale = ScaleSizeContinuous::new();
95                self.scales.push(Box::new(scale));
96            }
97            Aesthetic::Alpha => {
98                let scale = ScaleAlphaContinuous::new();
99                self.scales.push(Box::new(scale));
100            }
101            _ => {
102                if is_discrete {
103                    let scale = ScaleDiscrete::new().for_aesthetic(aes.clone());
104                    self.scales.push(Box::new(scale));
105                } else if is_datetime {
106                    let scale = ScaleDateTime::new().for_aesthetic(aes.clone());
107                    self.scales.push(Box::new(scale));
108                } else {
109                    let scale = ScaleContinuous::new().for_aesthetic(aes.clone());
110                    self.scales.push(Box::new(scale));
111                }
112            }
113        }
114    }
115
116    /// Train all scales on data from one layer.
117    pub fn train_layer(&mut self, data: &DataFrame) {
118        for scale in &mut self.scales {
119            let col_name = scale.aesthetic().col_name().to_string();
120            if let Some(values) = data.column(&col_name) {
121                scale.train(values);
122            }
123        }
124    }
125
126    /// Map a single value through the appropriate scale.
127    pub fn map_value(&self, aes: &Aesthetic, value: &Value) -> f64 {
128        self.get(aes).map(|s| s.map(value)).unwrap_or(0.0)
129    }
130
131    /// Map a value to an RGB color through the appropriate color/fill scale.
132    pub fn map_color(&self, aes: &Aesthetic, value: &Value) -> Option<(u8, u8, u8)> {
133        self.get(aes).and_then(|s| s.map_to_color(value))
134    }
135
136    /// Map a value to a point shape through the shape scale.
137    pub fn map_shape(&self, value: &Value) -> Option<PointShape> {
138        self.get(&Aesthetic::Shape)
139            .and_then(|s| s.map_to_shape(value))
140    }
141
142    /// Map a value to a linetype through the linetype scale.
143    pub fn map_linetype(&self, value: &Value) -> Option<Linetype> {
144        self.get(&Aesthetic::Linetype)
145            .and_then(|s| s.map_to_linetype(value))
146    }
147
148    /// Map a value to a point size through the size scale.
149    pub fn map_size(&self, value: &Value) -> Option<f64> {
150        self.get(&Aesthetic::Size)
151            .and_then(|s| s.map_to_size(value))
152    }
153
154    /// Map a value to an alpha (opacity) through the alpha scale.
155    pub fn map_alpha(&self, value: &Value) -> Option<f64> {
156        self.get(&Aesthetic::Alpha)
157            .and_then(|s| s.map_to_alpha(value))
158    }
159
160    /// Override the domain limits for a scale (used by coord_cartesian zoom).
161    pub fn set_limits(&mut self, aes: &Aesthetic, min: f64, max: f64) {
162        if let Some(scale) = self.get_mut(aes) {
163            scale.set_limits(min, max);
164        }
165    }
166
167    /// Get the secondary axis for an aesthetic, if one exists.
168    pub fn sec_axis(&self, aes: &Aesthetic) -> Option<&crate::scale::sec_axis::SecAxis> {
169        self.get(aes).and_then(|s| s.sec_axis())
170    }
171
172    /// Get all scales.
173    pub fn iter(&self) -> impl Iterator<Item = &dyn Scale> {
174        self.scales.iter().map(|s| s.as_ref())
175    }
176}
177
178impl Clone for ScaleSet {
179    fn clone(&self) -> Self {
180        ScaleSet {
181            scales: self.scales.iter().map(|s| s.clone_box()).collect(),
182        }
183    }
184}
185
186impl Default for ScaleSet {
187    fn default() -> Self {
188        Self::new()
189    }
190}