Skip to main content

ggplot_rs/aes/
mod.rs

1pub mod expr;
2pub mod mapping;
3
4pub use mapping::{apply_after_stat, resolve_mappings};
5
6/// All supported aesthetic channels.
7#[derive(Clone, Debug, PartialEq, Eq, Hash)]
8pub enum Aesthetic {
9    X,
10    Y,
11    Color,
12    Fill,
13    Size,
14    Shape,
15    Alpha,
16    Linetype,
17    Group,
18    Ymin,
19    Ymax,
20    Xmin,
21    Xmax,
22    Label,
23    Weight,
24    Xend,
25    Yend,
26    Angle,
27    Radius,
28    /// Financial OHLC aesthetics (`geom_candlestick` / `geom_ohlc`); they
29    /// share the y position scale.
30    Open,
31    High,
32    Low,
33    Close,
34}
35
36impl Aesthetic {
37    /// The canonical column name used in the working DataFrame after aes evaluation.
38    pub fn col_name(&self) -> &str {
39        match self {
40            Aesthetic::X => "x",
41            Aesthetic::Y => "y",
42            Aesthetic::Color => "color",
43            Aesthetic::Fill => "fill",
44            Aesthetic::Size => "size",
45            Aesthetic::Shape => "shape",
46            Aesthetic::Alpha => "alpha",
47            Aesthetic::Linetype => "linetype",
48            Aesthetic::Group => "group",
49            Aesthetic::Ymin => "ymin",
50            Aesthetic::Ymax => "ymax",
51            Aesthetic::Xmin => "xmin",
52            Aesthetic::Xmax => "xmax",
53            Aesthetic::Label => "label",
54            Aesthetic::Weight => "weight",
55            Aesthetic::Xend => "xend",
56            Aesthetic::Yend => "yend",
57            Aesthetic::Angle => "angle",
58            Aesthetic::Radius => "radius",
59            Aesthetic::Open => "open",
60            Aesthetic::High => "high",
61            Aesthetic::Low => "low",
62            Aesthetic::Close => "close",
63        }
64    }
65}
66
67/// When an aesthetic mapping should be resolved.
68#[derive(Clone, Debug, PartialEq)]
69pub enum MappingStage {
70    /// Resolve before stat computation (default — maps from raw data columns).
71    BeforeStat,
72    /// Resolve after stat computation (maps from stat-computed columns like `density`, `count`).
73    AfterStat,
74}
75
76/// Maps a source column to an aesthetic channel.
77#[derive(Clone, Debug)]
78pub struct AesMapping {
79    pub column: String,
80    pub aesthetic: Aesthetic,
81    pub stage: MappingStage,
82}
83
84/// A post-scale aesthetic derivation (R's `after_scale`): the `target` color
85/// aesthetic is set to the `source` aesthetic's *mapped* color, adjusted in
86/// lightness (`+` toward white, `-` toward black, in `-1.0..=1.0`).
87#[derive(Clone, Debug)]
88pub struct AfterScaleSpec {
89    pub target: Aesthetic,
90    pub source: Aesthetic,
91    pub lightness: f64,
92}
93
94/// Builder for aesthetic mappings.
95#[derive(Clone, Debug, Default)]
96pub struct Aes {
97    pub mappings: Vec<AesMapping>,
98    /// Post-scale color derivations (`after_scale`).
99    pub after_scale: Vec<AfterScaleSpec>,
100}
101
102impl Aes {
103    pub fn new() -> Self {
104        Self::default()
105    }
106
107    fn push(mut self, col: &str, aesthetic: Aesthetic) -> Self {
108        self.mappings.push(AesMapping {
109            column: col.to_string(),
110            aesthetic,
111            stage: MappingStage::BeforeStat,
112        });
113        self
114    }
115
116    fn push_after_stat(mut self, col: &str, aesthetic: Aesthetic) -> Self {
117        self.mappings.push(AesMapping {
118            column: col.to_string(),
119            aesthetic,
120            stage: MappingStage::AfterStat,
121        });
122        self
123    }
124
125    // ─── after_scale() — post-scale color derivation ────────────
126
127    /// Set `fill` to the mapped `color` with adjusted lightness (R's
128    /// `aes(fill = after_scale(...))`). `lightness` in `-1.0..=1.0`: positive
129    /// lightens toward white, negative darkens toward black.
130    pub fn after_scale_fill_from_color(mut self, lightness: f64) -> Self {
131        self.after_scale.push(AfterScaleSpec {
132            target: Aesthetic::Fill,
133            source: Aesthetic::Color,
134            lightness,
135        });
136        self
137    }
138
139    /// Set `color` to the mapped `fill` with adjusted lightness — useful for a
140    /// darker border around a filled shape (`color = after_scale(darken(fill))`).
141    pub fn after_scale_color_from_fill(mut self, lightness: f64) -> Self {
142        self.after_scale.push(AfterScaleSpec {
143            target: Aesthetic::Color,
144            source: Aesthetic::Fill,
145            lightness,
146        });
147        self
148    }
149
150    /// `stage(start, after_stat)`: map `aesthetic` from `start` before the stat
151    /// and re-map it from `after_stat` afterwards (R's `stage()`).
152    pub fn stage(mut self, aesthetic: Aesthetic, start: &str, after_stat: &str) -> Self {
153        self.mappings.push(AesMapping {
154            column: start.to_string(),
155            aesthetic: aesthetic.clone(),
156            stage: MappingStage::BeforeStat,
157        });
158        self.mappings.push(AesMapping {
159            column: after_stat.to_string(),
160            aesthetic,
161            stage: MappingStage::AfterStat,
162        });
163        self
164    }
165
166    pub fn x(self, col: &str) -> Self {
167        self.push(col, Aesthetic::X)
168    }
169    pub fn y(self, col: &str) -> Self {
170        self.push(col, Aesthetic::Y)
171    }
172    pub fn color(self, col: &str) -> Self {
173        self.push(col, Aesthetic::Color)
174    }
175    pub fn fill(self, col: &str) -> Self {
176        self.push(col, Aesthetic::Fill)
177    }
178    pub fn size(self, col: &str) -> Self {
179        self.push(col, Aesthetic::Size)
180    }
181    pub fn shape(self, col: &str) -> Self {
182        self.push(col, Aesthetic::Shape)
183    }
184    pub fn alpha(self, col: &str) -> Self {
185        self.push(col, Aesthetic::Alpha)
186    }
187    pub fn group(self, col: &str) -> Self {
188        self.push(col, Aesthetic::Group)
189    }
190    pub fn ymin(self, col: &str) -> Self {
191        self.push(col, Aesthetic::Ymin)
192    }
193    pub fn ymax(self, col: &str) -> Self {
194        self.push(col, Aesthetic::Ymax)
195    }
196    pub fn label(self, col: &str) -> Self {
197        self.push(col, Aesthetic::Label)
198    }
199    pub fn weight(self, col: &str) -> Self {
200        self.push(col, Aesthetic::Weight)
201    }
202    pub fn xend(self, col: &str) -> Self {
203        self.push(col, Aesthetic::Xend)
204    }
205    pub fn yend(self, col: &str) -> Self {
206        self.push(col, Aesthetic::Yend)
207    }
208    pub fn xmin(self, col: &str) -> Self {
209        self.push(col, Aesthetic::Xmin)
210    }
211    pub fn xmax(self, col: &str) -> Self {
212        self.push(col, Aesthetic::Xmax)
213    }
214    pub fn angle(self, col: &str) -> Self {
215        self.push(col, Aesthetic::Angle)
216    }
217    pub fn radius(self, col: &str) -> Self {
218        self.push(col, Aesthetic::Radius)
219    }
220    pub fn linetype(self, col: &str) -> Self {
221        self.push(col, Aesthetic::Linetype)
222    }
223    /// Opening price (`geom_candlestick` / `geom_ohlc`).
224    pub fn open(self, col: &str) -> Self {
225        self.push(col, Aesthetic::Open)
226    }
227    /// Period high (`geom_candlestick` / `geom_ohlc`).
228    pub fn high(self, col: &str) -> Self {
229        self.push(col, Aesthetic::High)
230    }
231    /// Period low (`geom_candlestick` / `geom_ohlc`).
232    pub fn low(self, col: &str) -> Self {
233        self.push(col, Aesthetic::Low)
234    }
235    /// Closing price (`geom_candlestick` / `geom_ohlc`).
236    pub fn close(self, col: &str) -> Self {
237        self.push(col, Aesthetic::Close)
238    }
239
240    // ─── after_stat() mappings ──────────────────────────────────
241    // Map stat-computed columns (e.g., `density`, `count`, `ncount`, `ndensity`)
242    // to an aesthetic. These are resolved after the stat step in the build pipeline.
243
244    /// Map a stat-computed column to the y aesthetic (e.g., `after_stat_y("density")`).
245    pub fn after_stat_y(self, col: &str) -> Self {
246        self.push_after_stat(col, Aesthetic::Y)
247    }
248
249    /// Map a stat-computed column to the x aesthetic.
250    pub fn after_stat_x(self, col: &str) -> Self {
251        self.push_after_stat(col, Aesthetic::X)
252    }
253
254    /// Map a stat-computed column to the fill aesthetic.
255    pub fn after_stat_fill(self, col: &str) -> Self {
256        self.push_after_stat(col, Aesthetic::Fill)
257    }
258
259    /// Map a stat-computed column to the color aesthetic.
260    pub fn after_stat_color(self, col: &str) -> Self {
261        self.push_after_stat(col, Aesthetic::Color)
262    }
263
264    /// Map a stat-computed column to the size aesthetic.
265    pub fn after_stat_size(self, col: &str) -> Self {
266        self.push_after_stat(col, Aesthetic::Size)
267    }
268
269    /// Map a stat-computed column to the alpha aesthetic.
270    pub fn after_stat_alpha(self, col: &str) -> Self {
271        self.push_after_stat(col, Aesthetic::Alpha)
272    }
273
274    /// Get the column mapped to a specific aesthetic.
275    pub fn get_mapping(&self, aes: &Aesthetic) -> Option<&str> {
276        self.mappings
277            .iter()
278            .find(|m| m.aesthetic == *aes)
279            .map(|m| m.column.as_str())
280    }
281
282    /// Merge another Aes into this one. The other's mappings override on conflict.
283    pub fn merge(&self, other: &Aes) -> Aes {
284        let mut result = self.clone();
285        for m in &other.mappings {
286            // Remove existing mapping for same aesthetic
287            result
288                .mappings
289                .retain(|existing| existing.aesthetic != m.aesthetic);
290            result.mappings.push(m.clone());
291        }
292        result
293    }
294}