Skip to content

Commit fa000f7

Browse files
authored
Make position guides customizable (#3398, closes #3322)
* Position guides can now be customized using the new `guide_axis()`, which can be passed to position `scale_*()` functions or via `guides()`. The new axis guide (`guide_axis()`) comes with arguments `check.overlap` (automatic removal of overlapping labels), `angle` (easy rotation of axis labels), and `n.dodge` (dodge labels into multiple rows/columns) * `CoordCartesian` gets new methods to resolve/train the new position guides
1 parent 23e3241 commit fa000f7

37 files changed

+1422
-170
lines changed

DESCRIPTION

+1
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ Collate:
158158
'guides-.r'
159159
'guides-axis.r'
160160
'guides-grid.r'
161+
'guides-none.r'
161162
'hexbin.R'
162163
'labeller.r'
163164
'labels.r'

NAMESPACE

+14
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,25 @@ S3method(grobWidth,absoluteGrob)
6767
S3method(grobWidth,zeroGrob)
6868
S3method(grobX,absoluteGrob)
6969
S3method(grobY,absoluteGrob)
70+
S3method(guide_gengrob,axis)
7071
S3method(guide_gengrob,colorbar)
72+
S3method(guide_gengrob,guide_none)
7173
S3method(guide_gengrob,legend)
74+
S3method(guide_geom,axis)
7275
S3method(guide_geom,colorbar)
76+
S3method(guide_geom,guide_none)
7377
S3method(guide_geom,legend)
78+
S3method(guide_merge,axis)
7479
S3method(guide_merge,colorbar)
80+
S3method(guide_merge,guide_none)
7581
S3method(guide_merge,legend)
82+
S3method(guide_train,axis)
7683
S3method(guide_train,colorbar)
84+
S3method(guide_train,guide_none)
7785
S3method(guide_train,legend)
86+
S3method(guide_transform,axis)
87+
S3method(guide_transform,default)
88+
S3method(guide_transform,guide_none)
7889
S3method(heightDetails,titleGrob)
7990
S3method(heightDetails,zeroGrob)
8091
S3method(interleave,default)
@@ -358,13 +369,16 @@ export(ggproto)
358369
export(ggproto_parent)
359370
export(ggsave)
360371
export(ggtitle)
372+
export(guide_axis)
361373
export(guide_colorbar)
362374
export(guide_colourbar)
363375
export(guide_gengrob)
364376
export(guide_geom)
365377
export(guide_legend)
366378
export(guide_merge)
379+
export(guide_none)
367380
export(guide_train)
381+
export(guide_transform)
368382
export(guides)
369383
export(is.Coord)
370384
export(is.facet)

NEWS.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# ggplot2 (development version)
22

3+
* Position guides can now be customized using the new `guide_axis()`,
4+
which can be passed to position `scale_*()` functions or via
5+
`guides()`. The new axis guide (`guide_axis()`) comes with
6+
arguments `check.overlap` (automatic removal of overlapping
7+
labels), `angle` (easy rotation of axis labels), and
8+
`n.dodge` (dodge labels into multiple rows/columns) (@paleolimbot, #3322).
9+
310
* `Geom` now gains a `setup_params()` method in line with the other ggproto
411
classes (@thomasp85, #3509)
512

R/axis-secondary.R

+10-4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
#' - A character vector giving labels (must be same length as `breaks`)
2121
#' - A function that takes the breaks as input and returns labels as output
2222
#'
23+
#' @param guide A position guide that will be used to render
24+
#' the axis on the plot. Usually this is [guide_axis()].
25+
#'
2326
#' @details
2427
#' `sec_axis` is used to create the specifications for a secondary axis.
2528
#' Except for the `trans` argument any of the arguments can be set to
@@ -79,7 +82,8 @@
7982
#' labels = scales::time_format("%b %d %I %p")))
8083
#'
8184
#' @export
82-
sec_axis <- function(trans = NULL, name = waiver(), breaks = waiver(), labels = waiver()) {
85+
sec_axis <- function(trans = NULL, name = waiver(), breaks = waiver(), labels = waiver(),
86+
guide = waiver()) {
8387
# sec_axis() historically accpeted two-sided formula, so be permissive.
8488
if (length(trans) > 2) trans <- trans[c(1,3)]
8589

@@ -88,14 +92,15 @@ sec_axis <- function(trans = NULL, name = waiver(), breaks = waiver(), labels =
8892
trans = trans,
8993
name = name,
9094
breaks = breaks,
91-
labels = labels
95+
labels = labels,
96+
guide = guide
9297
)
9398
}
9499
#' @rdname sec_axis
95100
#'
96101
#' @export
97-
dup_axis <- function(trans = ~., name = derive(), breaks = derive(), labels = derive()) {
98-
sec_axis(trans, name, breaks, labels)
102+
dup_axis <- function(trans = ~., name = derive(), breaks = derive(), labels = derive(), guide = derive()) {
103+
sec_axis(trans, name, breaks, labels, guide)
99104
}
100105

101106
is.sec_axis <- function(x) {
@@ -148,6 +153,7 @@ AxisSecondary <- ggproto("AxisSecondary", NULL,
148153
if (is.derived(self$breaks)) self$breaks <- scale$breaks
149154
if (is.waive(self$breaks)) self$breaks <- scale$trans$breaks
150155
if (is.derived(self$labels)) self$labels <- scale$labels
156+
if (is.derived(self$guide)) self$guide <- scale$guide
151157
},
152158

153159
transform_range = function(self, range) {

R/coord-.r

+9-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Coord <- ggproto("Coord",
5959

6060
aspect = function(ranges) NULL,
6161

62-
labels = function(panel_params) panel_params,
62+
labels = function(labels, panel_params) labels,
6363

6464
render_fg = function(panel_params, theme) element_render(theme, "panel.border"),
6565

@@ -91,6 +91,14 @@ Coord <- ggproto("Coord",
9191
list()
9292
},
9393

94+
setup_panel_guides = function(self, panel_params, guides, params = list()) {
95+
panel_params
96+
},
97+
98+
train_panel_guides = function(self, panel_params, layers, default_mapping, params = list()) {
99+
panel_params
100+
},
101+
94102
transform = function(data, range) NULL,
95103

96104
distance = function(x, y, panel_params) NULL,

R/coord-cartesian-.r

+92-17
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,75 @@ CoordCartesian <- ggproto("CoordCartesian", Coord,
103103
)
104104
},
105105

106+
setup_panel_guides = function(self, panel_params, guides, params = list()) {
107+
aesthetics <- c("x", "y", "x.sec", "y.sec")
108+
names(aesthetics) <- aesthetics
109+
110+
# resolve the specified guide from the scale and/or guides
111+
guides <- lapply(aesthetics, function(aesthetic) {
112+
resolve_guide(
113+
aesthetic,
114+
panel_params[[aesthetic]],
115+
guides,
116+
default = guide_axis(),
117+
null = guide_none()
118+
)
119+
})
120+
121+
# resolve the guide definition as a "guide" S3
122+
guides <- lapply(guides, validate_guide)
123+
124+
# if there is an "position" specification in the scale, pass this on to the guide
125+
# ideally, this should be specified in the guide
126+
guides <- lapply(aesthetics, function(aesthetic) {
127+
guide <- guides[[aesthetic]]
128+
scale <- panel_params[[aesthetic]]
129+
# position could be NULL here for an empty scale
130+
guide$position <- guide$position %|W|% scale$position
131+
guide
132+
})
133+
134+
panel_params$guides <- guides
135+
panel_params
136+
},
137+
138+
train_panel_guides = function(self, panel_params, layers, default_mapping, params = list()) {
139+
aesthetics <- c("x", "y", "x.sec", "y.sec")
140+
names(aesthetics) <- aesthetics
141+
142+
panel_params$guides <- lapply(aesthetics, function(aesthetic) {
143+
axis <- substr(aesthetic, 1, 1)
144+
guide <- panel_params$guides[[aesthetic]]
145+
guide <- guide_train(guide, panel_params[[aesthetic]])
146+
guide <- guide_transform(guide, self, panel_params)
147+
guide <- guide_geom(guide, layers, default_mapping)
148+
guide
149+
})
150+
151+
panel_params
152+
},
153+
154+
labels = function(self, labels, panel_params) {
155+
positions_x <- c("top", "bottom")
156+
positions_y <- c("left", "right")
157+
158+
list(
159+
x = lapply(c(1, 2), function(i) {
160+
panel_guide_label(
161+
panel_params$guides,
162+
position = positions_x[[i]],
163+
default_label = labels$x[[i]]
164+
)
165+
}),
166+
y = lapply(c(1, 2), function(i) {
167+
panel_guide_label(
168+
panel_params$guides,
169+
position = positions_y[[i]],
170+
default_label = labels$y[[i]])
171+
})
172+
)
173+
},
174+
106175
render_bg = function(panel_params, theme) {
107176
guide_grid(
108177
theme,
@@ -114,24 +183,16 @@ CoordCartesian <- ggproto("CoordCartesian", Coord,
114183
},
115184

116185
render_axis_h = function(panel_params, theme) {
117-
arrange <- panel_params$x.arrange %||% c("secondary", "primary")
118-
arrange_scale_keys <- c("primary" = "x", "secondary" = "x.sec")[arrange]
119-
arrange_scales <- panel_params[arrange_scale_keys]
120-
121186
list(
122-
top = draw_view_scale_axis(arrange_scales[[1]], "top", theme),
123-
bottom = draw_view_scale_axis(arrange_scales[[2]], "bottom", theme)
187+
top = panel_guides_grob(panel_params$guides, position = "top", theme = theme),
188+
bottom = panel_guides_grob(panel_params$guides, position = "bottom", theme = theme)
124189
)
125190
},
126191

127192
render_axis_v = function(panel_params, theme) {
128-
arrange <- panel_params$y.arrange %||% c("primary", "secondary")
129-
arrange_scale_keys <- c("primary" = "y", "secondary" = "y.sec")[arrange]
130-
arrange_scales <- panel_params[arrange_scale_keys]
131-
132193
list(
133-
left = draw_view_scale_axis(arrange_scales[[1]], "left", theme),
134-
right = draw_view_scale_axis(arrange_scales[[2]], "right", theme)
194+
left = panel_guides_grob(panel_params$guides, position = "left", theme = theme),
195+
right = panel_guides_grob(panel_params$guides, position = "right", theme = theme)
135196
)
136197
}
137198
)
@@ -153,10 +214,24 @@ view_scales_from_scale <- function(scale, coord_limits = NULL, expand = TRUE) {
153214
view_scales
154215
}
155216

156-
draw_view_scale_axis <- function(view_scale, axis_position, theme) {
157-
if(is.null(view_scale) || view_scale$is_empty()) {
158-
return(zeroGrob())
159-
}
217+
panel_guide_label <- function(guides, position, default_label) {
218+
guide <- guide_for_position(guides, position) %||% guide_none(title = NULL)
219+
guide$title %|W|% default_label
220+
}
221+
222+
panel_guides_grob <- function(guides, position, theme) {
223+
guide <- guide_for_position(guides, position) %||% guide_none()
224+
guide_gengrob(guide, theme)
225+
}
226+
227+
guide_for_position <- function(guides, position) {
228+
has_position <- vapply(
229+
guides,
230+
function(guide) identical(guide$position, position),
231+
logical(1)
232+
)
160233

161-
draw_axis(view_scale$break_positions(), view_scale$get_labels(), axis_position, theme)
234+
guides <- guides[has_position]
235+
guides_order <- vapply(guides, function(guide) as.numeric(guide$order)[1], numeric(1))
236+
Reduce(guide_merge, guides[order(guides_order)])
162237
}

R/coord-flip.r

+22-7
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ coord_flip <- function(xlim = NULL, ylim = NULL, expand = TRUE, clip = "on") {
4040
CoordFlip <- ggproto("CoordFlip", CoordCartesian,
4141

4242
transform = function(data, panel_params) {
43-
data <- flip_labels(data)
43+
data <- flip_axis_labels(data)
4444
CoordCartesian$transform(data, panel_params)
4545
},
4646

@@ -58,11 +58,11 @@ CoordFlip <- ggproto("CoordFlip", CoordCartesian,
5858
setup_panel_params = function(self, scale_x, scale_y, params = list()) {
5959
parent <- ggproto_parent(CoordCartesian, self)
6060
panel_params <- parent$setup_panel_params(scale_x, scale_y, params)
61-
flip_labels(panel_params)
61+
flip_axis_labels(panel_params)
6262
},
6363

64-
labels = function(panel_params) {
65-
flip_labels(CoordCartesian$labels(panel_params))
64+
labels = function(labels, panel_params) {
65+
flip_axis_labels(CoordCartesian$labels(labels, panel_params))
6666
},
6767

6868
setup_layout = function(layout, params) {
@@ -72,14 +72,29 @@ CoordFlip <- ggproto("CoordFlip", CoordCartesian,
7272
},
7373

7474
modify_scales = function(scales_x, scales_y) {
75-
lapply(scales_x, scale_flip_position)
76-
lapply(scales_y, scale_flip_position)
75+
lapply(scales_x, scale_flip_axis)
76+
lapply(scales_y, scale_flip_axis)
7777
}
7878

7979
)
8080

81+
# In-place modification of a scale position to swap axes
82+
scale_flip_axis <- function(scale) {
83+
scale$position <- switch(scale$position,
84+
top = "right",
85+
bottom = "left",
86+
left = "bottom",
87+
right = "top",
88+
scale$position
89+
)
90+
91+
invisible(scale)
92+
}
8193

82-
flip_labels <- function(x) {
94+
# maintaining the position of the x* and y* names is
95+
# important for re-using the same guide_transform()
96+
# as CoordCartesian
97+
flip_axis_labels <- function(x) {
8398
old_names <- names(x)
8499

85100
new_names <- old_names

R/coord-polar.r

+3-3
Original file line numberDiff line numberDiff line change
@@ -305,11 +305,11 @@ CoordPolar <- ggproto("CoordPolar", Coord,
305305
)
306306
},
307307

308-
labels = function(self, panel_params) {
308+
labels = function(self, labels, panel_params) {
309309
if (self$theta == "y") {
310-
list(x = panel_params$y, y = panel_params$x)
310+
list(x = labels$y, y = labels$x)
311311
} else {
312-
panel_params
312+
labels
313313
}
314314
},
315315

R/coord-sf.R

+2
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,8 @@ CoordSf <- ggproto("CoordSf", CoordCartesian,
195195
diff(panel_params$y_range) / diff(panel_params$x_range) / ratio
196196
},
197197

198+
labels = function(labels, panel_params) labels,
199+
198200
render_bg = function(self, panel_params, theme) {
199201
el <- calc_element("panel.grid.major", theme)
200202

0 commit comments

Comments
 (0)