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/// Builder for aesthetic mappings.
75#[derive(Clone, Debug, Default)]
76pub struct Aes {
77    pub mappings: Vec<AesMapping>,
78}
79
80impl Aes {
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    fn push(mut self, col: &str, aesthetic: Aesthetic) -> Self {
86        self.mappings.push(AesMapping {
87            column: col.to_string(),
88            aesthetic,
89            stage: MappingStage::BeforeStat,
90        });
91        self
92    }
93
94    fn push_after_stat(mut self, col: &str, aesthetic: Aesthetic) -> Self {
95        self.mappings.push(AesMapping {
96            column: col.to_string(),
97            aesthetic,
98            stage: MappingStage::AfterStat,
99        });
100        self
101    }
102
103    pub fn x(self, col: &str) -> Self {
104        self.push(col, Aesthetic::X)
105    }
106    pub fn y(self, col: &str) -> Self {
107        self.push(col, Aesthetic::Y)
108    }
109    pub fn color(self, col: &str) -> Self {
110        self.push(col, Aesthetic::Color)
111    }
112    pub fn fill(self, col: &str) -> Self {
113        self.push(col, Aesthetic::Fill)
114    }
115    pub fn size(self, col: &str) -> Self {
116        self.push(col, Aesthetic::Size)
117    }
118    pub fn shape(self, col: &str) -> Self {
119        self.push(col, Aesthetic::Shape)
120    }
121    pub fn alpha(self, col: &str) -> Self {
122        self.push(col, Aesthetic::Alpha)
123    }
124    pub fn group(self, col: &str) -> Self {
125        self.push(col, Aesthetic::Group)
126    }
127    pub fn ymin(self, col: &str) -> Self {
128        self.push(col, Aesthetic::Ymin)
129    }
130    pub fn ymax(self, col: &str) -> Self {
131        self.push(col, Aesthetic::Ymax)
132    }
133    pub fn label(self, col: &str) -> Self {
134        self.push(col, Aesthetic::Label)
135    }
136    pub fn weight(self, col: &str) -> Self {
137        self.push(col, Aesthetic::Weight)
138    }
139    pub fn xend(self, col: &str) -> Self {
140        self.push(col, Aesthetic::Xend)
141    }
142    pub fn yend(self, col: &str) -> Self {
143        self.push(col, Aesthetic::Yend)
144    }
145    pub fn xmin(self, col: &str) -> Self {
146        self.push(col, Aesthetic::Xmin)
147    }
148    pub fn xmax(self, col: &str) -> Self {
149        self.push(col, Aesthetic::Xmax)
150    }
151    pub fn angle(self, col: &str) -> Self {
152        self.push(col, Aesthetic::Angle)
153    }
154    pub fn radius(self, col: &str) -> Self {
155        self.push(col, Aesthetic::Radius)
156    }
157    pub fn linetype(self, col: &str) -> Self {
158        self.push(col, Aesthetic::Linetype)
159    }
160
161    // ─── after_stat() mappings ──────────────────────────────────
162    // Map stat-computed columns (e.g., `density`, `count`, `ncount`, `ndensity`)
163    // to an aesthetic. These are resolved after the stat step in the build pipeline.
164
165    /// Map a stat-computed column to the y aesthetic (e.g., `after_stat_y("density")`).
166    pub fn after_stat_y(self, col: &str) -> Self {
167        self.push_after_stat(col, Aesthetic::Y)
168    }
169
170    /// Map a stat-computed column to the x aesthetic.
171    pub fn after_stat_x(self, col: &str) -> Self {
172        self.push_after_stat(col, Aesthetic::X)
173    }
174
175    /// Map a stat-computed column to the fill aesthetic.
176    pub fn after_stat_fill(self, col: &str) -> Self {
177        self.push_after_stat(col, Aesthetic::Fill)
178    }
179
180    /// Map a stat-computed column to the color aesthetic.
181    pub fn after_stat_color(self, col: &str) -> Self {
182        self.push_after_stat(col, Aesthetic::Color)
183    }
184
185    /// Map a stat-computed column to the size aesthetic.
186    pub fn after_stat_size(self, col: &str) -> Self {
187        self.push_after_stat(col, Aesthetic::Size)
188    }
189
190    /// Map a stat-computed column to the alpha aesthetic.
191    pub fn after_stat_alpha(self, col: &str) -> Self {
192        self.push_after_stat(col, Aesthetic::Alpha)
193    }
194
195    /// Get the column mapped to a specific aesthetic.
196    pub fn get_mapping(&self, aes: &Aesthetic) -> Option<&str> {
197        self.mappings
198            .iter()
199            .find(|m| m.aesthetic == *aes)
200            .map(|m| m.column.as_str())
201    }
202
203    /// Merge another Aes into this one. The other's mappings override on conflict.
204    pub fn merge(&self, other: &Aes) -> Aes {
205        let mut result = self.clone();
206        for m in &other.mappings {
207            // Remove existing mapping for same aesthetic
208            result
209                .mappings
210                .retain(|existing| existing.aesthetic != m.aesthetic);
211            result.mappings.push(m.clone());
212        }
213        result
214    }
215}