Skip to contents

Everything so far has been about mapping data. This article is about the rest: the panel, the gridlines, the fonts, and a set of render effects that change how marks are painted. vellumplot keeps these separate. A theme controls the non-data furniture of the whole plot; a layer effect changes how one mark is drawn; a gradient is a fancy fill value; and sketch mode turns the whole thing hand-drawn.

Built-in themes

A theme sets the look of everything that is not a mark. vellumplot ships several. theme_gray() is the default (grey panel, white gridlines); theme_minimal() drops the panel fill; theme_bw() is a white panel with light grey gridlines; theme_classic() gives axis lines and no gridlines; theme_void() strips everything but the marks, legend, and titles.

vplot(mtcars) |>
  mark_point(x = wt, y = mpg, color = factor(cyl)) |>
  theme_minimal()
A scatter plot. It plots mpg (vertical axis) against wt (horizontal axis), where colour shows factor(cyl). Based on 32 observations.152025302345mpgwtfactor(cyl)468

Customising with theme elements

theme() overrides individual slots, and each slot is described by a typed element: element_text(), element_line(), element_rect(), or element_blank() to draw nothing. Slot names follow the familiar dotted scheme (panel.grid.minor, axis.title, plot.title, and so on), and any property left NULL is inherited from its parent in the theme tree.

vplot(mtcars) |>
  mark_point(x = wt, y = mpg) |>
  theme_bw() |>
  theme(
    panel.grid.minor = element_blank(),
    plot.title = element_text(size = 16, face = "bold"),
    axis.title = element_text(color = "grey30")
  ) |>
  labs(title = "Fuel economy")
Fuel economy A scatter plot. It plots mpg (vertical axis) against wt (horizontal axis). Based on 32 observations.152025302345mpgwtFuel economy

Rotating and wrapping axis labels

Long category names on a discrete axis are a perennial nuisance: side by side they collide. Two theme controls handle this. Setting an angle on axis.text.x rotates the tick labels, and the gutter reserves the extra height automatically; an explicit hjust/vjust sets the anchor, and otherwise the end of the run is pinned at the tick so the slant clears the panel.

gdp <- data.frame(
  country = c(
    "United States", "United Kingdom", "United Arab Emirates",
    "Republic of Korea", "Czech Republic"
  ),
  score = c(9, 6, 7, 8, 5)
)
vplot(gdp) |>
  mark_bar(x = country, y = score) |>
  theme(axis.text.x = element_text(angle = 45))
A bar chart. It plots score (vertical axis) against country (horizontal axis). Based on 5 observations.0.02.55.07.5Czech RepublicRepublic of KoreaUnited Arab EmiratesUnited KingdomUnited Statesscorecountry

Left horizontal, a long label that would overrun the width of its tick instead wraps onto multiple lines, and the label row grows to fit — the per-tick companion of the title/subtitle/caption wrapping. Labels that already fit are untouched, so a plain numeric axis is unchanged.

vplot(gdp) |>
  mark_bar(x = country, y = score)
A bar chart. It plots score (vertical axis) against country (horizontal axis). Based on 5 observations.0.02.55.07.5Czech RepublicRepublic ofKoreaUnited ArabEmiratesUnited KingdomUnited Statesscorecountry

Layer effects

Effects change how a single mark is painted, and they are passed to a mark’s effects argument as a list. They apply to stroked and point marks. glow() softens a widened copy with a real Gaussian blur for a neon halo; shadow() adds a blurred, offset drop shadow; outline() puts a sharp contrasting halo behind the mark so it stays legible over a busy backdrop. motion() and echo() draw a fading trail of copies marching off along a direction — a speed-blur (motion(), many close copies) or discrete ghost repeats (echo()).

Because glow() and shadow() are real blurs, they also work on text marks — a neon mark_text() needs no glyph-stroking:

vplot(data.frame(x = 1, y = 1, lab = "NEON"), width = 5, height = 2.2) |>
  mark_text(x = x, y = y, label = lab, size = 42, color = "#00e5ff",
    effects = list(glow())) |>
  theme_cyberpunk()
A text-label plot. It plots y (vertical axis) against x (horizontal axis). Based on 1 observation.NEONNEON0.500.751.001.251.500.500.751.001.251.50yx
vplot(pressure) |>
  mark_line(
    x = temperature, y = pressure,
    effects = list(shadow(), outline(color = "white", size = 2))
  )
A line chart. It plots pressure (vertical axis) against temperature (horizontal axis). Based on 19 observations.2004006008000100200300pressuretemperature
vplot(mtcars) |>
  mark_point(x = wt, y = mpg, size = 4, effects = list(motion(x = 4)))
A scatter plot. It plots mpg (vertical axis) against wt (horizontal axis). Based on 32 observations.152025302345mpgwt

glow() pairs naturally with theme_cyberpunk(), a dark neon theme.

vplot(mtcars) |>
  mark_point(x = wt, y = mpg, color = factor(cyl), effects = list(glow())) |>
  theme_cyberpunk()
A scatter plot. It plots mpg (vertical axis) against wt (horizontal axis), where colour shows factor(cyl). Based on 32 observations.152025302345mpgwtfactor(cyl)468

Gradient fills

A gradient is an unscaled value for the fill aesthetic, not a mapped scale. Pass linear_gradient() or radial_gradient() directly as a fill and the region is painted with it as one paint. Using a transparent stop gives the “fade out under a line” look.

vplot(pressure) |>
  mark_area(
    x = temperature, y = pressure,
    fill = linear_gradient(c("#00e5ff", "#00e5ff00"))
  ) |>
  mark_line(x = temperature, y = pressure, color = "#00e5ff") |>
  theme_cyberpunk()
A plot combining area chart and line chart. It plots pressure (vertical axis) against temperature (horizontal axis). Based on 19 observations.02004006008000100200300pressuretemperature

Because a gradient is one paint per region, it cannot be mapped to a data column; for that you want a colour scale (see Scales and guides).

Pattern (hatch) fills

A pattern is the texture counterpart of a gradient: another unscaled fill value, built by the pattern_*() family. Distinguishing regions by texture (not only hue) keeps a plot legible in greyscale print and under colour-vision deficiency. Pass one directly as a fill:

bars <- data.frame(method = c("A", "B", "C"), score = c(4, 7, 5))
vplot(bars) |>
  mark_bar(x = method, y = score, fill = pattern_crosshatch(color = "grey20"))
A bar chart. It plots score (vertical axis) against method (horizontal axis). Based on 3 observations.0246ABCscoremethod

pattern_stripe(), pattern_crosshatch(), pattern_grid(), pattern_dot(), and pattern_checker() each take a color, a transparent-or-solid bg, and a spacing (in mm by default); stripes/crosshatch also take an angle restricted to 0, 45, 90, or 135. Patterns work on any filled mark — bars, areas, ribbons, rects, tiles, boxplots, violins, ridgelines, half-eyes, hulls, and sf polygons — and render on every backend including PDF. For a custom motif, build the tile yourself with vl_pattern().

To vary the texture by a variable (rather than a single constant fill), map the pattern aesthetic and let scale_pattern() assign a distinct texture to each level — the greyscale- and colour-vision-safe way to tell a few series apart:

bars <- data.frame(method = c("A", "B", "C", "D"), score = c(4, 7, 5, 6))
vplot(bars) |>
  mark_bar(x = method, y = score, pattern = method) |>
  scale_pattern(name = NULL)
A bar chart. It plots score (vertical axis) against method (horizontal axis). Based on 4 observations.0246ABCDscoremethodmethodABCD

scale_pattern(values = ) overrides the palette with builder names ("stripe", "dot", …) or your own pattern_*() objects; the legend keys are drawn as patterned swatches.

Clipping and masking to a shape

clip_to() restricts a plot’s marks to a geometry instead of the panel rectangle — the way to pour a raster or tile heatmap into a region outline. Give it an sf object or a data frame of x/y polygon vertices.

grid <- expand.grid(x = 1:24, y = 1:24)
grid$z <- with(grid, sin(x / 4) + cos(y / 4))
diamond <- data.frame(x = c(12, 22, 12, 2), y = c(2, 12, 22, 12))

vplot(grid) |>
  mark_tile(x = x, y = y, fill = z) |>
  clip_to(diamond)
A heatmap. It plots y (vertical axis) against x (horizontal axis), where colour shows z. Based on 576 observations.51015205101520yxz-101

invert = TRUE keeps the marks outside the shape (punching it out as a hole). set_mask() instead applies a soft radial mask — a vignette that fades the panel towards its edges, with feather setting how gentle the fade is:

vplot(grid) |>
  mark_tile(x = x, y = y, fill = z) |>
  set_mask(feather = 0.5)
A heatmap. It plots y (vertical axis) against x (horizontal axis), where colour shows z. Based on 576 observations.51015205101520yxz-101

clip_to() and set_mask() mask the whole panel; clip_layer() masks only the most-recently-added layer, so one raster layer can be clipped to a shape while the axes and other layers stay full-bleed:

set.seed(1)
pts <- data.frame(x = runif(60, 1, 24), y = runif(60, 1, 24))
vplot(grid) |>
  mark_tile(x = x, y = y, fill = z) |>
  clip_layer(diamond) |>
  mark_point(data = pts, x = x, y = y, size = 1.5, color = "white")
A plot combining heatmap and scatter plot. It plots y (vertical axis) against x (horizontal axis), where colour shows z. Based on 576 observations.51015205101520yxz-101

All three work under cartesian coordinates (a smooth feathered edge on an arbitrary polygon is not available yet, so clip_to() / clip_layer() are hard-edged; reach for set_mask() when you want a soft fade).

Hand-drawn sketch mode

theme_sketch() turns an entire plot hand-drawn in one line: wobbly gridlines, axis lines, and marks, on a warm paper background. The look is generated natively in the engine, so it is exact and works across PNG, SVG, and PDF.

vplot(mtcars) |>
  mark_bar(x = factor(cyl), fill = factor(cyl)) |>
  theme_sketch()
A bar chart. It plots count (vertical axis) against factor(cyl) (horizontal axis), where colour shows factor(cyl). Based on 32 observations.0510468countfactor(cyl)factor(cyl)468

For finer control, sketch() is the vocabulary: roughness, bowing, fill_style ("hachure", "crosshatch", "zigzag", and more), and so on. Pass a sketch() value to a single mark’s sketch = argument to rough up just that layer, or to an element_line() / element_rect() slot inside theme(). Set sketch = NA on a mark to force it crisp even under a plot-wide theme_sketch().

vplot(mtcars) |>
  mark_bar(
    x = factor(cyl), fill = factor(cyl),
    sketch = sketch(roughness = 2, fill_style = "crosshatch")
  )
A bar chart. It plots count (vertical axis) against factor(cyl) (horizontal axis), where colour shows factor(cyl). Based on 32 observations.0510468countfactor(cyl)factor(cyl)468