Skip to contents

Every filled geom (geom_sketch_col(), geom_sketch_rect(), geom_sketch_tile(), geom_sketch_polygon(), geom_sketch_ribbon(), geom_sketch_area(), geom_sketch_density(), and the boxplot box) takes a fill_style. Under the hood, all of them are produced by a single Active-Edge-Table scan-line filler (hachure_fill() and friends) that works on arbitrary — including concave — polygons.

The seven styles

styles <- c("hachure", "cross_hatch", "zigzag", "zigzag_line",
            "dots", "dashed", "solid")

bars <- do.call(rbind, lapply(styles, function(s) {
  data.frame(style = s, x = c("A", "B", "C"), y = c(4, 6, 3))
}))
bars$style <- factor(bars$style, levels = styles)

# Draw a small panel per style by faceting and re-drawing each with its style.
# (fill_style is a layer parameter, so we build one layer per facet.)
plots <- lapply(styles, function(s) {
  ggplot(subset(bars, style == s), aes(x, y)) +
    geom_sketch_col(fill = "#E8A87C", fill_style = s, seed = 4L) +
    labs(subtitle = s, x = NULL, y = NULL) +
    theme_sketch(base_size = 9)
})

# Show them individually:
for (p in plots) print(p)

Style Look Notes
hachure Parallel diagonal strokes The default; classic pencil shading.
cross_hatch Hachure at angle and angle + 90° Denser, “darker” shading.
zigzag Hachure lines joined by diagonal connectors A continuous scribble feel.
zigzag_line Just the connectors, as one path Lighter than zigzag.
dots Tiny rough circles sampled along the lines Stippled fill.
dashed Hachure broken into dashes Airy, sketchy texture.
solid No fill lines (outline only) Use when you want only the rough outline.

Tuning the hachure

Three parameters control the texture.

hachure_angle

The angle (degrees) of the fill lines:

df <- data.frame(x = c("A", "B", "C"), y = c(4, 6, 3))
for (a in c(0, 45, 90, 135)) {
  print(
    ggplot(df, aes(x, y)) +
      geom_sketch_col(fill = "seagreen", hachure_angle = a, seed = 1L) +
      labs(subtitle = paste("hachure_angle =", a), x = NULL) +
      theme_sketch(base_size = 9)
  )
}

hachure_gap

Spacing between fill lines, in data units — smaller means denser shading:

for (g in c(0.6, 1.2, 2.4)) {
  print(
    ggplot(df, aes(x, y)) +
      geom_sketch_col(fill = "#5DADE2", hachure_gap = g, seed = 1L) +
      labs(subtitle = paste("hachure_gap =", g), x = NULL) +
      theme_sketch(base_size = 9)
  )
}

fill_weight

The stroke weight of the fill lines:

for (w in c(0.3, 0.8, 1.6)) {
  print(
    ggplot(df, aes(x, y)) +
      geom_sketch_col(fill = "#AF7AC5", fill_weight = w, seed = 1L) +
      labs(subtitle = paste("fill_weight =", w), x = NULL) +
      theme_sketch(base_size = 9)
  )
}

Fills follow the shape

Because the filler is a true scan-line algorithm, the texture conforms to any polygon outline, not just rectangles:

ang  <- seq(0, 2 * pi, length.out = 9)[-9]
hex  <- data.frame(x = cos(ang), y = sin(ang))
ggplot(hex, aes(x, y)) +
  geom_sketch_polygon(fill = "#E74C3C", fill_style = "cross_hatch",
                      hachure_angle = 30, seed = 1L) +
  coord_equal() +
  labs(title = "Cross-hatch on a hexagon") +
  theme_sketch()

td <- expand.grid(x = 1:5, y = 1:3)
td$z <- td$x
ggplot(td, aes(x, y, fill = z)) +
  geom_sketch_tile(fill_style = "dots", seed = 1L) +
  scale_fill_viridis_c() +
  labs(title = "Dotted tiles") +
  theme_sketch()

Working directly with the filler

The fill geometry comes from pure Layer-1 functions you can call yourself — useful for testing or custom grobs. sketch_fill() dispatches to the styles and returns a list of line segments:

square_x <- c(0, 1, 1, 0)
square_y <- c(0, 0, 1, 1)
segs <- sketch_fill(square_x, square_y, fill_style = "hachure",
                    hachure_gap = 0.15, seed = 1L)
length(segs)      # number of fill segments
#> [1] 8
head(segs[[1]])   # each segment is a matrix of x/y points
#>              x          y
#> [1,] 0.7093499 0.07633409
#> [2,] 0.8440814 0.20310311
#> [3,] 0.9270041 0.27937282