Skip to content

Layer parameter controlling facet layout #6336

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

Merged
merged 10 commits into from
Mar 25, 2025
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# ggplot2 (development version)

* New `layer(layout)` argument to interact with facets (@teunbrand, #3062)
* New `stat_connect()` to connect points via steps or other shapes
(@teunbrand, #6228)
* Fixed regression with incorrectly drawn gridlines when using `coord_flip()`
Expand Down
92 changes: 92 additions & 0 deletions R/facet-.R
Original file line number Diff line number Diff line change
Expand Up @@ -872,3 +872,95 @@
}
ranges
}

map_facet_data <- function(data, layout, params) {

if (empty(data)) {
return(vec_cbind(data %|W|% NULL, PANEL = integer(0)))

Check warning on line 879 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L878-L879

Added lines #L878 - L879 were not covered by tests
}

vars <- params$facet %||% c(params$rows, params$cols)

Check warning on line 882 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L882

Added line #L882 was not covered by tests

if (length(vars) == 0) {
data$PANEL <- layout$PANEL
return(data)

Check warning on line 886 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L884-L886

Added lines #L884 - L886 were not covered by tests
}

grid_layout <- all(c("rows", "cols") %in% names(params))
layer_layout <- attr(data, "layout")
if (identical(layer_layout, "fixed")) {
n <- vec_size(data)
data <- vec_rep(data, vec_size(layout))
data$PANEL <- vec_rep_each(layout$PANEL, n)
return(data)

Check warning on line 895 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L889-L895

Added lines #L889 - L895 were not covered by tests
}

# Compute faceting values
facet_vals <- eval_facets(vars, data, params$.possible_columns)

Check warning on line 899 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L899

Added line #L899 was not covered by tests

include_margins <- !isFALSE(params$margin %||% FALSE) &&
nrow(facet_vals) == nrow(data) && grid_layout
if (include_margins) {

Check warning on line 903 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L901-L903

Added lines #L901 - L903 were not covered by tests
# Margins are computed on evaluated faceting values (#1864).
facet_vals <- reshape_add_margins(
vec_cbind(facet_vals, .index = seq_len(nrow(facet_vals))),
list(intersect(names(params$rows), names(facet_vals)),
intersect(names(params$cols), names(facet_vals))),
params$margins %||% FALSE
)

Check warning on line 910 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L905-L910

Added lines #L905 - L910 were not covered by tests
# Apply recycling on original data to fit margins
# We're using base subsetting here because `data` might have a superclass
# that isn't handled well by vctrs::vec_slice
data <- data[facet_vals$.index, , drop = FALSE]
facet_vals$.index <- NULL

Check warning on line 915 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L914-L915

Added lines #L914 - L915 were not covered by tests
}

# If we need to fix rows or columns, we make the corresponding faceting
# variables missing on purpose
if (grid_layout) {
if (identical(layer_layout, "fixed_rows")) {
facet_vals <- facet_vals[setdiff(names(facet_vals), names(params$cols))]

Check warning on line 922 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L920-L922

Added lines #L920 - L922 were not covered by tests
}
if (identical(layer_layout, "fixed_cols")) {
facet_vals <- facet_vals[setdiff(names(facet_vals), names(params$rows))]

Check warning on line 925 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L924-L925

Added lines #L924 - L925 were not covered by tests
}
}

# If any faceting variables are missing, add them in by
# duplicating the data
missing_facets <- setdiff(names(vars), names(facet_vals))
if (length(missing_facets) > 0) {

Check warning on line 932 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L931-L932

Added lines #L931 - L932 were not covered by tests

to_add <- unique0(layout[missing_facets])

Check warning on line 934 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L934

Added line #L934 was not covered by tests

data_rep <- rep.int(seq_len(nrow(data)), nrow(to_add))
facet_rep <- rep(seq_len(nrow(to_add)), each = nrow(data))

Check warning on line 937 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L936-L937

Added lines #L936 - L937 were not covered by tests

data <- unrowname(data[data_rep, , drop = FALSE])
facet_vals <- unrowname(vec_cbind(
unrowname(facet_vals[data_rep, , drop = FALSE]),
unrowname(to_add[facet_rep, , drop = FALSE])
))

Check warning on line 943 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L939-L943

Added lines #L939 - L943 were not covered by tests
}

if (nrow(facet_vals) < 1) {

Check warning on line 946 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L946

Added line #L946 was not covered by tests
# Add PANEL variable
data$PANEL <- NO_PANEL
return(data)

Check warning on line 949 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L948-L949

Added lines #L948 - L949 were not covered by tests
}

facet_vals[] <- lapply(facet_vals, as_unordered_factor)
facet_vals[] <- lapply(facet_vals, addNA, ifany = TRUE)
layout[] <- lapply(layout, as_unordered_factor)

Check warning on line 954 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L952-L954

Added lines #L952 - L954 were not covered by tests

# Add PANEL variable
keys <- join_keys(facet_vals, layout, by = names(vars))
data$PANEL <- layout$PANEL[match(keys$x, keys$y)]

Check warning on line 958 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L957-L958

Added lines #L957 - L958 were not covered by tests

# Filter panels when layer_layout is an integer
if (is_integerish(layer_layout)) {
data <- vec_slice(data, data$PANEL %in% layer_layout)

Check warning on line 962 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L961-L962

Added lines #L961 - L962 were not covered by tests
}

data

Check warning on line 965 in R/facet-.R

View check run for this annotation

Codecov / codecov/patch

R/facet-.R#L965

Added line #L965 was not covered by tests
}
74 changes: 12 additions & 62 deletions R/facet-grid-.R
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@ NULL
#' labels and the interior axes get none. When `"all_x"` or `"all_y"`, only
#' draws the labels at the interior axes in the x- or y-direction
#' respectively.
#'
#' @section Layer layout:
#' The [`layer(layout)`][layer()] argument in context of `facet_grid()` can take
#' the following values:
#' * `NULL` (default) to use the faceting variables to assign panels.
#' * An integer vector to include selected panels. Panel numbers not included in
#' the integer vector are excluded.
#' * `"fixed"` to repeat data across every panel.
#' * `"fixed_rows"` to repeat data across rows.
#' * `"fixed_cols"` to repeat data across columns.
#'
#' @export
#' @seealso
#' The `r link_book("facet grid section", "facet#facet-grid")`
Expand Down Expand Up @@ -282,69 +293,8 @@ FacetGrid <- ggproto("FacetGrid", Facet,

panels
},
map_data = function(data, layout, params) {
if (empty(data)) {
return(vec_cbind(data %|W|% NULL, PANEL = integer(0)))
}

rows <- params$rows
cols <- params$cols
vars <- c(names(rows), names(cols))

if (length(vars) == 0) {
data$PANEL <- layout$PANEL
return(data)
}

# Compute faceting values
facet_vals <- eval_facets(c(rows, cols), data, params$.possible_columns)
if (nrow(facet_vals) == nrow(data)) {
# Margins are computed on evaluated faceting values (#1864).
facet_vals <- reshape_add_margins(
# We add an index column to track data recycling
vec_cbind(facet_vals, .index = seq_len(nrow(facet_vals))),
list(intersect(names(rows), names(facet_vals)),
intersect(names(cols), names(facet_vals))),
params$margins
)
# Apply recycling on original data to fit margins
# We're using base subsetting here because `data` might have a superclass
# that isn't handled well by vctrs::vec_slice
data <- data[facet_vals$.index, , drop = FALSE]
facet_vals$.index <- NULL
}

# If any faceting variables are missing, add them in by
# duplicating the data
missing_facets <- setdiff(vars, names(facet_vals))
if (length(missing_facets) > 0) {
to_add <- unique0(layout[missing_facets])

data_rep <- rep.int(seq_len(nrow(data)), nrow(to_add))
facet_rep <- rep(seq_len(nrow(to_add)), each = nrow(data))

data <- unrowname(data[data_rep, , drop = FALSE])
facet_vals <- unrowname(vec_cbind(
unrowname(facet_vals[data_rep, , drop = FALSE]),
unrowname(to_add[facet_rep, , drop = FALSE]))
)
}

# Add PANEL variable
if (nrow(facet_vals) == 0) {
# Special case of no faceting
data$PANEL <- NO_PANEL
} else {
facet_vals[] <- lapply(facet_vals[], as_unordered_factor)
facet_vals[] <- lapply(facet_vals[], addNA, ifany = TRUE)
layout[] <- lapply(layout[], as_unordered_factor)

keys <- join_keys(facet_vals, layout, by = vars)

data$PANEL <- layout$PANEL[match(keys$x, keys$y)]
}
data
},
map_data = map_facet_data,

attach_axes = function(table, layout, ranges, coord, theme, params) {

Expand Down
3 changes: 3 additions & 0 deletions R/facet-null.R
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ NULL
#' @inheritParams facet_grid
#' @keywords internal
#' @export
#' @section Layer layout:
#' The [`layer(layout)`][layer()] argument in context of `facet_null()` is
#' completely ignored.
#' @examples
#' # facet_null is the default faceting specification if you
#' # don't override it with facet_grid or facet_wrap
Expand Down
45 changes: 10 additions & 35 deletions R/facet-wrap.R
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ NULL
#' the exterior axes get labels, and the interior axes get none. When
#' `"all_x"` or `"all_y"`, only draws the labels at the interior axes in the
#' x- or y-direction respectively.
#'
#' @section Layer layout:
#' The [`layer(layout)`][layer()] argument in context of `facet_wrap()` can take
#' the following values:
#' * `NULL` (default) to use the faceting variables to assign panels.
#' * An integer vector to include selected panels. Panel numbers not included in
#' the integer vector are excluded.
#' * `"fixed"` to repeat data across every panel.
#'
#' @inheritParams facet_grid
#' @seealso
#' The `r link_book("facet wrap section", "facet#sec-facet-wrap")`
Expand Down Expand Up @@ -247,42 +256,8 @@ FacetWrap <- ggproto("FacetWrap", Facet,

panels
},
map_data = function(data, layout, params) {
if (empty(data)) {
return(vec_cbind(data %|W|% NULL, PANEL = integer(0)))
}

vars <- params$facets

if (length(vars) == 0) {
data$PANEL <- layout$PANEL
return(data)
}

facet_vals <- eval_facets(vars, data, params$.possible_columns)
facet_vals[] <- lapply(facet_vals[], as_unordered_factor)
layout[] <- lapply(layout[], as_unordered_factor)

missing_facets <- setdiff(names(vars), names(facet_vals))
if (length(missing_facets) > 0) {

to_add <- unique0(layout[missing_facets])

data_rep <- rep.int(seq_len(nrow(data)), nrow(to_add))
facet_rep <- rep(seq_len(nrow(to_add)), each = nrow(data))

data <- data[data_rep, , drop = FALSE]
facet_vals <- vec_cbind(
facet_vals[data_rep, , drop = FALSE],
to_add[facet_rep, , drop = FALSE]
)
}

keys <- join_keys(facet_vals, layout, by = names(vars))

data$PANEL <- layout$PANEL[match(keys$x, keys$y)]
data
},
map_data = map_facet_data,

attach_axes = function(table, layout, ranges, coord, theme, params) {

Expand Down
10 changes: 7 additions & 3 deletions R/layer.R
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
#' @param params Additional parameters to the `geom` and `stat`.
#' @param key_glyph A legend key drawing function or a string providing the
#' function name minus the `draw_key_` prefix. See [draw_key] for details.
#' @param layout Argument to control layout at the layer level. Consult the
#' faceting documentation to view appropriate values.
#' @param layer_class The type of layer object to be constructed. This is
#' intended for ggplot2 internal use only.
#' @keywords internal
Expand Down Expand Up @@ -98,7 +100,7 @@ layer <- function(geom = NULL, stat = NULL,
data = NULL, mapping = NULL,
position = NULL, params = list(),
inherit.aes = TRUE, check.aes = TRUE, check.param = TRUE,
show.legend = NA, key_glyph = NULL, layer_class = Layer) {
show.legend = NA, key_glyph = NULL, layout = NULL, layer_class = Layer) {
call_env <- caller_env()
user_env <- caller_env(2)

Expand Down Expand Up @@ -132,7 +134,7 @@ layer <- function(geom = NULL, stat = NULL,
geom_params <- params[intersect(names(params), geom$parameters(TRUE))]
stat_params <- params[intersect(names(params), stat$parameters(TRUE))]

ignore <- c("key_glyph", "name")
ignore <- c("key_glyph", "name", "layout")
all <- c(geom$parameters(TRUE), stat$parameters(TRUE), geom$aesthetics(), position$aesthetics(), ignore)

# Take care of plain patterns provided as aesthetic
Expand Down Expand Up @@ -192,7 +194,8 @@ layer <- function(geom = NULL, stat = NULL,
position = position,
inherit.aes = inherit.aes,
show.legend = show.legend,
name = params$name
name = params$name,
layout = layout %||% params$layout
)
}

Expand Down Expand Up @@ -282,6 +285,7 @@ Layer <- ggproto("Layer", NULL,
} else {
self$computed_mapping <- self$mapping
}
attr(data, "layout") <- self$layout

data
},
Expand Down
14 changes: 14 additions & 0 deletions man/facet_grid.Rd

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

6 changes: 6 additions & 0 deletions man/facet_null.Rd

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

12 changes: 12 additions & 0 deletions man/facet_wrap.Rd

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

4 changes: 4 additions & 0 deletions man/layer.Rd

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

Loading
Loading