A grammar of graphics for SQL

Thomas Lin Pedersen

25th September 2026

Why am I speaking to you?

  • Software engineer at Posit, PBC
  • If you have created graphics in R, my code has probably been involved
  • I’m creating something new and exciting

+ SQL =

Why am I speaking to you?

Why ggsql?

Why ggsql?

  • SQL is hugely influential and still heavily used
  • Many SQL-first or SQL-only data scientists and analysts
  • They are massively under-served in tooling and visualization
  • GoG and SQL just fits so good together that it would be criminal not to

A bit about SQL

  • One of the oldest programming language still in use
  • Based on Relational Algebra (same theoretical backbone as dplyr)
  • ANSI SQL (the standard) has been joined by many dialects
  • DECLARATIVE
  • TABULAR

A bit about grammar of graphics

  • A theoretical deconstruction of standard plots into core parts
  • A theory with many implementations (ggplot2, Vega, D3)
  • DECLARATIVE
  • TABULAR

A bit about grammar of graphics

A bit about grammar of graphics

  • Raw data you want to plot
  • In tabular form, one record per row (tidy format)

A bit about grammar of graphics

  • How columns relate to visual aesthetics
  • How columns relate to statistical properties

A bit about grammar of graphics

  • Transformation of observation into derived statistics
  • E.g. binning for a histogram

A bit about grammar of graphics

  • Mapping of measured values to the aesthetic domain
  • Definition of mapping function

A bit about grammar of graphics

  • How to visually represent data
    • point
    • rectangle
    • area

A bit about grammar of graphics

  • Which subset(s) of data to show
  • and how…

A bit about grammar of graphics

  • How the positional aesthetics relate spatially
    • cartesian
    • polar
    • ternary

A bit about grammar of graphics

  • Non-data-driven styling
    • Title font
    • Panel background color
    • Grid line width

SQL and GoG

Declarative

Modular

Composable

Speakable

SQL and GoG

  • These qualities makes for exceptional software
  • They are good for humans
  • They are good for LLMs

Demo time

  Data

SELECT * FROM ggsql:penguins
LIMIT 14
species island bill_len bill_dep flipper_len body_mass sex year
Adelie Torgersen 39.1 18.7 181 3750 male 2007
Adelie Torgersen 39.5 17.4 186 3800 female 2007
Adelie Torgersen 40.3 18 195 3250 female 2007
Adelie Torgersen null null null null null 2007
Adelie Torgersen 36.7 19.3 193 3450 female 2007
Adelie Torgersen 39.3 20.6 190 3650 male 2007
Adelie Torgersen 38.9 17.8 181 3625 female 2007
Adelie Torgersen 39.2 19.6 195 4675 male 2007
Adelie Torgersen 34.1 18.1 193 3475 null 2007
Adelie Torgersen 42 20.2 190 4250 null 2007
Adelie Torgersen 37.8 17.1 186 3300 null 2007
Adelie Torgersen 37.8 17.3 180 3700 null 2007
Adelie Torgersen 41.1 17.6 182 3200 female 2007
Adelie Torgersen 38.6 21.2 191 3800 male 2007

  Mappings

SELECT * FROM ggsql:penguins
VISUALIZE 
  bill_len AS x, 
  bill_dep AS y, 
  species AS fill
DRAW point

/*
penguins |>
  ggplot(
    aes(
      x = bill_len, 
      y = bill_dep, 
      color = species
    )
  ) + 
  geom_point()
*/

  Statistics

SELECT * FROM ggsql:penguins
VISUALIZE 
  species AS x, 
  body_mass AS y,
  species AS fill
DRAW boxplot

/*
penguins |>
  ggplot(
    aes(
      x = species, 
      y = body_mass, 
      fill = species
    )
  ) + 
  geom_boxplot()
*/

  Statistics

SELECT * FROM ggsql:penguins
VISUALIZE 
  bill_dep AS x, 
  bill_len AS y
DRAW point
  MAPPING species AS fill
DRAW point
  SETTING 
    aggregate => 'mean',
    fill => 'red',
    size => 15
  PARTITION BY species

  Statistics

SELECT * FROM ggsql:penguins
VISUALIZE 
  bill_dep AS x, 
  bill_len AS y
DRAW point
  MAPPING species AS fill
DRAW range
  MAPPING bill_len AS ymin, bill_len AS ymax
  SETTING 
    aggregate => ('x:mean', 'ymin:min', 'ymax:max')
  PARTITION BY species

  Scales

SELECT * FROM ggsql:penguins
VISUALIZE 
  bill_dep AS x, 
  bill_len AS y,
  body_mass AS size
DRAW point
SCALE size FROM (0, null) TO (0, 10)

/*
penguins |>
  ggplot(
    aes(
      x = bill_dep, 
      y = bill_len, 
      size = body_mass
    )
  ) + 
  geom_point() + 
  scale_size(
    limits = c(0, NA), 
    range = c(0, 10)
  )
*/

  Geometries

SELECT * FROM ggsql:penguins
VISUALIZE 
  species AS x, 
  body_mass AS y,
  species AS fill
DRAW boxplot
  SETTING side => 'left'
DRAW violin
  SETTING side => 'right'
DRAW point
  MAPPING null AS fill
  SETTING 
    position => 'jitter',
    distribution => 'density',
    side => 'right',
    size => 1

  Facets

SELECT * FROM ggsql:penguins
VISUALIZE 
  bill_len AS x, 
  bill_dep AS y, 
  species AS fill
DRAW point
FACET island

  Facets

SELECT * FROM ggsql:penguins
VISUALIZE 
  bill_len AS x, 
  bill_dep AS y, 
  species AS fill
DRAW point
FACET body_mass
SCALE BINNED panel
  SETTING breaks => (2000, 3000, 4000, 5000, 6000, 7000)

  Coordinates

SELECT * FROM ggsql:penguins
VISUALIZE species AS fill
DRAW bar
PROJECT TO polar

  Coordinates

INSTALL spatial;
WITH data(island, lat, lon) AS (
  VALUES
    ('Torgersen', -64.77, -64.08),
    ('Dream',    -64.73, -64.23),
    ('Biscoe',   -65.43, -65.50)
)
SELECT * FROM data

VISUALIZE *
DRAW spatial
  MAPPING FROM ggsql:world
DRAW point
  MAPPING island AS fill
PROJECT TO orthographic
  SETTING 
    origin => (-64.8, -65.1),
    bounds => (-150000, -150000, 150000, 150000)

  Theme

TBD, but…

  Theme

VISUALIZE species AS fill, species AS x FROM ggsql:penguins
DRAW bar
SCALE fill TO ('steelblue', 'goldenrod', 'forestgreen')
LABEL 
  title => 'Distribution of penguin (*Pygoscelis*) species',
  subtitle => 'This plot focuses on 3 species: {.steelblue *P. adeliae*}, {.goldenrod *P. antarcticus*}, {.forestgreen *P. papua*}'

So, what is this witchcraft actually?

  • ggsql is written in pure Rust
  • We currently provide:
    • A CLI
    • A Positron/VS Code extension
    • A Jupyter kernel
    • R and Python bindings
    • A DuckDB extension
    • A WebAssembly binary

ggsql in Positron

  • We are leveling up our SQL love in Positrion
  • Positron will be the best place to run SQL
  • ggsql is a big part of that equation

ggsql in Quarto/Jupyter

  • The Jupyter kernel makes ggsql work automatically
  • The R bindings provide a knitr engine
    • Run R, Python, and ggsql in the same document
    • Share data automatically between the processes

My data lives in PostgreSQL

Now what?

  • ggsql is built to interact directly with your database
  • Can interact with most through ODBC/ADBC driver support
  • (All) computations are send to the backend
  • Only data needed for plotting is returned

We want to meet you were your data is

What’s in the future?

  • gt inspired table formatting
  • patchwork inspired plot composition
  • Interactivity

Questions

…or perhaps a demo of something cool