Skip to content

Commit 850a362

Browse files
authored
New faceting syntax (#147)
1 parent eeaca4a commit 850a362

32 files changed

Lines changed: 3397 additions & 227 deletions

File tree

CLAUDE.md

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -367,16 +367,18 @@ pub enum ScaleType {
367367
}
368368

369369
pub enum Facet {
370-
/// FACET WRAP variables
370+
/// FACET variables (wrap layout)
371371
Wrap {
372372
variables: Vec<String>,
373373
scales: FacetScales,
374+
properties: HashMap<String, ParameterValue>, // From SETTING clause
374375
},
375-
/// FACET rows BY cols
376+
/// FACET rows BY cols (grid layout)
376377
Grid {
377378
rows: Vec<String>,
378379
cols: Vec<String>,
379380
scales: FacetScales,
381+
properties: HashMap<String, ParameterValue>, // From SETTING clause
380382
},
381383
}
382384

@@ -1177,7 +1179,7 @@ Where `<global_mapping>` can be:
11771179
| `VISUALISE` | ✅ Yes | Entry point | `VISUALISE date AS x, revenue AS y` |
11781180
| `DRAW` | ✅ Yes | Define layers | `DRAW line MAPPING date AS x, value AS y` |
11791181
| `SCALE` | ✅ Yes | Configure scales | `SCALE x VIA date` |
1180-
| `FACET` | ❌ No | Small multiples | `FACET WRAP region` |
1182+
| `FACET` | ❌ No | Small multiples | `FACET region` |
11811183
| `COORD` | ❌ No | Coordinate system | `COORD cartesian SETTING xlim => [0,100]` |
11821184
| `LABEL` | ❌ No | Text labels | `LABEL title => 'My Chart', x => 'Date'` |
11831185
| `THEME` | ❌ No | Visual styling | `THEME minimal` |
@@ -1372,25 +1374,52 @@ SCALE x VIA date FROM ['2024-01-01', '2024-12-31'] SETTING breaks => '1 month'
13721374
**Syntax**:
13731375

13741376
```sql
1375-
-- Grid layout
1376-
FACET <row_vars> BY <col_vars> [SETTING scales => <sharing>]
1377+
-- Wrap layout (single variable = automatic wrap)
1378+
FACET <vars> [SETTING <param> => <value>, ...]
13771379

1378-
-- Wrapped layout
1379-
FACET WRAP <vars> [SETTING scales => <sharing>]
1380+
-- Grid layout (BY clause for row × column)
1381+
FACET <row_vars> BY <col_vars> [SETTING ...]
13801382
```
13811383

1382-
**Scale Sharing**:
1384+
**SETTING Properties**:
13831385

1384-
- `'fixed'` (default) - Same scales across all facets
1385-
- `'free'` - Independent scales for each facet
1386-
- `'free_x'` - Independent x-axis, shared y-axis
1387-
- `'free_y'` - Independent y-axis, shared x-axis
1386+
- `free => <axes>` - Which axes have independent scales (see below)
1387+
- `ncol => <number>` - Number of columns for wrap layout
1388+
- `spacing => <number>` - Space between facets
13881389

1389-
**Example**:
1390+
**Free Scales** (`free` property):
1391+
1392+
- `null` or omitted (default) - Shared/fixed scales across all facets
1393+
- `'x'` - Independent x-axis, shared y-axis
1394+
- `'y'` - Shared x-axis, independent y-axis
1395+
- `['x', 'y']` - Independent scales for both axes
1396+
1397+
**Customizing Strip Labels**:
1398+
1399+
To customize facet strip labels, use `SCALE panel RENAMING ...` (for wrap) or `SCALE row/column RENAMING ...` (for grid).
1400+
1401+
**Examples**:
13901402

13911403
```sql
1392-
FACET WRAP region SETTING scales => 'free_y'
1393-
FACET region BY category SETTING scales => 'fixed'
1404+
-- Simple wrap facet
1405+
FACET region
1406+
1407+
-- Grid facet with BY
1408+
FACET region BY category
1409+
1410+
-- With free y-axis scales
1411+
FACET region SETTING free => 'y'
1412+
1413+
-- With column count for wrap
1414+
FACET region SETTING ncol => 3
1415+
1416+
-- With label renaming via scale
1417+
FACET region
1418+
SCALE panel RENAMING 'N' => 'North', 'S' => 'South'
1419+
1420+
-- Combined grid with settings
1421+
FACET region BY category
1422+
SETTING free => ['x', 'y'], spacing => 10
13941423
```
13951424

13961425
### COORD Clause
@@ -1536,7 +1565,7 @@ DRAW line
15361565
DRAW point
15371566
MAPPING sale_date AS x, total AS y, region AS color
15381567
SCALE x VIA date
1539-
FACET WRAP region
1568+
FACET region
15401569
LABEL title => 'Sales Trends by Region', x => 'Date', y => 'Total Quantity'
15411570
THEME minimal
15421571
```

EXAMPLES.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -215,13 +215,13 @@ THEME dark SETTING background => '#1a1a1a'
215215

216216
## Faceting
217217

218-
### Facet Wrap
218+
### Facet (Wrap Layout)
219219

220220
```sql
221221
SELECT date, value, region FROM sales
222222
VISUALISE date AS x, value AS y
223223
DRAW line
224-
FACET WRAP region
224+
FACET region
225225
```
226226

227227
### Facet Grid
@@ -239,7 +239,7 @@ FACET region BY product
239239
SELECT date, value, category FROM metrics
240240
VISUALISE date AS x, value AS y
241241
DRAW line
242-
FACET WRAP category SETTING scales => 'free_y'
242+
FACET category SETTING scales => 'free_y'
243243
```
244244

245245
---
@@ -373,7 +373,7 @@ WITH ranked_products AS (
373373
SELECT * FROM ranked_products WHERE rank <= 5
374374
VISUALISE product_name AS x, revenue AS y, category AS color
375375
DRAW bar
376-
FACET WRAP category SETTING scales => 'free_x'
376+
FACET category SETTING scales => 'free_x'
377377
COORD flip
378378
LABEL title => 'Top 5 Products per Category',
379379
x => 'Product',
@@ -450,7 +450,7 @@ VISUALISE sale_date AS x, total_quantity AS y, region AS color
450450
DRAW line
451451
DRAW point
452452
SCALE x SETTING type => 'date'
453-
FACET WRAP region
453+
FACET region
454454
LABEL title => 'Sales Trends by Region',
455455
x => 'Date',
456456
y => 'Total Quantity'

doc/examples.qmd

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -387,15 +387,15 @@ LABEL
387387

388388
## Faceting
389389

390-
### Facet Wrap by Region
390+
### Facet by Region
391391

392392
```{ggsql}
393393
SELECT sale_date, revenue, region FROM 'sales.csv'
394394
WHERE category = 'Electronics'
395395
VISUALISE sale_date AS x, revenue AS y
396396
DRAW line
397397
SCALE x VIA date
398-
FACET WRAP region
398+
FACET region
399399
LABEL
400400
title => 'Electronics Sales by Region',
401401
x => 'Date',
@@ -787,8 +787,8 @@ VISUALISE sale_date AS x, total_quantity AS y, region AS color
787787
DRAW line
788788
DRAW point
789789
SCALE x VIA date
790-
FACET WRAP region
791-
LABEL
790+
FACET region
791+
LABEL
792792
title => 'Sales Trends by Region',
793793
x => 'Date',
794794
y => 'Total Quantity'

doc/ggsql.xml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,9 @@
105105
<item>REMAPPING</item>
106106
<item>SETTING</item>
107107
<item>FILTER</item>
108-
<item>WRAP</item>
109108
<item>ORDER</item>
110109
<item>PARTITION</item>
110+
<item>RENAMING</item>
111111
<item>TO</item>
112112
<item>VIA</item>
113113
</list>
@@ -603,11 +603,13 @@
603603
<!-- Facet scales - only highlighted here -->
604604
<keyword attribute="Data Type" context="#stay" String="facet_scales"/>
605605

606-
<!-- WRAP keyword -->
607-
<WordDetect attribute="Keyword" context="#stay" String="WRAP" insensitive="true"/>
608-
609-
<!-- scales property -->
606+
<!-- Facet properties -->
610607
<WordDetect attribute="Attribute" context="#stay" String="scales" insensitive="true"/>
608+
<WordDetect attribute="Attribute" context="#stay" String="ncol" insensitive="true"/>
609+
<WordDetect attribute="Attribute" context="#stay" String="missing" insensitive="true"/>
610+
611+
<!-- Wildcard for RENAMING -->
612+
<DetectChar char="*" attribute="Operator" context="#stay"/>
611613

612614
<!-- Sub-keywords -->
613615
<keyword attribute="Keyword" context="#stay" String="viz_subkeywords"/>

doc/index.qmd

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ DRAW point
5252
MAPPING date AS x, value AS y
5353
SCALE x
5454
SETTING type => 'date'
55-
FACET WRAP region
55+
FACET region
5656
```
5757
:::
5858

@@ -144,7 +144,7 @@ SCALE x
144144
SCALE y
145145
SETTING type => 'linear', limits => [0, 100000]
146146
147-
FACET WRAP region SETTING scales => 'free_y'
147+
FACET region SETTING scales => 'free_y'
148148
149149
LABEL
150150
title => 'Revenue Trends by Region',

doc/syntax/clause/facet.qmd

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,42 @@
11
---
22
title: "Create small multiples with `FACET`"
33
---
4+
5+
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.
6+
7+
## Clause syntax
8+
The `FACET` syntax contains a number of subclauses that are all optional
9+
10+
```ggsql
11+
FACET <column> BY <column>
12+
SETTING <parameter> => <value>, ...
13+
```
14+
15+
The first `column` is mandatory. It names a column in the layer data that will be used for splitting the data. If the layer data does not contain the column the behavior for that layer depends on the `missing` parameter of the facet.
16+
17+
### `BY`
18+
The optional `BY` clause is used to define an additional column to split the data by. If it is missing the small multiples are laid out in a grid with the facet panels filling the cells in a row-wise fashion. If `BY` is present then the categories of the first `column` defines the rows of the grid and the categories of the second `column` the columns of the grid. Each multiple is then positioned according to that.
19+
20+
### `SETTING`
21+
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:
22+
23+
* `free`: Controls whether the positional scales are independent across the small multiples. Permissible values are:
24+
* `null` or omitted (default): Shared/fixed scales across all panels
25+
* `'x'`: Independent x-axis scale, shared y-axis scale
26+
* `'y'`: Shared x-axis scale, independent y-axis scale
27+
* `['x', 'y']`: Independent scales for both axes
28+
* `missing`: Determines how layers behave when the faceting column is missing. It can take two values: `'repeat'` (default), and `'null'`. If `'repeat'` is set, then the layer data is repeated in each panel. If `'null'`, then such layers are only displayed if a null panel is shown, as controlled by the facet scale.
29+
* `ncol`: The number of panel columns to use when faceting by a single variable. Default is 3 when fewer than 6 categories are present, 4 when fewer than 12 categeries are present and otherwise 5. When the `BY`-clause is used to set a second faceting variable, the `ncol` setting is not allowed. There is no `nrow` setting as this is derived from the number of panels and the `ncol` setting.
30+
31+
### Facet variables as aesthetics
32+
When you apply faceting to a plot you are creating new aesthetics you can control. For 1-dimensional faceting (no `BY` clause) the aesthetic is called `panel` and for 2-dimensional faceting the aesthetics are called `row` and `column`. You can read more about these aesthetics in [their documentation](../scale/aesthetic/Z_facetting.qmd)
33+
34+
### Customizing facet strip labels
35+
To customize facet strip labels (e.g., renaming categories), use the `RENAMING` clause on the facet scale:
36+
37+
```ggsql
38+
FACET region
39+
SCALE panel RENAMING 'N' => 'North', 'S' => 'South'
40+
```
41+
42+
See the [facet scale documentation](../scale/aesthetic/Z_facetting.qmd) for more details on label customization.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
title: Faceting
3+
---
4+
5+
ggsql provides one or two aesthetics related to faceting. These are special in the sense that they do not alter the display of the single data values, but rather alter in which plot they appear. While it is possible to map to these aesthetics in a layer they are most often applied globally as part of the [`FACET` clause](../../clause/facet.qmd).
6+
7+
The aesthetics provided are either `panel` (for 1-dimensional faceting) or `row` and `column` (for 2-dimensional faceting). These aesthetics have to compatible with the facet clause: it is not possible to map to `panel` in a 2-dimensional faceting plot nor is it possible to map `row` and `column` in a 1-dimensional plot.
8+
9+
## Literal values
10+
Scales for facet aesthetics never use an output range and always relate to the input range. This means that no concept of literal values applies.
11+
12+
## Scale types
13+
Since panels are discrete by nature it is not possible to have a continuous scale for a facet. If continuous data is mapped to a facet aesthetic, a binned scale will be applied by default.
14+
15+
```{ggsql}
16+
VISUALISE Date AS x, Temp AS y FROM ggsql:airquality
17+
DRAW line
18+
FACET Date
19+
SETTING free => 'x'
20+
SCALE panel
21+
SETTING breaks => 'month'
22+
SCALE x
23+
SETTING breaks => 'weeks'
24+
```
25+
26+
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.
27+
28+
```{ggsql}
29+
VISUALISE sex AS x FROM ggsql:penguins
30+
DRAW bar
31+
FACET species
32+
SCALE panel FROM ['Adelie', null]
33+
RENAMING null => 'The rest'
34+
```

ggsql-jupyter/tests/fixtures/sample_notebook.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@
188188
"ORDER BY date\n",
189189
"VISUALISE date AS x, revenue AS y\n",
190190
"DRAW line\n",
191-
"FACET WRAP region\n",
191+
"FACET region\n",
192192
"SCALE x SETTING type => 'date'\n",
193193
"LABEL title => 'Revenue by Region', x => 'Date', y => 'Revenue ($)'"
194194
]

ggsql-jupyter/tests/fixtures/test_queries.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ SELECT
5858
FROM generate_series(1, 10) as t(n)
5959
VISUALISE x, y
6060
DRAW point
61-
FACET WRAP group
61+
FACET group
6262
LABEL title => 'Faceted Plot';
6363

6464
-- Visualization with FILTER clause - global mapping with layer filter

ggsql-python/tests/test_ggsql.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ def test_layered_chart_can_round_trip(self):
275275
assert isinstance(recreated, altair.LayerChart)
276276

277277
def test_faceted_chart_returns_facet_chart(self):
278-
"""FACET WRAP specs produce FacetChart."""
278+
"""FACET specs produce FacetChart."""
279279
df = pl.DataFrame(
280280
{
281281
"x": [1, 2, 3, 4, 5, 6],
@@ -285,7 +285,7 @@ def test_faceted_chart_returns_facet_chart(self):
285285
)
286286
# Need validate=False because ggsql produces v6 specs
287287
chart = ggsql.render_altair(
288-
df, "VISUALISE x, y FACET WRAP group DRAW point", validate=False
288+
df, "VISUALISE x, y FACET group DRAW point", validate=False
289289
)
290290
assert isinstance(chart, altair.FacetChart)
291291

@@ -299,7 +299,7 @@ def test_faceted_chart_can_round_trip(self):
299299
}
300300
)
301301
chart = ggsql.render_altair(
302-
df, "VISUALISE x, y FACET WRAP group DRAW point", validate=False
302+
df, "VISUALISE x, y FACET group DRAW point", validate=False
303303
)
304304

305305
# Convert to dict (skip validation for ggsql specs)

0 commit comments

Comments
 (0)