Skip to content

Commit 85e50db

Browse files
teunbrandclaude
andauthored
Polish validation (#311)
* Improve errorbar validation * Move segment validation from writer to geom layer - Mark pos1end as Required in segment geom - Leverage bidirectional validation to enforce "at least one of xend or yend" - Remove validation logic from SegmentRenderer, keep only transformation - Add 4 validation tests in segment.rs - Simplify SegmentRenderer to use entry().or_insert() pattern The bidirectional validation checks for either: - Identity: pos1, pos2, pos1end (x, y, xend) - Flipped: pos1, pos2, pos2end (y, x, yend) This effectively enforces "at least one endpoint required" without custom logic. * Add validate_aesthetics hook and move rule validation to geom layer - Add GeomTrait::validate_aesthetics() method for custom validation - Mark pos1 as Required in rule geom - Implement validate_aesthetics() for Rule to enforce XOR (exactly one of x or y) - Call geom.validate_aesthetics() from Layer::validate_mapping() - Remove validation logic from RuleRenderer, keep only diagonal rendering - Add 3 validation tests in rule.rs Architecture improvement: - Geoms can now implement custom validation logic beyond Required/Null - Rule's XOR constraint (pos1 XOR pos2) enforced at geom layer - Writer only handles rendering, not validation * Centralize aesthetic-to-encoding channel mapping in writer Move pos*min/pos*max to x/y/x2/y2 transformation from individual renderers to the central map_position_to_vegalite() function. This eliminates ad-hoc transformations scattered across ErrorBarRenderer, RibbonRenderer, and RectRenderer. Convention: min → primary channel (x/y), max → secondary channel (x2/y2) Changes: - Update map_position_to_vegalite() to map pos*min/pos*max directly to Vega-Lite encoding channels (x/y/x2/y2) - Remove entire RibbonRenderer (now uses DefaultRenderer) - Remove vestigial modify_encoding() from ErrorBarRenderer and RectRenderer - Remove 2 redundant tests (test_errorbar_encoding_transformation, test_rect_continuous_both_axes) - Update rect tests to reflect upstream transformation Result: 155 lines removed, cleaner separation between domain (aesthetics) and presentation (encoding channels). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * rename 'rect' to 'tile' (fix #264) * appease 1.95.0 clippy * Make GeomRenderers position-agnostic via RenderContext channels Add coord_kind to RenderContext with a pre-computed PositionChannels tuple (pos1, pos1_end, pos1_offset, pos2, pos2_end, pos2_offset) that resolves to Vega-Lite channel names based on coordinate system. Replaces hardcoded "x"/"y"/"x2"/"y2" in 8 renderers (Bar, Path, Segment, Rule, Tile, Violin, ErrorBar, Boxplot) so they work with both Cartesian and polar coordinates. Also moves RenderContext from layer.rs to encoding.rs where it belongs, and adds &RenderContext to the finalize() trait method. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix rect -> tile leftover bug * Fix horizontal errorbar support and global mapping merge for bidirectional geoms Add ErrorBar to geom_has_implicit_orientation so the orientation system correctly detects and flips horizontal errorbars. Fix global mapping merge to accept flipped position counterparts (e.g., pos2 into a geom declaring pos1) using flip_position, enabling global aesthetics like `species AS y` to reach bidirectional geoms. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * cargo fmt * sneak in small correction * fix inconsistency --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 1307388 commit 85e50db

23 files changed

Lines changed: 522 additions & 598 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ pub struct Layer {
332332

333333
pub enum Geom {
334334
// Basic geoms
335-
Point, Line, Path, Bar, Col, Area, Rect, Polygon, Ribbon,
335+
Point, Line, Path, Bar, Col, Area, Tile, Polygon, Ribbon,
336336
// Statistical geoms
337337
Histogram, Density, Smooth, Boxplot, Violin,
338338
// Annotation geoms
@@ -1079,7 +1079,7 @@ All clauses (MAPPING, SETTING, PARTITION BY, FILTER) are optional.
10791079

10801080
**Geom Types**:
10811081

1082-
- **Basic**: `point`, `line`, `path`, `bar`, `col`, `area`, `rect`, `polygon`, `ribbon`
1082+
- **Basic**: `point`, `line`, `path`, `bar`, `col`, `area`, `tile`, `polygon`, `ribbon`
10831083
- **Statistical**: `histogram`, `density`, `smooth`, `boxplot`, `violin`
10841084
- **Annotation**: `text`, `label`, `segment`, `arrow`, `rule`, `errorbar`
10851085

doc/gallery/examples/heatmap.qmd

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: "Heatmap"
33
description: "Arranging tiles on a grid"
4-
image: thumbnails/violin-plot.svg
4+
image: thumbnails/heatmap.svg
55
categories: [basic, heatmap]
66
order: 3
77
---
@@ -14,31 +14,31 @@ It works best with discrete or ordinal arrangements.
1414

1515
```{ggsql}
1616
VISUALISE Day AS x, Month AS y, Temp AS fill FROM ggsql:airquality
17-
DRAW rect
17+
DRAW tile
1818
```
1919

2020
## Explanation
2121

2222
* The `VISUALISE ... FROM ggsql:airquality` loads the built-in air quality dataset.
2323
* `Day AS x, Month AS y` defines a 2D grid 'map'. The default width and height of each cell is 1. Because these variables are contiguous whole numbers, this creates a grid.
2424
* `Temp AS fill` declares the 'heat' variable to display as colour intensity.
25-
* `DRAW rect` gives instructions to draw a rectangle layer.
25+
* `DRAW tile` gives instructions to draw a tile layer.
2626

2727
## Variations
2828

2929
As a stylistic choice, you can set the cells to be opaque without borders.
3030

3131
```{ggsql}
3232
VISUALISE Month AS y, Day AS x, Temp AS fill FROM ggsql:airquality
33-
DRAW rect
33+
DRAW tile
3434
SETTING stroke => null, opacity => 1
3535
```
3636

3737
You can change the color by adapting the scale.
3838

3939
```{ggsql}
4040
VISUALISE Month AS y, Day AS x, Temp AS fill FROM ggsql:airquality
41-
DRAW rect
41+
DRAW tile
4242
SCALE fill TO magma
4343
SETTING reverse => true
4444
```
@@ -51,6 +51,6 @@ SELECT *,
5151
FROM ggsql:airquality
5252
5353
VISUALISE Month AS y, Day AS x, centered AS fill
54-
DRAW rect
54+
DRAW tile
5555
SCALE fill FROM (-20, 20) TO vik
5656
```

doc/ggsql.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
<item>path</item>
129129
<item>bar</item>
130130
<item>area</item>
131-
<item>rect</item>
131+
<item>tile</item>
132132
<item>polygon</item>
133133
<item>ribbon</item>
134134
<item>histogram</item>

doc/syntax/layer/type/errorbar.qmd

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,13 @@ DRAW errorbar
7373
MAPPING low AS ymax, high AS ymin
7474
SETTING width => null
7575
```
76+
77+
A horizontal errorbar can be rendered by swapping the `x` and `y` directions.
78+
79+
```{ggsql}
80+
VISUALISE species AS y FROM penguin_summary
81+
DRAW errorbar
82+
MAPPING low AS xmax, high AS xmin
83+
DRAW point
84+
MAPPING mean AS x
85+
```
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
---
2-
title: "Rectangle"
2+
title: "Tile"
33
---
44

55
> Layers are declared with the [`DRAW` clause](../../clause/draw.qmd). Read the documentation for this clause for a thorough description of how to use it.
66
7-
Rectangles can be used to draw heatmaps or indicate ranges.
7+
Tiles can be used to draw rectangles in heatmaps or indicate ranges.
88

99
## Aesthetics
10-
The following aesthetics are recognised by the rectangle layer.
10+
The following aesthetics are recognised by the tile layer.
1111

1212
### Required
1313

@@ -43,22 +43,22 @@ When the primary aesthetics are continuous, primary data is reparameterised to {
4343
When the secondary aesthetics are continuous, secondary data is reparameterised to {start, end}, e.g. `ymin` and `ymax`.
4444

4545
## Orientation
46-
The rectangle layer has no orientation. The axes are treated symmetrically.
46+
The tile layer has no orientation. The axes are treated symmetrically.
4747

4848
## Examples
4949

5050
Just using `x` and `y`. Note that `width` and `height` are set to 1.
5151

5252
```{ggsql}
5353
VISUALISE Day AS x, Month AS y, Temp AS colour FROM ggsql:airquality
54-
DRAW rect
54+
DRAW tile
5555
```
5656

5757
Customising `width` and `height` with either the `MAPPING` or `SETTING` clauses.
5858

5959
```{ggsql}
6060
VISUALISE Day AS x, Month AS y, Temp AS colour FROM ggsql:airquality
61-
DRAW rect
61+
DRAW tile
6262
MAPPING 0.5 AS width
6363
SETTING height => 0.8
6464
```
@@ -72,7 +72,7 @@ SELECT
7272
FROM ggsql:airquality
7373
7474
VISUALISE Day AS x, Month AS y, Temp AS colour
75-
DRAW rect
75+
DRAW tile
7676
MAPPING norm_temp AS width, norm_temp AS height
7777
```
7878

@@ -88,14 +88,14 @@ FROM ggsql:airquality
8888
GROUP BY Week
8989
9090
VISUALISE start AS xmin, end AS xmax, min AS ymin, max AS ymax
91-
DRAW rect
91+
DRAW tile
9292
```
9393

94-
Using a rectangle as an annotation. Note we're using the `PLACE` clause here instead of `DRAW` because we're not mapping from data.
94+
Using a tile as an annotation. Note we're using the `PLACE` clause here instead of `DRAW` because we're not mapping from data.
9595

9696
```{ggsql}
9797
VISUALISE FROM ggsql:airquality
98-
PLACE rect
98+
PLACE tile
9999
SETTING
100100
xmin => '1973-06-01',
101101
xmax => '1973-06-30',

ggsql-vscode/syntaxes/ggsql.tmLanguage.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@
334334
"patterns": [
335335
{
336336
"name": "support.type.geom.ggsql",
337-
"match": "\\b(point|line|path|bar|col|area|rect|polygon|ribbon|histogram|density|smooth|boxplot|violin|text|label|segment|arrow|rule|errorbar)\\b"
337+
"match": "\\b(point|line|path|bar|col|area|tile|polygon|ribbon|histogram|density|smooth|boxplot|violin|text|label|segment|arrow|rule|errorbar)\\b"
338338
},
339339
{ "include": "#common-clause-patterns" }
340340
]

ggsql-wasm/demo/src/quarto/links.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const GEOM_LINKS: Record<string, DocLink> = {
3636
path: { url: "syntax/layer/type/path", label: "path layer" },
3737
bar: { url: "syntax/layer/type/bar", label: "bar layer" },
3838
area: { url: "syntax/layer/type/area", label: "area layer" },
39-
rect: { url: "syntax/layer/type/rect", label: "rect layer" },
39+
tile: { url: "syntax/layer/type/tile", label: "tile layer" },
4040
polygon: { url: "syntax/layer/type/polygon", label: "polygon layer" },
4141
ribbon: { url: "syntax/layer/type/ribbon", label: "ribbon layer" },
4242
histogram: { url: "syntax/layer/type/histogram", label: "histogram layer" },
@@ -144,7 +144,7 @@ const CLAUSE_RE =
144144
/\b(VISUALISE|VISUALIZE|DRAW|PLACE|SCALE|FACET|PROJECT|LABEL)\b/gi;
145145

146146
const GEOM_RE =
147-
/\b(?:DRAW|PLACE)\s+(point|line|path|bar|area|rect|polygon|ribbon|histogram|density|smooth|boxplot|violin|text|segment|rule|linear|errorbar)\b/gi;
147+
/\b(?:DRAW|PLACE)\s+(point|line|path|bar|area|tile|polygon|ribbon|histogram|density|smooth|boxplot|violin|text|segment|rule|linear|errorbar)\b/gi;
148148

149149
const COORD_RE = /\bTO\s+(cartesian|polar)\b/gi;
150150

src/execute/layer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,7 @@ where
595595
.or_else(|| {
596596
// For variant position aesthetics (e.g., pos1min, pos2max),
597597
// fall back to the primary aesthetic's original name (pos1, pos2).
598-
// This ensures rect's expanded min/max aesthetics inherit the
598+
// This ensures tile's expanded min/max aesthetics inherit the
599599
// original column name from the user's x/y mapping.
600600
aesthetic::parse_position(aesthetic).and_then(|(slot, suffix)| {
601601
if !suffix.is_empty() {

src/execute/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,16 @@ fn merge_global_mappings_into_layers(specs: &mut [Plot], layer_schemas: &[Schema
183183
// Note: Use all_names (not supported) so that Delayed aesthetics like
184184
// pos2 on histogram can be targeted by explicit global mappings, matching
185185
// the behavior of layer-level MAPPING
186+
// Note: Also accept flipped position counterparts so bidirectional geoms
187+
// (e.g., errorbar: pos1+pos2min+pos2max or pos2+pos1min+pos1max) can
188+
// receive globals from either orientation.
186189
for (aesthetic, value) in &spec.global_mappings.aesthetics {
187190
let is_facet_aesthetic = crate::plot::scale::is_facet_aesthetic(aesthetic.as_str());
188-
if all_names.contains(&aesthetic.as_str()) || is_facet_aesthetic {
191+
let flipped = aesthetic_ctx.flip_position(aesthetic);
192+
if all_names.contains(&aesthetic.as_str())
193+
|| all_names.contains(&flipped.as_str())
194+
|| is_facet_aesthetic
195+
{
189196
layer
190197
.mappings
191198
.aesthetics

src/parser/builder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,7 @@ fn parse_geom_type(text: &str) -> Result<Geom> {
623623
"path" => Ok(Geom::path()),
624624
"bar" => Ok(Geom::bar()),
625625
"area" => Ok(Geom::area()),
626-
"rect" => Ok(Geom::rect()),
626+
"tile" => Ok(Geom::tile()),
627627
"polygon" => Ok(Geom::polygon()),
628628
"ribbon" => Ok(Geom::ribbon()),
629629
"histogram" => Ok(Geom::histogram()),

0 commit comments

Comments
 (0)