Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2aeda82
Setup hephaestus writer
thomasp85 Jun 22, 2026
22a3af4
Hook up scales
thomasp85 Jun 22, 2026
e81c1ae
Additional geoms
thomasp85 Jun 22, 2026
ee969a4
complete geoms
thomasp85 Jun 23, 2026
e65a070
Merged upstream/main into hephaestus-writer
thomasp85 Jun 23, 2026
65d5c75
Refactor wiring and setup polar
thomasp85 Jul 22, 2026
aac6982
Add faceting
thomasp85 Aug 3, 2026
8185811
Bump hephaestus to get correct legend merging
thomasp85 Aug 3, 2026
4c6ec82
Support spatial
thomasp85 Aug 3, 2026
2eda04b
A bunch of small fixes
thomasp85 Aug 4, 2026
10636a9
Fix rendering of densified layers
thomasp85 Aug 4, 2026
f4de96b
Fix "side" geoms
thomasp85 Aug 10, 2026
62973f8
Fix dodging issue in violin due to dropped partition_by grouping
thomasp85 Aug 10, 2026
7d44d21
Fix temporal axes
thomasp85 Aug 10, 2026
557de39
Wire up minor breaks
thomasp85 Aug 10, 2026
310bcc6
Update docs
thomasp85 Aug 10, 2026
604e400
add --output-option and shorthands
thomasp85 Aug 10, 2026
5cbf7db
Fix bug mapping raw values to scales
thomasp85 Aug 10, 2026
728179f
Fixes based on visual inspection
thomasp85 Aug 11, 2026
9064043
Incorporate fixes from upstream hephaestus
thomasp85 Aug 11, 2026
6d4a8ef
correct stroking of area and density
thomasp85 Aug 12, 2026
2760cfa
Pad spatial bbox like VegaLite
thomasp85 Aug 12, 2026
e79c50c
Fix axis label repetition in faceted plots
thomasp85 Aug 12, 2026
7095ae5
Last round of small fixes based on visual inspection
thomasp85 Aug 13, 2026
3f904ee
remove plan file
thomasp85 Aug 13, 2026
4290723
Review fixes
thomasp85 Aug 13, 2026
054b6a9
rename to png
thomasp85 Aug 17, 2026
38d5a61
Merge branch 'main' into hephaestus-writer
thomasp85 Aug 18, 2026
13561a8
update to release hephaestus
thomasp85 Aug 18, 2026
b7a6db7
Merge branch 'hephaestus-writer' of https://github.com/posit-dev/ggsq…
thomasp85 Aug 18, 2026
62a086f
Be clear about missing rich-text areas
thomasp85 Aug 18, 2026
de4c114
ix system dep
thomasp85 Aug 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
17 changes: 17 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ jobs:
# which invokes `protoc` at build time. Required for the ADBC test path.
run: sudo apt-get install -y protobuf-compiler

- name: Install png writer system libraries
# The png writer renders via wgpu/Vello, which needs a Vulkan
# adapter. ubuntu-latest has no GPU, so install Mesa's lavapipe software
# device. If no adapter is found anyway, the writer test skips its render
# assertion gracefully rather than failing.
# Text layout goes through parley/fontique, which links the system
# fontconfig on Linux to enumerate fonts, so its development files
# (fontconfig.pc plus headers) must be present at build time.
run: sudo apt-get install -y mesa-vulkan-drivers libfontconfig1-dev

- name: Install Rust
# 1.86 is the MSRV (declared as `rust-version` in /Cargo.toml, see
# /CLAUDE.md); this sets it as the default toolchain so plain `cargo`
Expand Down Expand Up @@ -98,6 +108,13 @@ jobs:
- name: Run ADBC SQLite equivalence tests
run: cargo +stable test --features "adbc sqlite" --lib -- --ignored equivalence

- name: Run png writer tests
# Non-default feature. The hephaestus crate it renders through needs
# rustc ≥1.88 (wgpu), so it builds on +stable and is excluded from the
# 1.86 library build. Default features (incl. duckdb) supply the
# in-memory reader the test uses.
run: cargo +stable test --features png --lib writer::hephaestus

- name: Build WASM library
working-directory: ggsql-wasm/library
run: npm install && npm run build
Expand Down
132 changes: 118 additions & 14 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,114 @@
## [Unreleased]

### Added
- New `PngWriter` renders a plot to a PNG raster image via
[hephaestus](https://github.com/posit-dev/hephaestus), behind a new
off-by-default `png` feature (`--writer png` in the CLI). `LABEL caption`
and the new `minor_breaks` setting have no Vega-Lite equivalent and render
only here. Requires a working GPU adapter — hardware or software, e.g.
lavapipe — at render time.
- Writers can be configured from key–value options: `Writer::from_options` takes
a `WriterOptions` set, and the CLI collects them from a repeatable
`--writer-option key=value` flag on `exec` and `run` (short `-D`, also
spellable `--writer-options`). Several settings can be collapsed into one flag
separated by `;` — `-D 'width=1600;dpi=150'`, quoted because shells read `;`
themselves — and the two forms mix. The png writer
takes `width`, `height`, `units` (`px`, `in`, `cm`, `mm`, `pt`), `dpi`, and
`background` (any CSS color, including `transparent`), defaulting to a
1500×1000 px white canvas at 300 dpi; the Vega-Lite writer takes none. An
unknown key or unusable value is an error naming the option, not a silently
ignored setting.
- `--reader`, `--writer`, and `--output` gained the short forms `-r`, `-w`, and
`-o` on `exec` and `run`; `validate --reader` also takes `-r`.
- Text is rendered as rich text (markdown) by the png writer. A text layer's
`label` is parsed for `**bold**`, `*italic*`, `_underline_`, `~~strike~~`,
`` `code` `` and marquee-style `{selector body}` spans that set a colour or
size (`{.red hot}`, `{#0072B2 blue}`, `{.20 big}`), and so are the plot title,
subtitle, caption and axis titles set with `LABEL`. Legend titles and break
labels (axis tick labels, legend keys) do not parse yet and show their markers.
The new `parse` setting on the text layer turns it off for that layer
(`SETTING parse => false`), drawing the label exactly as given; it defaults to
`true`. Chrome text has no switch yet. The Vega-Lite writer has no rich-text
equivalent and ignores `parse`, always drawing text literally.
- New `minor_breaks` setting on continuous scales, controlling the unlabelled
subdivisions between breaks: a whole number of minor breaks *per interval between
two breaks* (`0` removes them), an array of exact positions, or — for temporal
scales — an interval such as `'week'`. Defaults to a value chosen by the
transformation. This has no Vega-Lite equivalent and is ignored by that writer;
the png writer draws them.

### Changed
- Dodging now only takes effect where groups actually meet on a position. A
layer whose grouping gives every group a position of its own — `colour` mapped
to the same column as the discrete axis, say — is drawn at its full width
instead of being squeezed into `1/n` of the band and shifted off its own
category, which made a coloured ridgeline plot (`DRAW violin SETTING side =>
'top'`) land its violins between the axis ticks or outside the panel
altogether. Groups in different facet panels don't meet either. Where any
position does hold several groups the whole layer still dodges, so an element
keeps the same slot in every position. Jitter, which dodges before jittering,
follows the same rule.
- Categorical `y` axes now run bottom-up, so the first level sits at the bottom
of the panel as it does in ggplot2. This affects every plot with a discrete or
ordinal `y` — horizontal bars, boxplots and violins by category, points and
2D jitter — and brings the Vega-Lite writer in line with the raster one, which
already read this way.
- Banded marks now measure against the full step in the VegaLite writer. A band fraction
(a bar's `width`, a dodge displacement, a jitter spread, a violin or boxplot
half-width, a discrete tile's extent) is a fraction of the whole category step,
so `width => 0.9` leaves a 10% gap — ggplot2's convention. Vega-Lite previously
subtracted its own default band padding first, making every banded mark there
narrower than the same query rendered as a raster. This applies to dodged,
jittered and half-sided layers too, where Vega-Lite reserved a further 20% of
every step: their marks were narrower, their displacements smaller, and their
category ticks pulled toward the middle of the panel.

### Fixed
- A dodged violin or half-boxplot on a categorical `y` axis is no longer flipped
in the Vega-Lite writer. Both took their band displacement from an encoding of
their own that read a ggsql offset as pointing down the screen, so their groups
came out in the opposite order to every other mark — a violin put the first
group above the second where a boxplot of the same data put it below, and a
half-boxplot's box parted company with its own whiskers once dodged. Violins
are also clipped to the panel now, as every other mark is.
- An identity-scaled column is now read exactly like the equivalent literal.
`SCALE IDENTITY <aes>` hands its values straight to the aesthetic, so they mean
what the same value written with `SETTING` means, but several were passed to the
renderer unconverted: a `size` column was read as a symbol area in pixels²
rather than the radius in points `SETTING size => 3` gives (markers far too
small), a `shape` column of names such as `'star'` made Vega-Lite fail to render
at all, and a `linetype` column of names such as `'dashed'` drew a solid line in
both writers. `size`, `linewidth`, `fontsize`, `shape` and `linetype` identity
columns now convert per row, so an identity column and a setting produce the
same drawing. A value the aesthetic already understands still passes through
untouched.
- `DRAW bar MAPPING <category> AS y` produced a single bar against a synthetic
axis instead of horizontal bars. A layer whose geom synthesises its primary
position (bar, boxplot) now transposes when the user maps a *discrete* `y`, and
stays put when they map a continuous one — that being the value axis, where a
lone `DRAW boxplot MAPPING <value> AS y` already belongs.
- `RENAMING` was ignored on a discrete or ordinal scale over a non-string domain
(`SCALE ORDINAL color RENAMING 6 => 'June'` on a numeric month), because the
break label was formatted as `6.0` while the rename was keyed on `6`.
- A temporal axis given a calendar interval (`SETTING breaks => '2 months'`) no
longer draws ticks outside its own domain. The generator steps a whole interval
past each end, and the filter that trims them back compared only plain numbers,
so a date break was never constrained at all.
- Minor breaks are no longer extrapolated beyond the outermost major break when
the majors are unevenly spaced, as they are when set by hand
(`SETTING breaks => (37, 42, 55)`). Their spacing was taken from the first
interval alone, so they matched no part of the axis. Evenly spaced majors still
extend to the edge of the range.
- `Scale::break_labels()` — what a writer reads to label an axis, colorbar or
legend tick — labels a temporal break with its own date (`1973-04-23`) instead
of the epoch number its position projects to (`1208`), and keys `RENAMING`
overrides by that same string, so a rename on a temporal scale is found rather
than missed. Numeric and categorical labels are unchanged.
- A scale with an explicit input range that no layer trains — `SCALE x FROM (0, 10)`
alongside a diagonal `rule`, whose position is deliberately kept out of scale
training — takes its type from that range (numeric or temporal → continuous,
string or boolean → discrete) instead of staying untyped, so consumers get a
fully resolved scale.
- The VS Code / Positron extension now offers its "Source Current File" button
and code cells in plain `.sql` files, so existing SQL can be run against a
ggsql kernel without renaming it. `.sql` files keep their usual SQL syntax
Expand All @@ -11,9 +118,6 @@
turns the whole behaviour off.
- The VS Code / Positron extension contributes a "ggsql File" entry to the
New File dialog.

### Fixed

- The VS Code / Positron extension now ships a language icon that renders in the
session picker, editor tabs and the Explorer. It previously pointed at a file
that did not exist, which left the icon blank.
Expand All @@ -27,8 +131,8 @@
it at zero size with nothing left to correct it. It now recovers once the
container has a real width.
- ggsql interpreter sessions in Positron now come back after an extension host
restart as well as after a window reload. A session the user renamed also
keeps its name across the restore, and ggsql runtimes are rediscovered on
restart as well as after a window reload. A session the user renamed also
keeps its name across the restore, and ggsql runtimes are rediscovered on
every window open rather than risking a stale cache hit.

## 0.4.1 - 2026-06-22
Expand All @@ -54,18 +158,18 @@
- Added `radar` setting to polar coordinates for making radar plots (#418).
- New `side` SETTING on the `boxplot` layer and the `jitter` position, mirroring
the existing `violin` setting (#439).
- New `hinge` SETTING on the `boxplot` layer, mirroring the existing `range`
- New `hinge` SETTING on the `boxplot` layer, mirroring the existing `range`
setting (#438)
- New `DRAW spatial` layer for rendering simple features (WKT/WKB) for drawing
- New `DRAW spatial` layer for rendering simple features (WKT/WKB) for drawing
maps and choropleths (#370).
- New builtin dataset `ggsql:world` for showcasing spatial examples. Data is
a subset of columns from the [Natural Earth](https://www.naturalearthdata.com/)
a subset of columns from the [Natural Earth](https://www.naturalearthdata.com/)
country data at 1:110m resolution (#370).
- New `PROJECT TO <map>` family of spatial map projections. For general
projections, one can use `PROJECT TO crs SETTING target => '+proj=...'`.
Several named projections have explicit support using e.g.
`PROJECT TO mollweide`. Works for a subset of layers, notably `spatial`,
`point`, `text`, `path`, `polygon` and `tile`. Requires a spatial backend
- New `PROJECT TO <map>` family of spatial map projections. For general
projections, one can use `PROJECT TO crs SETTING target => '+proj=...'`.
Several named projections have explicit support using e.g.
`PROJECT TO mollweide`. Works for a subset of layers, notably `spatial`,
`point`, `text`, `path`, `polygon` and `tile`. Requires a spatial backend
like PostGIS, SpatiaLite, or DuckDB spatial extension (#455).

### Fixed
Expand All @@ -76,7 +180,7 @@
- Dodging of horizontal violin plots were broken due to a bad orientation
assumption in the VegaLite writer. We now correctly use the orientation to
dodge in the correct dimension (#439).
- Fixed misbehaviour of numeric scale's `RENAMING` clause due to pre-formatting
- Fixed misbehaviour of numeric scale's `RENAMING` clause due to pre-formatting
issues (#461)

### Changed
Expand Down
9 changes: 5 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ The Cargo workspace (`/Cargo.toml`) has five members: `tree-sitter-ggsql`, `src`

```
ggsql query ──► parser ──► Plot AST ──► executor ──► Spec ──► writer ──► output
(tree-sitter) (Reader runs SQL, (Vega-Lite JSON)
applies stats,
(tree-sitter) (Reader runs SQL, (Vega-Lite JSON
applies stats, or PNG)
resolves scales)
```

- The parser splits the query at the `VISUALISE` boundary. SQL goes to a pluggable `Reader` (DuckDB, SQLite, ODBC); the VISUALISE part becomes a typed `Plot`.
- The executor ties the two together: SQL → DataFrame, AST resolved against actual schema, stats and scales applied per layer.
- The writer renders the resolved `Spec` to an output format (today: Vega-Lite JSON).
- The writer renders the resolved `Spec` to an output format: Vega-Lite JSON (default), or PNG (non-default `png` feature). The PNG writer is implemented on top of the hephaestus renderer — a name that stays internal; users see `png`.

For details — module layout, traits, where extension points live — see [`src/CLAUDE.md`](src/CLAUDE.md). For the Vega-Lite renderer specifically, [`src/writer/vegalite/CLAUDE.md`](src/writer/vegalite/CLAUDE.md). For the AST types, [`src/plot/CLAUDE.md`](src/plot/CLAUDE.md).
For details — module layout, traits, where extension points live — see [`src/CLAUDE.md`](src/CLAUDE.md). For a specific renderer, [`src/writer/vegalite/CLAUDE.md`](src/writer/vegalite/CLAUDE.md) (Vega-Lite) or [`src/writer/hephaestus/CLAUDE.md`](src/writer/hephaestus/CLAUDE.md) (PNG). For the AST types, [`src/plot/CLAUDE.md`](src/plot/CLAUDE.md).

## Building

Expand Down Expand Up @@ -117,6 +117,7 @@ Per-folder CLAUDE.md files cover component-specific test guidance.
- *How does the parser work? How is a `Plot` built?* → [`src/CLAUDE.md`](src/CLAUDE.md), then `src/parser/`.
- *How do I add a new geom / scale type / coord?* → [`src/plot/CLAUDE.md`](src/plot/CLAUDE.md).
- *How does Vega-Lite output get assembled?* → [`src/writer/vegalite/CLAUDE.md`](src/writer/vegalite/CLAUDE.md).
- *How does the raster (PNG) writer work?* → [`src/writer/hephaestus/CLAUDE.md`](src/writer/hephaestus/CLAUDE.md), which also lists its known gaps.
- *How does a query become rendered output end-to-end?* → [`src/CLAUDE.md`](src/CLAUDE.md) (execution pipeline), then `src/execute/`.
- *How does the Jupyter kernel route messages?* → [`ggsql-jupyter/CLAUDE.md`](ggsql-jupyter/CLAUDE.md).
- *How does the VS Code / Positron extension talk to the kernel?* → [`ggsql-vscode/CLAUDE.md`](ggsql-vscode/CLAUDE.md).
Expand Down
Loading
Loading