Skip to content

Commit a2aedac

Browse files
committed
always draw axis titles as annotations, fixes for geom_bar() and coord_flip()
1 parent e58f93e commit a2aedac

File tree

4 files changed

+125
-141
lines changed

4 files changed

+125
-141
lines changed

NAMESPACE

-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ S3method(geom2trace,default)
1919
S3method(print,figure)
2020
S3method(print,plotly)
2121
S3method(to_basic,GeomAbline)
22-
S3method(to_basic,GeomBar)
2322
S3method(to_basic,GeomBoxplot)
2423
S3method(to_basic,GeomContour)
2524
S3method(to_basic,GeomDensity)

R/ggplotly.R

+121-123
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,16 @@ gg2list <- function(p, width = NULL, height = NULL) {
8383
panel <- ggfun("train_ranges")(panel, p$coordinates)
8484
data <- by_layer(function(l, d) l$compute_geom_2(d))
8585
# ------------------------------------------------------------------------
86-
# end of ggplot_build(), start of layer -> trace conversion
86+
# end of ggplot_build()
8787
# ------------------------------------------------------------------------
8888

89+
if (inherits(p$coordinates, "CoordFlip")) {
90+
# flip labels
91+
p$labels[c("x", "y")] <- p$labels[c("y", "x")]
92+
# TODO: is there anything else we need to flip? p$scales?
93+
}
94+
95+
# important panel summary stats
8996
nPanels <- nrow(panel$layout)
9097
nRows <- max(panel$layout$ROW)
9198
nCols <- max(panel$layout$COL)
@@ -143,37 +150,6 @@ gg2list <- function(p, width = NULL, height = NULL) {
143150
# we may tack on more traces with visible="legendonly"
144151
traces <- lapply(traces, function(x) { x$showlegend <- FALSE; x})
145152

146-
# Bars require all sorts of hackery:
147-
# (1) position_*() is layer-specific, but `layout.barmode` is plot-specific.
148-
# (2) coord_flip() is plot-specific, but `bar.orientiation` is trace-specific
149-
# (3) position_stack() non-sense
150-
traceTypes <- unlist(lapply(traces, "[[", "type"))
151-
idx <- which(traceTypes %in% "bar")
152-
if (length(idx)) {
153-
# determine `layout.barmode`
154-
positions <- sapply(layers, type, "position")
155-
geoms <- sapply(layers, type, "geom")
156-
# bar geometry requires us to flip the orientation for flipped coordinates
157-
if ("CoordFlip" %in% class(p$coordinates)) {
158-
for (i in idx) {
159-
traces[[i]]$orientation <- "h"
160-
y <- traces[[i]]$y
161-
traces[[i]]$y <- traces[[i]]$x
162-
traces[[i]]$x <- y
163-
}
164-
}
165-
}
166-
167-
#bargeoms <- geoms[grepl("^bar$", geoms)]
168-
#if (length(bargeoms)) {
169-
# list(
170-
# stack = "stack",
171-
# dodge = "group",
172-
#
173-
# )
174-
#}
175-
#
176-
177153
# ------------------------------------------------------------------------
178154
# axis/facet/margin conversion
179155
# ------------------------------------------------------------------------
@@ -198,8 +174,8 @@ gg2list <- function(p, width = NULL, height = NULL) {
198174
gglayout$titlefont <- text2font(theme$plot.title)
199175
gglayout$margin$t <- gglayout$margin$t + gglayout$titlefont$size
200176
}
201-
202-
# panel margins
177+
# panel margins must be computed before panel/axis loops
178+
# (in order to use get_domains())
203179
panelMarginX <- unitConvert(
204180
theme[["panel.margin.x"]] %||% theme[["panel.margin"]],
205181
"npc", "width"
@@ -222,6 +198,7 @@ gg2list <- function(p, width = NULL, height = NULL) {
222198
theme[["axis.ticks.x"]] %||% theme[["axis.ticks"]],
223199
"npc", "height"
224200
)
201+
# allocate enough space for the _longest_ text label
225202
axisTextX <- theme[["axis.text.x"]] %||% theme[["axis.text"]]
226203
labz <- unlist(lapply(panel$ranges, "[[", "x.labels"))
227204
lab <- labz[which.max(nchar(labz))]
@@ -233,6 +210,7 @@ gg2list <- function(p, width = NULL, height = NULL) {
233210
theme[["axis.ticks.y"]] %||% theme[["axis.ticks"]],
234211
"npc", "width"
235212
)
213+
# allocate enough space for the _longest_ text label
236214
axisTextY <- theme[["axis.text.y"]] %||% theme[["axis.text"]]
237215
labz <- unlist(lapply(panel$ranges, "[[", "y.labels"))
238216
lab <- labz[which.max(nchar(labz))]
@@ -263,17 +241,16 @@ gg2list <- function(p, width = NULL, height = NULL) {
263241
axisName <- lay[, paste0(xy, "axis")]
264242
anchor <- lay[, paste0(xy, "anchor")]
265243
rng <- panel$ranges[[i]]
266-
sc <- scales$get_scales(xy)
244+
sc <- if (inherits(p$coordinates, "CoordFlip")) {
245+
scales$get_scales(setdiff(c("x", "y"), xy))
246+
} else {
247+
scales$get_scales(xy)
248+
}
267249
# type of unit conversion
268250
type <- if (xy == "x") "height" else "width"
269-
# set some axis defaults (and override some of them later)
270251
# https://plot.ly/r/reference/#layout-xaxis
271-
#
272-
# TODO: implement minor grid lines with another axis object
273-
# and _always_ hide ticks/text?
274252
axisObj <- list(
275-
title = if (!is_blank(axisTitle)) sc$name %||% p$labels[[xy]],
276-
titlefont = text2font(axisTitle, type),
253+
# this might be changed later in re_scale()
277254
type = "linear",
278255
autorange = FALSE,
279256
tickmode = "array",
@@ -297,53 +274,21 @@ gg2list <- function(p, width = NULL, height = NULL) {
297274
zeroline = FALSE,
298275
anchor = anchor
299276
)
300-
# bold/italic axis title
301-
axisObj$title <- faced(axisObj$title, theme$axis.text$face)
302-
axisObj <- re_scale(axisObj, sc)
303-
304-
# tack axis object onto the layout
305-
gglayout[[axisName]] <- axisObj
306-
277+
# TODO: implement minor grid lines with another axis object
278+
# and _always_ hide ticks/text?
279+
gglayout[[axisName]] <- re_scale(axisObj, sc)
307280

281+
# do some stuff that should be done once for the entire plot
308282
if (i == 1) {
309283
# convert days to milliseconds, if necessary
310-
if ("date" %in% p$scales$get_scales(xy)$scale_name) {
284+
if ("date" %in% sc$scale_name) {
311285
traces <- lapply(traces, function(z) {
312286
z[[xy]] <- z[[xy]] * 24 * 60 * 60 * 1000
313287
z
314288
})
315289
}
316-
# account for (exterior) axis/strip text in plot margins
317-
side <- if (xy == "x") "b" else "l"
318-
way <- if (xy == "x") "v" else "h"
319-
tickText <- axisObj$ticktext[which.max(nchar(axisObj$ticktext))]
320-
# apparently ggplot2 doesn't support axis.title/axis.text margins
321-
gglayout$margin[[side]] <- gglayout$margin[[side]] + axisObj$ticklen +
322-
# account for rotated title (just like we've done for ticks?)
323-
axisObj$titlefont$size +
324-
bbox(tickText, axisObj$tickangle, axisObj$tickfont$size)[[way]]
325-
290+
# add space for exterior facet strips in `layout.margin`
326291
if (has_facet(p)) {
327-
# draw axis titles as annotations
328-
if (!is_blank(axisTitle) && nchar(axisObj$title %||% "") > 0) {
329-
# npc is on a 0-1 scale of the _entire_ device,
330-
# but we really need offsets relative to the plotting region
331-
# (to do this correctly, we need the terminal height/width of the plot)
332-
offset <- 2 * (0 -
333-
unitConvert(axisText, "npc", type) -
334-
unitConvert(axisTitle, "npc", type) / 2 -
335-
unitConvert(theme$axis.ticks.length, "npc", type))
336-
x <- if (xy == "x") 0.5 else offset
337-
y <- if (xy == "x") offset else 0.5
338-
gglayout$annotations <- c(
339-
gglayout$annotations,
340-
make_label(
341-
axisObj$title, x, y, el = axisTitle,
342-
xanchor = "center", yanchor = "middle"
343-
)
344-
)
345-
}
346-
# add space for exterior facet strips in `layout.margin`
347292
stripSize <- unitConvert(stripText, "pixels", type)
348293
if (xy == "x") {
349294
gglayout$margin$t <- gglayout$margin$t + stripSize
@@ -352,7 +297,39 @@ gg2list <- function(p, width = NULL, height = NULL) {
352297
gglayout$margin$r <- gglayout$margin$r + stripSize
353298
}
354299
}
355-
300+
axisTitleText <- sc$name %||% p$labels[[xy]] %||% ""
301+
axisTickText <- axisObj$ticktext[which.max(nchar(axisObj$ticktext))]
302+
side <- if (xy == "x") "b" else "l"
303+
way <- if (xy == "x") "v" else "h"
304+
# account for axis ticks, ticks text, and titles in plot margins
305+
# (apparently ggplot2 doesn't support axis.title/axis.text margins)
306+
gglayout$margin[[side]] <- gglayout$margin[[side]] + axisObj$ticklen +
307+
bbox(axisTickText, axisObj$tickangle, axisObj$tickfont$size)[[way]] +
308+
bbox(axisTitleText, axisTitle$angle, unitConvert(axisTitle, "pixels", type))[[way]]
309+
# draw axis titles as annotations
310+
# (plotly.js axis titles aren't smart enough to dodge ticks & text)
311+
if (!is_blank(axisTitle) && nchar(axisTitleText) > 0) {
312+
axisTextSize <- unitConvert(axisText, "npc", type)
313+
axisTitleSize <- unitConvert(axisTitle, "npc", type)
314+
offset <-
315+
(0 -
316+
bbox(axisTickText, axisText$angle, axisTextSize)[[way]] -
317+
bbox(axisTitleText, axisTitle$angle, axisTitleSize)[[way]] / 2 -
318+
unitConvert(theme$axis.ticks.length, "npc", type))
319+
# npc is on a 0-1 scale of the _entire_ device,
320+
# but these units _should_ be wrt to the plotting region
321+
# multiplying the offset by 2 seems to work, but this is a terrible hack
322+
offset <- 2 * offset
323+
x <- if (xy == "x") 0.5 else offset
324+
y <- if (xy == "x") offset else 0.5
325+
gglayout$annotations <- c(
326+
gglayout$annotations,
327+
make_label(
328+
faced(axisTitleText, axisTitle$face), x, y, el = axisTitle,
329+
xanchor = "center", yanchor = "middle"
330+
)
331+
)
332+
}
356333
}
357334

358335
} # end of axis loop
@@ -362,49 +339,40 @@ gg2list <- function(p, width = NULL, height = NULL) {
362339
ydom <- gglayout[[lay[, "yaxis"]]]$domain
363340
border <- make_panel_border(xdom, ydom, theme)
364341
gglayout$shapes <- c(gglayout$shapes, border)
342+
365343
# facet strips -> plotly annotations
366344
# TODO: use p$facet$labeller for the actual strip text!
367-
if (inherits(p$facet, "grid") && lay$COL == nCols) {
345+
if (has_facet(p) && lay$ROW == 1 && !is_blank(theme[["strip.text.x"]])){
346+
vars <- ifelse(inherits(p$facet, "wrap"), "facets", "cols")
368347
txt <- paste(
369-
lay[, as.character(p$facet$rows)],
370-
collapse = ", "
348+
lay[, as.character(p$facet[[vars]])], collapse = ", "
371349
)
372-
if (!is_blank(theme[["strip.text.y"]])) {
373-
lab <- make_label(
374-
txt, x = max(xdom), y = mean(ydom),
375-
el = theme[["strip.text.y"]] %||% theme[["strip.text"]],
376-
xanchor = "left", yanchor = "bottom"
377-
)
378-
gglayout$annotations <- c(gglayout$annotations, lab)
379-
strip <- make_strip_rect(xdom, ydom, theme, "right")
380-
gglayout$shapes <- c(gglayout$shapes, strip)
381-
}
350+
lab <- make_label(
351+
txt, x = mean(xdom), y = max(ydom),
352+
el = theme[["strip.text.x"]] %||% theme[["strip.text"]],
353+
xanchor = "center", yanchor = "bottom"
354+
)
355+
gglayout$annotations <- c(gglayout$annotations, lab)
356+
strip <- make_strip_rect(xdom, ydom, theme, "top")
357+
gglayout$shapes <- c(gglayout$shapes, strip)
382358
}
383-
if (inherits(p$facet, "wrap") || inherits(p$facet, "grid") && lay$ROW == 1){
384-
vars <- ifelse(inherits(p$facet, "wrap"), "facets", "cols")
359+
if (inherits(p$facet, "grid") && lay$COL == nCols &&
360+
!is_blank(theme[["strip.text.y"]])) {
385361
txt <- paste(
386-
lay[, as.character(p$facet[[vars]])],
387-
collapse = ", "
362+
lay[, as.character(p$facet$rows)], collapse = ", "
388363
)
389-
if (!is_blank(theme[["strip.text.x"]])) {
390-
lab <- make_label(
391-
txt, x = mean(xdom), y = max(ydom),
392-
el = theme[["strip.text.x"]] %||% theme[["strip.text"]],
393-
xanchor = "center", yanchor = "bottom"
394-
)
395-
gglayout$annotations <- c(gglayout$annotations, lab)
396-
strip <- make_strip_rect(xdom, ydom, theme, "top")
397-
gglayout$shapes <- c(gglayout$shapes, strip)
398-
}
364+
lab <- make_label(
365+
txt, x = max(xdom), y = mean(ydom),
366+
el = theme[["strip.text.y"]] %||% theme[["strip.text"]],
367+
xanchor = "left", yanchor = "bottom"
368+
)
369+
gglayout$annotations <- c(gglayout$annotations, lab)
370+
strip <- make_strip_rect(xdom, ydom, theme, "right")
371+
gglayout$shapes <- c(gglayout$shapes, strip)
399372
}
400373

401374
} # end of panel loop
402375

403-
# if facets are present, wipe out 'official' [x/y]axis title(s)
404-
if (has_facet(p)) {
405-
gglayout <- strip_axis(gglayout, c("title", "titlefont"))
406-
}
407-
408376
# ------------------------------------------------------------------------
409377
# guide/legend conversion
410378
# ------------------------------------------------------------------------
@@ -480,14 +448,52 @@ gg2list <- function(p, width = NULL, height = NULL) {
480448
return(NULL)
481449
}
482450

483-
traces <- c(traces, lapply(gdefs, gdef2trace))
451+
traces <- compact(c(traces, lapply(gdefs, gdef2trace)))
484452

485453
# TODO:
486454
# (1) shrink guide size(s). Set fractions in colorbar.lenmode
487455
# (2) position guide(s)?
488456
# (3)
489457
}
490458

459+
# Bar hackery:
460+
# (1) coord_flip() is plot-specific, but `bar.orientiation` is trace-specific
461+
# (2) position_*() is layer-specific, but `layout.barmode` is plot-specific.
462+
geoms <- sapply(layers, ggtype, "geom")
463+
if (any(idx <- geoms %in% "bar")) {
464+
gglayout$bargap <- 0
465+
# since `layout.barmode` is plot-specific, we can't support multiple bar
466+
# geoms with different positions
467+
positions <- sapply(layers, ggtype, "position")
468+
position <- unique(positions[geoms %in% "bar"])
469+
if (length(position) > 1) {
470+
warning("plotly doesn't support multiple positions\n",
471+
"across geom_bar() layers", call. = FALSE)
472+
position <- position[1]
473+
}
474+
# note: ggplot2 doesn't flip x/y scales when the coord is flipped
475+
# (i.e., at this point, y should be the count/density)
476+
is_hist <- inherits(p$scales$get_scales("x"), "ScaleContinuous")
477+
gglayout$barmode <- if (position %in% "identity" && is_hist) {
478+
"overlay"
479+
} else if (position %in% c("identity", "stack", "fill")) {
480+
"stack"
481+
} else {
482+
"group"
483+
}
484+
}
485+
486+
# flipped coordinates
487+
if (inherits(p$coordinates, "CoordFlip")) {
488+
for (i in seq_along(traces)) {
489+
tr <- traces[[i]]
490+
# flip x/y in traces
491+
traces[[i]][c("x", "y")] <- tr[c("y", "x")]
492+
if (identical(tr$type, "bar")) traces[[i]]$orientation <- "h"
493+
# TODO: do I have to flip axis objects?
494+
}
495+
}
496+
491497
l <- list(data = compact(traces), layout = compact(gglayout))
492498
# ensure properties are boxed correctly
493499
l <- add_boxed(rm_asis(l))
@@ -605,15 +611,6 @@ has_facet <- function(x) {
605611
inherits(x$facet, c("grid", "wrap"))
606612
}
607613

608-
# remove a property from an axis element
609-
strip_axis <- function(x, y = c("title", "titlefont")) {
610-
idx <- grepl("[x-y]axis", names(x))
611-
axes <- x[idx]
612-
axes <- lapply(axes, function(x) { x[y] <- NULL; x })
613-
x[idx] <- axes
614-
x
615-
}
616-
617614
#' Estimate bounding box of a rotated string
618615
#'
619616
#' @param txt a character string of length 1
@@ -738,6 +735,7 @@ rect2shape <- function(rekt = ggplot2::element_rect()) {
738735
# this helps us import functions in a way that R CMD check won't cry about
739736
ggfun <- function(x) getFromNamespace(x, "ggplot2")
740737

741-
type <- function(x, y = "geom") {
738+
ggtype <- function(x, y = "geom") {
742739
sub(y, "", tolower(class(x[[y]])[1]))
743740
}
741+

0 commit comments

Comments
 (0)