ggplot2: How to conditionally change geom_text's vjust when low bars make text exceed bar's bottom
Asked Answered
A

4

9

When plotting a bar chart, I often add labels to bars to signify the y-value for each bar. However, I run into trouble when the bar becomes too low, making the label unreadable or simply ugly.

Example


library(ggplot2)

df_blood <- data.frame(blood_type = c("O-", "O+",   "A-",   "A+",   "B-",   "B+",   "AB-",  "AB+"),
                       frequency  = c(0.13, 0.35, 0.08, 0.3, 0.02, 0.08, 0.01, 0.02))

ggplot(df_blood, aes(x = blood_type, y = frequency, fill = blood_type)) +
  geom_bar(stat = "identity") +
  geom_text(aes(label = frequency), color = "blue", vjust = 1, size = 7)

Created on 2021-01-25 by the reprex package (v0.3.0)
Looking at the bar of AB- we can see that the 0.01 text is exceeding the bar height (at the bar's bottom). In such cases, I'd like to change the vjust of geom_text() to 0.


Another Example with different y scale

Here I'm using the same size = 7 as above for geom_text():

library(ggplot2)

df_something <- data.frame(something = c("a", "b", "c"),
                   quantity = c(10000, 7800, 500))

ggplot(df_something, aes(x = something, y = quantity)) +
  geom_bar(stat = "identity", fill = "black") +
  geom_text(aes(label = quantity), color = "red", vjust = 1, size = 7)

Created on 2021-01-25 by the reprex package (v0.3.0)
Here we see that the bar for c has the 500 text exceeding the bottom of the bar. So in such case, I'd also like to change geom_text()'s vjust to 0, for bar c only.


To sum up

Although there are solutions to change vjust conditionally with a simple ifelse (see this SO solution) based on the y-value, I'm trying to figure out how to condition vjust such that it would work regardless of the values on the y scale. Rather, the rule should be that if the bar's height is lower than size of geom_text(), the text position will move to be on top. Thanks!


EDIT


Based on the discussion below with @Paul, I wonder whether it could be easier to condition vjust on whether geom_text() position overlies y = 0, and if it does, change vjust to 0.


EDIT 2


This SO solution (credit to @Paul for finding) seems close enough to what I'm asking. It dynamically changes the size of geom_text() to fit bar width, and is working even when resizing the plot. So I think this provides basis to what I'm after, just instead of tweaking size I need to tweak vjust, and instead of conditioning it on bar width I need to condition it on bar height. Unfortunately it is too complex for my understanding of ggproto and alike, so I don't know how to adapt it to my case.

Amendatory answered 25/1, 2021 at 11:23 Comment(4)
Do you know if it is possible to convert the size value into y axis value? Then it might be possible to achieve your goal with vjust = ifelse() inside aes()Trictrac
@Paul, I don't know how to make such conversion. I agree it's key to solving the problem, unless there are other methods that circumvent that need.Amendatory
From here we can read that "the size of text is measured in mm." Then, the text will remain of the same size even if you change the "final" size of your plot. Then I think we need to know the final size of your plot to find a solution. Is it an explicit style requirement or you can set it as you like?Trictrac
@Paul, I commented at your solution below. Maybe we need/could condition vjust not on the size of geom_text() but on whether geom_text() overlies y = 0? I think this will achieve the same desired output., just by a different (and perhaps more attainable) conditionAmendatory
Y
7

As an out-of-the-box option to achieve your desired result I would suggest to have a look at the ggfittext package which has some options to put the labels outside of the bars if they don't fit inside or to shrink the labels. Additionally there are also options to add some padding around the labels. However, it uses a no-default sizing policy so you you have to multiply default units by ggplot2::.pt:

library(ggplot2)
library(ggfittext)

df_something <- data.frame(something = c("a", "b", "c"),
                           quantity = c(10000, 7800, 500))

ggplot(df_something, aes(x = something, y = quantity)) +
  geom_bar(stat = "identity", fill = "black") +
  geom_bar_text(aes(label = quantity), 
                color = "red", 
                vjust = 1, 
                size = 7 * ggplot2::.pt, 
                min.size = 7 * ggplot2::.pt,
                padding.x = grid::unit(0, "pt"),
                padding.y = grid::unit(0, "pt"),
                outside = TRUE)
#> Warning: Ignoring unknown aesthetics: label

df_blood <- data.frame(blood_type = c("O-", "O+",   "A-",   "A+",   "B-",   "B+",   "AB-",  "AB+"),
                       frequency  = c(0.13, 0.35, 0.08, 0.3, 0.02, 0.08, 0.01, 0.02))

ggplot(df_blood, aes(x = blood_type, y = frequency, fill = blood_type)) +
  geom_bar(stat = "identity") +
  geom_bar_text(aes(label = frequency), 
                color = "blue", 
                vjust = 1, 
                size = 7 * ggplot2::.pt, 
                min.size = 7 * ggplot2::.pt,
                padding.x = grid::unit(0, "pt"),
                padding.y = grid::unit(0, "pt"),
                outside = TRUE)
#> Warning: Ignoring unknown aesthetics: label

Yukikoyukio answered 28/1, 2021 at 19:4 Comment(2)
First, this is outstanding. Second, would you mind elaborating more on "it uses a no-default sizing policy so you have to multiply default units by ggplot2::.pt" ? I don't understand the mechanism underlying the multiplication by ggplot2::.pt.Amendatory
Hi Emman. By default I mean that in ggplot2 all sizes are measured in "mm", including fontsize. See here. For convenience ggplot2 provides the constant .pt to convert from "mm" to the more usual "pt" or "points", i.e. if you want a font size of "7pt" you can do size = 7 / .pt. Having said this, by non-default I mean that ggfittext uses "pt" as the unit for fontsite, i.e. size = 7 will give you a font of size 7pt. Hence if you want the "default" ggplot2 sizes you have to multiply by ".pt". A bit confusing, but ..Yukikoyukio
S
2

vjust can take a vector of inputs equal to the size of the x - axis as well. The order of the vjust(where I put the 0) is based on the order of the dataset not the display shown in ggplot. You can factor blood_type to be very specific about where you would like each bar to be and control the vjust a little better.

library(ggplot2)

df_blood <- data.frame(blood_type = c("O-", "O+",   "A-",   "A+",   "B-",   "B+",   "AB-",  "AB+"),
                       frequency  = c(0.13, 0.35, 0.08, 0.3, 0.02, 0.08, 0.01, 0.02))

ggplot(df_blood, aes(x = blood_type, y = frequency, fill = blood_type)) +
  geom_bar(stat = "identity") +
  geom_text(aes(label = frequency), color = "blue", vjust = c(1,1,1,1,1,1,0,1), size = 7)
Schlueter answered 27/1, 2021 at 13:33 Comment(2)
This is an interesting feature I was unaware of. It might come handy in the future, but right now I don't know how to utilize this to condition vjust on whether geom_text()'s size makes the text exceed the bottom part of the bar.Amendatory
ok I understand, yea this feature would require you to know if the bar would be too small to fit the text. I am not sure how to determine the size of the bar and text to see if the text is too large for that barSchlueter
T
2

Just found this while googling around: Try to use vjust = "inward". However this might lead to other problems (as mentioned here)...

ggplot(df_something, aes(x = something, y = quantity)) +
  geom_bar(stat = "identity", fill = "black") +
  geom_text(aes(label = quantity), color = "red", vjust = "inward", size = 7)

enter image description here

[OLD post]

I post this as an "answer" just to show what I mean. It will be improved with feedback.

As stated here, "the size of text is measured in mm". Then it will always have the same size whatever the final size of your plot is.

I thing we need to know if you have to keep a final dimension for your plot. Ex:

ggsave(filename = "test_small.png", height = 5, units = "cm")

Small plot

ggsave(filename = "test_big.png", height = 20, units = "cm")

Big plot

Trictrac answered 27/1, 2021 at 14:35 Comment(6)
Thanks. I think that if there's no other solution, we could set specific dimensions for the entire plot and work with that. However, it would make the solution unscalable. Right? Unless we could write a function that would optimize geom_text's size too for any given plot dimensions.Amendatory
@Amendatory I agree, this was just to illustrate my comment. Just found vjust="inward", maybe it will do the trick.Trictrac
Very cool, but I cannot follow the rule. See here for running the following code: df_blood <- data.frame(blood_type = c("O-", "O+", "A-", "A+", "B-", "B+", "AB-", "AB+"), frequency = c(0.13, 0.35, 0.08, 0.3, 0.02, 0.08, 0.01, 0.02)) ggplot(df_blood, aes(x = blood_type, y = frequency, fill = blood_type)) + geom_bar(stat = "identity") + geom_text(aes(label = frequency), color = "blue", vjust = "inward", size = 7)Amendatory
yes, I was checking this and got overexcited ^^ it seems they are limitations to this, I try to find more infos about that.Trictrac
@Amendatory Well after some research, I found this SO post. Maybe it will be necessary to create your own geom as suggested by the validated answer. But this is far beyond my knowledge of ggplot2...Trictrac
Thanks @Paul, I appreciate your effort. I remember seeing that SO post but I forgot about it. Unfortunately nor am I proficient enough in ggplot2 programming to adapt it to my case.Amendatory
R
0

Instead of putting the labels within the columns, could you just put them on top and it would get around your problem? It works for all columns and you wouldn't need to worry about selecting individuals ones. Its probably cleaner to have all labels above rather than have some within and others above.

library(ggplot2)
df_something <- data.frame(something = c("a", "b", "c"),
                           quantity = c(10000, 7800, 500))
ggplot(data = df_something, aes(x = something, y = quantity)) +
  geom_col(position = "dodge", fill = "black") +
  geom_text(
    aes(label = quantity, y = quantity + 50),
    position = position_dodge(0.9),
    vjust = 0,
    color = "red",
    size = 7
  )

enter image description here

Reception answered 26/1, 2021 at 12:27 Comment(1)
Thanks. Placing the label within the bars is a choice guided by an explicit style requirement.Amendatory

© 2022 - 2025 — McMap. All rights reserved.