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 @@
+
+
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 @@
+
+
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 @@
+
+
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)
+
+})