4
4
# ' @param break_position position of ticks
5
5
# ' @param break_labels labels at ticks
6
6
# ' @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.
8
16
# '
9
17
# ' @noRd
10
18
# '
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 ) {
12
21
13
22
axis_position <- match.arg(axis_position , c(" top" , " bottom" , " right" , " left" ))
14
23
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) {
24
33
tick_length <- calc_element(tick_length_element_name , theme )
25
34
label_element <- calc_element(label_element_name , theme )
26
35
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
+
27
44
# conditionally set parameters that depend on axis orientation
28
45
is_vertical <- axis_position %in% c(" left" , " right" )
29
46
30
47
position_dim <- if (is_vertical ) " y" else " x"
31
48
non_position_dim <- if (is_vertical ) " x" else " y"
32
49
position_size <- if (is_vertical ) " height" else " width"
33
50
non_position_size <- if (is_vertical ) " width" else " height"
34
- label_margin_name <- if (is_vertical ) " margin_x" else " margin_y"
35
51
gtable_element <- if (is_vertical ) gtable_row else gtable_col
36
52
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
38
54
39
55
# conditionally set parameters that depend on which side of the panel
40
56
# the axis is on
@@ -47,8 +63,6 @@ draw_axis <- function(break_positions, break_labels, axis_position, theme) {
47
63
# conditionally set the gtable ordering
48
64
labels_first_gtable <- axis_position %in% c(" left" , " top" ) # refers to position in gtable
49
65
50
- table_order <- if (labels_first_gtable ) c(" labels" , " ticks" ) else c(" ticks" , " labels" )
51
-
52
66
# set common parameters
53
67
n_breaks <- length(break_positions )
54
68
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) {
80
94
}
81
95
}
82
96
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
+ })
89
110
90
111
ticks_grob <- exec(
91
112
element_grob , tick_element ,
@@ -98,14 +119,21 @@ draw_axis <- function(break_positions, break_labels, axis_position, theme) {
98
119
)
99
120
100
121
# create gtable
101
- table_order_int <- match(table_order , c(" labels" , " ticks" ))
102
122
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
+ }
103
131
104
132
gt <- exec(
105
133
gtable_element ,
106
134
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 ,
109
137
!! position_size : = unit(1 , " npc" )
110
138
)
111
139
@@ -124,3 +152,106 @@ draw_axis <- function(break_positions, break_labels, axis_position, theme) {
124
152
vp = justvp
125
153
)
126
154
}
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
+ }
0 commit comments