Open twest820 opened 3 years ago
The reprex below shows that the behavior for ggplot2::geom_text()
with negative values for hjust
looks pretty similar to the behavior for ggrepel::geom_text_repel()
...
I'm not sure what to think of this. I might need to read the source code to see what ggplot2::geom_text()
does with the hjust
option.
library(dplyr)
#>
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#>
#> filter, lag
#> The following objects are masked from 'package:base':
#>
#> intersect, setdiff, setequal, union
library(ggplot2)
library(ggrepel)
library(tidyr)
library(patchwork)
data <- crossing(x = c(0, 1), slope = 1 / 2^seq(0, 5)) %>% mutate(y = slope * x)
set.seed(0)
p <- ggplot() +
aes(x = x, y = y, label = slope) +
geom_line(data = data, aes(color = as.factor(slope), group = slope)) +
coord_cartesian(xlim = c(0, 1.1)) +
labs(color = "slope") +
theme(legend.justification = c(0, 1), legend.position = c(0.02, 0.98))
p0 <- p + geom_text(
data = data %>% group_by(slope) %>% slice_max(x, n = 1),
hjust = 0
) + labs(title = "geom_text() hjust = 0")
p1 <- p + geom_text(
data = data %>% group_by(slope) %>% slice_max(x, n = 1),
hjust = 1
) + labs(title = "geom_text() hjust = 1")
p5 <- p + geom_text(
data = data %>% group_by(slope) %>% slice_max(x, n = 1),
hjust = 0.5
) + labs(title = "geom_text() hjust = 0.5")
pn5 <- p + geom_text(
data = data %>% group_by(slope) %>% slice_max(x, n = 1),
hjust = -0.5
) + labs(title = "geom_text() hjust = -0.5")
q0 <- p + geom_text_repel(
data = data %>% group_by(slope) %>% slice_max(x, n = 1),
direction = "y",
hjust = 0,
xlim = c(NA, Inf)
) + labs(title = "geom_text_repel() hjust = 0")
q1 <- p + geom_text_repel(
data = data %>% group_by(slope) %>% slice_max(x, n = 1),
direction = "y",
hjust = 1,
xlim = c(NA, Inf)
) + labs(title = "geom_text_repel() hjust = 1")
q5 <- p + geom_text_repel(
data = data %>% group_by(slope) %>% slice_max(x, n = 1),
direction = "y",
hjust = 0.5,
xlim = c(NA, Inf)
) + labs(title = "geom_text_repel() hjust = 0.5")
qn5 <- p + geom_text_repel(
data = data %>% group_by(slope) %>% slice_max(x, n = 1),
direction = "y",
hjust = -0.5,
xlim = c(NA, Inf)
) + labs(title = "geom_text_repel() hjust = -0.5")
p0 + q0
p1 + q1
p5 + q5
pn5 + qn5
Created on 2021-04-07 by the reprex package (v0.3.0)
Thanks for the quick response. Sorry, that was a careless copy/paste on my part as I'd meant to put hjust = 0
in the reprex. The side by side hjust = 0
case you have is a better illustration anyway; it's like geom_text_repel()
ends up triggering a position_nudge(x = -something)
where probably what one would want to do would be geom_text_repel(direction = "y", hjust = 0, position_nudge(x = 0.01))
in order to get left justified text with clean spacing from the ends of the lines but without vertical encroachment or overlap between text instances. (A negative hjust is functionally equivalent to position_nudge(x)
in the special case where labels are all of the same length.)
There may well be some limitations on the ggplot side (e.g. https://github.com/tidyverse/ggplot2/issues/4401).
Something else which seems worth noting is the movement of labels whose vjust
doesn't need altering from vjust = 0.5
, such as for slopes 1 and 0.5. I'm also finding that, for example, the slope = 1, 0.5, and 0.25 labels will switch between something like vjust = -0.5
and vjust = 1.5
if plotting is repeated even though set.seed()
has been called.
OK, I think I get it.
We can disable the physical repulsion simulation with max.iter=0
, instructing the function to run 0 iterations of the repulsion simulation. This should show us where the text is placed before anything moves.
q0 <- p + geom_text_repel(
data = data %>% group_by(slope) %>% slice_max(x, n = 1),
direction = "y",
hjust = 0,
xlim = c(NA, Inf),
max.iter = 0
) + labs(title = "geom_text_repel() hjust = 0")
The figure below is showing what you mentioned in your comment: the initial starting position is shifted a bit to the left for geom_text_repel()
relative to to geom_text()
.
p0 + q0
This is caused by the default box.padding = 0.25
option, which is supposed to give a bit of padding to the left, top, bottom, and right sides of each text label's bounding box.
If we set box.padding=0
then we can recover the correct behavior:
p0 + p + geom_text_repel(
data = data %>% group_by(slope) %>% slice_max(x, n = 1),
direction = "y",
hjust = 0,
xlim = c(NA, Inf),
max.iter = 0,
box.padding = 0
) + labs(title = "geom_text_repel() hjust = 0")
So, I think you are correct to point out that the ggrepel code has a bug in the way that it is accounting for the box.padding
.
Thank you for reporting this!
I hope you can use nudge_x
to work around this issue.
Thanks for the hints! There are some interesting possibilities here accessible by controlling how far labels move with box.padding
and max.iter
and using nudge_x
to compensate for the leftward drift of labels (example below).
I think the logical endpoint of the approach would be something like direction = "x+ y+-"
so that labels in dense areas can move to the right and develop leader lines where needed without forcing the use of leader in sparse areas (the current direction
semantics being x+- y+- for "both"
, x+- for "x"
, and y+- for "y"
).
library(cowplot)
library(dplyr)
library(ggplot2)
library(ggrepel)
library(tidyr)
data = crossing(x = c(0, 1), slope = 1/2^seq(0, 6)) %>% mutate(y = slope * x)
plot_grid(ggplot() + geom_line(data = data, aes(x = x, y = y, color = as.factor(slope), group = slope)) +
geom_text(data = data %>% group_by(slope) %>% slice_max(x, n = 1), aes(x = x, y = y, label = slope), hjust = 0, position = position_nudge(x = 0.01), vjust = 0.5) +
coord_cartesian(xlim = c(0, 1.42)) + labs(color = "slope", title = "geom_text()") +
theme(legend.justification = c(0, 1), legend.position = c(0.02, 0.99)),
ggplot() + geom_line(data = data, aes(x = x, y = y, color = as.factor(slope), group = slope)) +
geom_text_repel(data = data %>% group_by(slope) %>% slice_max(x, n = 1), aes(x = x, y = y, label = slope), box.padding = 0.1, direction = "y", hjust = 0, max.iter = 5, nudge_x = 0.1, xlim = c(0, 1.5)) +
coord_cartesian(xlim = c(0, 1.42)) + labs(color = "slope", title = "geom_text_repel()") +
theme(legend.position = "none"),
nrow = 1, ncol = 2)
Summary
ggrepel does not implement the design behavior described in #188.
Minimal code example
This should result in tidy left justification of the labels just to the right of the ends of the lines. Instead, the labels wander left and right from line to line.
Suggestions
Bug fix. Looks like the passed in value of hjust is getting overridden.
Version information
R version 4.0.4 (2021-02-15) Platform: x86_64-w64-mingw32/x64 (64-bit) Running under: Windows 10 x64 (build 19041)
Matrix products: default
attached base packages: [1] stats graphics grDevices utils datasets methods base
other attached packages: [1] tidyr_1.1.3 ggrepel_0.9.1 ggplot2_3.3.3 dplyr_1.0.5
loaded via a namespace (and not attached): [1] Rcpp_1.0.6 magrittr_2.0.1 tidyselect_1.1.0 munsell_0.5.0 colorspace_2.0-0 R6_2.5.0
[7] rlang_0.4.10 fansi_0.4.2 tools_4.0.4 grid_4.0.4 gtable_0.3.0 utf8_1.1.4
[13] cli_2.3.1 DBI_1.1.1 withr_2.4.1 ellipsis_0.3.1 digest_0.6.27 assertthat_0.2.1 [19] tibble_3.1.0 lifecycle_1.0.0 crayon_1.4.1 farver_2.1.0 purrr_0.3.4 vctrs_0.3.6
[25] glue_1.4.2 labeling_0.4.2 compiler_4.0.4 pillar_1.5.1 generics_0.1.0 scales_1.1.1
[31] pkgconfig_2.0.3