Skip to content

Commit 559dec8

Browse files
authored
Consolidate aesthetic information (#135)
1 parent 871a2b1 commit 559dec8

20 files changed

Lines changed: 356 additions & 222 deletions

File tree

src/execute/mod.rs

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pub use schema::TypeInfo;
2323

2424
use crate::naming;
2525
use crate::parser;
26-
use crate::plot::layer::geom::GeomAesthetics;
26+
use crate::plot::aesthetic::{primary_aesthetic, ALL_POSITIONAL};
2727
use crate::plot::{AestheticValue, Layer, Scale, ScaleTypeKind, Schema};
2828
use crate::{DataFrame, GgsqlError, Plot, Result};
2929
use std::collections::{HashMap, HashSet};
@@ -285,12 +285,6 @@ fn add_discrete_columns_to_partition_by(
285285
layer_schemas: &[Schema],
286286
scales: &[Scale],
287287
) {
288-
// Positional aesthetics should NOT be auto-added to grouping.
289-
// Stats that need to group by positional aesthetics (like bar/histogram)
290-
// already handle this themselves via stat_consumed_aesthetics().
291-
const POSITIONAL_AESTHETICS: &[&str] =
292-
&["x", "y", "xmin", "xmax", "ymin", "ymax", "xend", "yend"];
293-
294288
// Build a map of aesthetic -> scale for quick lookup
295289
let scale_map: HashMap<&str, &Scale> =
296290
scales.iter().map(|s| (s.aesthetic.as_str(), s)).collect();
@@ -307,8 +301,10 @@ fn add_discrete_columns_to_partition_by(
307301
let consumed_aesthetics = layer.geom.stat_consumed_aesthetics();
308302

309303
for (aesthetic, value) in &layer.mappings.aesthetics {
310-
// Skip positional aesthetics - these should not trigger auto-grouping
311-
if POSITIONAL_AESTHETICS.contains(&aesthetic.as_str()) {
304+
// Skip positional aesthetics - these should not trigger auto-grouping.
305+
// Stats that need to group by positional aesthetics (like bar/histogram)
306+
// already handle this themselves via stat_consumed_aesthetics().
307+
if ALL_POSITIONAL.iter().any(|s| s == aesthetic) {
312308
continue;
313309
}
314310

@@ -329,7 +325,7 @@ fn add_discrete_columns_to_partition_by(
329325
//
330326
// Discrete and Binned scales produce categorical groupings.
331327
// Continuous scales don't group. Identity defers to column type.
332-
let primary_aesthetic = GeomAesthetics::primary_aesthetic(aesthetic);
328+
let primary_aesthetic = primary_aesthetic(aesthetic);
333329
let is_discrete = if let Some(scale) = scale_map.get(primary_aesthetic) {
334330
if let Some(ref scale_type) = scale.scale_type {
335331
match scale_type.scale_type_kind() {
@@ -1063,20 +1059,20 @@ mod tests {
10631059
let result = prepare_data_with_reader(query, &reader).unwrap();
10641060
let layer = &result.specs[0].layers[0];
10651061

1066-
// Layer should have y2 in mappings (added by default for bar)
1062+
// Layer should have yend in mappings (added by default for bar)
10671063
assert!(
1068-
layer.mappings.aesthetics.contains_key("y2"),
1069-
"Bar should have y2 mapping for baseline: {:?}",
1064+
layer.mappings.aesthetics.contains_key("yend"),
1065+
"Bar should have yend mapping for baseline: {:?}",
10701066
layer.mappings.aesthetics.keys().collect::<Vec<_>>()
10711067
);
10721068

1073-
// The DataFrame should have the y2 column with 0 values
1069+
// The DataFrame should have the yend column with 0 values
10741070
let layer_df = result.data.get(&naming::layer_key(0)).unwrap();
1075-
let y2_col = naming::aesthetic_column("y2");
1071+
let yend_col = naming::aesthetic_column("yend");
10761072
assert!(
1077-
layer_df.column(&y2_col).is_ok(),
1073+
layer_df.column(&yend_col).is_ok(),
10781074
"DataFrame should have '{}' column: {:?}",
1079-
y2_col,
1075+
yend_col,
10801076
layer_df.get_column_names_str()
10811077
);
10821078
}

src/execute/scale.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
//! and out-of-bounds (OOB) handling.
66
77
use crate::naming;
8-
use crate::plot::layer::geom::{get_aesthetic_family, GeomAesthetics};
8+
use crate::plot::aesthetic::primary_aesthetic;
9+
use crate::plot::layer::geom::get_aesthetic_family;
910
use crate::plot::scale::{
1011
default_oob, gets_default_scale, infer_scale_target_type, infer_transform_from_input_range,
1112
transform::Transform, OOB_CENSOR, OOB_KEEP, OOB_SQUISH,
@@ -32,11 +33,11 @@ pub fn create_missing_scales(spec: &mut Plot) {
3233
// (global mappings have already been merged into layers at this point)
3334
for layer in &spec.layers {
3435
for aesthetic in layer.mappings.aesthetics.keys() {
35-
let primary = GeomAesthetics::primary_aesthetic(aesthetic);
36+
let primary = primary_aesthetic(aesthetic);
3637
used_aesthetics.insert(primary.to_string());
3738
}
3839
for aesthetic in layer.remappings.aesthetics.keys() {
39-
let primary = GeomAesthetics::primary_aesthetic(aesthetic);
40+
let primary = primary_aesthetic(aesthetic);
4041
used_aesthetics.insert(primary.to_string());
4142
}
4243
}
@@ -73,7 +74,7 @@ pub fn create_missing_scales_post_stat(spec: &mut Plot) {
7374
// Collect all aesthetics currently in layer mappings
7475
for layer in &spec.layers {
7576
for aesthetic in layer.mappings.aesthetics.keys() {
76-
let primary = GeomAesthetics::primary_aesthetic(aesthetic);
77+
let primary = primary_aesthetic(aesthetic);
7778
current_aesthetics.insert(primary.to_string());
7879
}
7980
}
@@ -1254,15 +1255,15 @@ mod tests {
12541255
assert!(x_family.contains(&"x"));
12551256
assert!(x_family.contains(&"xmin"));
12561257
assert!(x_family.contains(&"xmax"));
1257-
assert!(x_family.contains(&"x2"));
12581258
assert!(x_family.contains(&"xend"));
1259+
assert_eq!(x_family.len(), 4);
12591260

12601261
let y_family = get_aesthetic_family("y");
12611262
assert!(y_family.contains(&"y"));
12621263
assert!(y_family.contains(&"ymin"));
12631264
assert!(y_family.contains(&"ymax"));
1264-
assert!(y_family.contains(&"y2"));
12651265
assert!(y_family.contains(&"yend"));
1266+
assert_eq!(y_family.len(), 4);
12661267

12671268
// Test non-family aesthetics return just themselves
12681269
let color_family = get_aesthetic_family("color");

src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ pub use plot::{
5555
AestheticValue, DataSource, Facet, Geom, Layer, Mappings, Plot, Scale, SqlExpression,
5656
};
5757

58+
// Re-export aesthetic classification utilities
59+
pub use plot::aesthetic::{
60+
get_aesthetic_family, is_aesthetic_name, is_positional_aesthetic, is_primary_positional,
61+
primary_aesthetic, AESTHETIC_FAMILIES, ALL_POSITIONAL, NON_POSITIONAL, PRIMARY_POSITIONAL,
62+
};
63+
5864
// Future modules - not yet implemented
5965
// #[cfg(feature = "engine")]
6066
// pub mod engine;

src/naming.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -285,21 +285,21 @@ pub fn is_synthetic_column(name: &str) -> bool {
285285
/// Generate bin end column name for a binned column.
286286
///
287287
/// Used by the Vega-Lite writer to store the upper bound of a bin
288-
/// when using `bin: "binned"` encoding with x2/y2 channels.
288+
/// when using `bin: "binned"` encoding with xend/yend channels.
289289
///
290290
/// If the column is an aesthetic column (e.g., `__ggsql_aes_x__`), returns
291-
/// the corresponding `2` aesthetic (e.g., `__ggsql_aes_x2__`).
291+
/// the corresponding `end` aesthetic (e.g., `__ggsql_aes_xend__`).
292292
///
293293
/// # Example
294294
/// ```
295295
/// use ggsql::naming;
296296
/// assert_eq!(naming::bin_end_column("temperature"), "__ggsql_bin_end_temperature__");
297-
/// assert_eq!(naming::bin_end_column("__ggsql_aes_x__"), "__ggsql_aes_x2__");
297+
/// assert_eq!(naming::bin_end_column("__ggsql_aes_x__"), "__ggsql_aes_xend__");
298298
/// ```
299299
pub fn bin_end_column(column: &str) -> String {
300-
// If it's an aesthetic column, use the x2/y2 naming convention
300+
// If it's an aesthetic column, use the xend/yend naming convention
301301
if let Some(aesthetic) = extract_aesthetic_name(column) {
302-
return aesthetic_column(&format!("{}2", aesthetic));
302+
return aesthetic_column(&format!("{}end", aesthetic));
303303
}
304304
format!("{}bin_end_{}{}", GGSQL_PREFIX, column, GGSQL_SUFFIX)
305305
}
@@ -452,9 +452,9 @@ mod tests {
452452
assert_eq!(bin_end_column("x"), "__ggsql_bin_end_x__");
453453
assert_eq!(bin_end_column("value"), "__ggsql_bin_end_value__");
454454

455-
// Aesthetic columns use the x2/y2 convention
456-
assert_eq!(bin_end_column("__ggsql_aes_x__"), "__ggsql_aes_x2__");
457-
assert_eq!(bin_end_column("__ggsql_aes_y__"), "__ggsql_aes_y2__");
455+
// Aesthetic columns use the xend/yend convention
456+
assert_eq!(bin_end_column("__ggsql_aes_x__"), "__ggsql_aes_xend__");
457+
assert_eq!(bin_end_column("__ggsql_aes_y__"), "__ggsql_aes_yend__");
458458
}
459459

460460
#[test]

src/parser/builder.rs

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! Takes a tree-sitter parse tree and builds a typed Plot,
44
//! handling all the node types defined in the grammar.
55
6+
use crate::plot::aesthetic::is_aesthetic_name;
67
use crate::plot::layer::geom::Geom;
78
use crate::plot::scale::{color_to_hex, is_color_aesthetic, Transform};
89
use crate::plot::*;
@@ -972,36 +973,6 @@ fn validate_coord_properties(
972973
Ok(())
973974
}
974975

975-
/// Check if a property name is an aesthetic name
976-
fn is_aesthetic_name(name: &str) -> bool {
977-
matches!(
978-
name,
979-
"x" | "y"
980-
| "xmin"
981-
| "xmax"
982-
| "ymin"
983-
| "ymax"
984-
| "xend"
985-
| "yend"
986-
| "color"
987-
| "colour"
988-
| "fill"
989-
| "stroke"
990-
| "opacity"
991-
| "size"
992-
| "shape"
993-
| "linetype"
994-
| "linewidth"
995-
| "width"
996-
| "height"
997-
| "label"
998-
| "family"
999-
| "fontface"
1000-
| "hjust"
1001-
| "vjust"
1002-
)
1003-
}
1004-
1005976
/// Parse coord type from a coord_type node
1006977
fn parse_coord_type(node: &Node, source: &SourceTree) -> Result<CoordType> {
1007978
let text = source.get_text(node);

0 commit comments

Comments
 (0)