Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 960
Emman Avatar asked Mar 01 '23 18:03

Emman


2 Answers

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

like image 194
stefan Avatar answered Apr 27 '23 01:04

stefan


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)
like image 30
Mike Avatar answered Apr 27 '23 03:04

Mike