Skip to content

Smarter keys #5302

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
* More informative error for mismatched
`direction`/`theme(legend.direction = ...)` arguments (#4364, #4930).
* `guide_coloursteps()` and `guide_bins()` sort breaks (#5152).
* `guide_legend()` now omits inappropriate key glyphs: if there isn't any
layer data matching a key value, the key isn't drawn. To show keys in the
legend regardless of whether the value occurs in the data
(the old behaviour), you can use `show.legend = c({aesthetic} = TRUE)`
(@teunbrand, #3648).

* `geom_label()` now uses the `angle` aesthetic (@teunbrand, #2785)
* 'lines' units in `geom_label()`, often used in the `label.padding` argument,
Expand Down
2 changes: 1 addition & 1 deletion R/guide-.R
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ Guide <- ggproto(

# Function for extracting information from the layers.
# Mostly applies to `guide_legend()` and `guide_binned()`
get_layer_key = function(params, layers) {
get_layer_key = function(params, layers, key_data) {
return(params)
},

Expand Down
2 changes: 1 addition & 1 deletion R/guide-colorbar.R
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ GuideColourbar <- ggproto(
return(list(guide = self, params = params))
},

get_layer_key = function(params, layers) {
get_layer_key = function(params, layers, key_data) {

guide_layers <- lapply(layers, function(layer) {

Expand Down
61 changes: 56 additions & 5 deletions R/guide-legend.R
Original file line number Diff line number Diff line change
Expand Up @@ -294,9 +294,9 @@ GuideLegend <- ggproto(
},

# Arrange common data for vertical and horizontal legends
get_layer_key = function(params, layers) {
get_layer_key = function(params, layers, key_data) {

decor <- lapply(layers, function(layer) {
decor <- Map(function(layer, index) {

matched_aes <- matched_aes(layer, params)

Expand All @@ -322,9 +322,11 @@ GuideLegend <- ggproto(
"Failed to apply {.fn after_scale} modifications to legend",
parent = cnd
)
layer$geom$use_defaults(params$key[matched], layer_params, list())
layer$geom$use_defaults(params$key[matched_aes], layer_params, list())
}
)
data$.draw <- keep_key_data(params$key, key_data, matched_aes,
layer$show.legend, index)
} else {
reps <- rep(1, nrow(params$key))
data <- layer$geom$use_defaults(NULL, layer$aes_params)[reps, ]
Expand All @@ -341,7 +343,7 @@ GuideLegend <- ggproto(
data = data,
params = c(layer$computed_geom_params, layer$computed_stat_params)
)
})
}, layer = layers, index = seq_along(layers))

# Remove NULL geoms
params$decor <- compact(decor)
Expand Down Expand Up @@ -472,7 +474,12 @@ GuideLegend <- ggproto(
draw <- function(i) {
bg <- elements$key
keys <- lapply(decor, function(g) {
g$draw_key(vec_slice(g$data, i), g$params, key_size)
data <- vec_slice(g$data, i)
if (data$.draw %||% TRUE) {
g$draw_key(data, g$params, key_size)
} else {
zeroGrob()
}
})
c(list(bg), keys)
}
Expand Down Expand Up @@ -749,3 +756,47 @@ measure_legend_keys <- function(decor, n, dim, byrow = FALSE,
heights = pmax(default_height, apply(size, 1, max))
)
}

# The `keep_key_data` function is for deciding whether keys should be drawn or
# not based on key data collected by the scales.
keep_key_data <- function(key, data, aes, show, index) {
# Figure out whether the layer should have trimmed keys based on the
# `show`, i.e. `layer$show.legend` parameter.
if (is_named(show)) {
trim <- is.na(show[aes])
aes <- aes[trim]
if (length(aes) == 0) {
# No matching aesthetic, probably should keep everything
return(TRUE)
}
trim <- any(trim)
} else {
trim <- is.na(show[1])
}
if (!trim) {
# No matching aesthetic, probably should keep everything
return(TRUE)
}
# Figure out if we have matching key data
match <- lapply(data, function(x) {which(aes %in% x$aesthetics)})
lengs <- lengths(match)
if (sum(lengs) == 0) {
# We don't have matching key data, probably should keep everything
return(TRUE)
}

# Subset for cases where we *do* have key data
data <- data[lengs > 0]
match <- unlist(match[lengs > 0])
data <- lapply(data, `[[`, "data")[match]

# Lookup if we have entries for the keys
for (i in seq_along(aes)) {
keep <- data[[i]]$pal %in% key[[aes[i]]]
data[[i]] <- vec_slice(data[[i]]$member, keep)[, index]
}

# If we have multiple matching aesthetics, either one of them is good enough
# to keep the data
Reduce(`|`, data)
}
8 changes: 4 additions & 4 deletions R/guides-.R
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ Guides <- ggproto(
# arrange all guide grobs

build = function(self, scales, layers, default_mapping,
position, theme, labels) {
position, theme, labels, key_data) {

position <- legend_position(position)
no_guides <- zeroGrob()
Expand Down Expand Up @@ -279,7 +279,7 @@ Guides <- ggproto(

# Merge and process layers
guides$merge()
guides$process_layers(layers)
guides$process_layers(layers, key_data)
if (length(guides$guides) == 0) {
return(no_guides)
}
Expand Down Expand Up @@ -438,9 +438,9 @@ Guides <- ggproto(
},

# Loop over guides to let them extract information from layers
process_layers = function(self, layers) {
process_layers = function(self, layers, key_data) {
self$params <- Map(
function(guide, param) guide$get_layer_key(param, layers),
function(guide, param) guide$get_layer_key(param, layers, key_data),
guide = self$guides,
param = self$params
)
Expand Down
4 changes: 3 additions & 1 deletion R/plot-build.R
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ ggplot_build.ggplot <- function(plot) {
if (npscales$n() > 0) {
lapply(data, npscales$train_df)
data <- lapply(data, npscales$map_df)
plot$key_data <- npscales$key_data(data)
Copy link
Collaborator Author

@teunbrand teunbrand May 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking the data has to happen before the defaults are filled in and after_scale() modifications are applied. Otherwise the information might be lost. I gave the scales list a method to check the data. The downside is that we have to check all the data regardless of whether the scale has a legend guide, as that isn't resolved at this point.

}

# Fill in defaults etc.
Expand Down Expand Up @@ -178,7 +179,8 @@ ggplot_gtable.ggplot_built <- function(data) {
}

legend_box <- plot$guides$build(
plot$scales, plot$layers, plot$mapping, position, theme, plot$labels
plot$scales, plot$layers, plot$mapping, position, theme, plot$labels,
plot$key_data
)

if (is.zero(legend_box)) {
Expand Down
31 changes: 31 additions & 0 deletions R/scales-.R
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,37 @@ ScalesList <- ggproto("ScalesList", NULL,
data_frame0(!!!mapped, df[setdiff(names(df), names(mapped))])
},

key_data = function(self, data) {

scales <- self$scales

lapply(scales, function(s) {
if (!s$is_discrete()) {
return(NULL)
}
n <- s$n.breaks.cache %||% sum(!is.na(s$limits %|W|% s$get_limits()))
if (n < 1) {
return(NULL)
}
pal <- s$palette.cache %||% s$palette(n)
pal <- c(pal, s$na.value)
aes <- s$aesthetics
out <- vapply(data, function(d) {
if (!any(aes %in% names(d))) {
return(rep.int(FALSE, length(pal)))
}
present <- vapply(aes, function(a) {
vec_in(pal, d[[a]])
}, logical(length(pal)))
if (length(dim(present)) > 1) {
present <- rowSums(present) > 0
}
present
}, logical(length(pal)))
list(aesthetics = aes, data = data_frame0(pal = pal, member = out))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point, member is a length(pal) * length(data) logical matrix tracking whether a palette value was seen.

Copy link
Collaborator Author

@teunbrand teunbrand May 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also aes can be a vector, e.g. c("colour", "fill"). The inclusion/exclusion rule in the guide only needs to match one of the aesthetics, not all, to be included.

})
},

transform_df = function(self, df) {
if (empty(df)) {
return(df)
Expand Down
89 changes: 89 additions & 0 deletions tests/testthat/_snaps/draw-key/legend-key-selection.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading