5

I use geom_label to plot text. By default, the width of the label (box) depends on the width of the text. However, I want to have a uniform label size. That is, I want a fixed size of the rectangle behind the text, irrespective of the length of the string.

Example of my current labels, with different size depending on string length:

1]

How do I make labels that look like these:

2

I've looked over the geom_label github and it doesn't look good, I was thinking maybe some way of modifying unit() but I can't get anything to work.

For some reproducible code:

library(ggplot2)
ggplot(mtcars, aes(wt, mpg, label = rownames(mtcars))) +
geom_text(check_overlap = TRUE) +
geom_label(aes(fill= factor(cyl)))

How would you make all the labels the same size?

Henrik
  • 65,555
  • 14
  • 143
  • 159
SCDCE
  • 1,603
  • 1
  • 15
  • 28
  • They look pretty similar to me. You should describe in natural language what you see as the relevant differences. – IRTFM Jan 10 '18 at 20:35
  • One has width/padding dependent on the text, the other has a standard width/padding. – SCDCE Jan 10 '18 at 20:37
  • It would be helpful if you provided a [reproducible example](https://stackoverflow.com/questions/5963269/how-to-make-a-great-r-reproducible-example) with sample input data and whatever code you are using to draw these pieces.Hard to tell from the partial pics. – MrFlick Jan 10 '18 at 20:38
  • The first image was made using geom_label(), my question is on manipulating the underlying code (the github posted) to possibly generate the second image which was made with image editing software. – SCDCE Jan 10 '18 at 20:42
  • Would padding your labels with spaces work? – troh Jan 10 '18 at 20:48

3 Answers3

8

Padding the data with spaces and having a mono family font looked pretty hacky to me. I adjusted some parameters in the geom_label code to produce what I wanted:

enter image description here

How the code looks with some reproducible code:

library(ggplot2)
ggplot(mtcars, aes(wt, mpg, label = rownames(mtcars))) +
  geom_text(check_overlap = TRUE) +
  geom_label(aes(fill= factor(cyl)))

enter image description here

library(ggplot2)
library(grid)
library(stringi)
ggname <- function (prefix, grob) {
  grob$name <- grobName(grob, prefix)
  grob
}

geom_label2 <- function(mapping = NULL, data = NULL,
                       stat = "identity", position = "identity",
                       ...,
                       parse = FALSE,
                       nudge_x = 0,
                       nudge_y = 0,
                       label.padding = unit(0.25, "lines"),
                       label.r = unit(0.15, "lines"),
                       label.size = 0.25,
                       na.rm = FALSE,
                       show.legend = NA,
                       inherit.aes = TRUE) {
  if (!missing(nudge_x) || !missing(nudge_y)) {
    if (!missing(position)) {
      stop("Specify either `position` or `nudge_x`/`nudge_y`", call. = FALSE)
    }

    position <- position_nudge(nudge_x, nudge_y)
  }

  layer(
    data = data,
    mapping = mapping,
    stat = stat,
    geom = GeomLabel2,
    position = position,
    show.legend = show.legend,
    inherit.aes = inherit.aes,
    params = list(
      parse = parse,
      label.padding = label.padding,
      label.r = label.r,
      label.size = label.size,
      na.rm = na.rm,
      ...
    )
  )
}

GeomLabel2 <- ggproto("GeomLabel2", Geom,
                     required_aes = c("x", "y", "label"),

                     default_aes = aes(
                       colour = "black", fill = "white", size = 3.88, angle = 0,
                       hjust = 0.5, vjust = 0.5, alpha = NA, family = "", fontface = 1,
                       lineheight = 1.2
                     ),

                     draw_panel = function(self, data, panel_params, coord, parse = FALSE,
                                           na.rm = FALSE,
                                           label.padding = unit(0.25, "lines"),
                                           label.r = unit(0.15, "lines"),
                                           label.size = 0.25) {
                       lab <- data$label
                       if (parse) {
                         lab <- parse(text = as.character(lab))
                       }

                       data <- coord$transform(data, panel_params)
                       if (is.character(data$vjust)) {
                         data$vjust <- compute_just(data$vjust, data$y)
                       }
                       if (is.character(data$hjust)) {
                         data$hjust <- compute_just(data$hjust, data$x)
                       }

                       grobs <- lapply(1:nrow(data), function(i) {
                         row <- data[i, , drop = FALSE]
                         labelGrob2(lab[i],
                                   x = unit(row$x, "native"),
                                   y = unit(row$y, "native"),
                                   just = "center",
                                   padding = label.padding,
                                   r = label.r,
                                   text.gp = gpar(
                                     col = row$colour,
                                     fontsize = row$size * .pt,
                                     fontfamily = row$family,
                                     fontface = row$fontface,
                                     lineheight = row$lineheight
                                   ),
                                   rect.gp = gpar(
                                     col = row$colour,
                                     fill = alpha(row$fill, row$alpha),
                                     lwd = label.size * .pt
                                   )
                         )
                       })
                       class(grobs) <- "gList"

                       ggname("geom_label", grobTree(children = grobs))
                     },

                     draw_key = draw_key_label
)

labelGrob2 <- function(label, x = unit(0.5, "npc"), y = unit(0.5, "npc"),
                      just = "center", padding = unit(0.25, "lines"), r = unit(0.1, "snpc"),
                      default.units = "npc", name = NULL,
                      text.gp = gpar(), rect.gp = gpar(fill = "white"), vp = NULL) {

  stopifnot(length(label) == 1)

  if (!is.unit(x))
    x <- unit(x, default.units)
  if (!is.unit(y))
    y <- unit(y, default.units)

  gTree(label = label, x = x, y = y, just = just, padding = padding, r = r,
        name = name, text.gp = text.gp, rect.gp = rect.gp, vp = vp, cl = "labelgrob2")
}

makeContent.labelgrob2 <- function(x) {
  hj <- resolveHJust(x$just, NULL)
  vj <- resolveVJust(x$just, NULL)

  t <- textGrob(
    x$label,
    x$x + 1 * (0.55 - hj) * unit(5, "mm"),
    x$y + 2 * (0.55 - vj) * x$padding,
    just = "center",
    gp = x$text.gp,
    name = "text"
  )

  r <- roundrectGrob(x$x, x$y, default.units = "native",
                     width =  1.5 * unit(max(stri_width(x$x)) + 1, "mm"),
                     height = grobHeight(t) + 2 * x$padding,
                     just = c(hj, vj),
                     r = x$r,
                     gp = x$rect.gp,
                     name = "box"
  )

  setChildren(x, gList(r, t))
}

ggplot(mtcars, aes(wt, mpg, label = rownames(mtcars))) +
  geom_text(check_overlap = TRUE) +
  geom_label2(aes(fill= factor(cyl)))

enter image description here

Dric
  • 87
  • 13
SCDCE
  • 1,603
  • 1
  • 15
  • 28
  • 1
    Thank you for this! For anyone trying to make this work, you'll need `compute_just()` and `just_dir()` from [here](https://rdrr.io/cran/ggtext/src/R/geom-richtext.R#sym-compute_just) – Unrelated Oct 27 '22 at 18:03
  • Also, if you want to change the size of the label, find the scaling ratios in `roundrectGrob()` – Unrelated Oct 27 '22 at 18:06
6

As mentioned in comment: one workaround is to pad labels with spaces. However, this method does not work with the default proportional font. We need to use a fixed width (monospaced) font family.

I use stringi convenience functions for padding, but sprintf or friends should be fine as well.

library(stringi)
library(ggplot2)

z <- row.names(mtcars)
z2 <- stri_pad_both(z, width = max(stri_width(z)))
d <- data.frame(x = 1, y = 1:length(z2), z2)

ggplot(d, aes(x, y, label = z2)) + geom_label(family = "mono")

enter image description here

Henrik
  • 65,555
  • 14
  • 143
  • 159
1

I realize this is a very old question, but as I just had the same problem and found a workaround in this answer, I thought I'll post it anyway (and this was the first link Google showed for my search).

Just print your label twice, once an empty one with blanks and then another one with no fill and border. You might have to find the correct amount of blanks, but in my case it worked without problems and with a proportional font.

So with the code from the linked thread the solution would look like this:

    #Create dataframe 
countries <- c("Mundo", "Argentina", "Brasil", "Chile", "Colombia", "Mexico", "Perú", "Estados Unidos")
countries <- rep(countries, 120)
vaccinations <- data.frame(countries)
vaccinations$countries <- factor(vaccinations$countries , levels = c("Mundo", "Argentina", "Brasil", "Chile",
                                                                     "Colombia", "Mexico", "Perú", "Estados Unidos"))
vaccinations <- vaccinations %>%
  group_by(countries) %>%
  mutate(month = 1:120,
         vaccines = runif(120, min = 0, max = 5))

#Write text to add manually in each country
saved_text <- data.frame(
  label_pais = c("10.10 per 100", "11.76 per 100", "12.58 per 100", "62.94 per 100", 
                 "5.98 per 100", "8.84 per 100", "3.18 per 100", "55.96 per 100"),
  countries  = c("Mundo", "Argentina", "Brasil", "Chile",
                 "Colombia", "Mexico", "Perú", "Estados Unidos"))

saved_text$countries <- factor(saved_text$countries, levels = c("Mundo", "Argentina", "Brasil", "Chile",
                                                                "Colombia", "Mexico", "Perú", "Estados Unidos"))


ggplot()+
  geom_line(
    data = vaccinations, 
    aes(x = month, y = vaccines, colour = factor(countries)), 
    size = 2, 
    show.legend = FALSE
  ) +
  geom_label(
    data = saved_text,
    mapping    = aes(y = 4, x = 20, label = "                                                   "),  # you have to adjust the blanks to your biggest label
    color      = "black", # desired color
    label.size = 0.9
  ) +
  facet_wrap( ~ countries, ncol = 1) +
  geom_label(
    data       = saved_text,
    mapping    = aes(y = 4, x = 20, label = label_pais, color = factor(countries)),
    fill = NA,
    label.size = NA
  ) 

The result would look like this: enter image description here

Martin
  • 63
  • 6