Skip to main content

ggplot_rs/aes/
mod.rs

1pub mod mapping;
2
3pub use mapping::{apply_after_stat, resolve_mappings};
4
5/// All supported aesthetic channels.
6#[derive(Clone, Debug, PartialEq, Eq, Hash)]
7pub enum Aesthetic {
8    X,
9    Y,
10    Color,
11    Fill,
12    Size,
13    Shape,
14    Alpha,
15    Linetype,
16    Group,
17    Ymin,
18    Ymax,
19    Xmin,
20    Xmax,
21    Label,
22    Weight,
23    Xend,
24    Yend,
25    Angle,
26    Radius,
27}
28
29impl Aesthetic {
30    /// The canonical column name used in the working DataFrame after aes evaluation.
31    pub fn col_name(&self) -> &str {
32        match self {
33            Aesthetic::X => "x",
34            Aesthetic::Y => "y",
35            Aesthetic::Color => "color",
36            Aesthetic::Fill => "fill",
37            Aesthetic::Size => "size",
38            Aesthetic::Shape => "shape",
39            Aesthetic::Alpha => "alpha",
40            Aesthetic::Linetype => "linetype",
41            Aesthetic::Group => "group",
42            Aesthetic::Ymin => "ymin",
43            Aesthetic::Ymax => "ymax",
44            Aesthetic::Xmin => "xmin",
45            Aesthetic::Xmax => "xmax",
46            Aesthetic::Label => "label",
47            Aesthetic::Weight => "weight",
48            Aesthetic::Xend => "xend",
49            Aesthetic::Yend => "yend",
50            Aesthetic::Angle => "angle",
51            Aesthetic::Radius => "radius",
52        }
53    }
54}
55
56/// When an aesthetic mapping should be resolved.
57#[derive(Clone, Debug, PartialEq)]
58pub enum MappingStage {
59    /// Resolve before stat computation (default — maps from raw data columns).
60    BeforeStat,
61    /// Resolve after stat computation (maps from stat-computed columns like `density`, `count`).
62    AfterStat,
63}
64
65/// Maps a source column to an aesthetic channel.
66#[derive(Clone, Debug)]
67pub struct AesMapping {
68    pub column: String,
69    pub aesthetic: Aesthetic,
70    pub stage: MappingStage,
71}
72
73/// Builder for aesthetic mappings.
74#[derive(Clone, Debug, Default)]
75pub struct Aes {
76    pub mappings: Vec<AesMapping>,
77}
78
79impl Aes {
80    pub fn new() -> Self {
81        Self::default()
82    }
83
84    fn push(mut self, col: &str, aesthetic: Aesthetic) -> Self {
85        self.mappings.push(AesMapping {
86            column: col.to_string(),
87            aesthetic,
88            stage: MappingStage::BeforeStat,
89        });
90        self
91    }
92
93    fn push_after_stat(mut self, col: &str, aesthetic: Aesthetic) -> Self {
94        self.mappings.push(AesMapping {
95            column: col.to_string(),
96            aesthetic,
97            stage: MappingStage::AfterStat,
98        });
99        self
100    }
101
102    pub fn x(self, col: &str) -> Self {
103        self.push(col, Aesthetic::X)
104    }
105    pub fn y(self, col: &str) -> Self {
106        self.push(col, Aesthetic::Y)
107    }
108    pub fn color(self, col: &str) -> Self {
109        self.push(col, Aesthetic::Color)
110    }
111    pub fn fill(self, col: &str) -> Self {
112        self.push(col, Aesthetic::Fill)
113    }
114    pub fn size(self, col: &str) -> Self {
115        self.push(col, Aesthetic::Size)
116    }
117    pub fn shape(self, col: &str) -> Self {
118        self.push(col, Aesthetic::Shape)
119    }
120    pub fn alpha(self, col: &str) -> Self {
121        self.push(col, Aesthetic::Alpha)
122    }
123    pub fn group(self, col: &str) -> Self {
124        self.push(col, Aesthetic::Group)
125    }
126    pub fn ymin(self, col: &str) -> Self {
127        self.push(col, Aesthetic::Ymin)
128    }
129    pub fn ymax(self, col: &str) -> Self {
130        self.push(col, Aesthetic::Ymax)
131    }
132    pub fn label(self, col: &str) -> Self {
133        self.push(col, Aesthetic::Label)
134    }
135    pub fn weight(self, col: &str) -> Self {
136        self.push(col, Aesthetic::Weight)
137    }
138    pub fn xend(self, col: &str) -> Self {
139        self.push(col, Aesthetic::Xend)
140    }
141    pub fn yend(self, col: &str) -> Self {
142        self.push(col, Aesthetic::Yend)
143    }
144    pub fn xmin(self, col: &str) -> Self {
145        self.push(col, Aesthetic::Xmin)
146    }
147    pub fn xmax(self, col: &str) -> Self {
148        self.push(col, Aesthetic::Xmax)
149    }
150    pub fn angle(self, col: &str) -> Self {
151        self.push(col, Aesthetic::Angle)
152    }
153    pub fn radius(self, col: &str) -> Self {
154        self.push(col, Aesthetic::Radius)
155    }
156    pub fn linetype(self, col: &str) -> Self {
157        self.push(col, Aesthetic::Linetype)
158    }
159
160    // ─── after_stat() mappings ──────────────────────────────────
161    // Map stat-computed columns (e.g., `density`, `count`, `ncount`, `ndensity`)
162    // to an aesthetic. These are resolved after the stat step in the build pipeline.
163
164    /// Map a stat-computed column to the y aesthetic (e.g., `after_stat_y("density")`).
165    pub fn after_stat_y(self, col: &str) -> Self {
166        self.push_after_stat(col, Aesthetic::Y)
167    }
168
169    /// Map a stat-computed column to the x aesthetic.
170    pub fn after_stat_x(self, col: &str) -> Self {
171        self.push_after_stat(col, Aesthetic::X)
172    }
173
174    /// Map a stat-computed column to the fill aesthetic.
175    pub fn after_stat_fill(self, col: &str) -> Self {
176        self.push_after_stat(col, Aesthetic::Fill)
177    }
178
179    /// Map a stat-computed column to the color aesthetic.
180    pub fn after_stat_color(self, col: &str) -> Self {
181        self.push_after_stat(col, Aesthetic::Color)
182    }
183
184    /// Map a stat-computed column to the size aesthetic.
185    pub fn after_stat_size(self, col: &str) -> Self {
186        self.push_after_stat(col, Aesthetic::Size)
187    }
188
189    /// Map a stat-computed column to the alpha aesthetic.
190    pub fn after_stat_alpha(self, col: &str) -> Self {
191        self.push_after_stat(col, Aesthetic::Alpha)
192    }
193
194    /// Get the column mapped to a specific aesthetic.
195    pub fn get_mapping(&self, aes: &Aesthetic) -> Option<&str> {
196        self.mappings
197            .iter()
198            .find(|m| m.aesthetic == *aes)
199            .map(|m| m.column.as_str())
200    }
201
202    /// Merge another Aes into this one. The other's mappings override on conflict.
203    pub fn merge(&self, other: &Aes) -> Aes {
204        let mut result = self.clone();
205        for m in &other.mappings {
206            // Remove existing mapping for same aesthetic
207            result
208                .mappings
209                .retain(|existing| existing.aesthetic != m.aesthetic);
210            result.mappings.push(m.clone());
211        }
212        result
213    }
214}