Skip to content

Commit ad540b7

Browse files
authored
Barebones support for <GridPattern> fills. (#5299)
* Write pattern utilities * Intercept non-list patterns * Support pattern fills in geoms * Support pattern fills in keys * Note that `geom_raster()` cannot use pattern fills * More informative call in error message * Write tests * Document * Some version protections * Use device checker * Set white alpha mask * Clarify error message * deal with unavailable functions/arguments * typo * Also handle unlisted pattern * Invert viewport backport * `geom_raster()` throws error when fill is pattern * device check warns instead of aborts * reimplement `pattern_alpha` as S3 generic + methods * accept new snapshot * Add news bullet
1 parent 15bde2f commit ad540b7

29 files changed

+883
-18
lines changed

DESCRIPTION

+1
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ Collate:
275275
'utilities-grid.R'
276276
'utilities-help.R'
277277
'utilities-matrix.R'
278+
'utilities-patterns.R'
278279
'utilities-resolution.R'
279280
'utilities-tidy-eval.R'
280281
'zxx.R'

NAMESPACE

+6
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ S3method(makeContext,dotstackGrob)
9595
S3method(merge_element,default)
9696
S3method(merge_element,element)
9797
S3method(merge_element,element_blank)
98+
S3method(pattern_alpha,GridPattern)
99+
S3method(pattern_alpha,GridTilingPattern)
100+
S3method(pattern_alpha,default)
101+
S3method(pattern_alpha,list)
98102
S3method(plot,ggplot)
99103
S3method(predictdf,default)
100104
S3method(predictdf,glm)
@@ -354,6 +358,7 @@ export(expr)
354358
export(facet_grid)
355359
export(facet_null)
356360
export(facet_wrap)
361+
export(fill_alpha)
357362
export(find_panel)
358363
export(flip_data)
359364
export(flipped_names)
@@ -476,6 +481,7 @@ export(new_guide)
476481
export(old_guide)
477482
export(panel_cols)
478483
export(panel_rows)
484+
export(pattern_alpha)
479485
export(position_dodge)
480486
export(position_dodge2)
481487
export(position_fill)

NEWS.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# ggplot2 (development version)
22

3+
* The `fill` aesthetic in many geoms now accepts grid's patterns and gradients.
4+
For developers of layer extensions, this feature can be enabled by switching
5+
from `fill = alpha(fill, alpha)` to `fill = fill_alpha(fill, alpha)` when
6+
providing fills to `grid::gpar()` (@teunbrand, #3997).
7+
38
* The plot's title, subtitle and caption now obey horizontal text margins
49
(#5533).
510

R/backports.R

+23
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,26 @@ if (getRversion() < "3.5") {
2222
isFALSE <- function(x) is.logical(x) && length(x) == 1L && !is.na(x) && !x
2323
isTRUE <- function(x) is.logical(x) && length(x) == 1L && !is.na(x) && x
2424
}
25+
26+
version_unavailable <- function(...) {
27+
fun <- as_label(current_call()[[1]])
28+
cli::cli_abort("{.fn {fun}} is not available in R version {getRversion()}.")
29+
}
30+
31+
# Ignore mask argument if on lower R version (<= 4.1)
32+
viewport <- function(..., mask) grid::viewport(...)
33+
pattern <- version_unavailable
34+
as.mask <- version_unavailable
35+
on_load({
36+
if ("mask" %in% fn_fmls_names(grid::viewport)) {
37+
viewport <- grid::viewport
38+
}
39+
# Replace version unavailable functions if found
40+
if ("pattern" %in% getNamespaceExports("grid")) {
41+
pattern <- grid::pattern
42+
}
43+
if ("as.mask" %in% getNamespaceExports("grid")) {
44+
as.mask <- grid::as.mask
45+
}
46+
})
47+

R/geom-.R

+4
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ Geom <- ggproto("Geom",
126126
deprecate_soft0("3.4.0", I("Using the `size` aesthetic in this geom"), I("`linewidth` in the `default_aes` field and elsewhere"))
127127
default_aes$linewidth <- default_aes$size
128128
}
129+
if (is_pattern(params$fill)) {
130+
params$fill <- list(params$fill)
131+
}
132+
129133
# Fill in missing aesthetics with their defaults
130134
missing_aes <- setdiff(names(default_aes), names(data))
131135

R/geom-boxplot.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ GeomBoxplot <- ggproto("GeomBoxplot", Geom,
239239
colour = data$colour,
240240
linewidth = data$linewidth,
241241
linetype = data$linetype,
242-
fill = alpha(data$fill, data$alpha),
242+
fill = fill_alpha(data$fill, data$alpha),
243243
group = data$group
244244
)
245245

R/geom-dotplot.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ GeomDotplot <- ggproto("GeomDotplot", Geom,
294294
stackposition = tdata$stackpos, stackdir = stackdir, stackratio = stackratio,
295295
default.units = "npc",
296296
gp = gpar(col = alpha(tdata$colour, tdata$alpha),
297-
fill = alpha(tdata$fill, tdata$alpha),
297+
fill = fill_alpha(tdata$fill, tdata$alpha),
298298
lwd = tdata$stroke, lty = tdata$linetype,
299299
lineend = lineend))
300300
)

R/geom-hex.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ GeomHex <- ggproto("GeomHex", Geom,
9090
coords$x, coords$y,
9191
gp = gpar(
9292
col = data$colour,
93-
fill = alpha(data$fill, data$alpha),
93+
fill = fill_alpha(data$fill, data$alpha),
9494
lwd = data$linewidth * .pt,
9595
lty = data$linetype,
9696
lineend = lineend,

R/geom-label.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ GeomLabel <- ggproto("GeomLabel", Geom,
103103
),
104104
rect.gp = gpar(
105105
col = if (isTRUE(all.equal(label.size, 0))) NA else row$colour,
106-
fill = alpha(row$fill, row$alpha),
106+
fill = fill_alpha(row$fill, row$alpha),
107107
lwd = label.size * .pt
108108
)
109109
)

R/geom-map.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ GeomMap <- ggproto("GeomMap", GeomPolygon,
146146
polygonGrob(coords$x, coords$y, default.units = "native", id = grob_id,
147147
gp = gpar(
148148
col = data$colour,
149-
fill = alpha(data$fill, data$alpha),
149+
fill = fill_alpha(data$fill, data$alpha),
150150
lwd = data$linewidth * .pt,
151151
lineend = lineend,
152152
linejoin = linejoin,

R/geom-point.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ GeomPoint <- ggproto("GeomPoint", Geom,
131131
pch = coords$shape,
132132
gp = gpar(
133133
col = alpha(coords$colour, coords$alpha),
134-
fill = alpha(coords$fill, coords$alpha),
134+
fill = fill_alpha(coords$fill, coords$alpha),
135135
# Stroke is added around the outside of the point
136136
fontsize = coords$size * .pt + stroke_size * .stroke / 2,
137137
lwd = coords$stroke * .stroke / 2

R/geom-polygon.R

+2-2
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ GeomPolygon <- ggproto("GeomPolygon", Geom,
132132
id = munched$group,
133133
gp = gpar(
134134
col = first_rows$colour,
135-
fill = alpha(first_rows$fill, first_rows$alpha),
135+
fill = fill_alpha(first_rows$fill, first_rows$alpha),
136136
lwd = first_rows$linewidth * .pt,
137137
lty = first_rows$linetype,
138138
lineend = lineend,
@@ -163,7 +163,7 @@ GeomPolygon <- ggproto("GeomPolygon", Geom,
163163
rule = rule,
164164
gp = gpar(
165165
col = first_rows$colour,
166-
fill = alpha(first_rows$fill, first_rows$alpha),
166+
fill = fill_alpha(first_rows$fill, first_rows$alpha),
167167
lwd = first_rows$linewidth * .pt,
168168
lty = first_rows$linetype,
169169
lineend = lineend,

R/geom-raster.R

+4
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ GeomRaster <- ggproto("GeomRaster", Geom,
102102
nrow <- max(y_pos) + 1
103103
ncol <- max(x_pos) + 1
104104

105+
if (is.list(data$fill) && is_pattern(data$fill[[1]])) {
106+
cli::cli_abort("{.fn {snake_class(self)}} cannot render pattern fills.")
107+
}
108+
105109
raster <- matrix(NA_character_, nrow = nrow, ncol = ncol)
106110
raster[cbind(nrow - y_pos, x_pos + 1)] <- alpha(data$fill, data$alpha)
107111

R/geom-rect.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ GeomRect <- ggproto("GeomRect", Geom,
5959
just = c("left", "top"),
6060
gp = gpar(
6161
col = coords$colour,
62-
fill = alpha(coords$fill, coords$alpha),
62+
fill = fill_alpha(coords$fill, coords$alpha),
6363
lwd = coords$linewidth * .pt,
6464
lty = coords$linetype,
6565
linejoin = linejoin,

R/geom-ribbon.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ GeomRibbon <- ggproto("GeomRibbon", Geom,
183183
munched_poly$x, munched_poly$y, id = munched_poly$id,
184184
default.units = "native",
185185
gp = gpar(
186-
fill = alpha(aes$fill, aes$alpha),
186+
fill = fill_alpha(aes$fill, aes$alpha),
187187
col = if (is_full_outline) aes$colour else NA,
188188
lwd = if (is_full_outline) aes$linewidth * .pt else 0,
189189
lty = if (is_full_outline) aes$linetype else 1,

R/geom-tile.R

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
#' corners (`xmin`, `xmax`, `ymin` and `ymax`), while
66
#' `geom_tile()` uses the center of the tile and its size (`x`,
77
#' `y`, `width`, `height`). `geom_raster()` is a high
8-
#' performance special case for when all the tiles are the same size.
8+
#' performance special case for when all the tiles are the same size, and no
9+
#' pattern fills are applied.
910
#'
1011
#' @eval rd_aesthetics("geom", "tile", "Note that `geom_raster()` ignores `colour`.")
1112
#' @inheritParams layer

R/legend-draw.R

+6-6
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ draw_key_point <- function(data, params, size) {
3838
pch = data$shape,
3939
gp = gpar(
4040
col = alpha(data$colour %||% "black", data$alpha),
41-
fill = alpha(data$fill %||% "black", data$alpha),
41+
fill = fill_alpha(data$fill %||% "black", data$alpha),
4242
fontsize = (data$size %||% 1.5) * .pt + stroke_size * .stroke / 2,
4343
lwd = stroke_size * .stroke / 2
4444
)
@@ -63,7 +63,7 @@ draw_key_abline <- function(data, params, size) {
6363
draw_key_rect <- function(data, params, size) {
6464
rectGrob(gp = gpar(
6565
col = NA,
66-
fill = alpha(data$fill %||% data$colour %||% "grey20", data$alpha),
66+
fill = fill_alpha(data$fill %||% data$colour %||% "grey20", data$alpha),
6767
lty = data$linetype %||% 1
6868
))
6969
}
@@ -81,7 +81,7 @@ draw_key_polygon <- function(data, params, size) {
8181
height = unit(1, "npc") - unit(lwd, "mm"),
8282
gp = gpar(
8383
col = data$colour %||% NA,
84-
fill = alpha(data$fill %||% "grey20", data$alpha),
84+
fill = fill_alpha(data$fill %||% "grey20", data$alpha),
8585
lty = data$linetype %||% 1,
8686
lwd = lwd * .pt,
8787
linejoin = params$linejoin %||% "mitre",
@@ -100,7 +100,7 @@ draw_key_blank <- function(data, params, size) {
100100
draw_key_boxplot <- function(data, params, size) {
101101
gp <- gpar(
102102
col = data$colour %||% "grey20",
103-
fill = alpha(data$fill %||% "white", data$alpha),
103+
fill = fill_alpha(data$fill %||% "white", data$alpha),
104104
lwd = (data$linewidth %||% 0.5) * .pt,
105105
lty = data$linetype %||% 1,
106106
lineend = params$lineend %||% "butt",
@@ -131,7 +131,7 @@ draw_key_boxplot <- function(data, params, size) {
131131
draw_key_crossbar <- function(data, params, size) {
132132
gp <- gpar(
133133
col = data$colour %||% "grey20",
134-
fill = alpha(data$fill %||% "white", data$alpha),
134+
fill = fill_alpha(data$fill %||% "white", data$alpha),
135135
lwd = (data$linewidth %||% 0.5) * .pt,
136136
lty = data$linetype %||% 1,
137137
lineend = params$lineend %||% "butt",
@@ -195,7 +195,7 @@ draw_key_dotplot <- function(data, params, size) {
195195
pch = 21,
196196
gp = gpar(
197197
col = alpha(data$colour %||% "black", data$alpha),
198-
fill = alpha(data$fill %||% "black", data$alpha),
198+
fill = fill_alpha(data$fill %||% "black", data$alpha),
199199
lty = data$linetype %||% 1,
200200
lineend = params$lineend %||% "butt"
201201
)

R/utilities-patterns.R

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
2+
#' Modify fill transparency
3+
#'
4+
#' This works much like [alpha()][scales::alpha] in that it modifies the
5+
#' transparency of fill colours. It differs in that `fill_alpha()` also attempts
6+
#' to set the transparency of `<GridPattern>` objects.
7+
#'
8+
#' @param fill A fill colour given as a `character` or `integer` vector, or as a
9+
#' (list of) `<GridPattern>` object(s).
10+
#' @param alpha A transparency value between 0 (transparent) and 1 (opaque),
11+
#' parallel to `fill`.
12+
#'
13+
#' @return A `character` vector of colours, or list of `<GridPattern>` objects.
14+
#' @export
15+
#' @keywords internal
16+
#'
17+
#' @examples
18+
#' # Typical colour input
19+
#' fill_alpha("red", 0.5)
20+
#'
21+
#' if (utils::packageVersion("grid") > "4.2") {
22+
#' # Pattern input
23+
#' fill_alpha(list(grid::linearGradient()), 0.5)
24+
#' }
25+
fill_alpha <- function(fill, alpha) {
26+
if (!is.list(fill)) {
27+
# Happy path for no patterns
28+
return(alpha(fill, alpha))
29+
}
30+
if (is_pattern(fill) || any(vapply(fill, is_pattern, logical(1)))) {
31+
check_device("patterns", action = "warn")
32+
fill <- pattern_alpha(fill, alpha)
33+
return(fill)
34+
} else {
35+
# We are either dealing with faulty fill specification, or we have a legend
36+
# key that is trying to draw a single colour. It can be given that colour
37+
# as a list due to patterns in other keys.
38+
msg <- paste0(
39+
"{.field fill} must be a vector of colours or list of ",
40+
"{.cls GridPattern} objects."
41+
)
42+
# If single colour list, try applying `alpha()`
43+
fill <- try_fetch(
44+
Map(alpha, colour = fill, alpha = alpha),
45+
error = function(cnd) {
46+
cli::cli_abort(msg, call = expr(fill_alpha()))
47+
}
48+
)
49+
# `length(input)` must be same as `length(output)`
50+
if (!all(lengths(fill) == 1)) {
51+
cli::cli_abort(msg)
52+
}
53+
return(unlist(fill))
54+
}
55+
}
56+
57+
# Similar to grid:::is.pattern
58+
is_pattern <- function(x) {
59+
inherits(x, "GridPattern")
60+
}
61+
62+
#' Modify transparency for patterns
63+
#'
64+
#' This generic allows you to add your own methods for adding transparency to
65+
#' pattern-like objects.
66+
#'
67+
#' @param x Object to be interpreted as pattern.
68+
#' @param alpha A `numeric` vector between 0 and 1. If `NA`, alpha values
69+
#' are preserved.
70+
#'
71+
#' @return `x` with modified transparency
72+
#' @export
73+
#' @keywords internal
74+
pattern_alpha <- function(x, alpha) {
75+
UseMethod("pattern_alpha")
76+
}
77+
78+
#' @export
79+
pattern_alpha.default <- function(x, alpha) {
80+
if (!is.atomic(x)) {
81+
cli::cli_abort("Can't apply {.arg alpha} to {obj_type_friendly(x)}.")
82+
}
83+
pattern(rectGrob(), gp = gpar(fill = alpha(x, alpha)))
84+
}
85+
86+
#' @export
87+
pattern_alpha.GridPattern <- function(x, alpha) {
88+
x$colours <- alpha(x$colours, alpha[1])
89+
x
90+
}
91+
92+
#' @export
93+
pattern_alpha.GridTilingPattern <- function(x, alpha) {
94+
if (all(is.na(alpha) | alpha == 1)) {
95+
return(x)
96+
}
97+
check_device("alpha_masks", "warn")
98+
grob <- env_get(environment(x$f), "grob")
99+
mask <- as.mask(rectGrob(gp = gpar(fill = alpha("white", alpha))))
100+
if (is.null(grob$vp)) {
101+
grob$vp <- viewport(mask = mask)
102+
} else {
103+
grob$vp <- editViewport(grob$vp, mask = mask)
104+
}
105+
new_env <- new.env(parent = environment(x$f))
106+
env_bind(new_env, grob = grob)
107+
environment(x$f) <- new_env
108+
x
109+
}
110+
111+
#' @export
112+
pattern_alpha.list <- function(x, alpha) {
113+
Map(pattern_alpha, x = x, alpha = alpha)
114+
}
115+

man/fill_alpha.Rd

+33
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)