diff --git a/NEWS.md b/NEWS.md index 768c18064b..d090cbcf46 100644 --- a/NEWS.md +++ b/NEWS.md @@ -21,6 +21,11 @@ * Changed `theme_grey()` setting for legend key so that it creates no border (`NA`) rather than drawing a white one. (@annennenne, #3180) + +* Themes have gained two new parameters, `plot.title.position` and + `plot.caption.position`, that can be used to customize how plot + title/subtitle and plot caption are positioned relative to the overall plot + (@clauswilke, #3252). * Added function `ggplot_add.by()` for lists created with `by()` (#2734, @Maschette) diff --git a/R/plot-build.r b/R/plot-build.r index 0bcb856338..e24a3d8882 100644 --- a/R/plot-build.r +++ b/R/plot-build.r @@ -261,20 +261,46 @@ ggplot_gtable.ggplot_built <- function(data) { caption <- element_render(theme, "plot.caption", plot$labels$caption, margin_y = TRUE) caption_height <- grobHeight(caption) - pans <- plot_table$layout[grepl("^panel", plot_table$layout$name), , - drop = FALSE] + # positioning of title and subtitle is governed by plot.title.position + # positioning of caption is governed by plot.caption.position + # "panel" means align to the panel(s) + # "plot" means align to the entire plot (except margins and tag) + title_pos <- theme$plot.title.position %||% "panel" + if (!(title_pos %in% c("panel", "plot"))) { + stop('plot.title.position should be either "panel" or "plot".', call. = FALSE) + } + caption_pos <- theme$plot.caption.position %||% "panel" + if (!(caption_pos %in% c("panel", "plot"))) { + stop('plot.caption.position should be either "panel" or "plot".', call. = FALSE) + } + + pans <- plot_table$layout[grepl("^panel", plot_table$layout$name), , drop = FALSE] + if (title_pos == "panel") { + title_l = min(pans$l) + title_r = max(pans$r) + } else { + title_l = 1 + title_r = ncol(plot_table) + } + if (caption_pos == "panel") { + caption_l = min(pans$l) + caption_r = max(pans$r) + } else { + caption_l = 1 + caption_r = ncol(plot_table) + } plot_table <- gtable_add_rows(plot_table, subtitle_height, pos = 0) plot_table <- gtable_add_grob(plot_table, subtitle, name = "subtitle", - t = 1, b = 1, l = min(pans$l), r = max(pans$r), clip = "off") + t = 1, b = 1, l = title_l, r = title_r, clip = "off") plot_table <- gtable_add_rows(plot_table, title_height, pos = 0) plot_table <- gtable_add_grob(plot_table, title, name = "title", - t = 1, b = 1, l = min(pans$l), r = max(pans$r), clip = "off") + t = 1, b = 1, l = title_l, r = title_r, clip = "off") plot_table <- gtable_add_rows(plot_table, caption_height, pos = -1) plot_table <- gtable_add_grob(plot_table, caption, name = "caption", - t = -1, b = -1, l = min(pans$l), r = max(pans$r), clip = "off") + t = -1, b = -1, l = caption_l, r = caption_r, clip = "off") plot_table <- gtable_add_rows(plot_table, unit(0, 'pt'), pos = 0) plot_table <- gtable_add_cols(plot_table, unit(0, 'pt'), pos = 0) diff --git a/R/theme-defaults.r b/R/theme-defaults.r index 43fca0526b..00a92bb99e 100644 --- a/R/theme-defaults.r +++ b/R/theme-defaults.r @@ -214,6 +214,7 @@ theme_grey <- function(base_size = 11, base_family = "", hjust = 0, vjust = 1, margin = margin(b = half_line) ), + plot.title.position = "panel", plot.subtitle = element_text( # font size "regular" hjust = 0, vjust = 1, margin = margin(b = half_line) @@ -223,6 +224,7 @@ theme_grey <- function(base_size = 11, base_family = "", hjust = 1, vjust = 1, margin = margin(t = half_line) ), + plot.caption.position = "panel", plot.tag = element_text( size = rel(1.2), hjust = 0.5, vjust = 0.5 @@ -487,6 +489,7 @@ theme_void <- function(base_size = 11, base_family = "", hjust = 0, vjust = 1, margin = margin(t = half_line) ), + plot.title.position = "panel", plot.subtitle = element_text( hjust = 0, vjust = 1, margin = margin(t = half_line) @@ -496,6 +499,7 @@ theme_void <- function(base_size = 11, base_family = "", hjust = 1, vjust = 1, margin = margin(t = half_line) ), + plot.caption.position = "panel", plot.tag = element_text( size = rel(1.2), hjust = 0.5, vjust = 0.5 @@ -615,6 +619,7 @@ theme_test <- function(base_size = 11, base_family = "", hjust = 0, vjust = 1, margin = margin(b = half_line) ), + plot.title.position = "panel", plot.subtitle = element_text( hjust = 0, vjust = 1, margin = margin(b = half_line) @@ -624,6 +629,7 @@ theme_test <- function(base_size = 11, base_family = "", hjust = 1, vjust = 1, margin = margin(t = half_line) ), + plot.caption.position = "panel", plot.tag = element_text( size = rel(1.2), hjust = 0.5, vjust = 0.5 diff --git a/R/theme-elements.r b/R/theme-elements.r index 316c35a1f4..f2916d71e8 100644 --- a/R/theme-elements.r +++ b/R/theme-elements.r @@ -370,8 +370,10 @@ el_def <- function(class = NULL, inherit = NULL, description = NULL) { plot.background = el_def("element_rect", "rect"), plot.title = el_def("element_text", "title"), + plot.title.position = el_def("character"), plot.subtitle = el_def("element_text", "title"), plot.caption = el_def("element_text", "title"), + plot.caption.position = el_def("character"), plot.tag = el_def("element_text", "title"), plot.tag.position = el_def("character"), # Need to also accept numbers plot.margin = el_def("margin"), diff --git a/R/theme.r b/R/theme.r index 2fa96b4c8a..fe461f2bf5 100644 --- a/R/theme.r +++ b/R/theme.r @@ -124,6 +124,12 @@ #' inherits from `title`) left-aligned by default #' @param plot.caption caption below the plot (text appearance) #' ([element_text()]; inherits from `title`) right-aligned by default +#' @param plot.title.position,plot.caption.position Alignment of the plot title/subtitle +#' and caption. The setting for `plot.title.position` applies to both +#' the title and the subtitle. A value of "panel" (the default) means that +#' titles and/or caption are aligned to the plot panels. A value of "plot" means +#' that titles and/or caption are aligned to the entire plot (minus any space +#' for margins and plot tag). #' @param plot.tag upper-left label to identify a plot (text appearance) #' ([element_text()]; inherits from `title`) left-aligned by default #' @param plot.tag.position The position of the tag as a string ("topleft", @@ -334,8 +340,10 @@ theme <- function(line, panel.ontop, plot.background, plot.title, + plot.title.position, plot.subtitle, plot.caption, + plot.caption.position, plot.tag, plot.tag.position, plot.margin, diff --git a/man/theme.Rd b/man/theme.Rd index 3d5af33dbf..c06deb4d74 100644 --- a/man/theme.Rd +++ b/man/theme.Rd @@ -23,11 +23,11 @@ theme(line, rect, text, title, aspect.ratio, axis.title, axis.title.x, panel.spacing.y, panel.grid, panel.grid.major, panel.grid.minor, panel.grid.major.x, panel.grid.major.y, panel.grid.minor.x, panel.grid.minor.y, panel.ontop, plot.background, plot.title, - plot.subtitle, plot.caption, plot.tag, plot.tag.position, plot.margin, - strip.background, strip.background.x, strip.background.y, - strip.placement, strip.text, strip.text.x, strip.text.y, - strip.switch.pad.grid, strip.switch.pad.wrap, ..., complete = FALSE, - validate = TRUE) + plot.title.position, plot.subtitle, plot.caption, plot.caption.position, + plot.tag, plot.tag.position, plot.margin, strip.background, + strip.background.x, strip.background.y, strip.placement, strip.text, + strip.text.x, strip.text.y, strip.switch.pad.grid, strip.switch.pad.wrap, + ..., complete = FALSE, validate = TRUE) } \arguments{ \item{line}{all line elements (\code{\link[=element_line]{element_line()}})} @@ -153,6 +153,13 @@ inherits from \code{rect})} \item{plot.title}{plot title (text appearance) (\code{\link[=element_text]{element_text()}}; inherits from \code{title}) left-aligned by default} +\item{plot.title.position, plot.caption.position}{Alignment of the plot title/subtitle +and caption. The setting for \code{plot.title.position} applies to both +the title and the subtitle. A value of "panel" (the default) means that +titles and/or caption are aligned to the plot panels. A value of "plot" means +that titles and/or caption are aligned to the entire plot (minus any space +for margins and plot tag).} + \item{plot.subtitle}{plot subtitle (text appearance) (\code{\link[=element_text]{element_text()}}; inherits from \code{title}) left-aligned by default} diff --git a/tests/figs/themes/caption-aligned-to-entire-plot.svg b/tests/figs/themes/caption-aligned-to-entire-plot.svg new file mode 100644 index 0000000000..8edbac9519 --- /dev/null +++ b/tests/figs/themes/caption-aligned-to-entire-plot.svg @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +a + + + + + + + + + + + +b + + + + + + + + + + + +c + + + + + + + + + + +1.0 +1.5 +2.0 +2.5 +3.0 + + + + + +1.0 +1.5 +2.0 +2.5 +3.0 + + + + + +1.0 +1.5 +2.0 +2.5 +3.0 +1.0 +1.5 +2.0 +2.5 +3.0 + + + + + +x +y + +z + + + + + + +a +b +c +Subtitle aligned to panels +Plot title aligned to panels +Caption aligned to entire plot + diff --git a/tests/figs/themes/title-aligned-to-entire-plot.svg b/tests/figs/themes/title-aligned-to-entire-plot.svg new file mode 100644 index 0000000000..4d86911d17 --- /dev/null +++ b/tests/figs/themes/title-aligned-to-entire-plot.svg @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +a + + + + + + + + + + + +b + + + + + + + + + + + +c + + + + + + + + + + +1.0 +1.5 +2.0 +2.5 +3.0 + + + + + +1.0 +1.5 +2.0 +2.5 +3.0 + + + + + +1.0 +1.5 +2.0 +2.5 +3.0 +1.0 +1.5 +2.0 +2.5 +3.0 + + + + + +x +y + +z + + + + + + +a +b +c +Subtitle aligned to entire plot +Plot title aligned to entire plot +Caption aligned to entire plot + diff --git a/tests/figs/themes/titles-aligned-to-entire-plot.svg b/tests/figs/themes/titles-aligned-to-entire-plot.svg new file mode 100644 index 0000000000..3e329cf8c4 --- /dev/null +++ b/tests/figs/themes/titles-aligned-to-entire-plot.svg @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +a + + + + + + + + + + + +b + + + + + + + + + + + +c + + + + + + + + + + +1.0 +1.5 +2.0 +2.5 +3.0 + + + + + +1.0 +1.5 +2.0 +2.5 +3.0 + + + + + +1.0 +1.5 +2.0 +2.5 +3.0 +1.0 +1.5 +2.0 +2.5 +3.0 + + + + + +x +y + +z + + + + + + +a +b +c +Subtitle aligned to entire plot +Plot title aligned to entire plot +Caption aligned to panels + diff --git a/tests/testthat/test-theme.r b/tests/testthat/test-theme.r index a9f8afb3e5..192c253169 100644 --- a/tests/testthat/test-theme.r +++ b/tests/testthat/test-theme.r @@ -400,3 +400,32 @@ test_that("rotated axis tick labels work", { theme(axis.text.x = element_text(angle = 50, hjust = 1)) expect_doppelganger("rotated x axis tick labels", plot) }) + +test_that("plot titles and caption can be aligned to entire plot", { + df <- data_frame( + x = 1:3, + y = 1:3, + z = letters[1:3] + ) + + plot <- ggplot(df, aes(x, y, color = z)) + + geom_point() + facet_wrap(~z) + + labs( + title = "Plot title aligned to entire plot", + subtitle = "Subtitle aligned to entire plot", + caption = "Caption aligned to panels" + ) + + theme(plot.title.position = "plot") + expect_doppelganger("titles aligned to entire plot", plot) + + plot <- ggplot(df, aes(x, y, color = z)) + + geom_point() + facet_wrap(~z) + + labs( + title = "Plot title aligned to panels", + subtitle = "Subtitle aligned to panels", + caption = "Caption aligned to entire plot" + ) + + theme(plot.caption.position = "plot") + expect_doppelganger("caption aligned to entire plot", plot) + +})