vplot(mtcars) |>
mark_point(x = wt, y = mpg, color = factor(cyl)) |>
theme_minimal()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.
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")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))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.
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()
vplot(pressure) |>
mark_line(
x = temperature, y = pressure,
effects = list(shadow(), outline(color = "white", size = 2))
)
vplot(mtcars) |>
mark_point(x = wt, y = mpg, size = 4, effects = list(motion(x = 4)))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()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()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"))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)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)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:
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")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()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().
