Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a9cabab
Add PLACE clause for annotation layers
teunbrand Mar 4, 2026
214b53a
Implement execution for PLACE annotation layers
teunbrand Mar 4, 2026
cada5bd
Transform position aesthetics for PLACE annotation layers
teunbrand Mar 5, 2026
d541c34
Add length parameter to DataSource::Annotation
teunbrand Mar 5, 2026
a6ab787
Add array recycling support for PLACE annotation layers
teunbrand Mar 5, 2026
17d0de2
Fix PLACE annotation layers with mixed-type array parameters
teunbrand Mar 5, 2026
16abba9
Refactor SQL generation and array recycling to methods on types
teunbrand Mar 5, 2026
6b41815
Add is_scaled field to prevent annotation literals from using scales
teunbrand Mar 5, 2026
8bc43e8
error when writing array value encoding
teunbrand Mar 6, 2026
3c63669
add docs
teunbrand Mar 6, 2026
c8d571a
Refactor annotation layer SQL generation for clarity
teunbrand Mar 6, 2026
5876901
Move array recycling from AST to execution layer
teunbrand Mar 6, 2026
6aa345d
Replace is_scaled flag with AnnotationColumn variant
teunbrand Mar 6, 2026
82bfe38
Filter NULL aesthetics in annotation layers
teunbrand Mar 6, 2026
00337d5
Add unit tests for annotation layer functions
teunbrand Mar 6, 2026
46e46fe
Fix annotation layers to work with stat geoms
teunbrand Mar 9, 2026
86549a2
bit of cleanup
teunbrand Mar 9, 2026
3a4584f
Merge branch 'main' into annotate
teunbrand Mar 9, 2026
d22fe47
soothe clippy
teunbrand Mar 17, 2026
b7c13e0
Merge branch 'main' into annotate
teunbrand Mar 17, 2026
70ce8ca
Document PLACE clause in CLAUDE.md
teunbrand Mar 17, 2026
8510e6b
use place in examples
teunbrand Mar 17, 2026
78b3cc4
Merge branch 'main' into annotate
teunbrand Mar 17, 2026
81774c0
Add PLACE clause syntax highlighting to VS Code and Quarto
teunbrand Mar 17, 2026
7e99f9d
try to cast literals as date
teunbrand Mar 17, 2026
586df16
add some more examples
teunbrand Mar 17, 2026
d193ce1
Fix failing PLACE annotation layer tests
teunbrand Mar 18, 2026
a263e62
cargo fmt
teunbrand Mar 18, 2026
7ed076c
Apply suggestions from code review
teunbrand Mar 18, 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
42 changes: 41 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,10 @@ pub enum GlobalMappingItem {

pub struct Layer {
pub geom: Geom, // Geometric object type
pub position: Position, // Position adjustment (from SETTING position)
pub aesthetics: HashMap<String, AestheticValue>, // Aesthetic mappings (from MAPPING)
pub parameters: HashMap<String, ParameterValue>, // Geom parameters (from SETTING)
pub source: Option<DataSource>, // Data source (from MAPPING ... FROM or PLACE)
pub filter: Option<FilterExpression>, // Layer filter (from FILTER)
pub partition_by: Vec<String>, // Grouping columns (from PARTITION BY)
}
Expand All @@ -337,8 +339,22 @@ pub enum Geom {
Text, Segment, Arrow, Rule, Linear, ErrorBar,
}

pub enum DataSource {
Identifier(String), // Table/CTE name
FilePath(String), // File path (quoted)
Annotation, // PLACE clause (no external data)
}

pub enum Position {
Identity, // No adjustment
Stack, // Stack elements (bars, areas)
Dodge, // Dodge side-by-side (bars)
Jitter, // Jitter points randomly
}

pub enum AestheticValue {
Column(String), // Unquoted column reference: revenue AS x
Column(String), // Column from data: revenue AS x
AnnotationColumn(String), // Annotation literal (PLACE): uses identity scale
Literal(ParameterValue), // Quoted literal: 'value' AS fill
}

Expand Down Expand Up @@ -1178,6 +1194,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` |
| `PLACE` | ✅ Yes | Annotation layers | `PLACE point SETTING x => 5, y => 10` |
| `SCALE` | ✅ Yes | Configure scales | `SCALE x VIA date` |
| `FACET` | ❌ No | Small multiples | `FACET region` |
| `PROJECT` | ❌ No | Coordinate system | `PROJECT TO cartesian` |
Expand Down Expand Up @@ -1289,6 +1306,29 @@ DRAW line
FILTER year >= 2020
```

### PLACE Clause (Annotation Layers)

**Syntax**:

```sql
PLACE <geom> SETTING <aesthetic/parameter> => <value>, ...
```

Creates annotation layers with literal values only (no data mappings). All aesthetics set via SETTING; supports arrays that are recycled to longest length. No FILTER/PARTITION BY/ORDER BY support.

**Examples**:

```sql
-- Single annotation
PLACE point SETTING x => 5, y => 10, color => 'red'

-- Multiple annotations (array recycling)
PLACE point SETTING x => [1, 2, 3], y => [10, 20, 30]

-- Reference line
PLACE rule SETTING x => 5, color => 'red'
```

### SCALE Clause

**Syntax**:
Expand Down
4 changes: 4 additions & 0 deletions doc/_quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ website:
href: syntax/clause/visualise.qmd
- text: "`DRAW`"
href: syntax/clause/draw.qmd
- text: "`PLACE`"
href: syntax/clause/place.qmd
- text: "`SCALE`"
href: syntax/clause/scale.qmd
- text: "`FACET`"
Expand Down Expand Up @@ -77,6 +79,8 @@ website:
href: syntax/clause/visualise.qmd
- text: "`DRAW`"
href: syntax/clause/draw.qmd
- text: "`PLACE`"
href: syntax/clause/place.qmd
- text: "`SCALE`"
href: syntax/clause/scale.qmd
- text: "`FACET`"
Expand Down
49 changes: 49 additions & 0 deletions doc/ggsql.xml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
<!-- ggsql Main Clause Keywords (triggers context switch) -->
<list name="clause_keywords">
<item>DRAW</item>
<item>PLACE</item>
<item>SCALE</item>
<item>PROJECT</item>
<item>FACET</item>
Expand Down Expand Up @@ -404,6 +405,7 @@

<!-- Clause keywords trigger context switches -->
<WordDetect attribute="Keyword" context="DrawClause" String="DRAW" insensitive="true"/>
<WordDetect attribute="Keyword" context="PlaceClause" String="PLACE" insensitive="true"/>
<WordDetect attribute="Keyword" context="ScaleClause" String="SCALE" insensitive="true"/>
<WordDetect attribute="Keyword" context="ProjectClause" String="PROJECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="FacetClause" String="FACET" insensitive="true"/>
Expand Down Expand Up @@ -451,6 +453,7 @@

<!-- Exit to other clause contexts -->
<WordDetect attribute="Keyword" context="DrawClause" String="DRAW" insensitive="true"/>
<WordDetect attribute="Keyword" context="PlaceClause" String="PLACE" insensitive="true"/>
<WordDetect attribute="Keyword" context="ScaleClause" String="SCALE" insensitive="true"/>
<WordDetect attribute="Keyword" context="ProjectClause" String="PROJECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="FacetClause" String="FACET" insensitive="true"/>
Expand Down Expand Up @@ -481,6 +484,46 @@
<DetectChar char="," attribute="Symbol" context="#stay"/>
</context>

<!-- PLACE clause context (annotation layers) -->
<context name="PlaceClause" attribute="Normal Text" lineEndContext="#stay">
<Detect2Chars char="-" char1="-" attribute="Comment" context="Comment"/>
<Detect2Chars char="/" char1="*" attribute="Comment" context="CommentMulti" beginRegion="comment"/>
<DetectChar char="'" attribute="String" context="StringSingle"/>
<DetectChar char="&quot;" attribute="String" context="StringDouble"/>
<RegExpr attribute="Number" context="#stay" String="-?[0-9]+\.?[0-9]*([eE][+-]?[0-9]+)?"/>

<!-- Exit to other clause contexts -->
<WordDetect attribute="Keyword" context="DrawClause" String="DRAW" insensitive="true"/>
<WordDetect attribute="Keyword" context="PlaceClause" String="PLACE" insensitive="true"/>
<WordDetect attribute="Keyword" context="ScaleClause" String="SCALE" insensitive="true"/>
<WordDetect attribute="Keyword" context="ProjectClause" String="PROJECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="FacetClause" String="FACET" insensitive="true"/>
<WordDetect attribute="Keyword" context="LabelClause" String="LABEL" insensitive="true"/>
<WordDetect attribute="Keyword" context="ThemeClause" String="THEME" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="VISUALISE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="VISUALIZE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="SELECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="WHERE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="WITH" insensitive="true"/>

<!-- Geom types - same as DRAW -->
<keyword attribute="Data Type" context="#stay" String="geoms"/>

<!-- Aesthetics -->
<keyword attribute="Attribute" context="#stay" String="aesthetics"/>

<!-- Sub-keywords (only SETTING supported, not FILTER/PARTITION/ORDER) -->
<WordDetect attribute="Keyword" context="#stay" String="SETTING" insensitive="true"/>

<StringDetect attribute="Operator" context="#stay" String="=&gt;"/>
<AnyChar attribute="Operator" context="#stay" String="=!&lt;&gt;+-*/%"/>
<DetectChar char="(" attribute="Symbol" context="#stay"/>
<DetectChar char=")" attribute="Symbol" context="#stay"/>
<DetectChar char="[" attribute="Symbol" context="#stay"/>
<DetectChar char="]" attribute="Symbol" context="#stay"/>
<DetectChar char="," attribute="Symbol" context="#stay"/>
</context>

<!-- SCALE clause context -->
<context name="ScaleClause" attribute="Normal Text" lineEndContext="#stay">
<Detect2Chars char="-" char1="-" attribute="Comment" context="Comment"/>
Expand All @@ -491,6 +534,7 @@

<!-- Exit to other clause contexts -->
<WordDetect attribute="Keyword" context="DrawClause" String="DRAW" insensitive="true"/>
<WordDetect attribute="Keyword" context="PlaceClause" String="PLACE" insensitive="true"/>
<WordDetect attribute="Keyword" context="ScaleClause" String="SCALE" insensitive="true"/>
<WordDetect attribute="Keyword" context="ProjectClause" String="PROJECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="FacetClause" String="FACET" insensitive="true"/>
Expand Down Expand Up @@ -537,6 +581,7 @@

<!-- Exit to other clause contexts -->
<WordDetect attribute="Keyword" context="DrawClause" String="DRAW" insensitive="true"/>
<WordDetect attribute="Keyword" context="PlaceClause" String="PLACE" insensitive="true"/>
<WordDetect attribute="Keyword" context="ScaleClause" String="SCALE" insensitive="true"/>
<WordDetect attribute="Keyword" context="ProjectClause" String="PROJECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="FacetClause" String="FACET" insensitive="true"/>
Expand Down Expand Up @@ -580,6 +625,7 @@

<!-- Exit to other clause contexts -->
<WordDetect attribute="Keyword" context="DrawClause" String="DRAW" insensitive="true"/>
<WordDetect attribute="Keyword" context="PlaceClause" String="PLACE" insensitive="true"/>
<WordDetect attribute="Keyword" context="ScaleClause" String="SCALE" insensitive="true"/>
<WordDetect attribute="Keyword" context="ProjectClause" String="PROJECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="FacetClause" String="FACET" insensitive="true"/>
Expand Down Expand Up @@ -622,6 +668,7 @@

<!-- Exit to other clause contexts -->
<WordDetect attribute="Keyword" context="DrawClause" String="DRAW" insensitive="true"/>
<WordDetect attribute="Keyword" context="PlaceClause" String="PLACE" insensitive="true"/>
<WordDetect attribute="Keyword" context="ScaleClause" String="SCALE" insensitive="true"/>
<WordDetect attribute="Keyword" context="ProjectClause" String="PROJECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="FacetClause" String="FACET" insensitive="true"/>
Expand Down Expand Up @@ -659,6 +706,7 @@

<!-- Exit to other clause contexts -->
<WordDetect attribute="Keyword" context="DrawClause" String="DRAW" insensitive="true"/>
<WordDetect attribute="Keyword" context="PlaceClause" String="PLACE" insensitive="true"/>
<WordDetect attribute="Keyword" context="ScaleClause" String="SCALE" insensitive="true"/>
<WordDetect attribute="Keyword" context="ProjectClause" String="PROJECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="FacetClause" String="FACET" insensitive="true"/>
Expand Down Expand Up @@ -699,6 +747,7 @@

<!-- Exit to other clause contexts -->
<WordDetect attribute="Keyword" context="DrawClause" String="DRAW" insensitive="true"/>
<WordDetect attribute="Keyword" context="PlaceClause" String="PLACE" insensitive="true"/>
<WordDetect attribute="Keyword" context="ScaleClause" String="SCALE" insensitive="true"/>
<WordDetect attribute="Keyword" context="ProjectClause" String="PROJECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="FacetClause" String="FACET" insensitive="true"/>
Expand Down
37 changes: 37 additions & 0 deletions doc/syntax/clause/place.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
title: "Create annotation layers with `PLACE`"
---

The `PLACE` clause is the little sibling of the [`DRAW` clause](draw.qmd) that also creates layers.
A layer created with `PLACE` has no mappings to data, all aesthetics are set using literal values instead.

## Clause syntax
The `PLACE` clause takes just a type and a setting clause, all of them required.

```ggsql
PLACE <layer-type>
SETTING <parameter/aesthetic> => <value>, ...
```

Like `DRAW`, the layer type is required and specifies the type of layer to draw, like `point` or `text`.
It defines how the remaining settings are interpreted.
The [main syntax page](../index.qmd#layers) has a list of all available layer types

Unlike the `DRAW` clause, the `PLACE` clause does not support `FILTER`, `PARTITION BY`, and `ORDER BY` clauses since
everything is declared inline.

### `SETTING`
```ggsql
SETTING <parameter/aesthetic> => <value>, ...
```

The `SETTING` clause can be used for two different things:

* *Setting aesthetics*: All aesthetics in `PLACE` layers are specified using literal value, e.g. 'red' (as in the color red).
Aesthetics that are set will not go through a scale but will use the provided value as-is.
You cannot set an aesthetic to a column, only to a literal values.
Contrary to `DRAW` layers, `PLACE` layers can take multiple literal values in an array.
* *Setting parameters*: Some layers take additional arguments that control how they behave.
Often, but not always, these modify the statistical transformation in some way.
An example would be the binwidth parameter in histogram which controls the width of each bin during histogram calculation.
This is not a statistical property since it is not related to each record, but to the calculation as a whole.
1 change: 1 addition & 0 deletions doc/syntax/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ ggsql augments the standard SQL syntax with a number of new clauses to describe

- [`VISUALISE`](clause/visualise.qmd) initiates the visualisation part of the query
- [`DRAW`](clause/draw.qmd) adds a new layer to the visualisation
- [`PLACE`](clause/place.qmd) adds an annotation layer
- [`SCALE`](clause/scale.qmd) specify how an aesthetic should be scaled
- [`FACET`](clause/facet.qmd) describes how data should be split into small multiples
- [`PROJECT`](clause/project.qmd) is used for selecting the coordinate system to use
Expand Down
4 changes: 2 additions & 2 deletions doc/syntax/layer/type/linear.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ Add a simple reference line to a scatterplot:
```{ggsql}
VISUALISE FROM ggsql:penguins
DRAW point MAPPING bill_len AS x, bill_dep AS y
DRAW linear MAPPING 0.4 AS coef, -1 AS intercept
PLACE linear SETTING coef => 0.4, intercept => -1
```

Add multiple reference lines with different colors from a separate dataset:
Add multiple reference lines with different colors from a separate dataset. Note we're mapping from data here, so we use `DRAW` instead of `PLACE`.

```{ggsql}
WITH lines AS (
Expand Down
16 changes: 7 additions & 9 deletions doc/syntax/layer/type/rect.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,15 @@ VISUALISE start AS xmin, end AS xmax, min AS ymin, max AS ymax
DRAW rect
```

Using a rectangle as an annotation.

<!-- When annotations work, replace this with an annotation layer -->
Using a rectangle as an annotation. Note we're using the `PLACE` clause here instead of `DRAW` because we're not mapping from data.

```{ggsql}
VISUALISE FROM ggsql:airquality
DRAW rect MAPPING
'1973-06-01' AS xmin,
'1973-06-30' AS xmax,
50 AS ymin,
100 AS ymax,
'June' AS colour
PLACE rect SETTING
xmin => '1973-06-01',
xmax => '1973-06-30',
ymin => 50,
ymax => 100,
colour => 'dodgerblue'
DRAW line MAPPING Date AS x, Temp AS y
```
6 changes: 3 additions & 3 deletions doc/syntax/layer/type/rule.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,18 @@ WHERE Month = 5

VISUALISE
DRAW line MAPPING date AS x, temperature AS y
DRAW rule MAPPING 70 AS y
PLACE rule SETTING y => 70
```

Add a vertical line to mark a specific value:

```{ggsql}
VISUALISE FROM ggsql:penguins
DRAW point MAPPING bill_len AS x, bill_dep AS y
DRAW rule MAPPING 45 AS x
PLACE rule SETTING x => 45
```

Add multiple threshold lines with different colors:
Add multiple threshold lines with different colors. Note that because we're mapping data from data, we use the `DRAW` clause instead of the `PLACE` clause.

```{ggsql}
WITH thresholds AS (
Expand Down
10 changes: 10 additions & 0 deletions doc/syntax/layer/type/text.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,13 @@ VISUALISE island AS x, n AS y
SCALE y FROM [0, 200]
```

You can use `PLACE` to annotate a plot directly without needing to map data.

```{ggsql}
VISUALISE bill_len AS x, bill_dep AS y FROM ggsql:penguins
DRAW point MAPPING species AS colour
PLACE text SETTING
label => ['Adelie', 'Chinstrap', 'Gentoo'],
x => [40, 50, 50],
y => [19, 19, 15]
```
Loading
Loading