diff --git a/CLAUDE.md b/CLAUDE.md index cf082c6a..b90735e7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1470,7 +1470,7 @@ PROJECT [, ...] TO [SETTING ] **Components**: -- **Aesthetics** (optional): Comma-separated list of positional aesthetic names. If omitted, uses coord defaults. +- **Aesthetics** (optional): Comma-separated list of position aesthetic names. If omitted, uses coord defaults. - **TO**: Required keyword separating aesthetics from coord type. - **coord_type**: Either `cartesian` or `polar`. - **SETTING** (optional): Additional properties. @@ -1511,7 +1511,7 @@ Note: For axis limits, use `SCALE x FROM [min, max]` or `SCALE y FROM [min, max] 1. **Axis limits**: Use `SCALE x/y FROM [min, max]` to set axis limits 2. **Aesthetic domains**: Use `SCALE FROM [...]` to set aesthetic domains -3. **Custom aesthetics**: User can define custom positional names (e.g., `PROJECT a, b TO cartesian`) +3. **Custom aesthetics**: User can define custom position names (e.g., `PROJECT a, b TO cartesian`) 4. **Multi-layer support**: All projection transforms apply to all layers **Status**: diff --git a/doc/syntax/clause/facet.qmd b/doc/syntax/clause/facet.qmd index 710fa48e..eac664a0 100644 --- a/doc/syntax/clause/facet.qmd +++ b/doc/syntax/clause/facet.qmd @@ -2,7 +2,9 @@ title: "Create small multiples with `FACET`" --- -The `FACET` clause allows you to split your data, by one or two variables, and plot each group as a small version of the plot: a technique called 'small multiples'. The technique is great for preventing overplotting. Because each small plot shares the same positional scales by default, visual comparisons between groups become easy. +The `FACET` clause allows you to split your data, by one or two variables, and plot each group as a small version of the plot: a technique called 'small multiples'. +The technique is great for preventing overplotting. +Because each small plot shares the same position scales by default, visual comparisons between groups become easy. ## Clause syntax The `FACET` syntax contains a number of subclauses that are all optional @@ -28,7 +30,7 @@ SETTING => , ... This clause behaves much like the `SETTINGS` clause in `DRAW`, in that it allows you to fine-tune specific behavior of the faceting. The following parameters exist: -* `free`: Controls whether the positional scales are independent across the small multiples. Permissible values are: +* `free`: Controls whether the position scales are independent across the small multiples. Permissible values are: * `null` or omitted (default): Shared/fixed scales across all panels * `'x'`: Independent x-axis scale, shared y-axis scale * `'y'`: Shared x-axis scale, independent y-axis scale diff --git a/doc/syntax/clause/project.qmd b/doc/syntax/clause/project.qmd index d7753ce5..35640bf8 100644 --- a/doc/syntax/clause/project.qmd +++ b/doc/syntax/clause/project.qmd @@ -2,7 +2,7 @@ title: "Control the coordinate system with `PROJECT`" --- -The `PROJECT` clause defines the projection of the plot, that is, how abstract positional aesthetics are translated (projected) onto the plane defined by the screen/paper where the plot is viewed on. +The `PROJECT` clause defines the projection of the plot, that is, how abstract position aesthetics are translated (projected) onto the plane defined by the screen/paper where the plot is viewed on. ## Clause syntax The `PROJECT` syntax contains a number of subclauses @@ -12,14 +12,19 @@ PROJECT , ... TO SETTING => , ... ``` -The comma-separated list of `aesthetic` names are optional but allows you to define the names of the positional aesthetics in the plot. If omitted, the default aesthetic names of the coordinate system is used. The order given matters as the first name is used for the primary aesthetic, the second name for the secondary aesthetic and so on. For instance, using `PROJECT y, x TO cartesian` will flip the plot as anything mapped to `y` will now relate to the horizontal axis, and anything mapped to `x` will relate to the vertical axis. Note that it is not allowed to use the name of already established aesthetics as positional aesthetics, e.g. `PROJECT fill, stroke TO polar` is not allowed. +The comma-separated list of `aesthetic` names are optional but allows you to define the names of the position aesthetics in the plot. +If omitted, the default aesthetic names of the coordinate system is used. +The order given matters as the first name is used for the primary aesthetic, the second name for the secondary aesthetic and so on. +For instance, using `PROJECT y, x TO cartesian` will flip the plot as anything mapped to `y` will now relate to the horizontal axis, and anything mapped to `x` will relate to the vertical axis. +Note that it is not allowed to use the name of already established aesthetics as position aesthetics, e.g. `PROJECT fill, stroke TO polar` is not allowed. ### `TO` ```ggsql TO ``` -The `TO` clause is required and is followed by the name of the [coordinate system](../index.qmd#coordinate-systems). The coordinate system provides default names for the positional aesthetics and is responsible for how to translate values mapped to these onto the plane defined by the screen or paper. +The `TO` clause is required and is followed by the name of the [coordinate system](../index.qmd#coordinate-systems). +The coordinate system provides default names for the position aesthetics and is responsible for how to translate values mapped to these onto the plane defined by the screen or paper. ### `SETTING` ```ggsql diff --git a/doc/syntax/clause/scale.qmd b/doc/syntax/clause/scale.qmd index 7dbac50b..68d1294a 100644 --- a/doc/syntax/clause/scale.qmd +++ b/doc/syntax/clause/scale.qmd @@ -23,7 +23,9 @@ The `type` defines the class of scale to use. It can be one of four different ty Read more about each type at their dedicated documentation. You do not have to specify the type as it is deduced from the transform, input range, or data if left blank. -You *must* specify an aesthetic so that the scale knows which mapping it belongs to. For positional aesthetics you will provide the base name (`x` or `y`) even though you are mapping to e.g. `xmin`. Creating a scale for `colour` (or `color`) will create a scale for both fill and stroke colour based on the settings. +You *must* specify an aesthetic so that the scale knows which mapping it belongs to. +For position aesthetics you will provide the base name (`x` or `y`) even though you are mapping to e.g. `xmin`. +Creating a scale for `colour` (or `color`) will create a scale for both fill and stroke colour based on the settings. ### `FROM` ```ggsql diff --git a/doc/syntax/coord/cartesian.qmd b/doc/syntax/coord/cartesian.qmd index c807b88d..fe94d80e 100644 --- a/doc/syntax/coord/cartesian.qmd +++ b/doc/syntax/coord/cartesian.qmd @@ -2,10 +2,11 @@ title: Cartesian --- -The Cartesian coordinate system is the most well-known and the default for ggsql. It maps the primary positional aesthetic along a horizontal axis and the secondary along a perpendicular vertical axis. +The Cartesian coordinate system is the most well-known and the default for ggsql. +It maps the primary position aesthetic along a horizontal axis and the secondary along a perpendicular vertical axis. ## Default aesthetics -The Cartesian coordinate system has the following default positional aesthetics which will be used if no others have been provided: +The Cartesian coordinate system has the following default position aesthetics which will be used if no others have been provided: * **Primary**: `x` (horizontal position) * **Secondary**: `y` (vertical position) @@ -16,7 +17,7 @@ Users can provide their own aesthetic names if needed, e.g. PROJECT p, q TO cartesian ``` -assuming they do not try to use a name that is already being used by any facet or non-positional aesthetics (e.g. `PROJECT fill, panel TO cartesian` is not allowed). +assuming they do not try to use a name that is already being used by any facet or material aesthetics (e.g. `PROJECT fill, panel TO cartesian` is not allowed). ## Settings * `clip`: Should data be removed if it appears outside the bounds of the coordinate system. Defaults to `true` @@ -24,7 +25,7 @@ assuming they do not try to use a name that is already being used by any facet o ## Examples -### Use custom positional aesthetic names +### Use custom position aesthetic names ```{ggsql} VISUALISE bill_len AS p, bill_dep AS q FROM ggsql:penguins diff --git a/doc/syntax/coord/polar.qmd b/doc/syntax/coord/polar.qmd index 75adaab0..65d2c491 100644 --- a/doc/syntax/coord/polar.qmd +++ b/doc/syntax/coord/polar.qmd @@ -5,7 +5,7 @@ title: Polar The polar coordinate system interprets its primary aesthetic as the angular position relative to the center, and the secondary aesthetic as the distance from the center. It is most often used for pie-charts and radar plots. ## Default aesthetics -The polar coordinate system has the following default positional aesthetics which will be used if no others have been provided: +The polar coordinate system has the following default position aesthetics which will be used if no others have been provided: * **Primary**: `radius` (distance from center) * **Secondary**: `theta` (angular position) diff --git a/doc/syntax/index.qmd b/doc/syntax/index.qmd index 8b2f8837..ee0393e6 100644 --- a/doc/syntax/index.qmd +++ b/doc/syntax/index.qmd @@ -62,7 +62,8 @@ A scale is responsible for translating a data value to an aesthetic literal, e.g - [`identity`](scale/type/identity.qmd) scales passes the data through unchanged ## Coordinate systems -The coordinate system defines how the abstract positional aesthetics are projected onto the screen or paper where the final plot appears. As such, it has great influence over the final look of the plot. +The coordinate system defines how the abstract position aesthetics are projected onto the screen or paper where the final plot appears. +As such, it has great influence over the final look of the plot. - [`cartesian`](coord/cartesian.qmd) is the classic coordinate system consisting of two perpendicular axes, one being horizontal and one being vertical - [`polar`](coord/polar.qmd) interprets the primary position as the angular location relative to the center and the secondary position as the distance (radius) from the center, and this creates a circular coordinate system diff --git a/doc/syntax/scale/aesthetic/0_position.qmd b/doc/syntax/scale/aesthetic/0_position.qmd index 69ec7d0c..2a3ed4f6 100644 --- a/doc/syntax/scale/aesthetic/0_position.qmd +++ b/doc/syntax/scale/aesthetic/0_position.qmd @@ -2,17 +2,21 @@ title: "Position" --- -Position aesthetics (`x` and `y`, plus all their variants) relate data to placement in the coordinate system of the plot. All layers need at least one of each positional aesthetic mapped in order to show its data. However, the layer may compute positional aesthetics from the mapping. For example, a bar plot calculates the `y` aesthetic by counting the number of records in each group. +Position aesthetics (`x` and `y`, plus all their variants) relate data to placement in the coordinate system of the plot. +All layers need at least one of each position aesthetic mapped in order to show its data. However, the layer may compute position aesthetics from the mapping. +For example, a bar plot calculates the `y` aesthetic by counting the number of records in each group. ## Literal values -Scales for position aesthetics never use an output range and always relate to the input range. This is a practical decision by ggsql because different writers may treat the positional aesthetic in different ways. ^[In reality one could easily think of positional literal values as either normalized position along the x or y axis, or absolute units of distance from the bottom left corner of the coordinate system.] +Scales for position aesthetics never use an output range and always relate to the input range. +This is a practical decision by ggsql because different writers may treat the position aesthetic in different ways. ^[In reality one could easily think of position literal values as either normalized position along the x or y axis, or absolute units of distance from the bottom left corner of the coordinate system.] ::: {.callout-note} The lack of true literal values in position means that it is currently hard to place data and annotation "in-between" breaks in a discrete position scale. ::: ## Aesthetic families -Positional aesthetics consist of two families: The `x` and `y` family. Each of these consist of their primary aesthetic along with a range of sub aesthetic defined by their suffix: +Position aesthetics consist of two families: The `x` and `y` family. +Each of these consist of their primary aesthetic along with a range of sub aesthetic defined by their suffix: * `2` * `end` @@ -22,4 +26,6 @@ Positional aesthetics consist of two families: The `x` and `y` family. Each of t Which version of aesthetic to use depends on the layer, but all aesthetics within a family is scaled by the same scale, which is named after its primary aesthetic. This means that even when rendering a layer that only uses `xmin` and `xmax`, you will still scale it by writing `SCALE x ...` and label it by writing `LABEL x => ...` ## Coordinate system -Another thing that makes positional aesthetics different from other aesthetic is that they are dependent on a coordinate system which takes position scales and defines how values should be converted to a location on a plane. The default Cartesian coordinate system does what is generally expected: it places points along two perpendicular axes in a 2D plane. Other systems such as polar coordinates may dramatically change the look of a layer, transforming both the straightness of lines and positional relation of data. +Another thing that makes position aesthetics different from material aesthetics is that they are dependent on a coordinate system which takes position scales and defines how values should be converted to a location on a plane. +The default Cartesian coordinate system does what is generally expected: it places points along two perpendicular axes in a 2D plane. +Other systems such as polar coordinates may dramatically change the look of a layer, transforming both the straightness of lines and position relation of data. diff --git a/doc/syntax/scale/aesthetic/Z_faceting.qmd b/doc/syntax/scale/aesthetic/Z_faceting.qmd index 2d0fc200..d67a18f8 100644 --- a/doc/syntax/scale/aesthetic/Z_faceting.qmd +++ b/doc/syntax/scale/aesthetic/Z_faceting.qmd @@ -23,7 +23,8 @@ SCALE x SETTING breaks => 'weeks' ``` -In order to show data where the facet variable is null, it is necessary to explicitly include `null` in the input range of a facet aesthetic scale. Just like discrete positional aesthetics. You can also use `RENAMING` on the scale to customize facet strip labels. +In order to show data where the facet variable is null, it is necessary to explicitly include `null` in the input range of a facet aesthetic scale. +Just like discrete position aesthetics. You can also use `RENAMING` on the scale to customize facet strip labels. ```{ggsql} VISUALISE sex AS x FROM ggsql:penguins diff --git a/doc/syntax/scale/type/binned.qmd b/doc/syntax/scale/type/binned.qmd index 414ab0a8..1e0c5ee7 100644 --- a/doc/syntax/scale/type/binned.qmd +++ b/doc/syntax/scale/type/binned.qmd @@ -11,7 +11,8 @@ The binned scale is never chosen automatically so it must be selected explicitly ## Input range The input range for binned scales are defined by their minimum and maximum values. These can be given explicitly or deduced from the mapped data. If `FROM` is omitted then the range will be given as the minimum and maximum break values, whether provided directly or calculated. If provided as an array of length 2 then the first element will set the minimum and the second element will set the maximum. If either of these elements are `null` then that part of the range will be deduced from the data. As an example `SCALE BINNED x FROM [0, null]` will set the minimum part of the range to 0 and the maximum part to the maximal value of the mapped data. However, if neither input range nor explicit breaks are provided then the input range will be modified so that the calculated bins are even sized and include all data. This means that the range in most cases will expand past the minimum and maximum data values. -Positional aesthetics (`x` and `y`) will have their range expanded based on the `expand` setting. If values in the mapped data falls outside of the input domain the values will be changed based on the `oob` setting. +Position aesthetics (`x` and `y`) will have their range expanded based on the `expand` setting. +If values in the mapped data falls outside of the input domain the values will be changed based on the `oob` setting. The input range is converted to the type defined by the transform. This means that a time range can both be given as a `%H:%M:%S` string or as a numeric giving the number of nanoseconds since midnight. diff --git a/doc/syntax/scale/type/continuous.qmd b/doc/syntax/scale/type/continuous.qmd index bd2d3be4..3bfcac22 100644 --- a/doc/syntax/scale/type/continuous.qmd +++ b/doc/syntax/scale/type/continuous.qmd @@ -9,7 +9,8 @@ The continuous scale type maps various continuous data types into a continuous o ## Input range The input range for continuous scales are defined by their minimum and maximum values. These can be given explicitly or deduced from the mapped data. If `FROM` is omitted then the range of the mapped data is used. If provided as an array of length 2 then the first element will set the minimum and the second element will set the maximum. If either of these elements are `null` then that part of the range will be deduced from the data. As an example `SCALE x FROM [0, null]` will set the minimum part of the range to 0 and the maximum part to the maximal value of the mapped data. -Positional aesthetics (`x` and `y`) will have their range expanded based on the `expand` setting. If values in the mapped data falls outside of the input domain the values will be changed based on the `oob` setting. +Position aesthetics (`x` and `y`) will have their range expanded based on the `expand` setting. +If values in the mapped data falls outside of the input domain the values will be changed based on the `oob` setting. The input range is converted to the type defined by the transform. This means that a time range can both be given as a `%H:%M:%S` string or as a numeric giving the number of nanoseconds since midnight. diff --git a/src/execute/layer.rs b/src/execute/layer.rs index 1c67dce3..0a80ffab 100644 --- a/src/execute/layer.rs +++ b/src/execute/layer.rs @@ -5,7 +5,7 @@ use crate::plot::aesthetic::AestheticContext; use crate::plot::layer::is_transposed; -use crate::plot::layer::orientation::{flip_positional_aesthetics, resolve_orientation}; +use crate::plot::layer::orientation::{flip_position_aesthetics, resolve_orientation}; use crate::plot::{ AestheticValue, DefaultAestheticValue, Layer, ParameterValue, Scale, Schema, StatResult, }; @@ -421,7 +421,7 @@ pub fn apply_layer_transforms( where F: Fn(&str) -> Result, { - use crate::plot::layer::orientation::flip_positional_aesthetics; + use crate::plot::layer::orientation::flip_position_aesthetics; // Clone order_by early to avoid borrow conflicts let order_by = layer.order_by.clone(); @@ -499,7 +499,7 @@ where // We flip them to aligned orientation so they're uniform with defaults. // At the end, we flip everything back together. if needs_flip { - flip_positional_aesthetics(&mut layer.remappings.aesthetics); + flip_position_aesthetics(&mut layer.remappings.aesthetics); } // Apply literal default remappings from geom defaults (e.g., y2 => 0.0 for bar baseline). @@ -638,7 +638,7 @@ where // later in mod.rs after apply_remappings_post_query creates the columns, // so that Phase 4.5 can flip those columns along with everything else. if needs_flip { - flip_positional_aesthetics(&mut layer.mappings.aesthetics); + flip_position_aesthetics(&mut layer.mappings.aesthetics); // Normalize mapping column names to match their aesthetic keys. // After flipping, pos1 might point to __ggsql_aes_pos2__ (and vice versa). @@ -662,15 +662,15 @@ where /// Generates SQL like: `(VALUES (val1_row1, val2_row1), (val1_row2, val2_row2)) AS t(col1, col2)` /// /// This function: -/// 1. Moves positional/required/array parameters from layer.parameters to layer.mappings +/// 1. Moves position/required/array parameters from layer.parameters to layer.mappings /// 2. Handles array recycling on-the-fly (determines max length, replicates scalars) /// 3. Validates that all arrays have compatible lengths (1 or max) /// 4. Builds the VALUES clause with raw aesthetic column names /// 5. Converts parameter values to Column/AnnotationColumn mappings /// /// For annotation layers: -/// - Positional aesthetics (pos1, pos2): use Column (data coordinate space, participate in scales) -/// - Non-positional aesthetics (color, size): use AnnotationColumn (visual space, identity scale) +/// - Position aesthetics (pos1, pos2): use Column (data coordinate space, participate in scales) +/// - Material aesthetics (color, size): use AnnotationColumn (visual space, identity scale) /// /// # Arguments /// @@ -683,8 +683,8 @@ fn process_annotation_layer(layer: &mut Layer) -> Result { use crate::plot::ArrayElement; // Step 1: Identify which parameters to use for annotation data - // Only process positional aesthetics, required aesthetics, and array parameters - // (non-positional non-required scalars stay in parameters as geom settings) + // Only process position aesthetics, required aesthetics, and array parameters + // (material non-required scalars stay in parameters as geom settings) let required_aesthetics = layer.geom.aesthetics().required(); let param_keys: Vec = layer.parameters.keys().cloned().collect(); @@ -706,13 +706,13 @@ fn process_annotation_layer(layer: &mut Layer) -> Result { continue; } - // Check if this is a positional aesthetic OR a required aesthetic OR an array - let is_positional = crate::plot::aesthetic::is_positional_aesthetic(¶m_name); + // Check if this is a position aesthetic OR a required aesthetic OR an array + let is_position = crate::plot::aesthetic::is_position_aesthetic(¶m_name); let is_required = required_aesthetics.contains(¶m_name.as_str()); let is_array = matches!(value, ParameterValue::Array(_)); - // Only process positional/required/array parameters - if is_positional || is_required || is_array { + // Only process position/required/array parameters + if is_position || is_required || is_array { annotation_params.push((param_name.clone(), value.clone())); } } @@ -767,16 +767,16 @@ fn process_annotation_layer(layer: &mut Layer) -> Result { column_names.push(aesthetic.clone()); // Create final mapping directly (no intermediate Literal step) - let is_positional = crate::plot::aesthetic::is_positional_aesthetic(aesthetic); - let mapping_value = if is_positional { - // Positional aesthetics use Column (participate in scales) + let is_position = crate::plot::aesthetic::is_position_aesthetic(aesthetic); + let mapping_value = if is_position { + // Position aesthetics use Column (participate in scales) AestheticValue::Column { name: aesthetic.clone(), // Raw aesthetic name from VALUES clause original_name: None, is_dummy: false, } } else { - // Non-positional aesthetics use AnnotationColumn (identity scale) + // Material aesthetics use AnnotationColumn (identity scale) AestheticValue::AnnotationColumn { name: aesthetic.clone(), // Raw aesthetic name from VALUES clause } @@ -812,7 +812,7 @@ fn process_annotation_layer(layer: &mut Layer) -> Result { /// Normalize mapping column names to match their aesthetic keys after flip-back. /// -/// After flipping positional aesthetics, the mapping values (column names) may not match the keys. +/// After flipping position aesthetics, the mapping values (column names) may not match the keys. /// For example, pos1 might point to `__ggsql_aes_pos2__`. /// This function updates the column names so pos1 → `__ggsql_aes_pos1__`, etc. /// @@ -824,7 +824,7 @@ fn normalize_mapping_column_names(layer: &mut Layer) { .mappings .aesthetics .keys() - .filter(|aes| crate::plot::aesthetic::is_positional_aesthetic(aes)) + .filter(|aes| crate::plot::aesthetic::is_position_aesthetic(aes)) .cloned() .collect(); @@ -865,12 +865,12 @@ pub fn resolve_orientations( ParameterValue::String(orientation.to_string()), ); if is_transposed(layer) { - flip_positional_aesthetics(&mut layer.mappings.aesthetics); + flip_position_aesthetics(&mut layer.mappings.aesthetics); // Also flip column names in type_info to match the flipped mappings if layer_idx < layer_type_info.len() { for (name, _, _) in &mut layer_type_info[layer_idx] { if let Some(aesthetic) = naming::extract_aesthetic_name(name) { - let flipped = aesthetic_ctx.flip_positional(aesthetic); + let flipped = aesthetic_ctx.flip_position(aesthetic); if flipped != aesthetic { *name = naming::aesthetic_column(&flipped); } diff --git a/src/execute/mod.rs b/src/execute/mod.rs index fc10d651..5a6a1d25 100644 --- a/src/execute/mod.rs +++ b/src/execute/mod.rs @@ -24,7 +24,7 @@ pub use schema::TypeInfo; use crate::naming; use crate::parser; -use crate::plot::aesthetic::{is_positional_aesthetic, AestheticContext}; +use crate::plot::aesthetic::{is_position_aesthetic, AestheticContext}; use crate::plot::facet::{resolve_properties as resolve_facet_properties, FacetDataContext}; use crate::plot::layer::is_transposed; use crate::plot::{AestheticValue, Layer, Scale, ScaleTypeKind, Schema}; @@ -704,10 +704,10 @@ fn add_discrete_columns_to_partition_by( excluded_aesthetics.insert("label"); for (aesthetic, value) in &layer.mappings.aesthetics { - // Skip positional aesthetics - these should not trigger auto-grouping. - // Stats that need to group by positional aesthetics (like bar/histogram) + // Skip position aesthetics - these should not trigger auto-grouping. + // Stats that need to group by position aesthetics (like bar/histogram) // already handle this themselves via stat_consumed_aesthetics(). - if is_positional_aesthetic(aesthetic) { + if is_position_aesthetic(aesthetic) { continue; } @@ -729,7 +729,7 @@ fn add_discrete_columns_to_partition_by( // Discrete and Binned scales produce categorical groupings. // Continuous scales don't group. Identity defers to column type. let primary_aes = aesthetic_ctx - .primary_internal_positional(aesthetic) + .primary_internal_position(aesthetic) .unwrap_or(aesthetic); let is_discrete = if let Some(scale) = scale_map.get(primary_aes) { if let Some(ref scale_type) = scale.scale_type { @@ -1197,7 +1197,7 @@ pub fn prepare_data_with_reader(query: &str, reader: &R) -> Result(query: &str, reader: &R) -> Result = HashSet::new(); for layer in specs[0].layers.iter() { if is_transposed(layer) { @@ -1224,7 +1224,7 @@ pub fn prepare_data_with_reader(query: &str, reader: &R) -> Result(query: &str, reader: &R) -> Result = spec.get_aesthetic_context().user_positional().to_vec(); + let position_names: Vec = spec.get_aesthetic_context().user_position().to_vec(); // Convert to &str slice for resolve_facet_properties - let positional_refs: Vec<&str> = positional_names.iter().map(|s| s.as_str()).collect(); + let position_refs: Vec<&str> = position_names.iter().map(|s| s.as_str()).collect(); if let Some(ref mut facet) = spec.facet { // Get the first layer's data for computing facet defaults @@ -1290,7 +1290,7 @@ pub fn prepare_data_with_reader(query: &str, reader: &R) -> Result Vec { let mut dtypes = Vec::new(); let aesthetics_to_check = aesthetic_ctx - .internal_positional_family(aesthetic) + .internal_position_family(aesthetic) .map(|f| f.to_vec()) .unwrap_or_else(|| vec![aesthetic.to_string()]); @@ -544,7 +544,7 @@ pub fn find_schema_columns_for_aesthetic( ) -> Vec { let mut infos = Vec::new(); let aesthetics_to_check = aesthetic_ctx - .internal_positional_family(aesthetic) + .internal_position_family(aesthetic) .map(|f| f.to_vec()) .unwrap_or_else(|| vec![aesthetic.to_string()]); @@ -895,7 +895,7 @@ pub fn coerce_aesthetic_columns( aesthetic_ctx: &AestheticContext, ) -> Result<()> { let aesthetics_to_check = aesthetic_ctx - .internal_positional_family(aesthetic) + .internal_position_family(aesthetic) .map(|f| f.to_vec()) .unwrap_or_else(|| vec![aesthetic.to_string()]); @@ -1058,7 +1058,7 @@ pub fn find_columns_for_aesthetic<'a>( ) -> Vec<&'a Column> { let mut column_refs = Vec::new(); let aesthetics_to_check = aesthetic_ctx - .internal_positional_family(aesthetic) + .internal_position_family(aesthetic) .map(|f| f.to_vec()) .unwrap_or_else(|| vec![aesthetic.to_string()]); @@ -1067,7 +1067,7 @@ pub fn find_columns_for_aesthetic<'a>( if let Some(df) = data_map.get(&naming::layer_key(i)) { for aes_name in &aesthetics_to_check { if let Some(AestheticValue::Column { name, .. }) = layer.mappings.get(aes_name) { - // Regular columns (data and positional annotations) participate in scale training + // Regular columns (data and position annotations) participate in scale training if let Ok(column) = df.column(name) { column_refs.push(column); } @@ -1233,7 +1233,7 @@ pub fn find_columns_for_aesthetic_with_sources( ) -> Vec<(String, String)> { let mut results = Vec::new(); let aesthetics_to_check = aesthetic_ctx - .internal_positional_family(aesthetic) + .internal_position_family(aesthetic) .map(|f| f.to_vec()) .unwrap_or_else(|| vec![aesthetic.to_string()]); @@ -1418,25 +1418,25 @@ mod tests { let ctx = AestheticContext::from_static(&["x", "y"], &[]); // Test internal primary aesthetics include all family members - let pos1_family = ctx.internal_positional_family("pos1").unwrap(); + let pos1_family = ctx.internal_position_family("pos1").unwrap(); assert!(pos1_family.iter().any(|s| s == "pos1")); assert!(pos1_family.iter().any(|s| s == "pos1min")); assert!(pos1_family.iter().any(|s| s == "pos1max")); assert!(pos1_family.iter().any(|s| s == "pos1end")); assert_eq!(pos1_family.len(), 4); // pos1, pos1min, pos1max, pos1end - let pos2_family = ctx.internal_positional_family("pos2").unwrap(); + let pos2_family = ctx.internal_position_family("pos2").unwrap(); assert!(pos2_family.iter().any(|s| s == "pos2")); assert!(pos2_family.iter().any(|s| s == "pos2min")); assert!(pos2_family.iter().any(|s| s == "pos2max")); assert!(pos2_family.iter().any(|s| s == "pos2end")); assert_eq!(pos2_family.len(), 4); // pos2, pos2min, pos2max, pos2end - // Test non-positional aesthetics don't have internal family - assert!(ctx.internal_positional_family("color").is_none()); + // Test material aesthetics don't have internal family + assert!(ctx.internal_position_family("color").is_none()); // Test internal variant aesthetics don't have internal family - assert!(ctx.internal_positional_family("pos1min").is_none()); + assert!(ctx.internal_position_family("pos1min").is_none()); } #[test] @@ -1704,7 +1704,7 @@ mod tests { let mut spec = Plot::new(); let coord = Coord::polar(); let aesthetics = coord - .positional_aesthetic_names() + .position_aesthetic_names() .iter() .map(|s| s.to_string()) .collect(); diff --git a/src/lib.rs b/src/lib.rs index 62761387..800ed79b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,7 +58,7 @@ pub use plot::{ // Re-export aesthetic classification utilities pub use plot::aesthetic::{ - is_positional_aesthetic, AestheticContext, NON_POSITIONAL, POSITIONAL_SUFFIXES, + is_position_aesthetic, AestheticContext, MATERIAL_AESTHETICS, POSITION_SUFFIXES, }; // Future modules - not yet implemented @@ -761,8 +761,8 @@ mod integration_tests { #[test] fn test_end_to_end_place_field_vs_value_encoding() { // Test that PLACE annotation layers render correctly: - // - Positional aesthetics (x, y) as field encodings (reference columns) - // - Non-positional aesthetics (size, stroke) as value encodings (datum values) + // - Position aesthetics (x, y) as field encodings (reference columns) + // - Material aesthetics (size, stroke) as value encodings (datum values) let reader = DuckDBReader::from_connection_string("duckdb://memory").unwrap(); @@ -796,7 +796,7 @@ mod integration_tests { let encoding = &point_layer["encoding"]; - // Positional aesthetics should be field encodings (have "field" key) + // Position aesthetics should be field encodings (have "field" key) assert!( encoding["x"]["field"].is_string(), "x should be a field encoding: {:?}", @@ -808,7 +808,7 @@ mod integration_tests { encoding["y"] ); - // Non-positional aesthetics should be value encodings (have "value" key) + // Material aesthetics should be value encodings (have "value" key) assert!( encoding["size"]["value"].is_number(), "size should be a value encoding with numeric value: {:?}", diff --git a/src/naming.rs b/src/naming.rs index 882f40dc..bb2773c8 100644 --- a/src/naming.rs +++ b/src/naming.rs @@ -507,8 +507,8 @@ mod tests { } #[test] - fn test_bin_end_column_internal_positional() { - // Internal positional aesthetic columns (pos1, pos2, etc.) + fn test_bin_end_column_internal_position() { + // Internal position aesthetic columns (pos1, pos2, etc.) // These are generated by the aesthetic transformation pipeline assert_eq!( bin_end_column("__ggsql_aes_pos1__"), diff --git a/src/parser/builder.rs b/src/parser/builder.rs index 7767679e..6e7a0cf1 100644 --- a/src/parser/builder.rs +++ b/src/parser/builder.rs @@ -517,7 +517,7 @@ fn build_layer(node: &Node, source: &SourceTree) -> Result { /// Build an annotation Layer from a place_clause node /// This is similar to build_layer but marks it as an annotation layer. -/// The transformation of positional/required aesthetics from SETTING to mappings +/// The transformation of position/required aesthetics from SETTING to mappings /// happens later in Plot::transform_aesthetics_to_internal(). /// Syntax: PLACE geom [MAPPING col AS x, ...] [SETTING param => val, ...] [FILTER condition] fn build_place_layer(node: &Node, source: &SourceTree) -> Result { @@ -993,7 +993,7 @@ fn build_project(node: &Node, source: &SourceTree) -> Result { // Resolve aesthetics: use provided or fall back to coord defaults let aesthetics = if let Some(aes) = user_aesthetics { // Validate aesthetic count matches coord requirements - let expected = coord.positional_aesthetic_names().len(); + let expected = coord.position_aesthetic_names().len(); if aes.len() != expected { return Err(GgsqlError::ParseError(format!( "PROJECT {} requires {} aesthetics, got {}", @@ -1003,14 +1003,14 @@ fn build_project(node: &Node, source: &SourceTree) -> Result { ))); } - // Validate no conflicts with non-positional or facet aesthetics - validate_positional_aesthetic_names(&aes)?; + // Validate no conflicts with material or facet aesthetics + validate_position_aesthetic_names(&aes)?; aes } else { // Use coord defaults - resolved immediately at build time coord - .positional_aesthetic_names() + .position_aesthetic_names() .iter() .map(|s| s.to_string()) .collect() @@ -1026,15 +1026,15 @@ fn build_project(node: &Node, source: &SourceTree) -> Result { }) } -/// Validate that positional aesthetic names don't conflict with reserved names -fn validate_positional_aesthetic_names(names: &[String]) -> Result<()> { - use crate::plot::aesthetic::{NON_POSITIONAL, USER_FACET_AESTHETICS}; +/// Validate that position aesthetic names don't conflict with reserved names +fn validate_position_aesthetic_names(names: &[String]) -> Result<()> { + use crate::plot::aesthetic::{MATERIAL_AESTHETICS, USER_FACET_AESTHETICS}; for name in names { - // Check against non-positional aesthetics - if NON_POSITIONAL.contains(&name.as_str()) { + // Check against material aesthetics + if MATERIAL_AESTHETICS.contains(&name.as_str()) { return Err(GgsqlError::ParseError(format!( - "PROJECT aesthetic '{}' conflicts with non-positional aesthetic. \ + "PROJECT aesthetic '{}' conflicts with material aesthetic. \ Choose a different name.", name ))); @@ -1303,7 +1303,7 @@ mod tests { #[test] fn test_project_custom_aesthetics() { - // Use identifiers as custom positional aesthetics in PROJECT + // Use identifiers as custom position aesthetics in PROJECT // Note: Custom aesthetics in PROJECT don't need to match grammar's aesthetic_name // since project_aesthetics uses identifier nodes, not aesthetic_name let query = r#" @@ -1385,7 +1385,7 @@ mod tests { let err = result.unwrap_err(); assert!(err .to_string() - .contains("conflicts with non-positional aesthetic")); + .contains("conflicts with material aesthetic")); } // ======================================== @@ -3442,8 +3442,8 @@ mod tests { } #[test] - fn test_no_positional_keeps_default() { - // Only color mapping, no positional aesthetics + fn test_no_position_keeps_default() { + // Only color mapping, no position aesthetics let query = "VISUALISE DRAW point MAPPING region AS color"; let result = parse_test_query(query); @@ -3451,7 +3451,7 @@ mod tests { let specs = result.unwrap(); // Should have no explicit project (defaults will be used later) - // The resolve_coord returns None when no positional aesthetics found + // The resolve_coord returns None when no position aesthetics found assert!(specs[0].project.is_none()); } @@ -3476,7 +3476,7 @@ mod tests { assert!(result.is_ok()); let specs = result.unwrap(); - // Should infer cartesian from positional variants + // Should infer cartesian from position variants let project = specs[0].project.as_ref().unwrap(); assert_eq!(project.coord.coord_kind(), CoordKind::Cartesian); } diff --git a/src/plot/aesthetic.rs b/src/plot/aesthetic.rs index 161d9cec..b7be5b35 100644 --- a/src/plot/aesthetic.rs +++ b/src/plot/aesthetic.rs @@ -4,11 +4,11 @@ //! aesthetic names in ggsql. Aesthetics are visual properties that can be mapped //! to data columns or set to literal values. //! -//! # Positional vs Legend Aesthetics +//! # Position vs Material Aesthetics //! //! Aesthetics fall into two categories: -//! - **Positional**: Map to axes (x, y, and variants like xmin, xmax, etc.) -//! - **Legend**: Map to visual properties shown in legends (color, size, shape, etc.) +//! - **Position**: Map to axes (x, y, and variants like xmin, xmax, etc.) +//! - **Material**: Map to visual properties shown in legends (color, size, shape, etc.) //! //! # Aesthetic Families //! @@ -18,23 +18,23 @@ //! //! # Internal vs User-Facing Aesthetics //! -//! The pipeline uses internal positional aesthetic names (pos1, pos2, etc.) that are +//! The pipeline uses internal position aesthetic names (pos1, pos2, etc.) that are //! transformed from user-facing names (x/y or theta/radius) early in the pipeline //! and transformed back for output. This is handled by `AestheticContext`. use std::collections::HashMap; // ============================================================================= -// Positional Suffixes (applied to primary names automatically) +// Position Suffixes (applied to primary names automatically) // ============================================================================= -/// Positional aesthetic suffixes - applied to primary names to create variant aesthetics +/// Position aesthetic suffixes - applied to primary names to create variant aesthetics /// e.g., "x" + "min" = "xmin", "pos1" + "end" = "pos1end" /// /// Note: "offset" is intentionally NOT included here because it's a positioning /// adjustment that shouldn't influence scale training or be part of aesthetic families. -/// The `flip_positional` method handles offset correctly via prefix detection. -pub const POSITIONAL_SUFFIXES: &[&str] = &["min", "max", "end"]; +/// The `flip_position` method handles offset correctly via prefix detection. +pub const POSITION_SUFFIXES: &[&str] = &["min", "max", "end"]; // ============================================================================= // Static Constants (for backward compatibility with existing code) @@ -52,14 +52,14 @@ pub const POSITIONAL_SUFFIXES: &[&str] = &["min", "max", "end"]; /// - `row` → `facet1`, `column` → `facet2` pub const USER_FACET_AESTHETICS: &[&str] = &["panel", "row", "column"]; -/// Non-positional aesthetics (visual properties shown in legends or applied to marks) +/// Material aesthetics (visual properties shown in legends or applied to marks) /// /// These include: /// - Color aesthetics: color, colour, fill, stroke, opacity /// - Size/shape aesthetics: size, shape, linetype, linewidth /// - Dimension aesthetics: width, height /// - Text aesthetics: label, typeface, fontweight, italic, hjust, vjust -pub const NON_POSITIONAL: &[&str] = &[ +pub const MATERIAL_AESTHETICS: &[&str] = &[ "color", "colour", "fill", @@ -131,19 +131,19 @@ pub struct AestheticContext { user_facet: Vec<&'static str>, internal_facet: Vec, - // Non-positional (static reference) - non_positional: &'static [&'static str], + // Material (static reference) + material: &'static [&'static str], } impl AestheticContext { - /// Create context from coord's positional names and facet's aesthetic names. + /// Create context from coord's position names and facet's aesthetic names. /// /// # Arguments /// - /// * `positional_names` - Primary positional aesthetic names (e.g., ["x", "y"] or custom names) + /// * `position_names` - Primary position aesthetic names (e.g., ["x", "y"] or custom names) /// * `facet_names` - User-facing facet aesthetic names from facet layout /// (e.g., ["panel"] for wrap, ["row", "column"] for grid) - pub fn new(positional_names: &[String], facet_names: &[&'static str]) -> Self { + pub fn new(position_names: &[String], facet_names: &[&'static str]) -> Self { // Initialize all HashMaps and vectors let mut user_to_internal = HashMap::new(); let mut internal_to_primary = HashMap::new(); @@ -152,8 +152,8 @@ impl AestheticContext { let mut user_primaries = Vec::new(); let mut internal_primaries = Vec::new(); - // Build positional mappings - for (i, user_primary) in positional_names.iter().enumerate() { + // Build position mappings + for (i, user_primary) in position_names.iter().enumerate() { let pos_num = i + 1; let internal_primary = format!("pos{}", pos_num); @@ -169,7 +169,7 @@ impl AestheticContext { internal_to_primary.insert(internal_primary.clone(), internal_primary.clone()); // Add suffixed variants - for suffix in POSITIONAL_SUFFIXES { + for suffix in POSITION_SUFFIXES { let user_variant = format!("{}{}", user_primary, suffix); let internal_variant = format!("{}{}", internal_primary, suffix); @@ -195,30 +195,29 @@ impl AestheticContext { internal_primaries, user_facet: facet_names.to_vec(), internal_facet, - non_positional: NON_POSITIONAL, + material: MATERIAL_AESTHETICS, } } - /// Create context from static positional names and facet names. + /// Create context from static position names and facet names. /// /// Convenience method for creating context from static string slices (e.g., from coord defaults). - pub fn from_static(positional_names: &[&'static str], facet_names: &[&'static str]) -> Self { - let owned_positional: Vec = - positional_names.iter().map(|s| s.to_string()).collect(); - Self::new(&owned_positional, facet_names) + pub fn from_static(position_names: &[&'static str], facet_names: &[&'static str]) -> Self { + let owned_position: Vec = position_names.iter().map(|s| s.to_string()).collect(); + Self::new(&owned_position, facet_names) } // === Mapping: User → Internal === - /// Map user aesthetic (positional or facet) to internal name. + /// Map user aesthetic (position or facet) to internal name. /// - /// Positional: "x" → "pos1", "ymin" → "pos2min", "theta" → "pos1" + /// Position: "x" → "pos1", "ymin" → "pos2min", "theta" → "pos1" /// Facet: "panel" → "facet1", "row" → "facet1", "column" → "facet2" /// /// Note: Facet mappings work regardless of whether a FACET clause exists, /// allowing layer-declared facet aesthetics to be transformed. pub fn map_user_to_internal(&self, user_aesthetic: &str) -> Option<&str> { - // Check positional first (O(1) HashMap lookup) + // Check position first (O(1) HashMap lookup) if let Some(internal) = self.user_to_internal.get(user_aesthetic) { return Some(internal.as_str()); } @@ -242,9 +241,9 @@ impl AestheticContext { /// Map internal aesthetic to user-facing name (reverse of map_user_to_internal). /// - /// Positional: "pos1" → "x", "pos2min" → "ymin", "pos1" → "theta" (for polar) + /// Position: "pos1" → "x", "pos2min" → "ymin", "pos1" → "theta" (for polar) /// Facet: "facet1" → "panel" (wrap), "facet1" → "row" (grid), "facet2" → "column" (grid) - /// Non-positional: "color" → "color" (unchanged) + /// Material: "color" → "color" (unchanged) /// /// Returns None if the internal aesthetic is not recognized. pub fn map_internal_to_user(&self, internal_aesthetic: &str) -> String { @@ -257,7 +256,7 @@ impl AestheticContext { return self.user_facet[idx].to_string(); } - // Check internal positional (pos1, pos1min, pos2, etc.) + // Check internal position (pos1, pos1min, pos2, etc.) // Iterate through user_to_internal to find reverse mapping for (user, internal) in &self.user_to_internal { if internal == internal_aesthetic { @@ -265,21 +264,21 @@ impl AestheticContext { } } - // Non-positional aesthetics (color, size, etc.) + // Material aesthetics (color, size, etc.) // Internal is the same as external internal_aesthetic.to_string() } // === Checking (O(1) HashMap lookups) === - /// Check if internal aesthetic is primary positional (pos1, pos2, ...) + /// Check if internal aesthetic is primary position (pos1, pos2, ...) pub fn is_primary_internal(&self, name: &str) -> bool { self.internal_primaries.iter().any(|s| s == name) } - /// Check if aesthetic is non-positional (color, size, etc.) - pub fn is_non_positional(&self, name: &str) -> bool { - self.non_positional.contains(&name) + /// Check if aesthetic is material (color, size, etc.) + pub fn is_material(&self, name: &str) -> bool { + self.material.contains(&name) } /// Check if name is a user-facing facet aesthetic (panel, row, column) @@ -302,14 +301,14 @@ impl AestheticContext { /// Get the primary aesthetic for an internal family member. /// /// e.g., "pos1min" → "pos1", "pos2end" → "pos2" - /// Non-positional aesthetics return themselves. - pub fn primary_internal_positional<'a>(&'a self, name: &'a str) -> Option<&'a str> { - // Check internal positional (O(1) lookup) + /// Material aesthetics return themselves. + pub fn primary_internal_position<'a>(&'a self, name: &'a str) -> Option<&'a str> { + // Check internal position (O(1) lookup) if let Some(primary) = self.internal_to_primary.get(name) { return Some(primary.as_str()); } - // Non-positional aesthetics are their own primary - if self.is_non_positional(name) { + // Material aesthetics are their own primary + if self.is_material(name) { return Some(name); } None @@ -318,7 +317,7 @@ impl AestheticContext { /// Get the internal aesthetic family for a primary aesthetic. /// /// e.g., "pos1" → ["pos1", "pos1min", "pos1max", "pos1end"] - pub fn internal_positional_family(&self, primary: &str) -> Option<&[String]> { + pub fn internal_position_family(&self, primary: &str) -> Option<&[String]> { self.primary_to_internal_family .get(primary) .map(|v| v.as_slice()) @@ -326,13 +325,13 @@ impl AestheticContext { // === Accessors === - /// Get primary internal positional aesthetics (pos1, pos2, ...) - pub fn internal_positional(&self) -> &[String] { + /// Get primary internal position aesthetics (pos1, pos2, ...) + pub fn internal_position(&self) -> &[String] { &self.internal_primaries } - /// Get user positional aesthetics (x, y or theta, radius or custom names) - pub fn user_positional(&self) -> &[String] { + /// Get user position aesthetics (x, y or theta, radius or custom names) + pub fn user_position(&self) -> &[String] { &self.user_primaries } @@ -343,22 +342,22 @@ impl AestheticContext { // === Orientation Flipping === - /// Flip a positional aesthetic to its opposite position. + /// Flip a position aesthetic to its opposite position. /// /// Swaps pos1 ↔ pos2 (and their suffixed variants like pos1min ↔ pos2min). - /// Non-positional aesthetics are returned unchanged. + /// Material aesthetics are returned unchanged. /// /// # Examples /// /// ```ignore /// let ctx = AestheticContext::from_static(&["x", "y"], &[]); - /// assert_eq!(ctx.flip_positional("pos1"), "pos2"); - /// assert_eq!(ctx.flip_positional("pos2min"), "pos1min"); - /// assert_eq!(ctx.flip_positional("pos1end"), "pos2end"); - /// assert_eq!(ctx.flip_positional("color"), "color"); // unchanged + /// assert_eq!(ctx.flip_position("pos1"), "pos2"); + /// assert_eq!(ctx.flip_position("pos2min"), "pos1min"); + /// assert_eq!(ctx.flip_position("pos1end"), "pos2end"); + /// assert_eq!(ctx.flip_position("color"), "color"); // unchanged /// ``` - pub fn flip_positional(&self, name: &str) -> String { - // Only flip if we have exactly 2 positional aesthetics + pub fn flip_position(&self, name: &str) -> String { + // Only flip if we have exactly 2 position aesthetics if self.internal_primaries.len() != 2 { return name.to_string(); } @@ -371,7 +370,7 @@ impl AestheticContext { return format!("pos1{}", rest); } - // Not a positional aesthetic, return unchanged + // Not a position aesthetic, return unchanged name.to_string() } } @@ -401,14 +400,14 @@ pub fn is_facet_aesthetic(aesthetic: &str) -> bool { false } -/// Check if aesthetic is an internal positional (pos1, pos1min, pos2max, etc.) +/// Check if aesthetic is an internal position (pos1, pos1min, pos2max, etc.) /// /// This function works with **internal** aesthetic names after transformation. /// Matches patterns like: pos1, pos2, pos1min, pos2max, pos1end, etc. /// -/// For user-facing checks before transformation, use `AestheticContext::is_user_positional()`. +/// For user-facing checks before transformation, use `AestheticContext::is_user_position()`. #[inline] -pub fn is_positional_aesthetic(name: &str) -> bool { +pub fn is_position_aesthetic(name: &str) -> bool { if !name.starts_with("pos") || name.len() <= 3 { return false; } @@ -420,7 +419,7 @@ pub fn is_positional_aesthetic(name: &str) -> bool { } // Check for variants: posN followed by a suffix - for suffix in POSITIONAL_SUFFIXES { + for suffix in POSITION_SUFFIXES { if let Some(base) = name.strip_suffix(suffix) { if base.starts_with("pos") && base.len() > 3 { let num_part = &base[3..]; @@ -434,15 +433,15 @@ pub fn is_positional_aesthetic(name: &str) -> bool { false } -/// Parse a positional aesthetic name to extract its slot number and suffix. +/// Parse a position aesthetic name to extract its slot number and suffix. /// -/// Returns `Some((slot, suffix))` for positional aesthetics: +/// Returns `Some((slot, suffix))` for position aesthetics: /// - `pos1` → (1, "") /// - `pos2min` → (2, "min") /// - `pos1end` → (1, "end") /// -/// Returns `None` for non-positional aesthetics. -pub fn parse_positional(name: &str) -> Option<(u8, &str)> { +/// Returns `None` for material aesthetics. +pub fn parse_position(name: &str) -> Option<(u8, &str)> { if !name.starts_with("pos") { return None; } @@ -494,37 +493,37 @@ mod tests { } #[test] - fn test_positional_aesthetic() { - // Checks internal positional names (pos1, pos2, etc. and variants) - // For user-facing checks, use AestheticContext::is_user_positional() + fn test_position_aesthetic() { + // Checks internal position names (pos1, pos2, etc. and variants) + // For user-facing checks, use AestheticContext::is_user_position() // Primary internal - assert!(is_positional_aesthetic("pos1")); - assert!(is_positional_aesthetic("pos2")); - assert!(is_positional_aesthetic("pos10")); // supports any number + assert!(is_position_aesthetic("pos1")); + assert!(is_position_aesthetic("pos2")); + assert!(is_position_aesthetic("pos10")); // supports any number // Variants - assert!(is_positional_aesthetic("pos1min")); - assert!(is_positional_aesthetic("pos1max")); - assert!(is_positional_aesthetic("pos2min")); - assert!(is_positional_aesthetic("pos2max")); - assert!(is_positional_aesthetic("pos1end")); - assert!(is_positional_aesthetic("pos2end")); - - // User-facing names are NOT positional (handled by AestheticContext) - assert!(!is_positional_aesthetic("x")); - assert!(!is_positional_aesthetic("y")); - assert!(!is_positional_aesthetic("xmin")); - assert!(!is_positional_aesthetic("theta")); - - // Non-positional - assert!(!is_positional_aesthetic("color")); - assert!(!is_positional_aesthetic("size")); - assert!(!is_positional_aesthetic("fill")); + assert!(is_position_aesthetic("pos1min")); + assert!(is_position_aesthetic("pos1max")); + assert!(is_position_aesthetic("pos2min")); + assert!(is_position_aesthetic("pos2max")); + assert!(is_position_aesthetic("pos1end")); + assert!(is_position_aesthetic("pos2end")); + + // User-facing names are NOT position (handled by AestheticContext) + assert!(!is_position_aesthetic("x")); + assert!(!is_position_aesthetic("y")); + assert!(!is_position_aesthetic("xmin")); + assert!(!is_position_aesthetic("theta")); + + // Material + assert!(!is_position_aesthetic("color")); + assert!(!is_position_aesthetic("size")); + assert!(!is_position_aesthetic("fill")); // Edge cases - assert!(!is_positional_aesthetic("pos")); // too short - assert!(!is_positional_aesthetic("position")); // not a valid pattern + assert!(!is_position_aesthetic("pos")); // too short + assert!(!is_position_aesthetic("position")); // not a valid pattern } // ======================================================================== @@ -535,15 +534,11 @@ mod tests { fn test_aesthetic_context_cartesian() { let ctx = AestheticContext::from_static(&["x", "y"], &[]); - // User positional names - assert_eq!(ctx.user_positional(), &["x", "y"]); + // User position names + assert_eq!(ctx.user_position(), &["x", "y"]); // Primary internal names - let primary: Vec<&str> = ctx - .internal_positional() - .iter() - .map(|s| s.as_str()) - .collect(); + let primary: Vec<&str> = ctx.internal_position().iter().map(|s| s.as_str()).collect(); assert_eq!(primary, vec!["pos1", "pos2"]); } @@ -551,15 +546,11 @@ mod tests { fn test_aesthetic_context_polar() { let ctx = AestheticContext::from_static(&["theta", "radius"], &[]); - // User positional names - assert_eq!(ctx.user_positional(), &["theta", "radius"]); + // User position names + assert_eq!(ctx.user_position(), &["theta", "radius"]); // Primary internal names - let primary: Vec<&str> = ctx - .internal_positional() - .iter() - .map(|s| s.as_str()) - .collect(); + let primary: Vec<&str> = ctx.internal_position().iter().map(|s| s.as_str()).collect(); assert_eq!(primary, vec!["pos1", "pos2"]); } @@ -579,7 +570,7 @@ mod tests { assert_eq!(ctx.map_user_to_internal("ymax"), Some("pos2max")); assert_eq!(ctx.map_user_to_internal("yend"), Some("pos2end")); - // Non-positional returns None + // Material returns None assert_eq!(ctx.map_user_to_internal("color"), None); assert_eq!(ctx.map_user_to_internal("fill"), None); } @@ -652,15 +643,15 @@ mod tests { let ctx = AestheticContext::from_static(&["x", "y"], &[]); // Get internal family (offset not included - it's a positioning adjustment) - let pos1_family = ctx.internal_positional_family("pos1").unwrap(); + let pos1_family = ctx.internal_position_family("pos1").unwrap(); let pos1_strs: Vec<&str> = pos1_family.iter().map(|s| s.as_str()).collect(); assert_eq!(pos1_strs, vec!["pos1", "pos1min", "pos1max", "pos1end"]); // Primary internal aesthetic - assert_eq!(ctx.primary_internal_positional("pos1"), Some("pos1")); - assert_eq!(ctx.primary_internal_positional("pos1min"), Some("pos1")); - assert_eq!(ctx.primary_internal_positional("pos2end"), Some("pos2")); - assert_eq!(ctx.primary_internal_positional("color"), Some("color")); + assert_eq!(ctx.primary_internal_position("pos1"), Some("pos1")); + assert_eq!(ctx.primary_internal_position("pos1min"), Some("pos1")); + assert_eq!(ctx.primary_internal_position("pos2end"), Some("pos2")); + assert_eq!(ctx.primary_internal_position("color"), Some("color")); } #[test] @@ -679,7 +670,7 @@ mod tests { assert_eq!(ctx.map_internal_to_user("pos2max"), "ymax"); assert_eq!(ctx.map_internal_to_user("pos2end"), "yend"); - // Non-positional aesthetics remain unchanged + // Material aesthetics remain unchanged assert_eq!(ctx.map_internal_to_user("color"), "color"); assert_eq!(ctx.map_internal_to_user("size"), "size"); assert_eq!(ctx.map_internal_to_user("fill"), "fill"); @@ -716,7 +707,7 @@ mod tests { // Test that user -> internal -> user roundtrips correctly let ctx = AestheticContext::from_static(&["x", "y"], &["panel"]); - // Positional + // Position let internal = ctx.map_user_to_internal("x").unwrap(); assert_eq!(ctx.map_internal_to_user(internal), "x"); @@ -728,19 +719,19 @@ mod tests { assert_eq!(ctx.map_internal_to_user(internal), "panel"); } #[test] - fn test_parse_positional() { - // Primary positional - assert_eq!(parse_positional("pos1"), Some((1, ""))); - assert_eq!(parse_positional("pos2"), Some((2, ""))); + fn test_parse_position() { + // Primary position + assert_eq!(parse_position("pos1"), Some((1, ""))); + assert_eq!(parse_position("pos2"), Some((2, ""))); // Variants with suffixes - assert_eq!(parse_positional("pos1min"), Some((1, "min"))); - assert_eq!(parse_positional("pos2max"), Some((2, "max"))); - assert_eq!(parse_positional("pos1end"), Some((1, "end"))); - - // Non-positional - assert_eq!(parse_positional("color"), None); - assert_eq!(parse_positional("x"), None); - assert_eq!(parse_positional("xmin"), None); + assert_eq!(parse_position("pos1min"), Some((1, "min"))); + assert_eq!(parse_position("pos2max"), Some((2, "max"))); + assert_eq!(parse_position("pos1end"), Some((1, "end"))); + + // Material + assert_eq!(parse_position("color"), None); + assert_eq!(parse_position("x"), None); + assert_eq!(parse_position("xmin"), None); } } diff --git a/src/plot/facet/resolve.rs b/src/plot/facet/resolve.rs index 670bd335..860c9de1 100644 --- a/src/plot/facet/resolve.rs +++ b/src/plot/facet/resolve.rs @@ -84,7 +84,7 @@ fn compute_default_ncol(num_levels: usize) -> i64 { /// 1. Skips if already resolved /// 2. Validates all properties are allowed for this layout /// 3. Validates property values: -/// - `free`: must be null, a valid positional aesthetic, or an array of them +/// - `free`: must be null, a valid position aesthetic, or an array of them /// - `ncol`: positive integer /// 4. Normalizes the `free` property to a boolean vector (position-indexed) /// 5. Applies defaults for missing properties: @@ -95,11 +95,11 @@ fn compute_default_ncol(num_levels: usize) -> i64 { /// /// * `facet` - The facet to resolve /// * `context` - Data context with unique values -/// * `positional_names` - Valid positional aesthetic names (e.g., ["x", "y"] or ["theta", "radius"]) +/// * `position_names` - Valid position aesthetic names (e.g., ["x", "y"] or ["theta", "radius"]) pub fn resolve_properties( facet: &mut Facet, context: &FacetDataContext, - positional_names: &[&str], + position_names: &[&str], ) -> Result<(), String> { // Skip if already resolved if facet.resolved { @@ -131,12 +131,12 @@ pub fn resolve_properties( } // Step 2: Validate property values - validate_free_property(facet, positional_names)?; + validate_free_property(facet, position_names)?; validate_layout_properties(facet)?; validate_missing_property(facet)?; // Step 3: Normalize free property to boolean vector - normalize_free_property(facet, positional_names); + normalize_free_property(facet, position_names); // Step 4: Apply defaults for missing properties apply_defaults(facet, context); @@ -151,14 +151,14 @@ pub fn resolve_properties( /// /// Accepts: /// - `null` (ParameterValue::Null) - shared scales (default when absent) -/// - A valid positional aesthetic name (string) - independent scale for that axis only -/// - An array of valid positional aesthetic names - independent scales for specified axes +/// - A valid position aesthetic name (string) - independent scale for that axis only +/// - An array of valid position aesthetic names - independent scales for specified axes /// /// # Arguments /// /// * `facet` - The facet to validate -/// * `positional_names` - Valid positional aesthetic names (e.g., ["x", "y"] or ["theta", "radius"]) -fn validate_free_property(facet: &Facet, positional_names: &[&str]) -> Result<(), String> { +/// * `position_names` - Valid position aesthetic names (e.g., ["x", "y"] or ["theta", "radius"]) +fn validate_free_property(facet: &Facet, position_names: &[&str]) -> Result<(), String> { if let Some(value) = facet.properties.get("free") { match value { ParameterValue::Null => { @@ -166,25 +166,25 @@ fn validate_free_property(facet: &Facet, positional_names: &[&str]) -> Result<() Ok(()) } ParameterValue::String(s) => { - if !positional_names.contains(&s.as_str()) { + if !position_names.contains(&s.as_str()) { return Err(format!( "invalid 'free' value '{}'. Expected one of: {}, or null", s, - format_options(positional_names) + format_options(position_names) )); } Ok(()) } ParameterValue::Array(arr) => { - // Validate each element is a valid positional name + // Validate each element is a valid position name if arr.is_empty() { return Err("invalid 'free' array: cannot be empty".to_string()); } - if arr.len() > positional_names.len() { + if arr.len() > position_names.len() { return Err(format!( "invalid 'free' array: too many elements ({} given, max {})", arr.len(), - positional_names.len() + position_names.len() )); } @@ -192,11 +192,11 @@ fn validate_free_property(facet: &Facet, positional_names: &[&str]) -> Result<() for elem in arr { match elem { crate::plot::ArrayElement::String(s) => { - if !positional_names.contains(&s.as_str()) { + if !position_names.contains(&s.as_str()) { return Err(format!( "invalid 'free' array element '{}'. Expected one of: {}", s, - format_options(positional_names) + format_options(position_names) )); } if !seen.insert(s.clone()) { @@ -209,7 +209,7 @@ fn validate_free_property(facet: &Facet, positional_names: &[&str]) -> Result<() _ => { return Err(format!( "invalid 'free' array: elements must be strings. Expected: {}", - format_options(positional_names) + format_options(position_names) )); } } @@ -217,8 +217,8 @@ fn validate_free_property(facet: &Facet, positional_names: &[&str]) -> Result<() Ok(()) } _ => Err(format!( - "'free' must be null, a string ({}), or an array of positional names", - format_options(positional_names) + "'free' must be null, a string ({}), or an array of position names", + format_options(position_names) )), } } else { @@ -226,7 +226,7 @@ fn validate_free_property(facet: &Facet, positional_names: &[&str]) -> Result<() } } -/// Format positional names for error messages +/// Format position names for error messages fn format_options(names: &[&str]) -> String { names .iter() @@ -244,14 +244,14 @@ fn format_options(names: &[&str]) -> String { /// - User writes: `free => null` or absent → stored as: `free => [false, false]` /// /// This allows the writer to use the vector directly without any parsing. -fn normalize_free_property(facet: &mut Facet, positional_names: &[&str]) { - let mut free_vec = vec![false; positional_names.len()]; +fn normalize_free_property(facet: &mut Facet, position_names: &[&str]) { + let mut free_vec = vec![false; position_names.len()]; if let Some(value) = facet.properties.get("free") { match value { ParameterValue::String(s) => { // Single string -> set that position to true - if let Some(idx) = positional_names.iter().position(|n| *n == s.as_str()) { + if let Some(idx) = position_names.iter().position(|n| *n == s.as_str()) { free_vec[idx] = true; } } @@ -259,7 +259,7 @@ fn normalize_free_property(facet: &mut Facet, positional_names: &[&str]) { // Array -> set each position to true for elem in arr { if let crate::plot::ArrayElement::String(s) = elem { - if let Some(idx) = positional_names.iter().position(|n| *n == s.as_str()) { + if let Some(idx) = position_names.iter().position(|n| *n == s.as_str()) { free_vec[idx] = true; } } @@ -384,9 +384,9 @@ mod tests { use crate::plot::facet::FacetLayout; use polars::prelude::*; - /// Default positional names for cartesian coords + /// Default position names for cartesian coords const CARTESIAN: &[&str] = &["x", "y"]; - /// Positional names for polar coords + /// Position names for polar coords const POLAR: &[&str] = &["theta", "radius"]; fn make_wrap_facet() -> Facet { @@ -725,7 +725,7 @@ mod tests { let context = make_context(5); let result = resolve_properties(&mut facet, &context, CARTESIAN); assert!(result.is_ok()); - // x is first positional -> [true, false] + // x is first position -> [true, false] assert_eq!(get_free_bools(&facet), Some(vec![true, false])); } @@ -739,7 +739,7 @@ mod tests { let context = make_context(5); let result = resolve_properties(&mut facet, &context, CARTESIAN); assert!(result.is_ok()); - // y is second positional -> [false, true] + // y is second position -> [false, true] assert_eq!(get_free_bools(&facet), Some(vec![false, true])); } @@ -878,7 +878,7 @@ mod tests { let context = make_context(5); let result = resolve_properties(&mut facet, &context, POLAR); assert!(result.is_ok()); - // theta is first positional -> [true, false] + // theta is first position -> [true, false] assert_eq!(get_free_bools(&facet), Some(vec![true, false])); } @@ -893,7 +893,7 @@ mod tests { let context = make_context(5); let result = resolve_properties(&mut facet, &context, POLAR); assert!(result.is_ok()); - // radius is second positional -> [false, true] + // radius is second position -> [false, true] assert_eq!(get_free_bools(&facet), Some(vec![false, true])); } diff --git a/src/plot/layer/geom/rect.rs b/src/plot/layer/geom/rect.rs index 8d7a2c90..f5a9aa83 100644 --- a/src/plot/layer/geom/rect.rs +++ b/src/plot/layer/geom/rect.rs @@ -30,7 +30,7 @@ impl GeomTrait for Rect { fn aesthetics(&self) -> DefaultAesthetics { DefaultAesthetics { defaults: &[ - // All positional aesthetics are optional inputs (Null) + // All position aesthetics are optional inputs (Null) // They become Delayed after stat transform ("pos1", DefaultAestheticValue::Null), // x (center) ("pos1min", DefaultAestheticValue::Null), // xmin @@ -40,7 +40,7 @@ impl GeomTrait for Rect { ("pos2min", DefaultAestheticValue::Null), // ymin ("pos2max", DefaultAestheticValue::Null), // ymax ("height", DefaultAestheticValue::Null), // height (aesthetic, can map to column) - // Visual aesthetics + // Material aesthetics ("fill", DefaultAestheticValue::String("black")), ("stroke", DefaultAestheticValue::String("black")), ("opacity", DefaultAestheticValue::Number(0.8)), diff --git a/src/plot/layer/geom/types.rs b/src/plot/layer/geom/types.rs index 5e04f386..c23e6ae3 100644 --- a/src/plot/layer/geom/types.rs +++ b/src/plot/layer/geom/types.rs @@ -2,7 +2,7 @@ //! //! These types are used by all geom implementations and are shared across the module. -use crate::plot::aesthetic::parse_positional; +use crate::plot::aesthetic::parse_position; use crate::{plot::types::DefaultAestheticValue, Mappings}; // Re-export shared types from the central location @@ -29,7 +29,7 @@ impl DefaultAesthetics { /// Get supported aesthetic names (excludes Delayed, for MAPPING validation) /// - /// Returns the literal names from defaults. For bidirectional positional checking, + /// Returns the literal names from defaults. For bidirectional position checking, /// use `is_supported()` which handles pos1/pos2 equivalence. pub fn supported(&self) -> Vec<&'static str> { self.defaults @@ -55,7 +55,7 @@ impl DefaultAesthetics { /// Check if an aesthetic is supported (not Delayed) /// - /// Positional aesthetics are bidirectional: if pos1* is supported, pos2* is also + /// Position aesthetics are bidirectional: if pos1* is supported, pos2* is also /// considered supported (and vice versa). pub fn is_supported(&self, name: &str) -> bool { // Check for direct match first @@ -67,8 +67,8 @@ impl DefaultAesthetics { return true; } - // Check for bidirectional positional match - if let Some((slot, suffix)) = parse_positional(name) { + // Check for bidirectional position match + if let Some((slot, suffix)) = parse_position(name) { let other_slot = if slot == 1 { 2 } else { 1 }; let equivalent = format!("pos{}{}", other_slot, suffix); return self.defaults.iter().any(|(n, value)| { diff --git a/src/plot/layer/mod.rs b/src/plot/layer/mod.rs index d539e195..0e8a00cd 100644 --- a/src/plot/layer/mod.rs +++ b/src/plot/layer/mod.rs @@ -28,7 +28,7 @@ pub use position::{Position, PositionTrait, PositionType}; use crate::{ plot::{ - is_facet_aesthetic, parse_positional, + is_facet_aesthetic, types::{AestheticValue, DataSource, Mappings, ParameterValue, SqlExpression}, }, AestheticContext, @@ -173,7 +173,7 @@ impl Layer { /// /// Performs three checks: /// 1. All required aesthetics are present - /// 2. Positional requirements allow bidirectional satisfaction (handles orientation flipping) + /// 2. Position requirements allow bidirectional satisfaction (handles orientation flipping) /// 3. No unsupported/exotic aesthetics are mapped /// /// # Parameters @@ -199,11 +199,11 @@ impl Layer { // Check if all required aesthetics exist. let mut missing = Vec::new(); - let mut positional_reqs: Vec<(&str, u8, &str)> = Vec::new(); + let mut position_reqs: Vec<(&str, u8, &str)> = Vec::new(); for aesthetic in self.geom.aesthetics().required() { - if let Some((slot, suffix)) = parse_positional(aesthetic) { - positional_reqs.push((aesthetic, slot, suffix)) + if let Some((slot, suffix)) = crate::plot::aesthetic::parse_position(aesthetic) { + position_reqs.push((aesthetic, slot, suffix)) } else if !self.mappings.contains_key(aesthetic) { missing.push(translate(aesthetic)); } @@ -218,11 +218,11 @@ impl Layer { )); } - // Validate positional requirements bidirectionally + // Validate position requirements bidirectionally // Try both slot assignments: (1→1, 2→2) and (1→2, 2→1) - if !positional_reqs.is_empty() { + if !position_reqs.is_empty() { // Pre-compute flipped versions to avoid repeated calculation - let pairs: Vec<_> = positional_reqs + let pairs: Vec<_> = position_reqs .iter() .map(|(name, slot, suffix)| { let flipped_slot = if *slot == 1 { 2 } else { 1 }; @@ -245,7 +245,7 @@ impl Layer { // Check if flipped version is present (mixed orientation case) if self.mappings.contains_key(flipped) { return Err(format!( - "Layer '{}' has mixed positional aesthetic orientations. \ + "Layer '{}' has mixed position aesthetic orientations. \ Found '{}' but expected '{}' to match the orientation of other aesthetics.", self.geom, translate(flipped), @@ -275,10 +275,7 @@ impl Layer { // At this point in execution we don't know orientation yet, // so we'll approve both flipped and upflipped aesthetics. if let Some(ctx) = context { - let flipped: Vec = supported - .iter() - .map(|aes| ctx.flip_positional(aes)) - .collect(); + let flipped: Vec = supported.iter().map(|aes| ctx.flip_position(aes)).collect(); supported.extend(flipped); } diff --git a/src/plot/layer/orientation.rs b/src/plot/layer/orientation.rs index 28f064cb..e6c525e7 100644 --- a/src/plot/layer/orientation.rs +++ b/src/plot/layer/orientation.rs @@ -1,7 +1,7 @@ //! Layer orientation detection and mapping flipping. //! //! This module provides orientation detection for geoms with implicit orientation -//! (bar, histogram, boxplot, violin, density, ribbon) and handles flipping positional +//! (bar, histogram, boxplot, violin, density, ribbon) and handles flipping position //! aesthetic mappings before stat computation. //! //! # Orientation @@ -25,7 +25,7 @@ use super::geom::GeomType; use super::Layer; -use crate::plot::aesthetic::{is_positional_aesthetic, AestheticContext}; +use crate::plot::aesthetic::{is_position_aesthetic, AestheticContext}; use crate::plot::scale::ScaleTypeKind; use crate::plot::{AestheticValue, Mappings, Scale}; use crate::{naming, DataFrame}; @@ -101,8 +101,8 @@ pub fn geom_has_implicit_orientation(geom: &GeomType) -> bool { /// /// Applies unified rules in order: /// -/// 0. **Remapping without mapping**: If no positional mappings exist but remappings -/// target a positional axis, the remapping target is the value axis: +/// 0. **Remapping without mapping**: If no position mappings exist but remappings +/// target a position axis, the remapping target is the value axis: /// - Remapping to pos1 only → Transposed (pos1 is value axis, main axis must be pos2) /// - Remapping to pos2 only → Aligned (pos2 is value axis, main axis is pos1) /// @@ -125,7 +125,7 @@ fn detect_from_scales( mappings: &Mappings, remappings: &Mappings, ) -> &'static str { - // Check for positional mappings + // Check for position mappings let has_pos1_mapping = mappings.contains_key("pos1"); let has_pos2_mapping = mappings.contains_key("pos2"); @@ -149,7 +149,7 @@ fn detect_from_scales( let has_pos2 = pos2_scale.is_some(); // Rule 1: Single scale present - that axis is primary - // Only apply when there are explicit positional mappings; otherwise the user + // Only apply when there are explicit position mappings; otherwise the user // is just customizing a scale (e.g., SCALE y SETTING expand) without intending // to change orientation. The geom's default_remappings will define orientation. if has_pos1_mapping || has_pos2_mapping { @@ -217,7 +217,7 @@ fn is_discrete_scale(scale: &Scale) -> bool { }) } -/// Swap positional aesthetic pairs in an aesthetics map. +/// Swap position aesthetic pairs in an aesthetics map. /// /// Swaps the following pairs: /// - pos1 ↔ pos2 @@ -227,7 +227,7 @@ fn is_discrete_scale(scale: &Scale) -> bool { /// - pos1offset ↔ pos2offset /// /// Used for both mappings and remappings when handling transposed orientation. -pub fn flip_positional_aesthetics( +pub fn flip_position_aesthetics( aesthetics: &mut std::collections::HashMap, ) { const PAIRS: [(&str, &str); 5] = [ @@ -251,14 +251,14 @@ pub fn flip_positional_aesthetics( } } -/// Flip positional column names in a DataFrame for Transposed orientation layers. +/// Flip position column names in a DataFrame for Transposed orientation layers. /// /// Swaps column names like `__ggsql_aes_pos1__` ↔ `__ggsql_aes_pos2__` so that /// the data matches the flipped mapping names. /// /// This is called after query execution for layers with Transposed orientation, /// in coordination with `normalize_mapping_column_names` which updates the mappings. -pub fn flip_dataframe_positional_columns( +pub fn flip_dataframe_position_columns( df: DataFrame, aesthetic_ctx: &AestheticContext, ) -> DataFrame { @@ -270,8 +270,8 @@ pub fn flip_dataframe_positional_columns( .iter() .filter_map(|col_name| { naming::extract_aesthetic_name(col_name).and_then(|aesthetic| { - if is_positional_aesthetic(aesthetic) { - let flipped = aesthetic_ctx.flip_positional(aesthetic); + if is_position_aesthetic(aesthetic) { + let flipped = aesthetic_ctx.flip_position(aesthetic); if flipped != aesthetic { return Some((col_name.to_string(), naming::aesthetic_column(&flipped))); } @@ -400,7 +400,7 @@ mod tests { #[test] fn test_resolve_orientation_scale_only_no_flip() { - // Scale specification without positional mapping shouldn't flip orientation + // Scale specification without position mapping shouldn't flip orientation // Real-world: `VISUALISE FROM data DRAW bar SCALE y SETTING expand => [...]` // The bar stat will produce pos1=category, pos2=count → should stay Aligned let layer = Layer::new(Geom::bar()); @@ -408,7 +408,7 @@ mod tests { scale.scale_type = Some(ScaleType::continuous()); let scales = vec![scale]; - // Without positional mappings, scale existence doesn't imply orientation + // Without position mappings, scale existence doesn't imply orientation assert_eq!(resolve_orientation(&layer, &scales), ALIGNED); } @@ -439,7 +439,7 @@ mod tests { } #[test] - fn test_flip_positional_aesthetics() { + fn test_flip_position_aesthetics() { let mut layer = Layer::new(Geom::bar()); layer.mappings.insert( "pos1".to_string(), @@ -454,7 +454,7 @@ mod tests { AestheticValue::standard_column("x2".to_string()), ); - flip_positional_aesthetics(&mut layer.mappings.aesthetics); + flip_position_aesthetics(&mut layer.mappings.aesthetics); // pos1 ↔ pos2 assert_eq!( @@ -474,15 +474,15 @@ mod tests { } #[test] - fn test_flip_positional_aesthetics_empty() { + fn test_flip_position_aesthetics_empty() { let mut layer = Layer::new(Geom::point()); // No crash with empty mappings - flip_positional_aesthetics(&mut layer.mappings.aesthetics); + flip_position_aesthetics(&mut layer.mappings.aesthetics); assert!(layer.mappings.aesthetics.is_empty()); } #[test] - fn test_flip_positional_aesthetics_partial() { + fn test_flip_position_aesthetics_partial() { let mut layer = Layer::new(Geom::bar()); // Only pos1 mapped layer.mappings.insert( @@ -490,7 +490,7 @@ mod tests { AestheticValue::standard_column("x".to_string()), ); - flip_positional_aesthetics(&mut layer.mappings.aesthetics); + flip_position_aesthetics(&mut layer.mappings.aesthetics); // pos1 moves to pos2 assert!(layer.mappings.get("pos1").is_none()); @@ -698,7 +698,7 @@ mod tests { #[test] fn test_resolve_orientation_mapping_overrides_remapping() { // Bar with pos1 mapping AND pos1 remapping → mapping takes precedence - // The remapping rule only applies when NO positional mappings exist + // The remapping rule only applies when NO position mappings exist let mut layer = Layer::new(Geom::bar()); layer.mappings.insert( "pos1".to_string(), diff --git a/src/plot/main.rs b/src/plot/main.rs index 523f9c96..fefb2d90 100644 --- a/src/plot/main.rs +++ b/src/plot/main.rs @@ -139,18 +139,18 @@ impl Plot { /// Build an aesthetic context from current project and facet settings fn build_aesthetic_context(&self) -> AestheticContext { - let default_positional: Vec = vec!["x".to_string(), "y".to_string()]; - let positional_names: &[String] = self + let default_position: Vec = vec!["x".to_string(), "y".to_string()]; + let position_names: &[String] = self .project .as_ref() .map(|p| p.aesthetics.as_slice()) - .unwrap_or(&default_positional); + .unwrap_or(&default_position); let facet_names: &[&'static str] = self .facet .as_ref() .map(|f| f.layout.user_facet_names()) .unwrap_or(&[]); - AestheticContext::new(positional_names, facet_names) + AestheticContext::new(position_names, facet_names) } /// Get the aesthetic context, creating a default one if not set @@ -266,7 +266,7 @@ impl Plot { for layer in &self.layers { for (aesthetic, value) in &layer.mappings.aesthetics { let primary = aesthetic_ctx - .primary_internal_positional(aesthetic) + .primary_internal_position(aesthetic) .unwrap_or(aesthetic); let is_primary = aesthetic == primary; @@ -555,23 +555,23 @@ mod tests { let ctx = AestheticContext::from_static(&["x", "y"], &[]); // Test that internal variant aesthetics map to their primary - assert_eq!(ctx.primary_internal_positional("pos1"), Some("pos1")); - assert_eq!(ctx.primary_internal_positional("pos1min"), Some("pos1")); - assert_eq!(ctx.primary_internal_positional("pos1max"), Some("pos1")); - assert_eq!(ctx.primary_internal_positional("pos1end"), Some("pos1")); - assert_eq!(ctx.primary_internal_positional("pos2"), Some("pos2")); - assert_eq!(ctx.primary_internal_positional("pos2min"), Some("pos2")); - assert_eq!(ctx.primary_internal_positional("pos2max"), Some("pos2")); - assert_eq!(ctx.primary_internal_positional("pos2end"), Some("pos2")); - - // Non-positional aesthetics return themselves - assert_eq!(ctx.primary_internal_positional("color"), Some("color")); - assert_eq!(ctx.primary_internal_positional("size"), Some("size")); - assert_eq!(ctx.primary_internal_positional("fill"), Some("fill")); + assert_eq!(ctx.primary_internal_position("pos1"), Some("pos1")); + assert_eq!(ctx.primary_internal_position("pos1min"), Some("pos1")); + assert_eq!(ctx.primary_internal_position("pos1max"), Some("pos1")); + assert_eq!(ctx.primary_internal_position("pos1end"), Some("pos1")); + assert_eq!(ctx.primary_internal_position("pos2"), Some("pos2")); + assert_eq!(ctx.primary_internal_position("pos2min"), Some("pos2")); + assert_eq!(ctx.primary_internal_position("pos2max"), Some("pos2")); + assert_eq!(ctx.primary_internal_position("pos2end"), Some("pos2")); + + // Material aesthetics return themselves + assert_eq!(ctx.primary_internal_position("color"), Some("color")); + assert_eq!(ctx.primary_internal_position("size"), Some("size")); + assert_eq!(ctx.primary_internal_position("fill"), Some("fill")); // User-facing names are not recognized as internal aesthetics - assert_eq!(ctx.primary_internal_positional("x"), None); - assert_eq!(ctx.primary_internal_positional("xmin"), None); + assert_eq!(ctx.primary_internal_position("x"), None); + assert_eq!(ctx.primary_internal_position("xmin"), None); } #[test] @@ -802,7 +802,7 @@ mod tests { spec.transform_aesthetics_to_internal(); let labels = spec.labels.as_ref().unwrap(); - // x maps to pos2 (second positional), y maps to pos1 (first positional) + // x maps to pos2 (second position), y maps to pos1 (first position) assert_eq!(labels.labels.get("pos1"), Some(&"Value".to_string())); assert_eq!(labels.labels.get("pos2"), Some(&"Category".to_string())); } @@ -834,7 +834,7 @@ mod tests { } #[test] - fn test_label_transform_preserves_non_positional() { + fn test_label_transform_preserves_material() { // LABEL title/color should be preserved unchanged use crate::plot::projection::{Coord, Projection}; @@ -856,10 +856,10 @@ mod tests { spec.transform_aesthetics_to_internal(); let labels = spec.labels.as_ref().unwrap(); - // Non-positional labels should remain unchanged + // Material labels should remain unchanged assert_eq!(labels.labels.get("title"), Some(&"My Chart".to_string())); assert_eq!(labels.labels.get("color"), Some(&"Category".to_string())); - // Positional label should be transformed + // Position label should be transformed assert_eq!(labels.labels.get("pos1"), Some(&"X Axis".to_string())); } } diff --git a/src/plot/projection/coord/cartesian.rs b/src/plot/projection/coord/cartesian.rs index 425026c0..70a69d3a 100644 --- a/src/plot/projection/coord/cartesian.rs +++ b/src/plot/projection/coord/cartesian.rs @@ -16,7 +16,7 @@ impl CoordTrait for Cartesian { "cartesian" } - fn positional_aesthetic_names(&self) -> &'static [&'static str] { + fn position_aesthetic_names(&self) -> &'static [&'static str] { &["x", "y"] } diff --git a/src/plot/projection/coord/mod.rs b/src/plot/projection/coord/mod.rs index 27023036..4e1e61ed 100644 --- a/src/plot/projection/coord/mod.rs +++ b/src/plot/projection/coord/mod.rs @@ -64,14 +64,14 @@ pub trait CoordTrait: std::fmt::Debug + std::fmt::Display + Send + Sync { /// Canonical name for parsing and display fn name(&self) -> &'static str; - /// Primary positional aesthetic names for this coord. + /// Primary position aesthetic names for this coord. /// - /// Returns the user-facing positional aesthetic names. + /// Returns the user-facing position aesthetic names. /// e.g., ["x", "y"] for cartesian, ["radius", "theta"] for polar. /// /// These names are transformed to internal names (pos1, pos2, etc.) /// early in the pipeline and transformed back for output. - fn positional_aesthetic_names(&self) -> &'static [&'static str]; + fn position_aesthetic_names(&self) -> &'static [&'static str]; /// Returns list of allowed properties with their default values. /// Default: empty (no properties allowed). @@ -159,10 +159,10 @@ impl Coord { self.0.name() } - /// Primary positional aesthetic names for this coord. + /// Primary position aesthetic names for this coord. /// e.g., ["x", "y"] for cartesian, ["radius", "theta"] for polar. - pub fn positional_aesthetic_names(&self) -> &'static [&'static str] { - self.0.positional_aesthetic_names() + pub fn position_aesthetic_names(&self) -> &'static [&'static str] { + self.0.position_aesthetic_names() } /// Returns list of allowed properties with their default values. @@ -281,11 +281,11 @@ mod tests { } #[test] - fn test_positional_aesthetic_names() { + fn test_position_aesthetic_names() { let cartesian = Coord::cartesian(); - assert_eq!(cartesian.positional_aesthetic_names(), &["x", "y"]); + assert_eq!(cartesian.position_aesthetic_names(), &["x", "y"]); let polar = Coord::polar(); - assert_eq!(polar.positional_aesthetic_names(), &["radius", "theta"]); + assert_eq!(polar.position_aesthetic_names(), &["radius", "theta"]); } } diff --git a/src/plot/projection/coord/polar.rs b/src/plot/projection/coord/polar.rs index 95ba5000..d88fdb38 100644 --- a/src/plot/projection/coord/polar.rs +++ b/src/plot/projection/coord/polar.rs @@ -16,7 +16,7 @@ impl CoordTrait for Polar { "polar" } - fn positional_aesthetic_names(&self) -> &'static [&'static str] { + fn position_aesthetic_names(&self) -> &'static [&'static str] { &["radius", "theta"] } diff --git a/src/plot/projection/resolve.rs b/src/plot/projection/resolve.rs index 24921206..550459e6 100644 --- a/src/plot/projection/resolve.rs +++ b/src/plot/projection/resolve.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use super::coord::{Coord, CoordKind}; use super::Projection; -use crate::plot::aesthetic::{NON_POSITIONAL, POSITIONAL_SUFFIXES}; +use crate::plot::aesthetic::{MATERIAL_AESTHETICS, POSITION_SUFFIXES}; use crate::plot::Mappings; /// Cartesian primary aesthetic names @@ -65,7 +65,7 @@ pub fn resolve_coord( // Infer polar coordinate system let coord = Coord::from_kind(CoordKind::Polar); let aesthetics = coord - .positional_aesthetic_names() + .position_aesthetic_names() .iter() .map(|s| s.to_string()) .collect(); @@ -80,7 +80,7 @@ pub fn resolve_coord( // Infer cartesian coordinate system let coord = Coord::from_kind(CoordKind::Cartesian); let aesthetics = coord - .positional_aesthetic_names() + .position_aesthetic_names() .iter() .map(|s| s.to_string()) .collect(); @@ -98,13 +98,13 @@ pub fn resolve_coord( /// Check if an aesthetic name indicates cartesian or polar coordinate system. /// Updates the found flags accordingly. fn check_aesthetic(aesthetic: &str, found_cartesian: &mut bool, found_polar: &mut bool) { - // Skip non-positional aesthetics (color, size, etc.) - if NON_POSITIONAL.contains(&aesthetic) { + // Skip material aesthetics (color, size, etc.) + if MATERIAL_AESTHETICS.contains(&aesthetic) { return; } - // Strip positional suffix if present (xmin -> x, thetamax -> theta) - let primary = strip_positional_suffix(aesthetic); + // Strip position suffix if present (xmin -> x, thetamax -> theta) + let primary = strip_position_suffix(aesthetic); // Check against cartesian primaries if CARTESIAN_PRIMARIES.contains(&primary) { @@ -117,10 +117,10 @@ fn check_aesthetic(aesthetic: &str, found_cartesian: &mut bool, found_polar: &mu } } -/// Strip positional suffix from an aesthetic name. +/// Strip position suffix from an aesthetic name. /// e.g., "xmin" -> "x", "thetamax" -> "theta", "y" -> "y" -fn strip_positional_suffix(name: &str) -> &str { - for suffix in POSITIONAL_SUFFIXES { +fn strip_position_suffix(name: &str) -> &str { + for suffix in POSITION_SUFFIXES { if let Some(base) = name.strip_suffix(suffix) { return base; } @@ -252,11 +252,11 @@ mod tests { } // ======================================== - // Test: Non-positional aesthetics ignored + // Test: Material aesthetics ignored // ======================================== #[test] - fn test_ignore_non_positional() { + fn test_ignore_material() { let global = mappings_with(&["color", "size", "fill", "opacity"]); let layers: Vec<&Mappings> = vec![]; @@ -266,7 +266,7 @@ mod tests { } #[test] - fn test_non_positional_with_cartesian() { + fn test_material_with_cartesian() { let global = mappings_with(&["x", "y", "color", "size"]); let layers: Vec<&Mappings> = vec![]; @@ -354,16 +354,16 @@ mod tests { // ======================================== #[test] - fn test_strip_positional_suffix() { - assert_eq!(strip_positional_suffix("x"), "x"); - assert_eq!(strip_positional_suffix("y"), "y"); - assert_eq!(strip_positional_suffix("xmin"), "x"); - assert_eq!(strip_positional_suffix("xmax"), "x"); - assert_eq!(strip_positional_suffix("xend"), "x"); - assert_eq!(strip_positional_suffix("ymin"), "y"); - assert_eq!(strip_positional_suffix("ymax"), "y"); - assert_eq!(strip_positional_suffix("theta"), "theta"); - assert_eq!(strip_positional_suffix("thetamin"), "theta"); - assert_eq!(strip_positional_suffix("radiusmax"), "radius"); + fn test_strip_position_suffix() { + assert_eq!(strip_position_suffix("x"), "x"); + assert_eq!(strip_position_suffix("y"), "y"); + assert_eq!(strip_position_suffix("xmin"), "x"); + assert_eq!(strip_position_suffix("xmax"), "x"); + assert_eq!(strip_position_suffix("xend"), "x"); + assert_eq!(strip_position_suffix("ymin"), "y"); + assert_eq!(strip_position_suffix("ymax"), "y"); + assert_eq!(strip_position_suffix("theta"), "theta"); + assert_eq!(strip_position_suffix("thetamin"), "theta"); + assert_eq!(strip_position_suffix("radiusmax"), "radius"); } } diff --git a/src/plot/projection/types.rs b/src/plot/projection/types.rs index d2cffd1c..595add76 100644 --- a/src/plot/projection/types.rs +++ b/src/plot/projection/types.rs @@ -13,7 +13,7 @@ use crate::plot::ParameterValue; pub struct Projection { /// Coordinate system type pub coord: Coord, - /// Positional aesthetic names (resolved: explicit or coord defaults) + /// Position aesthetic names (resolved: explicit or coord defaults) /// Always populated after building - never empty. /// e.g., ["x", "y"] for cartesian, ["radius", "theta"] for polar, /// or custom names like ["a", "b"] if user specifies them. @@ -23,9 +23,9 @@ pub struct Projection { } impl Projection { - /// Get the positional aesthetic names as string slices. + /// Get the position aesthetic names as string slices. /// (aesthetics are always resolved at build time) - pub fn positional_names(&self) -> Vec<&str> { + pub fn position_names(&self) -> Vec<&str> { self.aesthetics.iter().map(|s| s.as_str()).collect() } } diff --git a/src/plot/scale/breaks.rs b/src/plot/scale/breaks.rs index afc9f497..eba84c54 100644 --- a/src/plot/scale/breaks.rs +++ b/src/plot/scale/breaks.rs @@ -320,7 +320,7 @@ pub fn filter_breaks_to_range( /// For `pretty=true`: Uses 1-2-5 pattern across decades (e.g., 1, 2, 5, 10, 20, 50, 100). /// For `pretty=false`: Returns only powers of the base (e.g., 1, 10, 100, 1000 for base 10). /// -/// Non-positive values are filtered out since log is undefined for them. +/// Material values are filtered out since log is undefined for them. pub fn log_breaks(min: f64, max: f64, n: usize, base: f64, pretty: bool) -> Vec { // Filter to positive values only let pos_min = if min <= 0.0 { f64::MIN_POSITIVE } else { min }; diff --git a/src/plot/scale/mod.rs b/src/plot/scale/mod.rs index 8ad0cb19..600f8db5 100644 --- a/src/plot/scale/mod.rs +++ b/src/plot/scale/mod.rs @@ -13,7 +13,7 @@ mod types; pub use crate::format::apply_label_template; pub use crate::plot::aesthetic::{ - is_facet_aesthetic, is_positional_aesthetic, is_user_facet_aesthetic, + is_facet_aesthetic, is_position_aesthetic, is_user_facet_aesthetic, }; pub use crate::plot::types::CastTargetType; pub use crate::reader::SqlDialect; @@ -45,8 +45,8 @@ use crate::plot::{ArrayElement, ArrayElementType}; /// an unmapped aesthetic should get a scale with type inference (Continuous/Discrete) /// or an Identity scale (pass-through, no transformation). pub fn gets_default_scale(aesthetic: &str) -> bool { - // Positional aesthetics (pos1, pos1min, pos2max, etc.) - checked dynamically - if is_positional_aesthetic(aesthetic) { + // Position aesthetics (pos1, pos1min, pos2max, etc.) - checked dynamically + if is_position_aesthetic(aesthetic) { return true; } @@ -55,7 +55,7 @@ pub fn gets_default_scale(aesthetic: &str) -> bool { return true; } - // Non-positional visual aesthetics that get default scales + // Material aesthetics that get default scales matches!( aesthetic, // Color aesthetics (color/colour/col already split to fill/stroke) @@ -64,7 +64,7 @@ pub fn gets_default_scale(aesthetic: &str) -> bool { | "size" | "linewidth" // Dimension aesthetics | "width" | "height" - // Other visual aesthetics + // Other material aesthetics | "opacity" | "shape" | "linetype" ) } diff --git a/src/plot/scale/scale_type/binned.rs b/src/plot/scale/scale_type/binned.rs index b4bf9d55..6cd21e13 100644 --- a/src/plot/scale/scale_type/binned.rs +++ b/src/plot/scale/scale_type/binned.rs @@ -443,9 +443,9 @@ impl ScaleTypeTrait for Binned { breaks.first().unwrap().clone(), breaks.last().unwrap().clone(), ]; - // Only expand for positional aesthetics (x, y, etc.) - // Non-positional aesthetics (color, fill, size) don't get expansion - let final_range = if super::is_positional_aesthetic(aesthetic) { + // Only expand for position aesthetics (x, y, etc.) + // Material aesthetics (color, fill, size) don't get expansion + let final_range = if super::is_position_aesthetic(aesthetic) { expand_numeric_range(&terminal_range, mult, add) } else { terminal_range diff --git a/src/plot/scale/scale_type/continuous.rs b/src/plot/scale/scale_type/continuous.rs index e769636d..6b3c50c2 100644 --- a/src/plot/scale/scale_type/continuous.rs +++ b/src/plot/scale/scale_type/continuous.rs @@ -271,7 +271,7 @@ mod tests { #[test] fn test_pre_stat_transform_sql_keep() { let continuous = Continuous; - let mut scale = Scale::new("x"); // positional aesthetic defaults to keep + let mut scale = Scale::new("x"); // position aesthetic defaults to keep scale.input_range = Some(vec![ArrayElement::Number(0.0), ArrayElement::Number(100.0)]); scale.explicit_input_range = true; scale.properties.insert( @@ -302,33 +302,33 @@ mod tests { } #[test] - fn test_pre_stat_transform_sql_default_oob_for_positional() { - // NOTE: After transformation, positional aesthetics use internal names (pos1, pos2, etc.) + fn test_pre_stat_transform_sql_default_oob_for_position() { + // NOTE: After transformation, position aesthetics use internal names (pos1, pos2, etc.) let continuous = Continuous; - let mut scale = Scale::new("pos1"); // positional aesthetic (internal name) + let mut scale = Scale::new("pos1"); // position aesthetic (internal name) scale.input_range = Some(vec![ArrayElement::Number(0.0), ArrayElement::Number(100.0)]); scale.explicit_input_range = true; - // No oob property - should use default (keep for positional) + // No oob property - should use default (keep for position) let sql = continuous.pre_stat_transform_sql("value", &DataType::Float64, &scale, &AnsiDialect); - // Should return None since default for positional is "keep" + // Should return None since default for position is "keep" assert!(sql.is_none()); } #[test] - fn test_pre_stat_transform_sql_default_oob_for_non_positional() { + fn test_pre_stat_transform_sql_default_oob_for_material() { let continuous = Continuous; - let mut scale = Scale::new("color"); // non-positional aesthetic + let mut scale = Scale::new("color"); // material aesthetic scale.input_range = Some(vec![ArrayElement::Number(0.0), ArrayElement::Number(100.0)]); scale.explicit_input_range = true; - // No oob property - should use default (censor for non-positional) + // No oob property - should use default (censor for material) let sql = continuous.pre_stat_transform_sql("value", &DataType::Float64, &scale, &AnsiDialect); - // Should generate censor SQL since default for non-positional is "censor" + // Should generate censor SQL since default for material is "censor" assert!(sql.is_some()); let sql = sql.unwrap(); assert!(sql.contains("CASE WHEN")); diff --git a/src/plot/scale/scale_type/mod.rs b/src/plot/scale/scale_type/mod.rs index 4fa345f9..dd6af7fb 100644 --- a/src/plot/scale/scale_type/mod.rs +++ b/src/plot/scale/scale_type/mod.rs @@ -26,7 +26,7 @@ use std::collections::HashMap; use std::sync::Arc; use super::transform::{Transform, TransformKind}; -use crate::plot::aesthetic::{is_facet_aesthetic, is_positional_aesthetic}; +use crate::plot::aesthetic::{is_facet_aesthetic, is_position_aesthetic}; use crate::plot::types::{DefaultParam, DefaultParamValue}; use crate::plot::{ArrayElement, ColumnInfo, ParameterValue}; @@ -542,7 +542,7 @@ pub trait ScaleTypeTrait: std::fmt::Debug + std::fmt::Display + Send + Sync { /// Returns list of allowed properties with their default values. /// - /// Properties that vary by aesthetic (like `expand` for positional-only, or `oob` + /// Properties that vary by aesthetic (like `expand` for position-only, or `oob` /// with aesthetic-dependent defaults) should use `DefaultParamValue::Null` as their /// default value. The `resolve_properties()` method handles these special cases. /// @@ -632,12 +632,12 @@ pub trait ScaleTypeTrait: std::fmt::Debug + std::fmt::Display + Send + Sync { properties: &HashMap, ) -> Result, String> { let defaults = self.default_properties(); - let is_positional = is_positional_aesthetic(aesthetic); + let is_position = is_position_aesthetic(aesthetic); - // Build allowed list, excluding "expand" for non-positional aesthetics + // Build allowed list, excluding "expand" for material aesthetics let allowed: Vec<&str> = defaults .iter() - .filter(|p| p.name != "expand" || is_positional) + .filter(|p| p.name != "expand" || is_position) .map(|p| p.name) .collect(); @@ -662,8 +662,8 @@ pub trait ScaleTypeTrait: std::fmt::Debug + std::fmt::Display + Send + Sync { // Start with user properties, add defaults for missing ones let mut resolved = properties.clone(); for param in defaults { - // Skip expand for non-positional aesthetics - if param.name == "expand" && !is_positional { + // Skip expand for material aesthetics + if param.name == "expand" && !is_position { continue; } @@ -1384,13 +1384,13 @@ pub(super) const DEFAULT_EXPAND_ADD: f64 = 0.0; pub const OOB_CENSOR: &str = "censor"; /// Out-of-bounds mode: clamp values to the closest limit pub const OOB_SQUISH: &str = "squish"; -/// Out-of-bounds mode: don't modify values (default for positional aesthetics) +/// Out-of-bounds mode: don't modify values (default for position aesthetics) pub const OOB_KEEP: &str = "keep"; /// Default oob mode for an aesthetic. -/// Positional aesthetics default to "keep", others default to "censor". +/// Position aesthetics default to "keep", others default to "censor". pub fn default_oob(aesthetic: &str) -> &'static str { - if is_positional_aesthetic(aesthetic) { + if is_position_aesthetic(aesthetic) { OOB_KEEP } else { OOB_CENSOR @@ -1828,22 +1828,22 @@ pub(crate) fn resolve_common_steps( // Step 2: Apply expansion to the final merged range // Expansion should ONLY happen when ALL conditions are met: // 1. Scale uses continuous input range (not discrete/ordinal scales) - // 2. Aesthetic is positional (x, y, xmin, xmax, etc.) + // 2. Aesthetic is position (x, y, xmin, xmax, etc.) // 3. Input range was at least partially deduced (not fully explicit) // // Then clip to the transform's valid domain to prevent invalid values // (e.g., expansion producing negative values for log scales) if let Some(range) = base_range { - let is_positional = is_positional_aesthetic(aesthetic); + let is_position = is_position_aesthetic(aesthetic); let is_deduced = !scale.explicit_input_range || input_range_has_nulls(original_user_range.as_deref().unwrap_or(&[])); - if !is_discrete_range && is_positional && is_deduced { + if !is_discrete_range && is_position && is_deduced { let expanded = expand_numeric_range_selective(&range, mult, add, original_user_range.as_deref()); scale.input_range = Some(clip_to_transform_domain(&expanded, &resolved_transform)); } else { - // No expansion for discrete scales, non-positional aesthetics, or fully explicit ranges + // No expansion for discrete scales, material aesthetics, or fully explicit ranges scale.input_range = Some(range); } } @@ -2359,7 +2359,7 @@ mod tests { #[test] fn test_resolve_properties_defaults() { - // Continuous positional: default expand + // Continuous position: default expand let props = HashMap::new(); let resolved = ScaleType::continuous() .resolve_properties("pos1", &props) @@ -2370,7 +2370,7 @@ mod tests { _ => panic!("Expected Number"), } - // Continuous non-positional: no default expand, but has oob + // Continuous material: no default expand, but has oob let resolved = ScaleType::continuous() .resolve_properties("color", &props) .unwrap(); @@ -2428,17 +2428,17 @@ mod tests { } #[test] - fn test_expand_positional_vs_non_positional() { - // Internal positional aesthetics (after transformation) - let internal_positional = [ + fn test_expand_position_vs_material() { + // Internal position aesthetics (after transformation) + let internal_position = [ "pos1", "pos1min", "pos1max", "pos1end", "pos2", "pos2min", "pos2max", "pos2end", ]; let mut props = HashMap::new(); props.insert("expand".to_string(), ParameterValue::Number(0.1)); - // Positional aesthetics should allow expand - for aes in internal_positional.iter() { + // Position aesthetics should allow expand + for aes in internal_position.iter() { assert!( ScaleType::continuous() .resolve_properties(aes, &props) @@ -2448,7 +2448,7 @@ mod tests { ); } - // Non-positional aesthetics should reject expand + // Material aesthetics should reject expand for aes in &["color", "size", "opacity"] { let result = ScaleType::continuous().resolve_properties(aes, &props); assert!(result.is_err(), "{} should reject expand", aes); @@ -2461,27 +2461,27 @@ mod tests { #[test] fn test_oob_defaults_by_aesthetic_type() { - // Internal positional aesthetics (after transformation) - let internal_positional = [ + // Internal position aesthetics (after transformation) + let internal_position = [ "pos1", "pos1min", "pos1max", "pos1end", "pos2", "pos2min", "pos2max", "pos2end", ]; let props = HashMap::new(); - // Positional aesthetics default to 'keep' - for aesthetic in internal_positional.iter() { + // Position aesthetics default to 'keep' + for aesthetic in internal_position.iter() { let resolved = ScaleType::continuous() .resolve_properties(aesthetic, &props) .unwrap(); assert_eq!( resolved.get("oob"), Some(&ParameterValue::String("keep".into())), - "Positional '{}' should default to 'keep'", + "Position '{}' should default to 'keep'", aesthetic ); } - // Non-positional aesthetics default to 'censor' + // Material aesthetics default to 'censor' for aesthetic in &["color", "size", "opacity", "fill", "stroke"] { let resolved = ScaleType::continuous() .resolve_properties(aesthetic, &props) @@ -2489,7 +2489,7 @@ mod tests { assert_eq!( resolved.get("oob"), Some(&ParameterValue::String("censor".into())), - "Non-positional '{}' should default to 'censor'", + "Material '{}' should default to 'censor'", aesthetic ); } @@ -2775,7 +2775,7 @@ mod tests { Some(&ParameterValue::Boolean(false)) ); - // Same for non-positional aesthetics + // Same for material aesthetics let resolved = ScaleType::continuous() .resolve_properties("color", &props) .unwrap(); @@ -2938,7 +2938,7 @@ mod tests { } #[test] - fn test_breaks_available_for_non_positional_aesthetics() { + fn test_breaks_available_for_material_aesthetics() { // breaks should work for color legends too let mut props = HashMap::new(); props.insert("breaks".to_string(), ParameterValue::Number(4.0)); diff --git a/src/plot/types.rs b/src/plot/types.rs index ec28b525..f2deabb5 100644 --- a/src/plot/types.rs +++ b/src/plot/types.rs @@ -111,9 +111,9 @@ impl Mappings { /// Transform aesthetic keys from user-facing to internal names. /// - /// Uses the provided AestheticContext to map user-facing positional aesthetic names + /// Uses the provided AestheticContext to map user-facing position aesthetic names /// (e.g., "x", "y", "theta", "radius") to internal names (e.g., "pos1", "pos2"). - /// Non-positional aesthetics (e.g., "color", "size") are left unchanged. + /// Material aesthetics (e.g., "color", "size") are left unchanged. pub fn transform_to_internal(&mut self, ctx: &super::AestheticContext) { let original_aesthetics = std::mem::take(&mut self.aesthetics); for (aesthetic, value) in original_aesthetics { @@ -183,10 +183,10 @@ pub enum AestheticValue { /// Whether this is a dummy/placeholder column (e.g., for bar charts without x mapped) is_dummy: bool, }, - /// Annotation column for non-positional aesthetics (synthesized from PLACE literals) + /// Annotation column for material aesthetics (synthesized from PLACE literals) /// These columns are generated from user-specified literal values in visual space /// (e.g., color => 'red', size => 10) and use identity scales (no transformation). - /// Positional annotations (x, y) use Column instead since they're in data coordinate space. + /// Position annotations (x, y) use Column instead since they're in data coordinate space. AnnotationColumn { name: String }, /// Literal value (quoted string, number, or boolean) Literal(ParameterValue), diff --git a/src/reader/mod.rs b/src/reader/mod.rs index 9bc7da7d..41f8bcc6 100644 --- a/src/reader/mod.rs +++ b/src/reader/mod.rs @@ -599,7 +599,7 @@ mod tests { #[test] fn test_polar_encoding_keys_independent_of_user_names() { // This test verifies that polar projections always produce theta/radius encoding keys - // in Vega-Lite output, regardless of what positional names the user specified in PROJECT. + // in Vega-Lite output, regardless of what position names the user specified in PROJECT. // This is critical because Vega-Lite expects specific channel names for polar marks. let reader = DuckDBReader::from_connection_string("duckdb://memory").unwrap(); @@ -680,7 +680,7 @@ mod tests { #[test] fn test_cartesian_encoding_keys_with_custom_names() { // This test verifies that cartesian projections produce x/y encoding keys - // even when custom positional names are used in PROJECT. + // even when custom position names are used in PROJECT. let reader = DuckDBReader::from_connection_string("duckdb://memory").unwrap(); fn check_cartesian_keys(json: &serde_json::Value, test_name: &str) { @@ -787,7 +787,7 @@ mod tests { #[test] fn test_binned_fill_legend_renders_threshold_scale() { // End-to-end test for binned fill scale rendering to Vega-Lite - // Verifies that binned non-positional aesthetics use threshold scale type + // Verifies that binned material aesthetics use threshold scale type let reader = DuckDBReader::from_connection_string("duckdb://memory").unwrap(); // Create data with values that span the binned range @@ -1198,8 +1198,8 @@ mod tests { let encoding = &layer["encoding"]; // With PROJECT y, x TO cartesian: - // - y is pos1 (first positional), renders to VL x-axis in cartesian - // - x is pos2 (second positional), renders to VL y-axis in cartesian + // - y is pos1 (first position), renders to VL x-axis in cartesian + // - x is pos2 (second position), renders to VL y-axis in cartesian // So LABEL y => 'Category' should appear on VL x-axis, LABEL x => 'Value' on VL y-axis let x_title = encoding["x"]["title"].as_str(); let y_title = encoding["y"]["title"].as_str(); diff --git a/src/writer/vegalite/data.rs b/src/writer/vegalite/data.rs index d5dadda7..65da1a1b 100644 --- a/src/writer/vegalite/data.rs +++ b/src/writer/vegalite/data.rs @@ -346,7 +346,7 @@ pub(super) fn collect_binned_columns(spec: &Plot) -> HashMap> { pub(super) fn is_binned_aesthetic(aesthetic: &str, spec: &Plot) -> bool { let aesthetic_ctx = spec.get_aesthetic_context(); let primary = aesthetic_ctx - .primary_internal_positional(aesthetic) + .primary_internal_position(aesthetic) .unwrap_or(aesthetic); spec.find_scale(primary) .and_then(|s| s.scale_type.as_ref()) diff --git a/src/writer/vegalite/encoding.rs b/src/writer/vegalite/encoding.rs index b58619d7..98405e40 100644 --- a/src/writer/vegalite/encoding.rs +++ b/src/writer/vegalite/encoding.rs @@ -3,7 +3,7 @@ //! This module handles building Vega-Lite encoding channels from ggsql aesthetic mappings, //! including type inference, scale properties, and title handling. -use crate::plot::aesthetic::{is_positional_aesthetic, AestheticContext}; +use crate::plot::aesthetic::{is_position_aesthetic, AestheticContext}; use crate::plot::scale::{linetype_to_stroke_dash, shape_to_svg_path, ScaleTypeKind}; use crate::plot::{CoordKind, ParameterValue}; use crate::{AestheticValue, DataFrame, Plot, Result}; @@ -13,14 +13,14 @@ use std::collections::{HashMap, HashSet}; use super::{POINTS_TO_AREA, POINTS_TO_PIXELS}; -/// Check if a positional aesthetic has free scales enabled. +/// Check if a position aesthetic has free scales enabled. /// /// Maps aesthetic names to position indices: /// - pos1, pos1min, pos1max, pos1end -> index 0 /// - pos2, pos2min, pos2max, pos2end -> index 1 /// - etc. /// -/// Returns false for non-positional aesthetics or if no free_scales array is provided. +/// Returns false for material aesthetics or if no free_scales array is provided. fn is_position_free_for_aesthetic( aesthetic: &str, free_scales: Option<&[crate::plot::ArrayElement]>, @@ -191,7 +191,7 @@ pub(super) fn build_symbol_legend_label_mapping( result } -/// Count the number of binned non-positional scales in the spec. +/// Count the number of binned material scales in the spec. /// This is used to determine if legends should use symbol style (which requires /// removing the last terminal value) or gradient style (which keeps all values). pub(super) fn count_binned_legend_scales(spec: &Plot) -> usize { @@ -205,10 +205,10 @@ pub(super) fn count_binned_legend_scales(spec: &Plot) -> usize { .map(|st| st.scale_type_kind() == ScaleTypeKind::Binned) .unwrap_or(false); - // Check if non-positional (legend aesthetic) - let is_legend_aesthetic = !is_positional_aesthetic(&scale.aesthetic); + // Check if material aesthetic + let is_material_aesthetic = !is_position_aesthetic(&scale.aesthetic); - is_binned && is_legend_aesthetic + is_binned && is_material_aesthetic }) .count() } @@ -297,7 +297,7 @@ enum LegendStyle { /// Determine legend style for a binned aesthetic /// /// - fill/stroke alone: gradient legend -/// - fill/stroke with other binned legend aesthetics: symbol legend +/// - fill/stroke with other binned material aesthetics: symbol legend /// - all other aesthetics: symbol legend fn determine_legend_style(aesthetic: &str, spec: &Plot) -> LegendStyle { let is_gradient_aesthetic = matches!(aesthetic, "fill" | "stroke"); @@ -367,7 +367,7 @@ fn determine_field_type_for_aesthetic( aesthetic_ctx: &AestheticContext, ) -> String { let primary = aesthetic_ctx - .primary_internal_positional(aesthetic) + .primary_internal_position(aesthetic) .unwrap_or(aesthetic); let inferred = infer_field_type(df, col); @@ -402,7 +402,7 @@ fn apply_title_to_encoding( aesthetic_ctx: &AestheticContext, ) { let primary = aesthetic_ctx - .primary_internal_positional(aesthetic) + .primary_internal_position(aesthetic) .unwrap_or(aesthetic); let is_primary = aesthetic == primary; let primary_exists = primary_aesthetics.contains(primary); @@ -505,7 +505,7 @@ fn build_scale_properties( apply_transform_to_scale(&mut scale_obj, transform); } - // Handle binned non-positional aesthetics with threshold scale + // Handle binned material aesthetics with threshold scale if ctx.is_binned_legend { scale_obj.insert("type".to_string(), json!("threshold")); @@ -629,8 +629,8 @@ fn apply_reverse_legend(encoding: &mut Value, scale: &crate::plot::Scale, aesthe return; } - // Only for non-positional aesthetics (those with legends) - if is_positional_aesthetic(aesthetic) { + // Only for material aesthetics (those with legends) + if is_position_aesthetic(aesthetic) { return; } @@ -657,8 +657,8 @@ fn apply_breaks_to_encoding( let all_values: Vec = breaks.iter().map(|e| e.to_json()).collect(); - if is_positional_aesthetic(aesthetic) { - // For positional aesthetics (axes), filter out suppressed terminal breaks + if is_position_aesthetic(aesthetic) { + // For position aesthetics (axes), filter out suppressed terminal breaks let axis_values: Vec = if let Some(ref label_mapping) = scale.label_mapping { breaks .iter() @@ -674,7 +674,7 @@ fn apply_breaks_to_encoding( insert_axis_property(encoding, "values", json!(axis_values)); } else { - // For legend aesthetics, determine values based on legend style + // For material aesthetics, determine values based on legend style let legend_values = if is_binned_legend { let legend_style = determine_legend_style(aesthetic, spec); if legend_style == LegendStyle::Symbol && !all_values.is_empty() { @@ -762,7 +762,7 @@ fn apply_label_mapping_to_encoding( let label_expr = build_label_expr(&filtered_mapping, time_format, null_key.as_deref()); - if is_positional_aesthetic(aesthetic) { + if is_position_aesthetic(aesthetic) { insert_axis_property(encoding, "labelExpr", json!(label_expr)); } else { insert_legend_property(encoding, "labelExpr", json!(label_expr)); @@ -804,7 +804,7 @@ pub(super) fn build_encoding_channel( is_dummy, } => build_column_encoding(aesthetic, col, original_name, *is_dummy, true, ctx), AestheticValue::AnnotationColumn { name: col } => { - // Non-positional annotation columns use identity scale + // Material annotation columns use identity scale build_column_encoding(aesthetic, col, &None, false, false, ctx) } AestheticValue::Literal(lit) => build_literal_encoding(aesthetic, lit), @@ -822,7 +822,7 @@ fn build_column_encoding( ) -> Result { let aesthetic_ctx = ctx.spec.get_aesthetic_context(); let primary = aesthetic_ctx - .primary_internal_positional(aesthetic) + .primary_internal_position(aesthetic) .unwrap_or(aesthetic); let mut identity_scale = !is_scaled; @@ -844,8 +844,8 @@ fn build_column_encoding( .map(|st| st.scale_type_kind() == ScaleTypeKind::Binned) .unwrap_or(false); - // Binned legend = binned + non-positional (needs threshold scale) - let is_binned_legend = is_binned && !is_positional_aesthetic(aesthetic); + // Binned legend = binned + material (needs threshold scale) + let is_binned_legend = is_binned && !is_position_aesthetic(aesthetic); // Build base encoding let mut encoding = json!({ @@ -853,7 +853,7 @@ fn build_column_encoding( "type": field_type, }); - // bin: "binned" is only valid for positional channels in VL v6 + // bin: "binned" is only valid for position channels in VL v6 if is_binned && !is_binned_legend { encoding["bin"] = json!("binned"); } @@ -955,27 +955,27 @@ fn build_literal_encoding(aesthetic: &str, lit: &ParameterValue) -> Result String { - // For internal positional aesthetics, map directly to Vega-Lite channel names + // For internal position aesthetics, map directly to Vega-Lite channel names // based on coord type (ignoring user-facing names) - if let Some(vl_channel) = map_positional_to_vegalite(aesthetic, coord_kind) { + if let Some(vl_channel) = map_position_to_vegalite(aesthetic, coord_kind) { return vl_channel; } - // Non-positional aesthetics: apply Vega-Lite specific mappings + // Material aesthetics: apply Vega-Lite specific mappings match aesthetic { // Line aesthetics "linetype" => "strokeDash".to_string(), @@ -990,19 +990,19 @@ pub(super) fn map_aesthetic_name( } } -/// Map internal positional aesthetic to Vega-Lite channel name based on coord type. +/// Map internal position aesthetic to Vega-Lite channel name based on coord type. /// -/// Returns `Some(channel_name)` for internal positional aesthetics (pos1, pos2, etc.), -/// or `None` for non-positional aesthetics. -fn map_positional_to_vegalite(aesthetic: &str, coord_kind: CoordKind) -> Option { +/// Returns `Some(channel_name)` for internal position aesthetics (pos1, pos2, etc.), +/// or `None` for material aesthetics. +fn map_position_to_vegalite(aesthetic: &str, coord_kind: CoordKind) -> Option { let (primary, secondary) = match coord_kind { CoordKind::Cartesian => ("x", "y"), CoordKind::Polar => ("radius", "theta"), }; - // Match internal positional aesthetic patterns + // Match internal position aesthetic patterns match aesthetic { - // Primary positional + // Primary position "pos1" => Some(primary.to_string()), "pos2" => Some(secondary.to_string()), // End variants (Vega-Lite uses x2/y2/theta2/radius2) diff --git a/src/writer/vegalite/mod.rs b/src/writer/vegalite/mod.rs index dcff0244..f5ab115d 100644 --- a/src/writer/vegalite/mod.rs +++ b/src/writer/vegalite/mod.rs @@ -131,16 +131,16 @@ fn prepare_layer_data( /// - Creates layer spec with mark type /// - Builds transform array with source filter /// - Builds encoding channels for each aesthetic mapping -/// - Handles binned positional aesthetics (x2/y2 channels) +/// - Handles binned position aesthetics (x2/y2 channels) /// - Adds aesthetic parameters from SETTING as literal encodings /// - Adds detail encoding for partition_by columns /// - Applies geom-specific modifications via renderer /// - Finalizes layers (may expand composite geoms into multiple layers) /// -/// The `free_scales` array indicates which positional aesthetics have independent scales +/// The `free_scales` array indicates which position aesthetics have independent scales /// per facet panel. When a position is free, explicit domains should not be set. /// -/// The `coord_kind` determines how internal positional aesthetics are mapped to +/// The `coord_kind` determines how internal position aesthetics are mapped to /// Vega-Lite encoding channel names. fn build_layers( spec: &Plot, @@ -205,15 +205,15 @@ fn build_layers( /// Handles: /// - Tracking titled aesthetic families (one title per family) /// - Building encoding channels for each aesthetic mapping -/// - Binned positional aesthetics (x2/y2 channels for bin width) +/// - Binned position aesthetics (x2/y2 channels for bin width) /// - Aesthetic parameters from SETTING as literal encodings /// - Detail encoding for partition_by columns /// - Geom-specific encoding modifications via renderer /// -/// The `free_scales` array indicates which positional aesthetics have independent scales +/// The `free_scales` array indicates which position aesthetics have independent scales /// per facet panel. When a position is free, explicit domains should not be set. /// -/// The `coord_kind` determines how internal positional aesthetics (pos1, pos2) are +/// The `coord_kind` determines how internal position aesthetics (pos1, pos2) are /// mapped to Vega-Lite encoding channel names (x/y for cartesian, theta/radius for polar). fn build_layer_encoding( layer: &crate::plot::Layer, @@ -238,7 +238,7 @@ fn build_layer_encoding( .keys() .filter(|a| { aesthetic_ctx - .primary_internal_positional(a) + .primary_internal_position(a) .map(|p| p == a.as_str()) .unwrap_or(false) }) @@ -274,7 +274,7 @@ fn build_layer_encoding( channel_name = "fillOpacity".to_string(); } - // Secondary positional channels (x2, y2, theta2, radius2) only support + // Secondary position channels (x2, y2, theta2, radius2) only support // field/datum/value in Vega-Lite — not type, scale, axis, or title if matches!(channel_name.as_str(), "x2" | "y2" | "theta2" | "radius2") { let secondary_encoding = match value { @@ -289,7 +289,7 @@ fn build_layer_encoding( let channel_encoding = build_encoding_channel(aesthetic, value, &mut enc_ctx)?; encoding.insert(channel_name, channel_encoding); - // For binned positional aesthetics (pos1, pos2), add end channel with bin_end column + // For binned position aesthetics (pos1, pos2), add end channel with bin_end column // This enables proper bin width rendering in Vega-Lite (maps to x2/y2 channels) if aesthetic_ctx.is_primary_internal(aesthetic) && is_binned_aesthetic(aesthetic, spec) { if let AestheticValue::Column { name: col, .. } = value { @@ -1343,7 +1343,7 @@ mod tests { // Test with cartesian coord kind let ctx = AestheticContext::from_static(&["x", "y"], &[]); - // Internal positional names should map to Vega-Lite channel names based on coord kind + // Internal position names should map to Vega-Lite channel names based on coord kind assert_eq!(map_aesthetic_name("pos1", &ctx, CoordKind::Cartesian), "x"); assert_eq!(map_aesthetic_name("pos2", &ctx, CoordKind::Cartesian), "y"); assert_eq!( @@ -1355,7 +1355,7 @@ mod tests { "y2" ); - // Non-positional aesthetics pass through directly + // Material aesthetics pass through directly assert_eq!( map_aesthetic_name("color", &ctx, CoordKind::Cartesian), "color" @@ -1399,7 +1399,7 @@ mod tests { "size" ); - // Test with polar coord kind - internal positional maps to radius/theta + // Test with polar coord kind - internal position maps to radius/theta // regardless of the context's user-facing names let polar_ctx = AestheticContext::from_static(&["radius", "theta"], &[]); assert_eq!( @@ -1419,7 +1419,7 @@ mod tests { "theta2" ); - // Even with custom positional names (e.g., PROJECT y, x TO polar), + // Even with custom position names (e.g., PROJECT y, x TO polar), // internal pos1/pos2 should still map to radius/theta for Vega-Lite let custom_ctx = AestheticContext::from_static(&["y", "x"], &[]); assert_eq!( diff --git a/tree-sitter-ggsql/grammar.js b/tree-sitter-ggsql/grammar.js index f5f01d34..972f019e 100644 --- a/tree-sitter-ggsql/grammar.js +++ b/tree-sitter-ggsql/grammar.js @@ -210,7 +210,7 @@ module.exports = grammar({ cast_expression: $ => prec(3, seq( choice(caseInsensitive('CAST'), caseInsensitive('TRY_CAST')), '(', - $.positional_arg, + $.position_arg, caseInsensitive('AS'), $.type_name, ')' @@ -301,25 +301,25 @@ module.exports = grammar({ repeat(seq(',', $.function_arg)) ), - // Function argument: positional or named + // Function argument: position or named function_arg: $ => choice( $.named_arg, - $.positional_arg + $.position_arg ), named_arg: $ => seq( field('name', $.identifier), choice(':=', '=>'), - field('value', $.positional_arg) + field('value', $.position_arg) ), - // Positional argument: supports complex expressions including: + // Position argument: supports complex expressions including: // - Simple values: identifier, number, string, * // - Qualified names: table.column // - Nested function calls: ROUND(AVG(x), 2) // - Arithmetic expressions: quantity * price // - Type casts: value::type - positional_arg: $ => prec.left(choice( + position_arg: $ => prec.left(choice( // Simple values $.qualified_name, // Handles both simple identifiers and table.column $.number, @@ -332,9 +332,9 @@ module.exports = grammar({ // Scalar subquery: (SELECT ...) or (WITH ... SELECT ...) $.scalar_subquery, // Arithmetic/comparison expression (binary operators) - seq($.positional_arg, choice('+', '-', '*', '/', '%', '||', '::', '<', '>', '<=', '>=', '=', '!=', '<>'), $.positional_arg), + seq($.position_arg, choice('+', '-', '*', '/', '%', '||', '::', '<', '>', '<=', '>=', '=', '!=', '<>'), $.position_arg), // Parenthesized expression - seq('(', $.positional_arg, ')') + seq('(', $.position_arg, ')') )), // Namespaced identifier: matches "namespace:name" pattern @@ -804,7 +804,7 @@ module.exports = grammar({ optional(seq(caseInsensitive('SETTING'), $.project_properties)) ), - // Optional list of positional aesthetic names for PROJECT clause + // Optional list of position aesthetic names for PROJECT clause project_aesthetics: $ => seq( $.identifier, repeat(seq(',', $.identifier)) diff --git a/tree-sitter-ggsql/test/corpus/basic.txt b/tree-sitter-ggsql/test/corpus/basic.txt index 70eec146..1b79c1ab 100644 --- a/tree-sitter-ggsql/test/corpus/basic.txt +++ b/tree-sitter-ggsql/test/corpus/basic.txt @@ -1264,7 +1264,7 @@ DRAW line MAPPING x AS x, total AS y (bare_identifier)) (function_args (function_arg - (positional_arg + (position_arg (qualified_name (identifier (bare_identifier)))))) @@ -1388,13 +1388,13 @@ DRAW point MAPPING interval AS x (named_arg name: (identifier (bare_identifier)) - value: (positional_arg + value: (position_arg (number)))) (function_arg (named_arg name: (identifier (bare_identifier)) - value: (positional_arg + value: (position_arg (number)))))) (identifier (bare_identifier)) @@ -2403,7 +2403,7 @@ SELECT CAST(sale_date AS DATE) as period FROM sales VISUALISE period AS x DRAW p (select_statement (select_body (cast_expression - (positional_arg + (position_arg (qualified_name (identifier (bare_identifier)))) @@ -2452,9 +2452,9 @@ SELECT SUM(TRY_CAST(price AS INTEGER)) as total FROM data VISUALISE DRAW bar MAP (bare_identifier)) (function_args (function_arg - (positional_arg + (position_arg (cast_expression - (positional_arg + (position_arg (qualified_name (identifier (bare_identifier)))) @@ -2505,18 +2505,18 @@ DRAW point MAPPING x AS x (bare_identifier)) (function_args (function_arg - (positional_arg + (position_arg (function_call (identifier (bare_identifier)) (function_args (function_arg - (positional_arg + (position_arg (qualified_name (identifier (bare_identifier))))))))) (function_arg - (positional_arg + (position_arg (scalar_subquery (select_statement (select_body @@ -2525,7 +2525,7 @@ DRAW point MAPPING x AS x (bare_identifier)) (function_args (function_arg - (positional_arg + (position_arg (qualified_name (identifier (bare_identifier)))))))