问题
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