Skip to content

Commit

Permalink
Barebones support for <GridPattern> fills. (#5299)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
teunbrand authored Dec 8, 2023
1 parent 15bde2f commit ad540b7
Show file tree
Hide file tree
Showing 29 changed files with 883 additions and 18 deletions.
1 change: 1 addition & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ Collate:
'utilities-grid.R'
'utilities-help.R'
'utilities-matrix.R'
'utilities-patterns.R'
'utilities-resolution.R'
'utilities-tidy-eval.R'
'zxx.R'
Expand Down
6 changes: 6 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ S3method(makeContext,dotstackGrob)
S3method(merge_element,default)
S3method(merge_element,element)
S3method(merge_element,element_blank)
S3method(pattern_alpha,GridPattern)
S3method(pattern_alpha,GridTilingPattern)
S3method(pattern_alpha,default)
S3method(pattern_alpha,list)
S3method(plot,ggplot)
S3method(predictdf,default)
S3method(predictdf,glm)
Expand Down Expand Up @@ -354,6 +358,7 @@ export(expr)
export(facet_grid)
export(facet_null)
export(facet_wrap)
export(fill_alpha)
export(find_panel)
export(flip_data)
export(flipped_names)
Expand Down Expand Up @@ -476,6 +481,7 @@ export(new_guide)
export(old_guide)
export(panel_cols)
export(panel_rows)
export(pattern_alpha)
export(position_dodge)
export(position_dodge2)
export(position_fill)
Expand Down
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# ggplot2 (development version)

* The `fill` aesthetic in many geoms now accepts grid's patterns and gradients.
For developers of layer extensions, this feature can be enabled by switching
from `fill = alpha(fill, alpha)` to `fill = fill_alpha(fill, alpha)` when
providing fills to `grid::gpar()` (@teunbrand, #3997).

* The plot's title, subtitle and caption now obey horizontal text margins
(#5533).

Expand Down
23 changes: 23 additions & 0 deletions R/backports.R
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,26 @@ if (getRversion() < "3.5") {
isFALSE <- function(x) is.logical(x) && length(x) == 1L && !is.na(x) && !x
isTRUE <- function(x) is.logical(x) && length(x) == 1L && !is.na(x) && x
}

version_unavailable <- function(...) {
fun <- as_label(current_call()[[1]])
cli::cli_abort("{.fn {fun}} is not available in R version {getRversion()}.")
}

# Ignore mask argument if on lower R version (<= 4.1)
viewport <- function(..., mask) grid::viewport(...)
pattern <- version_unavailable
as.mask <- version_unavailable
on_load({
if ("mask" %in% fn_fmls_names(grid::viewport)) {
viewport <- grid::viewport
}
# Replace version unavailable functions if found
if ("pattern" %in% getNamespaceExports("grid")) {
pattern <- grid::pattern
}
if ("as.mask" %in% getNamespaceExports("grid")) {
as.mask <- grid::as.mask
}
})

4 changes: 4 additions & 0 deletions R/geom-.R
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ Geom <- ggproto("Geom",
deprecate_soft0("3.4.0", I("Using the `size` aesthetic in this geom"), I("`linewidth` in the `default_aes` field and elsewhere"))
default_aes$linewidth <- default_aes$size
}
if (is_pattern(params$fill)) {
params$fill <- list(params$fill)
}

# Fill in missing aesthetics with their defaults
missing_aes <- setdiff(names(default_aes), names(data))

Expand Down
2 changes: 1 addition & 1 deletion R/geom-boxplot.R
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ GeomBoxplot <- ggproto("GeomBoxplot", Geom,
colour = data$colour,
linewidth = data$linewidth,
linetype = data$linetype,
fill = alpha(data$fill, data$alpha),
fill = fill_alpha(data$fill, data$alpha),
group = data$group
)

Expand Down
2 changes: 1 addition & 1 deletion R/geom-dotplot.R
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ GeomDotplot <- ggproto("GeomDotplot", Geom,
stackposition = tdata$stackpos, stackdir = stackdir, stackratio = stackratio,
default.units = "npc",
gp = gpar(col = alpha(tdata$colour, tdata$alpha),
fill = alpha(tdata$fill, tdata$alpha),
fill = fill_alpha(tdata$fill, tdata$alpha),
lwd = tdata$stroke, lty = tdata$linetype,
lineend = lineend))
)
Expand Down
2 changes: 1 addition & 1 deletion R/geom-hex.R
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ GeomHex <- ggproto("GeomHex", Geom,
coords$x, coords$y,
gp = gpar(
col = data$colour,
fill = alpha(data$fill, data$alpha),
fill = fill_alpha(data$fill, data$alpha),
lwd = data$linewidth * .pt,
lty = data$linetype,
lineend = lineend,
Expand Down
2 changes: 1 addition & 1 deletion R/geom-label.R
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ GeomLabel <- ggproto("GeomLabel", Geom,
),
rect.gp = gpar(
col = if (isTRUE(all.equal(label.size, 0))) NA else row$colour,
fill = alpha(row$fill, row$alpha),
fill = fill_alpha(row$fill, row$alpha),
lwd = label.size * .pt
)
)
Expand Down
2 changes: 1 addition & 1 deletion R/geom-map.R
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ GeomMap <- ggproto("GeomMap", GeomPolygon,
polygonGrob(coords$x, coords$y, default.units = "native", id = grob_id,
gp = gpar(
col = data$colour,
fill = alpha(data$fill, data$alpha),
fill = fill_alpha(data$fill, data$alpha),
lwd = data$linewidth * .pt,
lineend = lineend,
linejoin = linejoin,
Expand Down
2 changes: 1 addition & 1 deletion R/geom-point.R
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ GeomPoint <- ggproto("GeomPoint", Geom,
pch = coords$shape,
gp = gpar(
col = alpha(coords$colour, coords$alpha),
fill = alpha(coords$fill, coords$alpha),
fill = fill_alpha(coords$fill, coords$alpha),
# Stroke is added around the outside of the point
fontsize = coords$size * .pt + stroke_size * .stroke / 2,
lwd = coords$stroke * .stroke / 2
Expand Down
4 changes: 2 additions & 2 deletions R/geom-polygon.R
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ GeomPolygon <- ggproto("GeomPolygon", Geom,
id = munched$group,
gp = gpar(
col = first_rows$colour,
fill = alpha(first_rows$fill, first_rows$alpha),
fill = fill_alpha(first_rows$fill, first_rows$alpha),
lwd = first_rows$linewidth * .pt,
lty = first_rows$linetype,
lineend = lineend,
Expand Down Expand Up @@ -163,7 +163,7 @@ GeomPolygon <- ggproto("GeomPolygon", Geom,
rule = rule,
gp = gpar(
col = first_rows$colour,
fill = alpha(first_rows$fill, first_rows$alpha),
fill = fill_alpha(first_rows$fill, first_rows$alpha),
lwd = first_rows$linewidth * .pt,
lty = first_rows$linetype,
lineend = lineend,
Expand Down
4 changes: 4 additions & 0 deletions R/geom-raster.R
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ GeomRaster <- ggproto("GeomRaster", Geom,
nrow <- max(y_pos) + 1
ncol <- max(x_pos) + 1

if (is.list(data$fill) && is_pattern(data$fill[[1]])) {
cli::cli_abort("{.fn {snake_class(self)}} cannot render pattern fills.")
}

raster <- matrix(NA_character_, nrow = nrow, ncol = ncol)
raster[cbind(nrow - y_pos, x_pos + 1)] <- alpha(data$fill, data$alpha)

Expand Down
2 changes: 1 addition & 1 deletion R/geom-rect.R
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ GeomRect <- ggproto("GeomRect", Geom,
just = c("left", "top"),
gp = gpar(
col = coords$colour,
fill = alpha(coords$fill, coords$alpha),
fill = fill_alpha(coords$fill, coords$alpha),
lwd = coords$linewidth * .pt,
lty = coords$linetype,
linejoin = linejoin,
Expand Down
2 changes: 1 addition & 1 deletion R/geom-ribbon.R
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ GeomRibbon <- ggproto("GeomRibbon", Geom,
munched_poly$x, munched_poly$y, id = munched_poly$id,
default.units = "native",
gp = gpar(
fill = alpha(aes$fill, aes$alpha),
fill = fill_alpha(aes$fill, aes$alpha),
col = if (is_full_outline) aes$colour else NA,
lwd = if (is_full_outline) aes$linewidth * .pt else 0,
lty = if (is_full_outline) aes$linetype else 1,
Expand Down
3 changes: 2 additions & 1 deletion R/geom-tile.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
#' corners (`xmin`, `xmax`, `ymin` and `ymax`), while
#' `geom_tile()` uses the center of the tile and its size (`x`,
#' `y`, `width`, `height`). `geom_raster()` is a high
#' performance special case for when all the tiles are the same size.
#' performance special case for when all the tiles are the same size, and no
#' pattern fills are applied.
#'
#' @eval rd_aesthetics("geom", "tile", "Note that `geom_raster()` ignores `colour`.")
#' @inheritParams layer
Expand Down
12 changes: 6 additions & 6 deletions R/legend-draw.R
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ draw_key_point <- function(data, params, size) {
pch = data$shape,
gp = gpar(
col = alpha(data$colour %||% "black", data$alpha),
fill = alpha(data$fill %||% "black", data$alpha),
fill = fill_alpha(data$fill %||% "black", data$alpha),
fontsize = (data$size %||% 1.5) * .pt + stroke_size * .stroke / 2,
lwd = stroke_size * .stroke / 2
)
Expand All @@ -63,7 +63,7 @@ draw_key_abline <- function(data, params, size) {
draw_key_rect <- function(data, params, size) {
rectGrob(gp = gpar(
col = NA,
fill = alpha(data$fill %||% data$colour %||% "grey20", data$alpha),
fill = fill_alpha(data$fill %||% data$colour %||% "grey20", data$alpha),
lty = data$linetype %||% 1
))
}
Expand All @@ -81,7 +81,7 @@ draw_key_polygon <- function(data, params, size) {
height = unit(1, "npc") - unit(lwd, "mm"),
gp = gpar(
col = data$colour %||% NA,
fill = alpha(data$fill %||% "grey20", data$alpha),
fill = fill_alpha(data$fill %||% "grey20", data$alpha),
lty = data$linetype %||% 1,
lwd = lwd * .pt,
linejoin = params$linejoin %||% "mitre",
Expand All @@ -100,7 +100,7 @@ draw_key_blank <- function(data, params, size) {
draw_key_boxplot <- function(data, params, size) {
gp <- gpar(
col = data$colour %||% "grey20",
fill = alpha(data$fill %||% "white", data$alpha),
fill = fill_alpha(data$fill %||% "white", data$alpha),
lwd = (data$linewidth %||% 0.5) * .pt,
lty = data$linetype %||% 1,
lineend = params$lineend %||% "butt",
Expand Down Expand Up @@ -131,7 +131,7 @@ draw_key_boxplot <- function(data, params, size) {
draw_key_crossbar <- function(data, params, size) {
gp <- gpar(
col = data$colour %||% "grey20",
fill = alpha(data$fill %||% "white", data$alpha),
fill = fill_alpha(data$fill %||% "white", data$alpha),
lwd = (data$linewidth %||% 0.5) * .pt,
lty = data$linetype %||% 1,
lineend = params$lineend %||% "butt",
Expand Down Expand Up @@ -195,7 +195,7 @@ draw_key_dotplot <- function(data, params, size) {
pch = 21,
gp = gpar(
col = alpha(data$colour %||% "black", data$alpha),
fill = alpha(data$fill %||% "black", data$alpha),
fill = fill_alpha(data$fill %||% "black", data$alpha),
lty = data$linetype %||% 1,
lineend = params$lineend %||% "butt"
)
Expand Down
115 changes: 115 additions & 0 deletions R/utilities-patterns.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@

#' Modify fill transparency
#'
#' This works much like [alpha()][scales::alpha] in that it modifies the
#' transparency of fill colours. It differs in that `fill_alpha()` also attempts
#' to set the transparency of `<GridPattern>` objects.
#'
#' @param fill A fill colour given as a `character` or `integer` vector, or as a
#' (list of) `<GridPattern>` object(s).
#' @param alpha A transparency value between 0 (transparent) and 1 (opaque),
#' parallel to `fill`.
#'
#' @return A `character` vector of colours, or list of `<GridPattern>` objects.
#' @export
#' @keywords internal
#'
#' @examples
#' # Typical colour input
#' fill_alpha("red", 0.5)
#'
#' if (utils::packageVersion("grid") > "4.2") {
#' # Pattern input
#' fill_alpha(list(grid::linearGradient()), 0.5)
#' }
fill_alpha <- function(fill, alpha) {
if (!is.list(fill)) {
# Happy path for no patterns
return(alpha(fill, alpha))
}
if (is_pattern(fill) || any(vapply(fill, is_pattern, logical(1)))) {
check_device("patterns", action = "warn")
fill <- pattern_alpha(fill, alpha)
return(fill)
} else {
# We are either dealing with faulty fill specification, or we have a legend
# key that is trying to draw a single colour. It can be given that colour
# as a list due to patterns in other keys.
msg <- paste0(
"{.field fill} must be a vector of colours or list of ",
"{.cls GridPattern} objects."
)
# If single colour list, try applying `alpha()`
fill <- try_fetch(
Map(alpha, colour = fill, alpha = alpha),
error = function(cnd) {
cli::cli_abort(msg, call = expr(fill_alpha()))
}
)
# `length(input)` must be same as `length(output)`
if (!all(lengths(fill) == 1)) {
cli::cli_abort(msg)
}
return(unlist(fill))
}
}

# Similar to grid:::is.pattern
is_pattern <- function(x) {
inherits(x, "GridPattern")
}

#' Modify transparency for patterns
#'
#' This generic allows you to add your own methods for adding transparency to
#' pattern-like objects.
#'
#' @param x Object to be interpreted as pattern.
#' @param alpha A `numeric` vector between 0 and 1. If `NA`, alpha values
#' are preserved.
#'
#' @return `x` with modified transparency
#' @export
#' @keywords internal
pattern_alpha <- function(x, alpha) {
UseMethod("pattern_alpha")
}

#' @export
pattern_alpha.default <- function(x, alpha) {
if (!is.atomic(x)) {
cli::cli_abort("Can't apply {.arg alpha} to {obj_type_friendly(x)}.")
}
pattern(rectGrob(), gp = gpar(fill = alpha(x, alpha)))
}

#' @export
pattern_alpha.GridPattern <- function(x, alpha) {
x$colours <- alpha(x$colours, alpha[1])
x
}

#' @export
pattern_alpha.GridTilingPattern <- function(x, alpha) {
if (all(is.na(alpha) | alpha == 1)) {
return(x)
}
check_device("alpha_masks", "warn")
grob <- env_get(environment(x$f), "grob")
mask <- as.mask(rectGrob(gp = gpar(fill = alpha("white", alpha))))
if (is.null(grob$vp)) {
grob$vp <- viewport(mask = mask)
} else {
grob$vp <- editViewport(grob$vp, mask = mask)
}
new_env <- new.env(parent = environment(x$f))
env_bind(new_env, grob = grob)
environment(x$f) <- new_env
x
}

#' @export
pattern_alpha.list <- function(x, alpha) {
Map(pattern_alpha, x = x, alpha = alpha)
}

33 changes: 33 additions & 0 deletions man/fill_alpha.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit ad540b7

Please sign in to comment.