Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
61 changes: 45 additions & 16 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,16 +367,18 @@ pub enum ScaleType {
}

pub enum Facet {
/// FACET WRAP variables
/// FACET variables (wrap layout)
Wrap {
variables: Vec<String>,
scales: FacetScales,
properties: HashMap<String, ParameterValue>, // From SETTING clause
},
/// FACET rows BY cols
/// FACET rows BY cols (grid layout)
Grid {
rows: Vec<String>,
cols: Vec<String>,
scales: FacetScales,
properties: HashMap<String, ParameterValue>, // From SETTING clause
},
}

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

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

-- Wrapped layout
FACET WRAP <vars> [SETTING scales => <sharing>]
-- Grid layout (BY clause for row × column)
FACET <row_vars> BY <col_vars> [SETTING ...]
```

**Scale Sharing**:
**SETTING Properties**:

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

**Example**:
**Free Scales** (`free` property):

- `null` or omitted (default) - Shared/fixed scales across all facets
- `'x'` - Independent x-axis, shared y-axis
- `'y'` - Shared x-axis, independent y-axis
- `['x', 'y']` - Independent scales for both axes

**Customizing Strip Labels**:

To customize facet strip labels, use `SCALE panel RENAMING ...` (for wrap) or `SCALE row/column RENAMING ...` (for grid).

**Examples**:

```sql
FACET WRAP region SETTING scales => 'free_y'
FACET region BY category SETTING scales => 'fixed'
-- Simple wrap facet
FACET region

-- Grid facet with BY
FACET region BY category

-- With free y-axis scales
FACET region SETTING free => 'y'

-- With column count for wrap
FACET region SETTING ncol => 3

-- With label renaming via scale
FACET region
SCALE panel RENAMING 'N' => 'North', 'S' => 'South'

-- Combined grid with settings
FACET region BY category
SETTING free => ['x', 'y'], spacing => 10
```

### COORD Clause
Expand Down Expand Up @@ -1536,7 +1565,7 @@ DRAW line
DRAW point
MAPPING sale_date AS x, total AS y, region AS color
SCALE x VIA date
FACET WRAP region
FACET region
LABEL title => 'Sales Trends by Region', x => 'Date', y => 'Total Quantity'
THEME minimal
```
Expand Down
10 changes: 5 additions & 5 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,13 @@ THEME dark SETTING background => '#1a1a1a'

## Faceting

### Facet Wrap
### Facet (Wrap Layout)

```sql
SELECT date, value, region FROM sales
VISUALISE date AS x, value AS y
DRAW line
FACET WRAP region
FACET region
```

### Facet Grid
Expand All @@ -239,7 +239,7 @@ FACET region BY product
SELECT date, value, category FROM metrics
VISUALISE date AS x, value AS y
DRAW line
FACET WRAP category SETTING scales => 'free_y'
FACET category SETTING scales => 'free_y'
```

---
Expand Down Expand Up @@ -373,7 +373,7 @@ WITH ranked_products AS (
SELECT * FROM ranked_products WHERE rank <= 5
VISUALISE product_name AS x, revenue AS y, category AS color
DRAW bar
FACET WRAP category SETTING scales => 'free_x'
FACET category SETTING scales => 'free_x'
COORD flip
LABEL title => 'Top 5 Products per Category',
x => 'Product',
Expand Down Expand Up @@ -450,7 +450,7 @@ VISUALISE sale_date AS x, total_quantity AS y, region AS color
DRAW line
DRAW point
SCALE x SETTING type => 'date'
FACET WRAP region
FACET region
LABEL title => 'Sales Trends by Region',
x => 'Date',
y => 'Total Quantity'
Expand Down
8 changes: 4 additions & 4 deletions doc/examples.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -387,15 +387,15 @@ LABEL

## Faceting

### Facet Wrap by Region
### Facet by Region

```{ggsql}
SELECT sale_date, revenue, region FROM 'sales.csv'
WHERE category = 'Electronics'
VISUALISE sale_date AS x, revenue AS y
DRAW line
SCALE x VIA date
FACET WRAP region
FACET region
LABEL
title => 'Electronics Sales by Region',
x => 'Date',
Expand Down Expand Up @@ -787,8 +787,8 @@ VISUALISE sale_date AS x, total_quantity AS y, region AS color
DRAW line
DRAW point
SCALE x VIA date
FACET WRAP region
LABEL
FACET region
LABEL
title => 'Sales Trends by Region',
x => 'Date',
y => 'Total Quantity'
Expand Down
12 changes: 7 additions & 5 deletions doc/ggsql.xml
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@
<item>REMAPPING</item>
<item>SETTING</item>
<item>FILTER</item>
<item>WRAP</item>
<item>ORDER</item>
<item>PARTITION</item>
<item>RENAMING</item>
<item>TO</item>
<item>VIA</item>
</list>
Expand Down Expand Up @@ -603,11 +603,13 @@
<!-- Facet scales - only highlighted here -->
<keyword attribute="Data Type" context="#stay" String="facet_scales"/>

<!-- WRAP keyword -->
<WordDetect attribute="Keyword" context="#stay" String="WRAP" insensitive="true"/>

<!-- scales property -->
<!-- Facet properties -->
<WordDetect attribute="Attribute" context="#stay" String="scales" insensitive="true"/>
<WordDetect attribute="Attribute" context="#stay" String="ncol" insensitive="true"/>
<WordDetect attribute="Attribute" context="#stay" String="missing" insensitive="true"/>

<!-- Wildcard for RENAMING -->
<DetectChar char="*" attribute="Operator" context="#stay"/>

<!-- Sub-keywords -->
<keyword attribute="Keyword" context="#stay" String="viz_subkeywords"/>
Expand Down
4 changes: 2 additions & 2 deletions doc/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ DRAW point
MAPPING date AS x, value AS y
SCALE x
SETTING type => 'date'
FACET WRAP region
FACET region
```
:::

Expand Down Expand Up @@ -144,7 +144,7 @@ SCALE x
SCALE y
SETTING type => 'linear', limits => [0, 100000]

FACET WRAP region SETTING scales => 'free_y'
FACET region SETTING scales => 'free_y'

LABEL
title => 'Revenue Trends by Region',
Expand Down
39 changes: 39 additions & 0 deletions doc/syntax/clause/facet.qmd
Original file line number Diff line number Diff line change
@@ -1,3 +1,42 @@
---
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.

## Clause syntax
The `FACET` syntax contains a number of subclauses that are all optional

```ggsql
FACET <column> BY <column>
SETTING <parameter> => <value>, ...
```

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.

### `BY`
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.

### `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:
* `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
* `['x', 'y']`: Independent scales for both axes
* `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.
* `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.

### Facet variables as aesthetics
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)

### Customizing facet strip labels
To customize facet strip labels (e.g., renaming categories), use the `RENAMING` clause on the facet scale:

```ggsql
FACET region
SCALE panel RENAMING 'N' => 'North', 'S' => 'South'
```

See the [facet scale documentation](../scale/aesthetic/Z_facetting.qmd) for more details on label customization.
34 changes: 34 additions & 0 deletions doc/syntax/scale/aesthetic/Z_facetting.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
title: Faceting
---

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).

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.

## Literal values
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.

## Scale types
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.

```{ggsql}
VISUALISE Date AS x, Temp AS y FROM ggsql:airquality
DRAW line
FACET Date
SETTING free => 'x'
SCALE panel
SETTING breaks => 'month'
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.

```{ggsql}
VISUALISE sex AS x FROM ggsql:penguins
DRAW bar
FACET species
SCALE panel FROM ['Adelie', null]
RENAMING null => 'The rest'
```
2 changes: 1 addition & 1 deletion ggsql-jupyter/tests/fixtures/sample_notebook.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@
"ORDER BY date\n",
"VISUALISE date AS x, revenue AS y\n",
"DRAW line\n",
"FACET WRAP region\n",
"FACET region\n",
"SCALE x SETTING type => 'date'\n",
"LABEL title => 'Revenue by Region', x => 'Date', y => 'Revenue ($)'"
]
Expand Down
2 changes: 1 addition & 1 deletion ggsql-jupyter/tests/fixtures/test_queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ SELECT
FROM generate_series(1, 10) as t(n)
VISUALISE x, y
DRAW point
FACET WRAP group
FACET group
LABEL title => 'Faceted Plot';

-- Visualization with FILTER clause - global mapping with layer filter
Expand Down
6 changes: 3 additions & 3 deletions ggsql-python/tests/test_ggsql.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ def test_layered_chart_can_round_trip(self):
assert isinstance(recreated, altair.LayerChart)

def test_faceted_chart_returns_facet_chart(self):
"""FACET WRAP specs produce FacetChart."""
"""FACET specs produce FacetChart."""
df = pl.DataFrame(
{
"x": [1, 2, 3, 4, 5, 6],
Expand All @@ -285,7 +285,7 @@ def test_faceted_chart_returns_facet_chart(self):
)
# Need validate=False because ggsql produces v6 specs
chart = ggsql.render_altair(
df, "VISUALISE x, y FACET WRAP group DRAW point", validate=False
df, "VISUALISE x, y FACET group DRAW point", validate=False
)
assert isinstance(chart, altair.FacetChart)

Expand All @@ -299,7 +299,7 @@ def test_faceted_chart_can_round_trip(self):
}
)
chart = ggsql.render_altair(
df, "VISUALISE x, y FACET WRAP group DRAW point", validate=False
df, "VISUALISE x, y FACET group DRAW point", validate=False
)

# Convert to dict (skip validation for ggsql specs)
Expand Down
4 changes: 2 additions & 2 deletions ggsql-vscode/examples/sample.gsql
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ DRAW line
DRAW point SETTING size => 3
SCALE x SETTING type => 'date'
SCALE color TO viridis
FACET WRAP region SETTING scales => 'free_y'
FACET region SETTING scales => 'free_y'
LABEL title => 'Sales Trends by Region',
x => 'Month',
y => 'Total Quantity',
Expand Down Expand Up @@ -73,7 +73,7 @@ WHERE age BETWEEN 18 AND 80
VISUALISE age AS x, gender AS fill
DRAW histogram
SCALE x SETTING type => 'linear', limits => [18, 80]
FACET WRAP gender SETTING scales => 'fixed'
FACET gender SETTING scales => 'fixed'
LABEL title => 'Age Distribution by Gender',
x => 'Age',
y => 'Count'
Expand Down
Loading