Skip to content

Get started

This guide introduces the SGL language and the sglduck package.

Setup

The examples use an in-memory DuckDB database loaded with the bundled cars and trees datasets. db_get_plot returns an SglPlot; for brevity this guide wraps it in a small render helper that displays the plot (in your own code you would use plot.show(), or display the plot in a Jupyter notebook).

import duckdb
import sglduck

con = duckdb.connect()
con.register("cars", sglduck.data.cars())
con.register("trees", sglduck.data.trees())


def render(sgl):
    print(sglduck.db_get_plot(con, sgl).to_svg())

The SGL language

The from clause

The from keyword precedes a data source, often the name of a table. Here we use the cars table.

render("""
    visualize
        horsepower as x,
        miles_per_gallon as y
    from cars
    using points
""")
40 60 80 100 120 140 160 180 200 220 10 15 20 25 30 35 40 45 miles_per_gallon horsepower

This resembles SQL's from, except that only a single data source is allowed. If data from multiple sources or pre-processing is needed, a SQL subquery can be provided:

render("""
    visualize
        horsepower as x,
        miles_per_gallon as y
    from (
        select *
        from cars
        where origin = 'Japan'
    )
    using points
""")
50 60 70 80 90 100 110 120 130 20 25 30 35 40 45 miles_per_gallon horsepower

The using clause

The using keyword precedes the geometric object(s) that represent the data. Following ggplot2 terminology, these are referred to as geoms. The examples above represent data with point geoms.

The visualize clause

The visualize keyword precedes the aesthetic-to-column mapping, which maps perceivable traits of the geoms to data source columns. The prior examples mapped the x and y positions of point geoms; aesthetics may also be non-positional, such as color. visualize most closely resembles SQL's select.

render("""
    visualize
        horsepower as x,
        miles_per_gallon as y,
        origin as color
    from cars
    using points
""")
50 100 150 200 10 15 20 25 30 35 40 45 miles_per_gallon horsepower origin USA Europe Japan

Column-level transformations and aggregations

SGL supports column-level transformations and aggregations. Below, a binning transformation is combined with a count aggregation to produce a histogram of miles_per_gallon. As in SQL, aggregation groupings are specified in a group by clause.

render("""
    visualize
        bin(miles_per_gallon) as x,
        count(*) as y
    from cars
    group by
        bin(miles_per_gallon)
    using bars
""")
10 15 20 25 30 35 40 45 0 5 10 15 20 25 30 35 count(*) bin(miles_per_gallon)

SGL's column-level transformations and aggregations are performed after scaling, which cannot easily be replicated in SQL. Below, binning and counting are applied after log scaling, producing a log-scaled histogram:

render("""
    visualize
        bin(miles_per_gallon) as x,
        count(*) as y
    from cars
    group by
        bin(miles_per_gallon)
    using bars
    scale by
        log(x)
""")
8 10 13 16 20 25 32 40 0 5 10 15 20 25 count(*) bin(miles_per_gallon)

The collect by clause

A geom is individual if it represents each record by a distinct object, and collective if it represents multiple records by one object. Points and lines are individual and collective, respectively — here the same data is shown with each:

render("""
    visualize
        year as x,
        avg(miles_per_gallon) as y
    from cars
    group by
        year
    using points
""")
1,970 1,972 1,974 1,976 1,978 1,980 1,982 18 20 22 24 26 28 30 32 34 avg(miles_per_gallon) year
render("""
    visualize
        year as x,
        avg(miles_per_gallon) as y
    from cars
    group by
        year
    using line
""")
1,970 1,972 1,974 1,976 1,978 1,980 1,982 18 20 22 24 26 28 30 32 34 avg(miles_per_gallon) year

For collective geoms, the records collected into each object are chosen automatically by default. The collect by clause overrides this — it is like group by, but defines collections to represent by one object rather than groups to aggregate. Below, the default collection is not ideal, followed by an explicit collection:

render("""
    visualize
        age as x,
        circumference as y
    from trees
    using line
""")
200 400 600 800 1,000 1,200 1,400 1,600 40 60 80 100 120 140 160 180 200 220 circumference age
render("""
    visualize
        age as x,
        circumference as y
    from trees
    collect by
        tree_id
    using lines
""")
200 400 600 800 1,000 1,200 1,400 1,600 40 60 80 100 120 140 160 180 200 220 circumference age

Geom qualifiers

Geom qualifiers modify how geoms positionally represent data, written as keywords before the geom name in the using clause. Statistical qualifiers apply a statistical transformation, such as linear regression:

render("""
    visualize
        age as x,
        circumference as y
    from trees
    using regression line
""")
200 400 600 800 1,000 1,200 1,400 1,600 40 60 80 100 120 140 160 180 circumference age

Collision qualifiers adjust the positions of overlapping objects. Below, the jittered qualifier adds a small amount of random variation so overlapping points are discernible:

render("""
    visualize
        origin as x,
        miles_per_gallon as y
    from cars
    using jittered points
""")
USA Europe Japan 10 15 20 25 30 35 40 45 miles_per_gallon origin

The layer operator

A graphic may combine multiple layers of geoms with the layer operator. For example, layering a regression line on a scatterplot:

render("""
    visualize
        horsepower as x,
        miles_per_gallon as y
    from cars
    using points

    layer

    visualize
        horsepower as x,
        miles_per_gallon as y
    from cars
    using regression line
""")
40 60 80 100 120 140 160 180 200 220 5 10 15 20 25 30 35 40 45 miles_per_gallon horsepower

When layers share a data source and aesthetic mapping, the layer operator can be applied directly to geom expressions to reduce verbosity:

render("""
    visualize
        horsepower as x,
        miles_per_gallon as y
    from cars
    using (
        points
        layer
        regression line
    )
""")
40 60 80 100 120 140 160 180 200 220 5 10 15 20 25 30 35 40 45 miles_per_gallon horsepower

A graphic has a single scale for each aesthetic across all layers, so an aesthetic must be mapped to a consistent type in every layer where it appears.

The scale by clause

Each mapped aesthetic has a scale that determines how data values map to the visual property. Scales are implicit by default but can be set in the scale by clause. Here log scales override the default linear scaling on x and y:

render("""
    visualize
        horsepower as x,
        miles_per_gallon as y
    from cars
    using (
        points
        layer
        regression line
    )
    scale by
        log(x), log(y)
""")
40 50 63 79 100 126 158 200 8 10 13 16 20 25 32 40 miles_per_gallon horsepower

Scaling is performed prior to column-level transformations, aggregations, and geom-qualifier adjustments — so the regression above is computed on the log-scaled values. Scaling functions like log apply to aesthetic names rather than column names, because they modify aesthetic scales rather than the data.

Coordinate systems

The coordinate system is inferred from the positional aesthetics: x and y imply Cartesian coordinates, while theta and r imply polar coordinates. In the grammar of graphics, a pie chart is a stacked bar chart in polar coordinates — and the bar geom is stacked by default. The same data, in Cartesian and polar coordinates:

render("""
    visualize
        count(*) as y,
        origin as color
    from cars
    group by
        origin
    using bars
""")
0 50 100 150 200 250 300 350 400 count(*) origin USA Europe Japan
render("""
    visualize
        count(*) as theta,
        origin as color
    from cars
    group by
        origin
    using bars
""")
0 50 100 150 200 250 300 350 400 count(*) origin USA Europe Japan

The facet by clause

Faceting generates small multiples, each panel a partition of the source data by the unique values of the facet by expressions. A single expression generates horizontal panels by default, which an orientation keyword can change:

render("""
    visualize
        horsepower as x,
        miles_per_gallon as y
    from cars
    using points
    facet by
        origin
""")
50 100 150 200 10 15 20 25 30 35 40 45 Europe 50 100 150 200 Japan 50 100 150 200 USA miles_per_gallon horsepower
render("""
    visualize
        horsepower as x,
        miles_per_gallon as y
    from cars
    using points
    facet by
        origin vertically
""")
10 20 30 40 Europe 10 20 30 40 Japan 40 60 80 100 120 140 160 180 200 220 10 20 30 40 USA miles_per_gallon horsepower

Two facet expressions place one horizontally and the other vertically:

render("""
    visualize
        horsepower as x,
        miles_per_gallon as y
    from (
        select
            *,
            case
                when year < 1977 then '< 1977'
                else '>= 1977'
            end as 'era'
        from cars
    )
    using points
    facet by
        era,
        origin
""")
10 20 30 40 < 1977 >= 1977 Europe 10 20 30 40 Japan 50 100 150 200 10 20 30 40 50 100 150 200 USA miles_per_gallon horsepower

The title clause

Aesthetic-scale titles are determined automatically from the mappings, but can be overridden in the title clause:

render("""
    visualize
        horsepower as x,
        miles_per_gallon as y
    from cars
    using points
    title
        x as 'Horsepower',
        y as 'Miles Per Gallon'
""")
40 60 80 100 120 140 160 180 200 220 10 15 20 25 30 35 40 45 Miles Per Gallon Horsepower

Next steps