Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
432694b
Centralize aesthetic metadata in unified DefaultAesthetics structure
teunbrand Feb 19, 2026
32f2b84
Apply default aesthetic values from geom definitions in writer
teunbrand Feb 19, 2026
7d508e3
Remove hardcoded aesthetic defaults from all renderers
teunbrand Feb 19, 2026
2e1f0bb
Add comprehensive tests for default aesthetic system
teunbrand Feb 19, 2026
b1cc361
Translate linetype strings to strokeDash arrays in writer.
teunbrand Feb 19, 2026
525b571
Adjust some aesthetics to taste
teunbrand Feb 19, 2026
beb28bd
Refactor SETTING and defaults processing in Vega-Lite writer
teunbrand Feb 19, 2026
b1d7999
Centralize SETTING/defaults precedence logic in Layer
teunbrand Feb 19, 2026
7dceccd
remove dangling test & cargo fmt
teunbrand Feb 19, 2026
c26407b
It turns out the unreachable statement did in fact become reachable
teunbrand Feb 20, 2026
6482335
review area/density opacity
teunbrand Feb 24, 2026
5803121
Move aesthetic resolution from writer to execution phase
teunbrand Feb 24, 2026
8ca6d52
Merge resolved_aesthetics into mappings for single source of truth
teunbrand Feb 24, 2026
84632ed
Merge branch 'main' into default_aesthetics
teunbrand Feb 24, 2026
05ee9c0
Fix broken test expectation
teunbrand Feb 24, 2026
f5cbb18
cargo fmt
teunbrand Feb 24, 2026
5724b50
set opaque default fill for area/density
teunbrand Feb 24, 2026
91613ee
retarget opacity to fillOpacity when fill is supported
teunbrand Feb 24, 2026
b8b4ae8
Don't bake in opacity into the fill colour
teunbrand Feb 24, 2026
f5a2306
update expectations based on new defaults
teunbrand Feb 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions doc/syntax/layer/density.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,14 @@ A typical KDE computation with different groups:

```{ggsql}
VISUALISE bill_dep AS x, species AS colour FROM ggsql:penguins
DRAW density SETTING opacity => 0.8
DRAW density
```

Changing the relative bandwidth through the `adjust` setting.

```{ggsql}
VISUALISE bill_dep AS x, species AS colour FROM ggsql:penguins
DRAW density SETTING opacity => 0.8, adjust => 0.1
DRAW density SETTING adjust => 0.1
```

Stacking the different groups instead of overlaying them.
Expand All @@ -94,9 +94,7 @@ Using weighted estimates by mapping a column to the optional weight aesthetic. N

```{ggsql}
VISUALISE bill_dep AS x, species AS colour FROM ggsql:penguins
DRAW density
MAPPING body_mass AS weight
SETTING opacity => 0.8
DRAW density MAPPING body_mass AS weight
```

If you want to compare a histogram and a density layer, you can use the `intensity` computed variable to match the histogram scale.
Expand All @@ -114,8 +112,6 @@ Note the relative height of the groups.

```{ggsql}
VISUALISE bill_dep AS x, species AS colour FROM ggsql:penguins
DRAW density
REMAPPING intensity AS y
SETTING opacity => 0.8
DRAW density REMAPPING intensity AS y
```

2 changes: 0 additions & 2 deletions doc/syntax/layer/point.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ Create a classic scatterplot
VISUALISE FROM ggsql:penguins
DRAW point
MAPPING bill_len AS x, bill_dep AS y, species AS fill
SETTING size => 30
```

Map to size to create a bubble chart
Expand All @@ -52,6 +51,5 @@ Use filter to only plot a subset of the data
VISUALISE FROM ggsql:penguins
DRAW point
MAPPING bill_len AS x, bill_dep AS y, species AS fill
SETTING size => 30
FILTER sex = 'female'
```
31 changes: 16 additions & 15 deletions src/execute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ use crate::reader::DuckDBReader;
fn validate(layers: &[Layer], layer_schemas: &[Schema]) -> Result<()> {
for (idx, (layer, schema)) in layers.iter().zip(layer_schemas.iter()).enumerate() {
let schema_columns: HashSet<&str> = schema.iter().map(|c| c.name.as_str()).collect();
let supported = layer.geom.aesthetics().supported;
let supported = layer.geom.aesthetics().supported();

// Validate required aesthetics for this geom
layer
Expand Down Expand Up @@ -97,14 +97,10 @@ fn validate(layers: &[Layer], layer_schemas: &[Schema]) -> Result<()> {
}

// Validate remapping target aesthetics are supported by geom
// Target can be in supported OR hidden (hidden = valid REMAPPING targets but not MAPPING targets)
// REMAPPING can target any aesthetic (including Delayed ones from stat transforms)
let aesthetics_info = layer.geom.aesthetics();
for target_aesthetic in layer.remappings.aesthetics.keys() {
let is_supported = aesthetics_info
.supported
.contains(&target_aesthetic.as_str());
let is_hidden = aesthetics_info.hidden.contains(&target_aesthetic.as_str());
if !is_supported && !is_hidden {
if !aesthetics_info.contains(target_aesthetic) {
return Err(GgsqlError::ValidationError(format!(
"Layer {}: REMAPPING targets unsupported aesthetic '{}' for geom '{}'",
idx + 1,
Expand Down Expand Up @@ -157,7 +153,7 @@ fn validate(layers: &[Layer], layer_schemas: &[Schema]) -> Result<()> {
fn merge_global_mappings_into_layers(specs: &mut [Plot], layer_schemas: &[Schema]) {
for spec in specs {
for (layer, schema) in spec.layers.iter_mut().zip(layer_schemas.iter()) {
let supported = layer.geom.aesthetics().supported;
let supported = layer.geom.aesthetics().supported();
let schema_columns: HashSet<&str> = schema.iter().map(|c| c.name.as_str()).collect();

// 1. First merge explicit global aesthetics (layer overrides global)
Expand All @@ -180,14 +176,14 @@ fn merge_global_mappings_into_layers(specs: &mut [Plot], layer_schemas: &[Schema
// 2. Smart wildcard expansion: only expand to columns that exist in schema
let has_wildcard = layer.mappings.wildcard || spec.global_mappings.wildcard;
if has_wildcard {
for &aes in supported {
for aes in &supported {
// Only create mapping if column exists in the schema
if schema_columns.contains(aes) {
if schema_columns.contains(*aes) {
layer
.mappings
.aesthetics
.entry(crate::parser::builder::normalise_aes_name(aes))
.or_insert(AestheticValue::standard_column(aes));
.or_insert(AestheticValue::standard_column(*aes));
}
}
}
Expand Down Expand Up @@ -228,10 +224,10 @@ fn split_color_aesthetic(spec: &mut Plot) {
// 2. Split color mapping to fill/stroke in layers, then remove color
for layer in &mut spec.layers {
if let Some(color_value) = layer.mappings.aesthetics.get("color").cloned() {
let supported = layer.geom.aesthetics().supported;
let aesthetics = layer.geom.aesthetics();

for &aes in &["stroke", "fill"] {
if supported.contains(&aes) {
if aesthetics.is_supported(aes) {
layer
.mappings
.aesthetics
Expand All @@ -248,10 +244,10 @@ fn split_color_aesthetic(spec: &mut Plot) {
// 3. Split color parameter (SETTING) to fill/stroke in layers
for layer in &mut spec.layers {
if let Some(color_value) = layer.parameters.get("color").cloned() {
let supported = layer.geom.aesthetics().supported;
let aesthetics = layer.geom.aesthetics();

for &aes in &["stroke", "fill"] {
if supported.contains(&aes) {
if aesthetics.is_supported(aes) {
layer
.parameters
.entry(aes.to_string())
Expand Down Expand Up @@ -1127,6 +1123,11 @@ pub fn prepare_data_with_reader<R: Reader>(query: &str, reader: &R) -> Result<Pr
// Update layer mappings for all layers (even if data shared)
l.update_mappings_for_remappings();
}

// Resolve aesthetics (SETTING/defaults) after all mapping updates
// This ensures query literals have been converted to columns, and SETTING/defaults
// are added as new Literal entries that remain as constant values
l.resolve_aesthetics();
}

// Validate we have some data (every layer should have its own data)
Expand Down
23 changes: 11 additions & 12 deletions src/plot/layer/geom/abline.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! AbLine geom implementation

use super::{GeomAesthetics, GeomTrait, GeomType};
use super::{DefaultAesthetics, GeomTrait, GeomType};
use crate::plot::types::DefaultAestheticValue;

/// AbLine geom - lines with slope and intercept
#[derive(Debug, Clone, Copy)]
Expand All @@ -11,18 +12,16 @@ impl GeomTrait for AbLine {
GeomType::AbLine
}

fn aesthetics(&self) -> GeomAesthetics {
GeomAesthetics {
supported: &[
"slope",
"intercept",
"stroke",
"linetype",
"linewidth",
"opacity",
fn aesthetics(&self) -> DefaultAesthetics {
DefaultAesthetics {
defaults: &[
("slope", DefaultAestheticValue::Required),
("intercept", DefaultAestheticValue::Required),
("stroke", DefaultAestheticValue::String("black")),
("linewidth", DefaultAestheticValue::Number(1.0)),
("opacity", DefaultAestheticValue::Number(1.0)),
("linetype", DefaultAestheticValue::String("solid")),
],
required: &["slope", "intercept"],
hidden: &[],
}
}
}
Expand Down
26 changes: 12 additions & 14 deletions src/plot/layer/geom/area.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! Area geom implementation

use crate::plot::{DefaultParam, DefaultParamValue};
use crate::plot::{types::DefaultAestheticValue, DefaultParam, DefaultParamValue};

use super::{GeomAesthetics, GeomTrait, GeomType};
use super::{DefaultAesthetics, GeomTrait, GeomType};

/// Area geom - filled area charts
#[derive(Debug, Clone, Copy)]
Expand All @@ -13,19 +13,17 @@ impl GeomTrait for Area {
GeomType::Area
}

fn aesthetics(&self) -> GeomAesthetics {
GeomAesthetics {
supported: &[
"x",
"y",
"fill",
"stroke",
"opacity",
"linewidth",
// "linetype", // vegalite doesn't support strokeDash
fn aesthetics(&self) -> DefaultAesthetics {
DefaultAesthetics {
defaults: &[
("x", DefaultAestheticValue::Required),
("y", DefaultAestheticValue::Required),
("fill", DefaultAestheticValue::String("black")),
("stroke", DefaultAestheticValue::String("black")),
("opacity", DefaultAestheticValue::Number(0.8)),
("linewidth", DefaultAestheticValue::Number(1.0)),
("linetype", DefaultAestheticValue::String("solid")),
],
required: &["x", "y"],
hidden: &[],
}
}

Expand Down
28 changes: 14 additions & 14 deletions src/plot/layer/geom/arrow.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Arrow geom implementation

use super::{GeomAesthetics, GeomTrait, GeomType};
use super::{DefaultAesthetics, GeomTrait, GeomType};
use crate::plot::types::DefaultAestheticValue;

/// Arrow geom - line segments with arrowheads
#[derive(Debug, Clone, Copy)]
Expand All @@ -11,20 +12,19 @@ impl GeomTrait for Arrow {
GeomType::Arrow
}

fn aesthetics(&self) -> GeomAesthetics {
GeomAesthetics {
supported: &[
"x",
"y",
"xend",
"yend",
"stroke",
"linetype",
"linewidth",
"opacity",
fn aesthetics(&self) -> DefaultAesthetics {
DefaultAesthetics {
defaults: &[
("x", DefaultAestheticValue::Required),
("y", DefaultAestheticValue::Required),
("xend", DefaultAestheticValue::Required),
("yend", DefaultAestheticValue::Required),
("stroke", DefaultAestheticValue::String("black")),
("linewidth", DefaultAestheticValue::Number(1.0)),
("opacity", DefaultAestheticValue::Number(1.0)),
("linetype", DefaultAestheticValue::String("solid")),
("fill", DefaultAestheticValue::Null),
],
required: &["x", "y", "xend", "yend"],
hidden: &[],
}
}
}
Expand Down
20 changes: 14 additions & 6 deletions src/plot/layer/geom/bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::collections::HashMap;
use std::collections::HashSet;

use super::types::get_column_name;
use super::{DefaultParam, DefaultParamValue, GeomAesthetics, GeomTrait, GeomType, StatResult};
use super::{DefaultAesthetics, DefaultParam, DefaultParamValue, GeomTrait, GeomType, StatResult};
use crate::naming;
use crate::plot::types::{DefaultAestheticValue, ParameterValue};
use crate::{DataFrame, GgsqlError, Mappings, Result};
Expand All @@ -20,15 +20,23 @@ impl GeomTrait for Bar {
GeomType::Bar
}

fn aesthetics(&self) -> GeomAesthetics {
GeomAesthetics {
fn aesthetics(&self) -> DefaultAesthetics {
DefaultAesthetics {
// Bar supports optional x and y - stat decides aggregation
// If x is missing: single bar showing total
// If y is missing: stat computes COUNT or SUM(weight)
// weight: optional, if mapped uses SUM(weight) instead of COUNT(*)
supported: &["x", "y", "weight", "fill", "stroke", "width", "opacity"],
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: width was removed. It is already a parameter and width cannot be a vegalite encoding (vegalite uses size when mapped to data).

required: &[],
hidden: &[],
// width is a parameter, not an aesthetic.
// if we ever want to make 'width' an aesthetic, we'd probably need to
// translate it to 'size'.
defaults: &[
("x", DefaultAestheticValue::Null), // Optional - stat may provide
("y", DefaultAestheticValue::Null), // Optional - stat may compute
("weight", DefaultAestheticValue::Null),
("fill", DefaultAestheticValue::String("black")),
("stroke", DefaultAestheticValue::String("black")),
("opacity", DefaultAestheticValue::Number(0.8)),
],
}
}

Expand Down
48 changes: 24 additions & 24 deletions src/plot/layer/geom/boxplot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use std::collections::HashMap;

use super::{GeomAesthetics, GeomTrait, GeomType};
use super::{DefaultAesthetics, GeomTrait, GeomType};
use crate::{
naming,
plot::{
Expand All @@ -21,22 +21,22 @@ impl GeomTrait for Boxplot {
GeomType::Boxplot
}

fn aesthetics(&self) -> GeomAesthetics {
GeomAesthetics {
supported: &[
"x",
"y",
"fill",
"stroke",
"opacity",
"linetype",
"linewidth",
"size",
"shape",
fn aesthetics(&self) -> DefaultAesthetics {
DefaultAesthetics {
defaults: &[
("x", DefaultAestheticValue::Required),
("y", DefaultAestheticValue::Required),
("stroke", DefaultAestheticValue::String("black")),
("fill", DefaultAestheticValue::String("white")),
("linewidth", DefaultAestheticValue::Number(1.0)),
("opacity", DefaultAestheticValue::Number(0.8)),
("linetype", DefaultAestheticValue::String("solid")),
("size", DefaultAestheticValue::Number(3.0)),
("shape", DefaultAestheticValue::String("circle")),
// Internal aesthetics produced by stat transform
("type", DefaultAestheticValue::Delayed),
("yend", DefaultAestheticValue::Delayed),
],
required: &["x", "y"],
// Internal aesthetics produced by stat transform
hidden: &["type", "y", "yend"],
}
}

Expand Down Expand Up @@ -547,21 +547,21 @@ mod tests {
let boxplot = Boxplot;
let aes = boxplot.aesthetics();

assert!(aes.required.contains(&"x"));
assert!(aes.required.contains(&"y"));
assert_eq!(aes.required.len(), 2);
assert!(aes.is_required("x"));
assert!(aes.is_required("y"));
assert_eq!(aes.required().len(), 2);
}

#[test]
fn test_boxplot_aesthetics_supported() {
let boxplot = Boxplot;
let aes = boxplot.aesthetics();

assert!(aes.supported.contains(&"x"));
assert!(aes.supported.contains(&"y"));
assert!(aes.supported.contains(&"fill"));
assert!(aes.supported.contains(&"stroke"));
assert!(aes.supported.contains(&"opacity"));
assert!(aes.is_supported("x"));
assert!(aes.is_supported("y"));
assert!(aes.is_supported("fill"));
assert!(aes.is_supported("stroke"));
assert!(aes.is_supported("opacity"));
}

#[test]
Expand Down
Loading