Skip to content

Commit 90de384

Browse files
authored
Refactor titleGrob() (#5273)
* reimplement titleGrob * strips don't adjust titleGrob's viewports * Further simplification * accept rounding errors in visual tests * Simplify `assemble_strips()` * Add back debug rectangles * Add (back) comments * Retire functions that are no longer needed * Add code review suggestions * Leave news bullet for extension maintainers
1 parent 89a7613 commit 90de384

21 files changed

+106
-188
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# ggplot2 (development version)
22

3+
* As an internal change, the `titleGrob()` has been refactored to be faster.
34
* The `translate_shape_string()` internal function is now exported for use in
45
extensions of point layers (@teunbrand, #5191).
56
* Fixed bug in `coord_sf()` where graticule lines didn't obey

R/labeller.R

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -557,31 +557,12 @@ assemble_strips <- function(grobs, theme, horizontal = TRUE, clip) {
557557
return(grobs)
558558
}
559559

560-
# Add margins to non-titleGrobs so they behave eqivalently
561-
grobs[] <- lapply(grobs, function(g) {
562-
if (inherits(g, "titleGrob")) return(g)
563-
add_margins(gList(g), grobHeight(g), grobWidth(g), margin_x = TRUE, margin_y = TRUE)
564-
})
565-
566560
if (horizontal) {
567-
height <- max_height(lapply(grobs, function(x) x$heights[2]))
561+
height <- max_height(grobs)
568562
width <- unit(1, "null")
569563
} else {
570564
height <- unit(1, "null")
571-
width <- max_width(lapply(grobs, function(x) x$widths[2]))
572-
}
573-
grobs[] <- lapply(grobs, function(x) {
574-
# Avoid unit subset assignment to support R 3.2
575-
x$widths <- unit.c(x$widths[1], width, x$widths[c(-1, -2)])
576-
x$heights <- unit.c(x$heights[1], height, x$heights[c(-1, -2)])
577-
x$vp$parent$layout$widths <- unit.c(x$vp$parent$layout$widths[1], width, x$vp$parent$layout$widths[c(-1, -2)])
578-
x$vp$parent$layout$heights <- unit.c(x$vp$parent$layout$heights[1], height, x$vp$parent$layout$heights[c(-1, -2)])
579-
x
580-
})
581-
if (horizontal) {
582-
height <- sum(grobs[[1]]$heights)
583-
} else {
584-
width <- sum(grobs[[1]]$widths)
565+
width <- max_width(grobs)
585566
}
586567

587568
background <- if (horizontal) "strip.background.x" else "strip.background.y"

R/margins.R

Lines changed: 85 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,31 @@ is.margin <- function(x) {
1212
inherits(x, "margin")
1313
}
1414

15-
margin_height <- function(grob, margins) {
16-
if (is.zero(grob)) return(unit(0, "cm"))
17-
18-
grobHeight(grob) + margins[1] + margins[3]
19-
}
20-
21-
margin_width <- function(grob, margins) {
22-
if (is.zero(grob)) return(unit(0, "cm"))
23-
24-
grobWidth(grob) + margins[2] + margins[4]
25-
}
26-
27-
#' Text grob, height, and width
15+
#' Create a text grob with the proper location and margins
2816
#'
29-
#' This function returns a list containing a text grob (and, optionally,
30-
#' debugging grobs) and the height and width of the text grob.
17+
#' `titleGrob()` is called when creating titles and labels for axes, legends,
18+
#' and facet strips.
3119
#'
32-
#' @param label Either `NULL`, a string (length 1 character vector), or
33-
#' an expression.
34-
#' @param x,y x and y locations where the text is to be placed. If `x` and `y`
35-
#' are `NULL`, `hjust` and `vjust` are used to determine the location.
36-
#' @inheritParams titleGrob
20+
#' @param label Text to place on the plot. These maybe axis titles, axis labels,
21+
#' facet strip titles, etc.
22+
#' @param x,y x and y locations where the text is to be placed.
23+
#' @param hjust,vjust Horizontal and vertical justification of the text.
24+
#' @param angle Angle of rotation of the text.
25+
#' @param gp Additional graphical parameters in a call to `gpar()`.
26+
#' @param margin Margins around the text. See [margin()] for more
27+
#' details.
28+
#' @param margin_x,margin_y Whether or not to add margins in the x/y direction.
29+
#' @param debug If `TRUE`, aids visual debugging by drawing a solid
30+
#' rectangle behind the complete text area, and a point where each label
31+
#' is anchored.
3732
#'
3833
#' @noRd
39-
title_spec <- function(label, x, y, hjust, vjust, angle, gp = gpar(),
40-
debug = FALSE, check.overlap = FALSE) {
41-
42-
if (is.null(label)) return(zeroGrob())
34+
titleGrob <- function(label, x, y, hjust, vjust, angle = 0, gp = gpar(),
35+
margin = NULL, margin_x = FALSE, margin_y = FALSE,
36+
debug = FALSE, check.overlap = FALSE) {
37+
if (is.null(label)) {
38+
return(zeroGrob())
39+
}
4340

4441
# We rotate the justifiation values to obtain the correct x and y reference point,
4542
# since hjust and vjust are applied relative to the rotated text frame in textGrob
@@ -48,16 +45,17 @@ title_spec <- function(label, x, y, hjust, vjust, angle, gp = gpar(),
4845
n <- max(length(x), length(y), 1)
4946
x <- x %||% unit(rep(just$hjust, n), "npc")
5047
y <- y %||% unit(rep(just$vjust, n), "npc")
48+
if (!is.unit(x)) {
49+
x <- unit(x, "npc")
50+
}
51+
if (!is.unit(y)) {
52+
y <- unit(y, "npc")
53+
}
5154

52-
text_grob <- textGrob(
53-
label,
54-
x,
55-
y,
56-
hjust = hjust,
57-
vjust = vjust,
58-
rot = angle,
59-
gp = gp,
60-
check.overlap = check.overlap
55+
grob <- textGrob(
56+
label, x, y,
57+
hjust = hjust, vjust = vjust,
58+
rot = angle, gp = gp, check.overlap = check.overlap
6159
)
6260

6361
# The grob dimensions don't include the text descenders, so these need to be added
@@ -71,140 +69,78 @@ title_spec <- function(label, x, y, hjust, vjust, angle, gp = gpar(),
7169
# Use trigonometry to calculate grobheight and width for rotated grobs. This is only
7270
# exactly correct when vjust = 1. We need to take the absolute value so we don't make
7371
# the grob smaller when it's flipped over.
74-
text_height <- unit(1, "grobheight", text_grob) + abs(cos(angle[1] / 180 * pi)) * descent
75-
text_width <- unit(1, "grobwidth", text_grob) + abs(sin(angle[1] / 180 * pi)) * descent
72+
rad <- (angle[1] %% 360) / 180 * pi
73+
x_descent <- abs(sin(rad)) * descent
74+
y_descent <- abs(cos(rad)) * descent
7675

77-
if (isTRUE(debug)) {
78-
children <- gList(
79-
rectGrob(gp = gpar(fill = "cornsilk", col = NA)),
80-
pointsGrob(x, y, pch = 20, gp = gpar(col = "gold")),
81-
text_grob
82-
)
83-
} else {
84-
children <- gList(text_grob)
85-
}
86-
87-
list(
88-
text_grob = children,
89-
text_height = text_height,
90-
text_width = text_width
91-
)
92-
}
93-
94-
#' Add margins
95-
#'
96-
#' Given a text grob, `add_margins()` adds margins around the grob in the
97-
#' directions determined by `margin_x` and `margin_y`.
98-
#'
99-
#' @param grob A gList containing a grob, such as a text grob
100-
#' @param height,width Usually the height and width of the text grob. Passed as
101-
#' separate arguments from the grob itself because in the special case of
102-
#' facet strip labels each set of strips should share the same height and
103-
#' width, even if the labels are of different length.
104-
#' @inheritParams titleGrob
105-
#'
106-
#' @noRd
107-
add_margins <- function(grob, height, width, margin = NULL,
108-
gp = gpar(), margin_x = FALSE, margin_y = FALSE) {
76+
# Set text size to actual size including descenders
77+
width <- unit(1, "grobwidth", grob) + x_descent
78+
height <- unit(1, "grobheight", grob) + y_descent
10979

80+
# Resolve margin
11081
if (is.null(margin)) {
11182
margin <- margin(0, 0, 0, 0)
11283
}
84+
margin_x <- isTRUE(margin_x)
85+
margin_y <- isTRUE(margin_y)
86+
87+
# Initialise new values for position and dimensions
88+
new_x <- NULL
89+
new_y <- NULL
90+
new_width <- NULL
91+
new_height <- NULL
92+
93+
# Calculate new x/width
94+
if (margin_x) {
95+
new_width <- unit.c(margin[4], width, margin[2])
96+
new_x <- x - margin[2] * just$hjust + margin[4] * (1 - just$hjust)
97+
}
11398

114-
if (margin_x && margin_y) {
115-
widths <- unit.c(margin[4], width, margin[2])
116-
heights <- unit.c(margin[1], height, margin[3])
99+
# Calculate new y/height
100+
if (margin_y) {
101+
new_height <- unit.c(margin[1], height, margin[3])
102+
new_y <- y - margin[1] * just$vjust + margin[3] * (1 - just$vjust)
103+
}
117104

118-
vp <- viewport(
119-
layout = grid.layout(3, 3, heights = heights, widths = widths),
120-
gp = gp
121-
)
122-
child_vp <- viewport(layout.pos.row = 2, layout.pos.col = 2)
123-
} else if (margin_x) {
124-
widths <- unit.c(margin[4], width, margin[2])
125-
vp <- viewport(layout = grid.layout(1, 3, widths = widths), gp = gp)
126-
child_vp <- viewport(layout.pos.col = 2)
105+
# If only one margin is set, the other dimension is a null unit
106+
if (xor(margin_x, margin_y)) {
107+
new_width <- new_width %||% unit(1, "null")
108+
new_height <- new_height %||% unit(1, "null")
109+
}
127110

128-
heights <- unit(1, "null")
129-
} else if (margin_y) {
130-
heights <- unit.c(margin[1], height, margin[3])
111+
# If we haven't touched the new positions/dimensions, use the previous ones
112+
new_width <- new_width %||% width
113+
new_height <- new_height %||% height
114+
x <- new_x %||% x
115+
y <- new_y %||% y
131116

132-
vp <- viewport(layout = grid.layout(3, 1, heights = heights), gp = gp)
133-
child_vp <- viewport(layout.pos.row = 2)
117+
# Adjust the grob
118+
grob$x <- x
119+
grob$y <- y
134120

135-
widths <- unit(1, "null")
136-
} else {
137-
widths <- width
138-
heights <- height
139-
return(
140-
gTree(
141-
children = grob,
142-
widths = widths,
143-
heights = heights,
144-
cl = "titleGrob"
145-
)
121+
# Add debug rectangles/points if necessary
122+
if (isTRUE(debug)) {
123+
children <- gList(
124+
rectGrob(
125+
x = x, y = y, width = width, height = height,
126+
hjust = just$hjust, vjust = just$vjust,
127+
gp = gpar(fill = "cornsilk", col = NA)
128+
),
129+
pointsGrob(x, y, pch = 20, gp = gpar(col = "gold")),
130+
grob
146131
)
132+
} else {
133+
children <- gList(grob)
147134
}
148135

149136
gTree(
150-
children = grob,
151-
vp = vpTree(vp, vpList(child_vp)),
152-
widths = widths,
153-
heights = heights,
137+
children = children,
138+
widths = new_width,
139+
heights = new_height,
154140
cl = "titleGrob"
155141
)
156142
}
157143

158-
#' Create a text grob with the proper location and margins
159-
#'
160-
#' `titleGrob()` is called when creating titles and labels for axes, legends,
161-
#' and facet strips.
162-
#'
163-
#' @param label Text to place on the plot. These maybe axis titles, axis labels,
164-
#' facet strip titles, etc.
165-
#' @param x,y x and y locations where the text is to be placed.
166-
#' @param hjust,vjust Horizontal and vertical justification of the text.
167-
#' @param angle Angle of rotation of the text.
168-
#' @param gp Additional graphical parameters in a call to `gpar()`.
169-
#' @param margin Margins around the text. See [margin()] for more
170-
#' details.
171-
#' @param margin_x,margin_y Whether or not to add margins in the x/y direction.
172-
#' @param debug If `TRUE`, aids visual debugging by drawing a solid
173-
#' rectangle behind the complete text area, and a point where each label
174-
#' is anchored.
175-
#'
176-
#' @noRd
177-
titleGrob <- function(label, x, y, hjust, vjust, angle = 0, gp = gpar(),
178-
margin = NULL, margin_x = FALSE, margin_y = FALSE,
179-
debug = FALSE, check.overlap = FALSE) {
180-
181-
if (is.null(label))
182-
return(zeroGrob())
183-
184-
# Get text grob, text height, and text width
185-
grob_details <- title_spec(
186-
label,
187-
x = x,
188-
y = y,
189-
hjust = hjust,
190-
vjust = vjust,
191-
angle = angle,
192-
gp = gp,
193-
debug = debug,
194-
check.overlap = check.overlap
195-
)
196-
197-
add_margins(
198-
grob = grob_details$text_grob,
199-
height = grob_details$text_height,
200-
width = grob_details$text_width,
201-
gp = gp,
202-
margin = margin,
203-
margin_x = margin_x,
204-
margin_y = margin_y
205-
)
206-
}
207-
208144
#' @export
209145
widthDetails.titleGrob <- function(x) {
210146
sum(x$widths)

tests/testthat/_snaps/facet-/left-justified-facet-labels-with-margins.svg

Lines changed: 1 addition & 1 deletion
Loading

tests/testthat/_snaps/facet-layout/axes-are-positioned-correctly-in-non-table-layout.svg

Lines changed: 1 addition & 1 deletion
Loading

tests/testthat/_snaps/geom-dotplot/bin-y-dodged.svg

Lines changed: 1 addition & 1 deletion
Loading

tests/testthat/_snaps/geom-hex/single-hex-bin-with-width-and-height-of-0-1.svg

Lines changed: 1 addition & 1 deletion
Loading

tests/testthat/_snaps/guides/align-facet-labels-facets-horizontal.svg

Lines changed: 1 addition & 1 deletion
Loading

tests/testthat/_snaps/guides/align-facet-labels-facets-vertical.svg

Lines changed: 1 addition & 1 deletion
Loading

tests/testthat/_snaps/position-stack/area-stacking.svg

Lines changed: 1 addition & 1 deletion
Loading

tests/testthat/_snaps/scales-breaks-labels/character.svg

Lines changed: 1 addition & 1 deletion
Loading

tests/testthat/_snaps/scales-breaks-labels/date.svg

Lines changed: 1 addition & 1 deletion
Loading

0 commit comments

Comments
 (0)