Skip to content

Commit 1ae034e

Browse files
authored
Add strategies to deal with overlapping text in draw_axis() (#3375)
* add visual test cases for basic axis drawing * add check.overlap option for removing overlapping axis labels (recursively prioritizing the "middle" label) * add axis label rotation and heuristics to set best hjust and vjust to draw_axis() * add support for axis label dodging into multiple rows/cols * update documentation for new axis options
1 parent 6030a5b commit 1ae034e

14 files changed

+1417
-94
lines changed

R/guides-axis.r

Lines changed: 146 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,20 @@
44
#' @param break_position position of ticks
55
#' @param break_labels labels at ticks
66
#' @param axis_position position of axis (top, bottom, left or right)
7-
#' @param theme A [theme()] object
7+
#' @param theme A complete [theme()] object
8+
#' @param check.overlap silently remove overlapping labels,
9+
#' (recursively) prioritizing the first, last, and middle labels.
10+
#' @param angle Compared to setting the angle in [theme()] / [element_text()],
11+
#' this also uses some heuristics to automatically pick the `hjust` and `vjust` that
12+
#' you probably want.
13+
#' @param n_dodge The number of rows (for vertical axes) or columns (for
14+
#' horizontal axes) that should be used to render the labels. This is
15+
#' useful for displaying labels that would otherwise overlap.
816
#'
917
#' @noRd
1018
#'
11-
draw_axis <- function(break_positions, break_labels, axis_position, theme) {
19+
draw_axis <- function(break_positions, break_labels, axis_position, theme,
20+
check.overlap = FALSE, angle = NULL, n_dodge = 1) {
1221

1322
axis_position <- match.arg(axis_position, c("top", "bottom", "right", "left"))
1423
aesthetic <- if (axis_position %in% c("top", "bottom")) "x" else "y"
@@ -24,17 +33,24 @@ draw_axis <- function(break_positions, break_labels, axis_position, theme) {
2433
tick_length <- calc_element(tick_length_element_name, theme)
2534
label_element <- calc_element(label_element_name, theme)
2635

36+
# override label element parameters for rotation
37+
if (inherits(label_element, "element_text")) {
38+
label_element <- merge_element(
39+
axis_label_element_overrides(axis_position, angle),
40+
label_element
41+
)
42+
}
43+
2744
# conditionally set parameters that depend on axis orientation
2845
is_vertical <- axis_position %in% c("left", "right")
2946

3047
position_dim <- if (is_vertical) "y" else "x"
3148
non_position_dim <- if (is_vertical) "x" else "y"
3249
position_size <- if (is_vertical) "height" else "width"
3350
non_position_size <- if (is_vertical) "width" else "height"
34-
label_margin_name <- if (is_vertical) "margin_x" else "margin_y"
3551
gtable_element <- if (is_vertical) gtable_row else gtable_col
3652
measure_gtable <- if (is_vertical) gtable_width else gtable_height
37-
measure_labels <- if (is_vertical) grobWidth else grobHeight
53+
measure_labels_non_pos <- if (is_vertical) grobWidth else grobHeight
3854

3955
# conditionally set parameters that depend on which side of the panel
4056
# the axis is on
@@ -47,8 +63,6 @@ draw_axis <- function(break_positions, break_labels, axis_position, theme) {
4763
# conditionally set the gtable ordering
4864
labels_first_gtable <- axis_position %in% c("left", "top") # refers to position in gtable
4965

50-
table_order <- if (labels_first_gtable) c("labels", "ticks") else c("ticks", "labels")
51-
5266
# set common parameters
5367
n_breaks <- length(break_positions)
5468
opposite_positions <- c("top" = "bottom", "bottom" = "top", "right" = "left", "left" = "right")
@@ -80,12 +94,19 @@ draw_axis <- function(break_positions, break_labels, axis_position, theme) {
8094
}
8195
}
8296

83-
labels_grob <- exec(
84-
element_grob, label_element,
85-
!!position_dim := unit(break_positions, "native"),
86-
!!label_margin_name := TRUE,
87-
label = break_labels
88-
)
97+
# calculate multiple rows/columns of labels (which is usually 1)
98+
dodge_pos <- rep(seq_len(n_dodge), length.out = n_breaks)
99+
dodge_indices <- split(seq_len(n_breaks), dodge_pos)
100+
101+
label_grobs <- lapply(dodge_indices, function(indices) {
102+
draw_axis_labels(
103+
break_positions = break_positions[indices],
104+
break_labels = break_labels[indices],
105+
label_element = label_element,
106+
is_vertical = is_vertical,
107+
check.overlap = check.overlap
108+
)
109+
})
89110

90111
ticks_grob <- exec(
91112
element_grob, tick_element,
@@ -98,14 +119,21 @@ draw_axis <- function(break_positions, break_labels, axis_position, theme) {
98119
)
99120

100121
# create gtable
101-
table_order_int <- match(table_order, c("labels", "ticks"))
102122
non_position_sizes <- paste0(non_position_size, "s")
123+
label_dims <- do.call(unit.c, lapply(label_grobs, measure_labels_non_pos))
124+
grobs <- c(list(ticks_grob), label_grobs)
125+
grob_dims <- unit.c(tick_length, label_dims)
126+
127+
if (labels_first_gtable) {
128+
grobs <- rev(grobs)
129+
grob_dims <- rev(grob_dims)
130+
}
103131

104132
gt <- exec(
105133
gtable_element,
106134
name = "axis",
107-
grobs = list(labels_grob, ticks_grob)[table_order_int],
108-
!!non_position_sizes := unit.c(measure_labels(labels_grob), tick_length)[table_order_int],
135+
grobs = grobs,
136+
!!non_position_sizes := grob_dims,
109137
!!position_size := unit(1, "npc")
110138
)
111139

@@ -124,3 +152,106 @@ draw_axis <- function(break_positions, break_labels, axis_position, theme) {
124152
vp = justvp
125153
)
126154
}
155+
156+
draw_axis_labels <- function(break_positions, break_labels, label_element, is_vertical,
157+
check.overlap = FALSE) {
158+
159+
position_dim <- if (is_vertical) "y" else "x"
160+
label_margin_name <- if (is_vertical) "margin_x" else "margin_y"
161+
162+
n_breaks <- length(break_positions)
163+
break_positions <- unit(break_positions, "native")
164+
165+
if (check.overlap) {
166+
priority <- axis_label_priority(n_breaks)
167+
break_labels <- break_labels[priority]
168+
break_positions <- break_positions[priority]
169+
}
170+
171+
labels_grob <- exec(
172+
element_grob, label_element,
173+
!!position_dim := break_positions,
174+
!!label_margin_name := TRUE,
175+
label = break_labels,
176+
check.overlap = check.overlap
177+
)
178+
}
179+
180+
#' Determine the label priority for a given number of labels
181+
#'
182+
#' @param n The number of labels
183+
#'
184+
#' @return The vector `seq_len(n)` arranged such that the
185+
#' first, last, and middle elements are recursively
186+
#' placed at the beginning of the vector.
187+
#' @noRd
188+
#'
189+
axis_label_priority <- function(n) {
190+
if (n <= 0) {
191+
return(numeric(0))
192+
}
193+
194+
c(1, n, axis_label_priority_between(1, n))
195+
}
196+
197+
axis_label_priority_between <- function(x, y) {
198+
n <- y - x + 1
199+
if (n <= 2) {
200+
return(numeric(0))
201+
}
202+
203+
mid <- x - 1 + (n + 1) %/% 2
204+
c(
205+
mid,
206+
axis_label_priority_between(x, mid),
207+
axis_label_priority_between(mid, y)
208+
)
209+
}
210+
211+
#' Override axis text angle and alignment
212+
#'
213+
#' @param axis_position One of bottom, left, top, or right
214+
#' @param angle The text angle, or NULL to override nothing
215+
#'
216+
#' @return An [element_text()] that contains parameters that should be
217+
#' overridden from the user- or theme-supplied element.
218+
#' @noRd
219+
#'
220+
axis_label_element_overrides <- function(axis_position, angle = NULL) {
221+
if (is.null(angle)) {
222+
return(element_text(angle = NULL, hjust = NULL, vjust = NULL))
223+
}
224+
225+
# it is not worth the effort to align upside-down labels properly
226+
if (angle > 90 || angle < -90) {
227+
stop("`angle` must be between 90 and -90", call. = FALSE)
228+
}
229+
230+
if (axis_position == "bottom") {
231+
element_text(
232+
angle = angle,
233+
hjust = if (angle > 0) 1 else if (angle < 0) 0 else 0.5,
234+
vjust = if (abs(angle) == 90) 0.5 else 1
235+
)
236+
} else if (axis_position == "left") {
237+
element_text(
238+
angle = angle,
239+
hjust = if (abs(angle) == 90) 0.5 else 1,
240+
vjust = if (angle > 0) 0 else if (angle < 0) 1 else 0.5,
241+
)
242+
} else if (axis_position == "top") {
243+
element_text(
244+
angle = angle,
245+
hjust = if (angle > 0) 0 else if (angle < 0) 1 else 0.5,
246+
vjust = if (abs(angle) == 90) 0.5 else 0
247+
)
248+
} else if (axis_position == "right") {
249+
element_text(
250+
angle = angle,
251+
hjust = if (abs(angle) == 90) 0.5 else 0,
252+
vjust = if (angle > 0) 1 else if (angle < 0) 0 else 0.5,
253+
)
254+
} else {
255+
stop("Unrecognized position: '", axis_position, "'", call. = FALSE)
256+
}
257+
}

R/margins.R

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ margin_width <- function(grob, margins) {
3737
#'
3838
#' @noRd
3939
title_spec <- function(label, x, y, hjust, vjust, angle, gp = gpar(),
40-
debug = FALSE) {
40+
debug = FALSE, check.overlap = FALSE) {
4141

4242
if (is.null(label)) return(zeroGrob())
4343

@@ -56,7 +56,8 @@ title_spec <- function(label, x, y, hjust, vjust, angle, gp = gpar(),
5656
hjust = hjust,
5757
vjust = vjust,
5858
rot = angle,
59-
gp = gp
59+
gp = gp,
60+
check.overlap = check.overlap
6061
)
6162

6263
# The grob dimensions don't include the text descenders, so these need to be added
@@ -175,7 +176,7 @@ add_margins <- function(grob, height, width, margin = NULL,
175176
#' @noRd
176177
titleGrob <- function(label, x, y, hjust, vjust, angle = 0, gp = gpar(),
177178
margin = NULL, margin_x = FALSE, margin_y = FALSE,
178-
debug = FALSE) {
179+
debug = FALSE, check.overlap = FALSE) {
179180

180181
if (is.null(label))
181182
return(zeroGrob())
@@ -189,7 +190,8 @@ titleGrob <- function(label, x, y, hjust, vjust, angle = 0, gp = gpar(),
189190
vjust = vjust,
190191
angle = angle,
191192
gp = gp,
192-
debug = debug
193+
debug = debug,
194+
check.overlap = check.overlap
193195
)
194196

195197
add_margins(

R/theme-elements.r

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ element_grob.element_text <- function(element, label = "", x = NULL, y = NULL,
215215

216216
titleGrob(label, x, y, hjust = hj, vjust = vj, angle = angle,
217217
gp = modify_list(element_gp, gp), margin = margin,
218-
margin_x = margin_x, margin_y = margin_y, debug = element$debug)
218+
margin_x = margin_x, margin_y = margin_y, debug = element$debug, ...)
219219
}
220220

221221

Lines changed: 84 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)