obsplot Gallery : facets and transforms

This document is an attempt to reproduce some of the charts shown in Observable Plot documentation notebooks.

As always, there are several ways to do things, in particular data manipulation could be done either in R before plotting, or in JavaScript via transforms and JS() calls. Each example below is one way to achieve a given result, not necessarily the best one or the most elegant.

Facets

data(anscombe_obs)

obsplot(anscombe_obs, height = 240) |>
    mark_frame() |>
    mark_dot(x = x, y = y) |>
    facet(x = series) |>
    opts(grid = TRUE, inset = 10)
data(barley)

obsplot(barley, height = 800) |>
    mark_frame() |>
    mark_dot(
        x = yield, y = variety, stroke = year,
        sort = list(fy = "x", y = "x", reduce = "median", reverse = TRUE)
    ) |>
    facet(y = site, marginRight = 90) |>
    scale_color(type = "categorical") |>
    opts(marginLeft = 110, grid = TRUE)
library(palmerpenguins)
data(penguins)

obsplot(penguins, height = 600) |>
    mark_frame() |>
    mark_dot(penguins, x = bill_depth_mm, y = bill_length_mm, r = 2, fill = "#ddd") |>
    mark_dot(x = bill_depth_mm, y = bill_length_mm) |>
    facet(x = sex, y = species, marginRight = 80) |>
    opts(grid = TRUE)

Group transform

data(penguins)

obsplot(penguins) |>
    mark_barY(
        transform_groupX(y = "count", x = species)
    ) |>
    mark_ruleY(0) |>
    scale_y(grid = TRUE)
species_domain <- names(sort(table(penguins$species)))

obsplot(penguins) |>
    mark_barY(
        transform_groupX(y = "count", x = species)
    ) |>
    mark_ruleY(0) |>
    scale_x(domain = species_domain, reverse = TRUE) |>
    scale_y(grid = TRUE)
obsplot(penguins) |>
    mark_barY(
        transform_groupX(y = "proportion", x = species)
    ) |>
    mark_ruleY(0) |>
    scale_y(grid = TRUE, percent = TRUE)
penguins$body_mass_kg <- penguins$body_mass_g / 1000

obsplot(penguins) |>
    mark_barY(
        transform_groupX(y = "sum", x = species, y = body_mass_kg)
    ) |>
    mark_ruleY(0) |>
    scale_y(grid = TRUE, label = "↑ Total mass (kg)")
obsplot(penguins, height = 170) |>
    mark_dot(y = species, x = body_mass_g) |>
    mark_ruleY(
        transform_groupY(
            list(x1 = "min", x2 = "max"), 
            y = species, x = body_mass_g
        )
    ) |>
    mark_tickX(
        transform_groupY(
            list(x = "median"), 
            y = species, x = body_mass_g, stroke = "red"
        )
    ) |>
    scale_x(inset = 6) |>
    scale_y(label = NULL) |>
    opts(marginLeft = 60)
data(mobydick1)

obsplot(mobydick1) |>
    mark_barY(
        transform_groupX(y = "count")
    ) |>
    mark_ruleY(0) |>
    scale_y(grid = TRUE)
obsplot(mobydick1) |>
    mark_barY(
        transform_groupX(y = "proportion", filter = JS('d => /[AEIOUY]/.test(d)'))
    ) |>
    mark_ruleY(0) |>
    scale_y(grid = TRUE, percent = TRUE)
data(penguins)

obsplot(penguins) |>
    mark_barY(
        transform_groupX(y = "count", x = sex)
    ) |>
    mark_ruleY(0) |>
    facet(x = species) |>
    scale_x(tickFormat = JS('d => d === null ? "N/A" : d')) |>
    scale_y(grid = TRUE)
obsplot(penguins) |>
    mark_barY(
        transform_groupX(y = "proportion-facet", x = sex)
    ) |>
    mark_ruleY(0) |>
    facet(x = species) |>
    scale_x(tickFormat = JS('d => d === null ? "N/A" : d')) |>
    scale_y(grid = TRUE, percent = TRUE)
obsplot(penguins) |>
    mark_barY(
        transform_groupX(y = "count", x = species, fill = sex)
    ) |>
    mark_ruleY(0) |>
    scale_y(grid = TRUE)
obsplot(penguins, height = 60) |>
    mark_barX(
        transform_stackX(
            transform_groupZ(x = "proportion", fill = sex)
        )
    ) |>
    mark_text(
        transform_stackX(
            transform_groupZ(
                list(x = "proportion", text = "first"),
                z = sex, text = sex)
        )
    ) |>
    mark_ruleX(c(0, 1)) |>
    scale_x(percent = TRUE)
obsplot(penguins) |>
    mark_barY(
        transform_groupZ(y = "proportion-facet", fill = sex)
    ) |>
    mark_ruleY(c(0, 1)) |>
    facet(x = species) |>
    scale_y(grid = TRUE, percent = TRUE)
data(seattle)

obsplot(seattle, height = 300) |>
    mark_cell(
        transform_group(
            fill = "max",
            x = JS('d => d.date.getUTCDate()'),
            y = JS('d => d.date.getUTCMonth()'),
            fill = temp_max,
            inset = 0.5
        )
    ) |>
    scale_color(scheme = "BuRd") |>
    scale_y(tickFormat = JS('Plot.formatMonth()')) |>
    opts(padding = 0)

Bin transform

data(athletes)
# Only keep used variables
athletes <- athletes[, c("weight", "height", "sex", "sport")]

obsplot(athletes) |>
    mark_rectY(
        transform_binX(y = "count", x = weight)
    ) |>
    mark_ruleY(0) |>
    scale_y(grid = TRUE)
obsplot(athletes) |>
    mark_rectY(
        transform_binX(y = "count", x = weight, inset = 0)
    ) |>
    mark_ruleY(0) |>
    scale_x(round = TRUE) |>
    scale_y(grid = TRUE)
obsplot(athletes) |>
    mark_areaY(
        transform_binX(y = "count", x = weight, fill = "#ccc")
    ) |>
    mark_lineY(
        transform_binX(y = "count", x = weight)
    ) |>
    mark_ruleY(0) |>
    scale_y(grid = TRUE)
obsplot(athletes) |>
    mark_rectY(
        transform_binX(y = "count", x = weight, thresholds = "sturges")
    ) |>
    mark_ruleY(0) |>
    scale_y(grid = TRUE)
obsplot(athletes) |>
    mark_rectY(
        transform_binX(y = "count", x = weight, cumulative = TRUE)
    ) |>
    mark_ruleY(0) |>
    scale_y(grid = TRUE)
obsplot(athletes) |>
    mark_rectY(
        transform_binX(
            y2 = "count", 
            x = weight, fill = sex, mixBlendMode = "multiply"
        )
    ) |>
    mark_ruleY(0) |>
    scale_y(grid = TRUE)
obsplot(athletes) |>
    mark_rectY(
        transform_binX(y = "count", x = weight, fill = sex)
    ) |>
    mark_ruleY(0) |>
    scale_y(grid = TRUE)
obsplot(athletes, height = 60) |>
    mark_barX(
        transform_binX(fill = "count", x = weight)
    ) |>
    scale_x(round = TRUE) |>
    scale_color(scheme = "YlGnBu")
obsplot(athletes) |>
    mark_rect(
        transform_bin(fill = "count", x = weight, y = height, inset = 0)
    ) |>
    scale_color(scheme = "YlGnBu") |>
    opts(round = TRUE, grid = TRUE)
obsplot(athletes) |>
    mark_rect(
        transform_bin(
            fillOpacity = "count", 
            x = weight, y = height, fill = sex, inset = 0
        )
    ) |>
    opts(round = TRUE, grid = TRUE)
obsplot(athletes) |>
    mark_dot(
        transform_bin(r = "count", x = weight, y = height)
    ) |>
    scale_r(range = c(0, 10)) |>
    opts(round = TRUE, grid = TRUE)
obsplot(athletes) |>
    mark_dot(
        transform_bin(r = "count", x = weight, y = height, stroke = sex)
    ) |>
    scale_r(range = c(0, 10)) |>
    opts(round = TRUE, grid = TRUE)
obsplot(athletes, height = 60) |>
    mark_dot(
        transform_binX(r = "count", x = weight)
    ) |>
    scale_r(range = c(0, 14))
sports_by_weight <- levels(reorder(athletes$sport, athletes$weight, median, na.rm = TRUE))

obsplot(athletes, height = 600) |>
    mark_barX(
        transform_binX(fill = "proportion-facet", x = weight, inset = 0.5)
    ) |>
    facet(marginLeft = 100, y = sport) |>
    scale_color(scheme = "YlGnBu") |>
    scale_x(round = TRUE, grid = TRUE) |>
    scale_fy(domain = sports_by_weight, label = NULL) |>
    opts(marginLeft = 100, padding = 0)

Stack transform

data(crimea)

obsplot(crimea) |>
    mark_lineY(x = date, y = deaths, stroke = cause) |>
    mark_ruleY(0) |>
    scale_y(grid = TRUE)
obsplot(crimea) |>
    mark_areaY(x = date, y2 = deaths, fill = cause, mixBlendMode = "multiply") |>
    mark_ruleY(0) |>
    scale_y(grid = TRUE)
obsplot(crimea) |>
    mark_areaY(x = date, y = deaths, fill = cause) |>
    mark_ruleY(0) |>
    scale_y(grid = TRUE)
obsplot(crimea) |>
    mark_barY(x = date, y = deaths, fill = cause) |>
    mark_ruleY(0) |>
    scale_x(label = NULL, tickFormat = JS('d => d.toLocaleString("en", {month: "narrow"})'))
data(unemployment)

obsplot(unemployment) |>
    mark_areaY(
        transform_stackY(x = date, y = unemployed, fill = industry, title = industry)
    ) |>
    mark_ruleY(0) |>
    scale_y(grid = TRUE, label = '↑ Unemployed (thousands)')
obsplot(unemployment) |>
    mark_areaY(
        transform_stackY(offset = "silhouette", x = date, y = unemployed, fill = industry)
    ) |>
    scale_y(grid = TRUE, label = '↑ Unemployed (thousands)')
obsplot(unemployment) |>
    mark_areaY(
        transform_stackY(offset = "wiggle", x = date, y = unemployed, fill = industry)
    ) |>
    scale_y(grid = TRUE, label = '↑ Unemployed (thousands)')
obsplot(unemployment) |>
    mark_areaY(
        transform_stackY(
            curve = "catmull-rom", 
            x = date, y = unemployed, fill = industry, order = value
        )
    ) |>
    mark_ruleY(0) |>
    scale_y(grid = TRUE, label = '↑ Unemployed (thousands)')
data(riaa)

xy <- list(x = "year", y = "revenue", z = "format", order = "appearance", reverse = TRUE)
obsplot(riaa) |>
    mark_areaY(
        transform_stackY(
            append(
                xy,
                list(fill = "group", title = JS('d => `${d.format}\n${d.group}`'))
            )
        )
    ) |>
    mark_lineY(
        transform_stackY1(
            append(
                xy,
                list(stroke = "white", strokeWidth = 1)
            )
        )
    ) |>
    mark_ruleY(0) |>
    scale_y(
        grid = TRUE, label = "↑ Annual revenue (billions, adj.)", 
        transform = JS('d => d / 1000')
    )
xy <- list(
    x = "year", y = "revenue", z = "format", 
    order = "appearance", reverse = TRUE, offset = "expand"
)
obsplot(riaa) |>
    mark_areaY(
        transform_stackY(
            append(
                xy,
                list(fill = "group", title = JS('d => `${d.format}\n${d.group}`'))
            )
        )
    ) |>
    mark_lineY(
        transform_stackY1(
            append(
                xy,
                list(stroke = "white", strokeWidth = 1)
            )
        )
    ) |>
    mark_ruleY(c(0, 1)) |>
    scale_y(
        grid = TRUE, label = "↑ Annual revenue (billions, adj.)", 
        transform = JS('d => d / 1000')
    )
data(congress)

obsplot(congress, height = 300) |>
    mark_dot(
        transform_stackY2(
            x = JS('d => 2021 - d.birth'),
            y = JS('d => d.gender === "M" ? 1 : d.gender === "F" ? -1 : 0'),
            fill = gender
        )
    ) |>
    mark_ruleY(0) |>
    scale_x(label = "Age →", nice = TRUE) |>
    scale_y(
        grid = TRUE, label = "← Women · Men →",
        labelAnchor =  "center",
        tickFormat = JS('Math.abs')
    )
obsplot(congress, height = 280) |>
    mark_barX(
        transform_stackX(
            transform_groupZ(x = "proportion-facet", fill = gender)
        )
    ) |>
    mark_text(
        transform_stackX(
            transform_groupZ(
                list(x = "proportion-facet", text = "first"),
                z = gender,
                text = JS('d => d.gender === "F" ? "Women" : d.gender === "M" ? "Men" : null')
            )
        )
    ) |>
    mark_ruleX(c(0, 1)) |>
    facet(y = JS('d => Math.floor((2021 - d.birth) / 10) * 10')) |>
    scale_x(percent = TRUE)
data(iowa)

obsplot(iowa) |>
    mark_areaY(
        transform_stackY(x = year, y = net_generation, fill = source, title = source)
    ) |>
    mark_ruleY(0) |>
    scale_y(
        grid = TRUE,  label = "↑ Net generation (million MWh)", 
        transform = JS('d => d / 1000')
    )

Select transform

data(stocks)

obsplot(stocks) |>
    mark_line(x = Date, y = Close, stroke = Symbol) |>
    mark_text(
        transform_selectLast(
            x = Date, y = Close, stroke = Symbol,
            text = Symbol, textAnchor = "start", dx = 3
        )
    ) |>
    scale_y(grid = TRUE, label = "↑ Price ($)") |>
    opts(marginRight = 40)

Map transform

values <- rnorm(500)

obsplot(values, height = 200) |>
    mark_lineY(transform_map(list(y = "cumsum"), y = values))
obsplot(values, height = 200) |>
    mark_lineY(transform_mapY("cumsum", y = values))
data(sftemp)

obsplot(sftemp) |>
    mark_areaY(x = date, y1 = low, y2 = high, curve = "step", fill = "#ccc") |>
    mark_line(
        transform_windowY(x = date, y = low, k = 7, curve = "step", stroke = "blue")
    ) |>
    mark_line(
        transform_windowY(x = date, y = high, k = 7, curve = "step", stroke = "red")
    ) |>
    scale_y(grid = TRUE, label = "↑ Daily temperature range (°F)")
data(stocks)

obsplot(stocks) |>
    mark_ruleY(1) |>
    mark_line(transform_normalizeY(x = Date, y = Close, stroke = Symbol)) |>
    mark_text(
        transform_selectLast(
            transform_normalizeY(
                x = Date, y = Close, stroke = Symbol,
                text = Symbol, textAnchor = "start", dx = 3
            )
        )
    ) |>
    scale_y(
        type = "log", grid = TRUE, label = "↑ Change in price (%)",
        tickFormat = JS("x => d3.format('+d')((x - 1) * 100)")
    ) |>
    opts(marginRight = 40)
data(stateage)

# Get states order by proportion of >=80
library(dplyr)
state_order <- stateage |>
    group_by(name) |>
    mutate(
        total_pop = sum(population),
        prop = population / total_pop
    ) |>
    filter(age == "≥80") |>
    arrange(desc(prop))

xy <- transform_normalizeX(basis = "sum", z = name, x = population, y = name)

obsplot(stateage, height = 660) |>
    mark_ruleX(x = 0) |>
    mark_ruleY(
        transform_groupY(list(x1 = "min", x2 = "max"), xy)
    ) |>
    mark_dot(xy, fill = age, title = age) |>
    mark_text(
        transform_selectMinX(xy), textAnchor = "end", dx = -6, text = name
    ) |>
    scale_x(axis = "top",   label =  "Percent (%) →", transform = JS("d => d * 100")) |>
    scale_y(
        domain = state_order$name,
        axis = NULL
    ) |>
    scale_color(scheme = "spectral", domain = unique(stateage$age)) |>
    opts(grid = TRUE)