Skip to content

Commit f5cff40

Browse files
teunbrandclaude
andauthored
Rectangles (#168)
* add resolution helper * Add rect geom with flexible parameter specification Implements a new rect geom that supports flexible rectangle specification: - X-direction: any 2 of {x (center), width, xmin, xmax} - Y-direction: any 2 of {y (center), height, ymin, ymax} Key features: - Stat-based parameter consolidation via SQL generation - Automatic discrete vs continuous scale detection using Schema - Validates exactly 2 params per direction - Generates appropriate SQL for all 6 parameter combinations per axis - Returns different stat columns based on scale type: - Continuous: pos1min, pos1max, pos2min, pos2max - Discrete: pos1, pos2 (for band-based rendering) Implementation follows existing patterns from histogram and bar geoms. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Add rect to grammar and parser - Add 'rect' to tree-sitter grammar geom_type rule - Add "rect" case to parser builder geom type mapping Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Fix rect geom: width/height as aesthetics only Changes: - Remove width/height from default_params() (use trait default) - Treat width/height as aesthetics (columns or literals), not parameters - RectRenderer handles x and y directions independently - For discrete scales: extract literal width/height from encoding or default to 0.9 - Error if width/height are mapped to variable columns on discrete scales - Support mixed continuous/discrete (e.g., continuous x + discrete y) - Extract band size logic into helper function with early returns - Early return from modify_spec when both directions are continuous Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: enable scale training for width/height in rect geom Key changes: - Add width/height to gets_default_scale() so they get proper scales with domains instead of Identity scales (which have scale: null) - Simplify rect.rs stat transform using get_column_name() directly - Merge discrete checking blocks to build SELECT and stat_columns together - Refactor RectRenderer to avoid Result<Option<>> anti-pattern - Flatten nesting with early exits and error closure for DRY This fixes discrete rect layers with literal width/height (e.g., 0.7 AS width) by ensuring scales are trained and domains are available for constant detection. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * test: add comprehensive tests for rect geom Add 10 parameterized tests covering: - All 6 x-direction parameter combinations (continuous) - All 6 y-direction parameter combinations (continuous) - Discrete scales with width/height - Validation errors (param count, discrete+min/max) - Group by filtering for width/height Tests use a grid/loop approach to systematically verify all rect parameter combinations and error cases. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * tests for the rectrenderer * fix(rect): preserve non-positional aesthetics in stat transform The rect stat transform was only including group_by columns in its SELECT list, causing non-positional aesthetics like fill and color to be dropped unless they were literal values. Now uses the schema to determine which columns to pass through: - Defines consumed aesthetics once (positional params that get transformed) - Iterates through schema and includes all non-consumed columns - Eliminates duplicate "consumed columns" logic Also increases default rect opacity from 0.5 to 0.8 for better visibility. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Revert "add resolution helper" This reverts commit a0c7092. * delete tile layer * feat: default width/height to 1.0 for rect geom Allow rect to work with just x (or y) specified, defaulting width (or height) to 1.0 for both discrete and continuous scales. Changes: - Split position expression generation into discrete and continuous variants - generate_discrete_position_expressions: returns (center, size) with size defaulting to "1.0" when not provided - generate_continuous_position_expressions: returns (min_expr, max_expr) with 7 valid parameter combinations including center-only - Discrete scales: center-only specification defaults to 1.0 bandwidth - Continuous scales: center-only specification defaults to width=1.0 (xmin = x - 0.5, xmax = x + 0.5) - Improved variable naming: x_expr_1/x_expr_2 instead of x_expr_min/x_expr_max to reflect dual usage (center/size for discrete, min/max for continuous) - Updated tests to verify default behavior All 18 rect tests passing. * feat: support SETTING width/height for rect geom Add support for specifying width/height via SETTING clause in addition to MAPPING. Implements precedence: MAPPING > SETTING > default 1.0. Changes: - stat_rect now checks both aesthetics (MAPPING) and parameters (SETTING) for width/height values - Uses ParameterValue::to_string() to convert SETTING values to SQL literals - Precedence order ensures MAPPING columns take priority over SETTING literals, which take priority over default 1.0 - Stat transform happens before resolve_aesthetics(), so we check parameters directly rather than relying on aesthetic resolution Example usage: DRAW rect MAPPING x AS x, ymin AS ymin, ymax AS ymax SETTING width => 0.8 All 19 rect tests passing. * refactor: unify x/y direction logic in rect stat transform Extract duplicated x and y direction logic into a single process_direction helper function that handles both axes. Changes: - New process_direction() function processes a single direction - Takes only axis ("x" or "y"), derives all aesthetic names from it - Returns SELECT parts and stat column names - Handles both discrete and continuous cases - Determine stat_cols first, then format SELECT parts outside if-block to eliminate duplication of format! calls - stat_rect() now calls process_direction() twice (once for x, once for y) - Eliminates ~100 lines of duplicated logic Call sites simplified from: process_direction("pos1", "pos1min", "pos1max", "width", "width", "x", ...) to: process_direction("x", aesthetics, parameters, schema) Net reduction: 26 lines (114 deletions, 88 additions) All 19 rect tests passing. * add docs * cargo fmt * add position parameter * update docs * simplify discrete variable widths * remove accidentally committed file * cargo fmt --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 63492a8 commit f5cff40

15 files changed

Lines changed: 1343 additions & 68 deletions

File tree

CLAUDE.md

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

331331
pub enum Geom {
332332
// Basic geoms
333-
Point, Line, Path, Bar, Col, Area, Tile, Polygon, Ribbon,
333+
Point, Line, Path, Bar, Col, Area, Rect, Polygon, Ribbon,
334334
// Statistical geoms
335335
Histogram, Density, Smooth, Boxplot, Violin,
336336
// Annotation geoms
@@ -1200,7 +1200,7 @@ All clauses (MAPPING, SETTING, PARTITION BY, FILTER) are optional.
12001200

12011201
**Geom Types**:
12021202

1203-
- **Basic**: `point`, `line`, `path`, `bar`, `col`, `area`, `tile`, `polygon`, `ribbon`
1203+
- **Basic**: `point`, `line`, `path`, `bar`, `col`, `area`, `rect`, `polygon`, `ribbon`
12041204
- **Statistical**: `histogram`, `density`, `smooth`, `boxplot`, `violin`
12051205
- **Annotation**: `text`, `label`, `segment`, `arrow`, `rule`, `linear`, `errorbar`
12061206

doc/ggsql.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@
129129
<item>bar</item>
130130
<item>col</item>
131131
<item>area</item>
132-
<item>tile</item>
132+
<item>rect</item>
133133
<item>polygon</item>
134134
<item>ribbon</item>
135135
<item>histogram</item>

doc/syntax/layer/type/rect.qmd

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
---
2+
title: "Rectangle"
3+
---
4+
5+
> 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.
6+
7+
Rectangles can be used to draw heatmaps or indicate ranges.
8+
9+
## Aesthetics
10+
The following aesthetics are recognised by the rectangle layer.
11+
12+
### Required
13+
14+
* Pick two of the following for the primary axis:
15+
* Center (e.g. `x`)
16+
* `width`
17+
* Start position (e.g. `xmin`). Unavailable when the center is discrete.
18+
* End position (e.g. `xmax`). Unavailable when the center is discrete.
19+
20+
Alternatively, use only the center, which will set `width` to 1 by default.
21+
22+
* Pick two of the following for the secondary axis:
23+
* Center (e.g. `y`)
24+
* `height`: The size of the rectangle in the vertical dimension.
25+
* Start position (e.g. `ymin`). Unavailable when the center is discrete.
26+
* End position (e.g. `ymax`) Unavailable when the center is discrete.
27+
28+
Alternatively, use only the center, which will set `height` to 1 by default.
29+
30+
### Optional
31+
* `stroke`: The colour of the contour lines.
32+
* `fill`: The colour of the inner area.
33+
* `colour`: Shorthand for setting `stroke` and `fill` simultaneously.
34+
* `opacity`: The opacity of colours.
35+
* `linewidth`: The width of the contour lines.
36+
* `linetype`: The dash pattern of the contour line.
37+
38+
## Settings
39+
* `position`: Determines the position adjustment to use for the layer (default is `'identity'`)
40+
41+
## Data transformation.
42+
When the primary aesthetics are continuous, primary data is reparameterised to {start, end}, e.g. `xmin` and `xmax`.
43+
When the secondary aesthetics are continuous, secondary data is reparameterised to {start, end}, e.g. `ymin` and `ymax`.
44+
45+
## Orientation
46+
The rectangle layer has no orientation. The axes are treated symmetrically.
47+
48+
## Examples
49+
50+
Just using `x` and `y`. Note that `width` and `height` are set to 1.
51+
52+
```{ggsql}
53+
VISUALISE Day AS x, Month AS y, Temp AS colour FROM ggsql:airquality
54+
DRAW rect
55+
```
56+
57+
Customising `width` and `height` with either the `MAPPING` or `SETTING` clauses.
58+
59+
```{ggsql}
60+
VISUALISE Day AS x, Month AS y, Temp AS colour FROM ggsql:airquality
61+
DRAW rect MAPPING 0.5 AS width SETTING height => 0.8
62+
```
63+
64+
If `x` is continuous, then `width` can be variable. Likewise for `y` and `height`.
65+
66+
```{ggsql}
67+
SELECT *, Temp / (SELECT MAX(Temp) FROM ggsql:airquality) AS norm_temp
68+
FROM ggsql:airquality
69+
VISUALISE
70+
Day AS x,
71+
Month AS y,
72+
Temp AS colour
73+
DRAW rect
74+
MAPPING
75+
norm_temp AS width,
76+
norm_temp AS height
77+
```
78+
79+
Using top, right, bottom, left parameterisation instead.
80+
81+
```{ggsql}
82+
SELECT
83+
MIN(Date) AS start,
84+
MAX(Date) AS end,
85+
MIN(Temp) AS min,
86+
MAX(Temp) AS max
87+
FROM ggsql:airquality
88+
GROUP BY
89+
WEEKOFYEAR(Date)
90+
91+
VISUALISE start AS xmin, end AS xmax, min AS ymin, max AS ymax
92+
DRAW rect
93+
```
94+
95+
Using a rectangle as an annotation.
96+
97+
<!-- When annotations work, replace this with an annotation layer -->
98+
99+
```{ggsql}
100+
VISUALISE FROM ggsql:airquality
101+
DRAW rect MAPPING
102+
'1973-06-01' AS xmin,
103+
'1973-06-30' AS xmax,
104+
50 AS ymin,
105+
100 AS ymax,
106+
'June' AS colour
107+
DRAW line MAPPING Date AS x, Temp AS y
108+
```

ggsql-vscode/syntaxes/ggsql.tmLanguage.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@
294294
"patterns": [
295295
{
296296
"name": "support.type.geom.ggsql",
297-
"match": "\\b(point|line|path|bar|col|area|tile|polygon|ribbon|histogram|density|smooth|boxplot|violin|text|label|segment|arrow|rule|linear|errorbar)\\b"
297+
"match": "\\b(point|line|path|bar|col|area|rect|polygon|ribbon|histogram|density|smooth|boxplot|violin|text|label|segment|arrow|rule|linear|errorbar)\\b"
298298
},
299299
{ "include": "#common-clause-patterns" }
300300
]

src/parser/builder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@ fn parse_geom_type(text: &str) -> Result<Geom> {
602602
"path" => Ok(Geom::path()),
603603
"bar" => Ok(Geom::bar()),
604604
"area" => Ok(Geom::area()),
605-
"tile" => Ok(Geom::tile()),
605+
"rect" => Ok(Geom::rect()),
606606
"polygon" => Ok(Geom::polygon()),
607607
"ribbon" => Ok(Geom::ribbon()),
608608
"histogram" => Ok(Geom::histogram()),

src/parser/mod.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,12 @@ mod tests {
146146
let query = r#"
147147
SELECT x, y FROM data
148148
VISUALIZE x, y
149-
DRAW tile
149+
DRAW point
150150
"#;
151151

152152
let specs = parse_query(query).unwrap();
153153
assert_eq!(specs.len(), 1);
154-
assert_eq!(specs[0].layers[0].geom, Geom::tile());
154+
assert_eq!(specs[0].layers[0].geom, Geom::point());
155155
}
156156

157157
#[test]
@@ -163,7 +163,7 @@ mod tests {
163163
VISUALIZE
164164
DRAW bar MAPPING x AS x, y AS y
165165
VISUALISE z AS x, y AS y
166-
DRAW tile
166+
DRAW point
167167
"#;
168168

169169
let specs = parse_query(query).unwrap();
@@ -219,15 +219,15 @@ mod tests {
219219
VISUALISE x, y
220220
DRAW line
221221
VISUALIZE
222-
DRAW tile MAPPING x AS x, y AS y
222+
DRAW point MAPPING x AS x, y AS y
223223
VISUALISE
224224
DRAW bar MAPPING x AS x, y AS y
225225
"#;
226226

227227
let specs = parse_query(query).unwrap();
228228
assert_eq!(specs.len(), 3);
229229
assert_eq!(specs[0].layers[0].geom, Geom::line());
230-
assert_eq!(specs[1].layers[0].geom, Geom::tile());
230+
assert_eq!(specs[1].layers[0].geom, Geom::point());
231231
assert_eq!(specs[2].layers[0].geom, Geom::bar());
232232
}
233233

@@ -245,7 +245,7 @@ mod tests {
245245
VISUALIZE
246246
DRAW bar MAPPING date AS x, revenue AS y
247247
VISUALISE
248-
DRAW tile MAPPING date AS x, revenue AS y
248+
DRAW point MAPPING date AS x, revenue AS y
249249
"#;
250250

251251
let specs = parse_query(query).unwrap();

src/plot/layer/geom/mod.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@ mod linear;
4040
mod path;
4141
mod point;
4242
mod polygon;
43+
mod rect;
4344
mod ribbon;
4445
mod rule;
4546
mod segment;
4647
mod smooth;
4748
mod text;
48-
mod tile;
4949
mod violin;
5050

5151
// Re-export types
@@ -64,12 +64,12 @@ pub use linear::Linear;
6464
pub use path::Path;
6565
pub use point::Point;
6666
pub use polygon::Polygon;
67+
pub use rect::Rect;
6768
pub use ribbon::Ribbon;
6869
pub use rule::Rule;
6970
pub use segment::Segment;
7071
pub use smooth::Smooth;
7172
pub use text::Text;
72-
pub use tile::Tile;
7373
pub use violin::Violin;
7474

7575
use crate::plot::types::{DefaultAestheticValue, ParameterValue, Schema};
@@ -84,7 +84,7 @@ pub enum GeomType {
8484
Path,
8585
Bar,
8686
Area,
87-
Tile,
87+
Rect,
8888
Polygon,
8989
Ribbon,
9090
Histogram,
@@ -108,7 +108,7 @@ impl std::fmt::Display for GeomType {
108108
GeomType::Path => "path",
109109
GeomType::Bar => "bar",
110110
GeomType::Area => "area",
111-
GeomType::Tile => "tile",
111+
GeomType::Rect => "rect",
112112
GeomType::Polygon => "polygon",
113113
GeomType::Ribbon => "ribbon",
114114
GeomType::Histogram => "histogram",
@@ -262,9 +262,9 @@ impl Geom {
262262
Self(Arc::new(Area))
263263
}
264264

265-
/// Create a Tile geom
266-
pub fn tile() -> Self {
267-
Self(Arc::new(Tile))
265+
/// Create a Rect geom
266+
pub fn rect() -> Self {
267+
Self(Arc::new(Rect))
268268
}
269269

270270
/// Create a Polygon geom
@@ -340,7 +340,7 @@ impl Geom {
340340
GeomType::Path => Self::path(),
341341
GeomType::Bar => Self::bar(),
342342
GeomType::Area => Self::area(),
343-
GeomType::Tile => Self::tile(),
343+
GeomType::Rect => Self::rect(),
344344
GeomType::Polygon => Self::polygon(),
345345
GeomType::Ribbon => Self::ribbon(),
346346
GeomType::Histogram => Self::histogram(),

0 commit comments

Comments
 (0)