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