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

ⅰ亾dé卋堺 提交于 2021-02-09 09:23:02

问题


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.


回答1:


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




回答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)



回答3:


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)

[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")

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




回答4:


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
  )



来源:https://stackoverflow.com/questions/65883795/ggplot2-how-to-conditionally-change-geom-texts-vjust-when-low-bars-make-text-e

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!