ggplot: Centre and move the vertical axis labels - r

I'm looking to set up a mirrored bar chart with one set of axis labels in the middle. This image shows what I have so far (code to reproduce at the end):
I'd like the names to be centred between the charts. Methods tried:
using axis labels (best attempt shown here)
using annotation_custom (I found placing the labels to be very difficult and disliked the combination of ggplot references and base plot references)
creating a separate "chart object" to put into the grid.arrange panel (difficult to get the correct vertical spacing between labels without there being any bars)
I'd welcome any suggestions around the easiest way to achieve this layout. The base has to be ggplot, but happy to use other packages to arrange charts.
require("ggplot2")
require("gridExtra")
dataToPlot <- data.frame(
"Person" = c("Alice", "Bob", "Carlton"),
"Age" = c(14, 63, 24),
"Score" = c(73, 62.1, 21.5))
plot1 <- ggplot(dataToPlot) +
geom_bar(data = dataToPlot, aes(x = Person, y = Score), stat = "identity",
fill = "blue", width = 0.8) +
scale_y_continuous(trans = "reverse", expand = c(0, 0)) +
scale_x_discrete(position = "top") +
theme(
axis.text.y = element_blank()
) +
labs(x = NULL) +
coord_flip()
plot2 <- ggplot(dataToPlot) +
geom_bar(data = dataToPlot, aes(x = Person, y = Age), stat = "identity",
fill = "red", width = 0.8) +
scale_y_continuous(expand = c(0, 0)) +
theme(
axis.text.y = element_text(size = 20, hjust = 0.5)
) +
labs(x = "") +
coord_flip()
gridExtra::grid.arrange(plot1, plot2, ncol = 2, widths = c(1, 1.2))

There are two ways (perhaps in combination)...
Add a margin to the right of the axis labels in the right-hand chart...
element_text(size = 20, hjust = 0.5, margin=margin(r=30))
...or move the two charts closer together
grid.arrange(plot1, plot2, ncol = 2, widths = c(1, 1.2),padding=0)

Related

Why are colours appearing in the labels of my gganimate sketch?

I have a gganimate sketch in R and I would like to have the percentages of my bar chart appear as labels.
But for some bizarre reason, I am getting seemingly random colours in place of the labels that I'm requesting.
If I run the ggplot part without animating then it's a mess (as it should be), but it's obvious that the percentages are appearing correctly.
Any ideas? The colour codes don't correspond to the colours of the bars which I have chosen separately. The codes displayed also cycle through about half a dozen different codes, at a rate different to the frame rate that I selected. And while the bars are the same height (they grow until they reach the chosen height displayed in the animation) then they display the same code until they stop and it gets frozen.
Code snippet:
df_new <- data.frame(index, rate, year, colour)
df_new$rate_label <- ifelse(round(df_new$rate, 1) %% 1 == 0,
paste0(round(df_new$rate, 1), ".0%"), paste0(round(df_new$rate, 1), "%"))
p <- ggplot(df_new, aes(x = year, y = rate, fill = year)) +
geom_bar(stat = "identity", position = "dodge") +
scale_fill_manual(values = colour) +
#geom_text(aes(y = rate, label = paste0(rate, "%")), vjust = -0.7) +
geom_shadowtext(aes(y = rate, label = rate_label),
bg.colour='white',
colour = 'black',
size = 9,
fontface = "bold",
vjust = -0.7,
alpha = 1
) +
coord_cartesian(clip = 'off') +
ggtitle("% population belonging to 'No religion', England and Wales census") +
theme_minimal() +
xlab("") + ylab("") +
theme(legend.position = "none") +
theme(plot.title = element_text(size = 18, face = "bold")) +
theme(axis.text = element_text(size = 14)) +
scale_y_continuous(limits = c(0, 45), breaks = 10*(0:4))
p
p <- p + transition_reveal(index) + view_follow(fixed_y = T)
animate(p, renderer = gifski_renderer(), nframes = 300, fps = frame_rate, height = 500, width = 800,
end_pause = 0)
anim_save("atheism.gif")
I think you have missed some delicate points about ggplot2. I will try my best to describe them to you. First of all, you need to enter the discrete values as factor or integer. So you can use as.factor() before plotting or just factor() in the aesthetic. Also, you should consider rounding the percentages as you wish. Here is an example:
set.seed(2023)
df_new <- data.frame(index=1:10, rate=runif(10), year=2001:2010, colour=1:10)
df_new$rate_label <- ifelse(round(df_new$rate, 1) %% 1 == 0,
paste0(round(df_new$rate, 1), ".0%"),
paste0(round(df_new$rate, 1), "%"))
The ggplot for this data is:
library(ggplot2)
p <- ggplot(df_new, aes(x = factor(year), y = rate, fill = factor(colour))) +
geom_bar(stat = "identity", position = "dodge") +
geom_text(aes(y = rate, label = paste0(round(rate,2), "%")), vjust = -0.7) +
coord_cartesian(clip = 'off') +
ggtitle("% population belonging to 'No religion', England and Wales census") +
theme_minimal() +
xlab("") + ylab("") +
theme(legend.position = "none",
plot.title = element_text(size = 18, face = "bold"),
axis.text = element_text(size = 14))
p
And you can combine all theme element in one theme() function (as did I). The output is:
And you can easily animate the plot using the following code:
library(gganimate)
p + transition_reveal(index)
And the output is as below:
Hope it helps.
So it was answered here although I don't know why the fix works.
For some reason, labels need to go into gganimate as factors
as.factor()
I just had to add the line:
df_new$rate_label <- as.factor(df_new$rate_label)
and it works fine.

ggplot: Add annotations using separate data above faceted chart

I'm trying to add set of markers with text above the top of a faceted chart to indicate certain points of interest in the value of x. Its important that they appear in the right position left to right (as per the main scale), including when the overall ggplot changes size.
Something like this...
However, I'm struggling to:
place it in the right vertical position (above the facets). In my
reprex below (a simplified version of the original), I tried using a
value of the factor (Merc450 SLC), but this causes issues such as adding that to
every facet including when it is not part of that facet and doesn't
actually go high enough. I also tried converting the factor to a number using as.integer, but this causes every facet to include all factor values, when they obviously shouldn't
apply to the chart as a whole, not each
facet
Note that in the full solution, the marker x values are independent of the main data.
I have tried using cowplot to draw it separately and overlay it, but that seems to:
affect the overall scale of the main plot, with the facet titles on the right being cropped
is not reliable in placing the markers at the exact location along the x scale
Any pointers welcome.
library(tidyverse)
mtcars2 <- rownames_to_column(mtcars, var = "car") %>%
mutate(make = stringr::word(car, 1)) %>%
filter(make >= "m" & make < "n")
markers <- data.frame(x = c(max(mtcars2$mpg), rep(runif(nrow(mtcars2), 1, max(mtcars2$mpg))), max(mtcars2$mpg))) %>%
mutate(name = paste0("marker # ", round(x)))
ggplot(mtcars2, aes()) +
# Main Plot
geom_tile(aes(x = mpg, y = car, fill = cyl), color = "white") +
# Add Markers
geom_point(data = markers, aes(x = x, y = "Merc450 SLC"), color = "red") +
# Marker Labels
geom_text(data = markers, aes(x = x, "Merc450 SLC",label = name), angle = 45, size = 2.5, hjust=0, nudge_x = -0.02, nudge_y = 0.15) +
facet_grid(make ~ ., scales = "free", space = "free") +
theme_minimal() +
theme(
# Facets
strip.background = element_rect(fill="Gray90", color = "white"),
panel.background = element_rect(fill="Gray95", color = "white"),
panel.spacing.y = unit(.7, "lines"),
plot.margin = margin(50, 20, 20, 20)
)
Perhaps draw two separate plots and assemble them together with patchwork:
library(patchwork)
p1 <- ggplot(markers, aes(x = x, y = 0)) +
geom_point(color = 'red') +
geom_text(aes(label = name),
angle = 45, size = 2.5, hjust=0, nudge_x = -0.02, nudge_y = 0.02) +
scale_y_continuous(limits = c(-0.01, 0.15), expand = c(0, 0)) +
theme_minimal() +
theme(axis.text = element_blank(),
axis.title = element_blank(),
panel.grid = element_blank())
p2 <- ggplot(mtcars2, aes(x = mpg, y = car, fill = cyl)) +
geom_tile(color = "white") +
facet_grid(make ~ ., scales = "free", space = "free") +
theme_minimal() +
theme(
strip.background = element_rect(fill="Gray90", color = "white"),
panel.background = element_rect(fill="Gray95", color = "white"),
panel.spacing.y = unit(.7, "lines")
)
p1/p2 + plot_layout(heights = c(1, 9))
It required some workaround with plot on different plot and using cowplot alignment function to align them on the same axis. Here is a solution
library(tidyverse)
library(cowplot)
# define a common x_axis to ensure that the plot are on same scales
# This may not needed as cowplot algin_plots also adjust the scale however
# I tended to do this extra step to ensure.
x_axis_common <- c(min(mtcars2$mpg, markers$x) * .8,
max(mtcars2$mpg, markers$x) * 1.1)
# Plot contain only marker
plot_marker <- ggplot() +
geom_point(data = markers, aes(x = x, y = 0), color = "red") +
# Marker Labels
geom_text(data = markers, aes(x = x, y = 0,label = name),
angle = 45, size = 2.5, hjust=0, nudge_x = 0, nudge_y = 0.001) +
# using coord_cartesian to set the zone of plot for some scales
coord_cartesian(xlim = x_axis_common,
ylim = c(-0.005, 0.03), expand = FALSE) +
# using theme_nothing from cow_plot which remove all element
# except the drawing
theme_nothing()
# main plot with facet
main_plot <- ggplot(mtcars2, aes()) +
# Main Plot
geom_tile(aes(x = mpg, y = car, fill = cyl), color = "white") +
coord_cartesian(xlim = x_axis_common, expand = FALSE) +
# Add Markers
facet_grid(make ~ ., scales = "free_y", space = "free") +
theme_minimal() +
theme(
# Facets
strip.background = element_rect(fill="Gray90", color = "white"),
panel.background = element_rect(fill="Gray95", color = "white"),
panel.spacing.y = unit(.7, "lines"),
plot.margin = margin(0, 20, 20, 20)
)
Then align the plot and plot them using cow_plot
# align the plots together
temp <- align_plots(plot_marker, main_plot, axis = "rl",
align = "hv")
# plot them with plot_grid also from cowplot - using rel_heights for some
# adjustment
plot_grid(temp[[1]], temp[[2]], ncol = 1, rel_heights = c(1, 8))
Created on 2021-05-03 by the reprex package (v2.0.0)

Left-adjust (hjust = 0) vertical x axis labels on facets with free scale

I have decided to rephrase this question. (Editing would have taken more time and in my opinion would also not have helped the OP.)
How can one left-adjust (hjust = 0, i.e., in text direction) over facets, when scale = 'free_x'?
I don't really think that left-adjustment of x-labels is a very necessary thing to do (long labels generally being difficult to read, and right-adjusting probably the better choice) - but I find the problem interesting enough.
I tried with empty padding to the maximum character length, but this doesn't result in the same length for all strings. Also, setting axis.text.x = element.text(margin = margin()) doesn't help. Needless to say, hjust = 0 does not help, because it is adjusting within each facet.
library(ggplot2)
diamonds$cut_label <- paste("Super Dee-Duper", as.character(diamonds$cut))
ggplot(data = diamonds, aes(cut_label, carat)) +
facet_grid(~ cut, scales = "free_x") +
theme(axis.text.x = element_text(angle = 90))
The red arrows and dashed line indicate how the labels should adjust. hjust = 0 or margins or empty padding do not result in adjustment of those labels over all facets.
Data modification from this famous question
I tried with empty padding to the maximum character length, but this
doesn't result in the same length for all strings.
This caught my attention. Actually, it would result in the same length for all strings if you padded the labels with spaces, made them all the same length, and ensured the font family was non-proportionally spaced.
First, pad the labels with spaces such that all labels have the same length. I'm going to ustilise the str_pad function from the stringr package.
library(ggplot2)
data("diamonds")
diamonds$cut_label <- paste("Super Dee-Duper", as.character(diamonds$cut))
library(stringr)
diamonds$cut_label <- str_pad(diamonds$cut_label, side="right",
width=max(nchar(diamonds$cut_label)), pad=" ")
Then, you may need to load a non-proportionally-spaced font using the extrafont package.
library(extrafont)
font_import(pattern='consola') # Or any other of your choice.
Then, run the ggplot command and specify a proportionally spaced font using the family argument.
ggplot(data = diamonds, aes(cut_label, carat)) +
facet_grid(~cut, scales = "free_x") +
theme(axis.text.x = element_text(angle = 90, family="Consolas"))
One way, and possibly the most straight forward hack, would be to annotate outside the coordinates.
Disadvantage is that the parameters would need manual adjustments (y coordinate, and plot margin), and I don't see how to automate this.
library(ggplot2)
diamonds$cut_label <- paste("Super Dee-Duper", as.character(diamonds$cut))
ann_x <- data.frame(x = unique(diamonds$cut_label), y = -16, cut = unique(diamonds$cut))
ggplot(data = diamonds, aes(cut_label, carat)) +
facet_grid(~cut, scales = "free_x") +
geom_text(data = ann_x, aes(x, y, label = x), angle = 90, hjust = 0) +
theme(
axis.text.x = element_blank(),
plot.margin = margin(t = 0.1, r = 0.1, b = 2.2, l = 0.1, unit = "in")
) +
coord_cartesian(ylim = c(0, 14), clip = "off")
Created on 2020-03-14 by the reprex package (v0.3.0)
I'd approach this by making 2 plots, one of the plot area and one of the axis labels, then stick them together with a package like cowplot. You can use some theme settings to disguise the fact that the axis labels are actually made by a geom_text.
The first plot is fairly straightforward. For the second which becomes the axis labels, use dummy data with the same variables and adjust spacing how you want via text size and scale expansion. You'll probably also want to mess with the rel_heights argument in plot_grid to change the ratio of the two charts' heights.
library(ggplot2)
library(cowplot)
p1 <- ggplot(diamonds, aes(x = cut_label, y = carat)) +
facet_grid(cols = vars(cut), scales = "free_x") +
theme(axis.text.x = element_blank()) +
labs(x = NULL)
axis <- ggplot(dplyr::distinct(diamonds, cut_label, cut), aes(x = cut_label, y = 1)) +
geom_text(aes(label = cut_label), angle = 90, hjust = 0, size = 3.5) +
facet_grid(cols = vars(cut), scales = "free_x") +
scale_x_discrete(breaks = NULL) +
scale_y_continuous(expand = expansion(add = c(0.1, 1)), breaks = NULL) +
labs(y = NULL) +
theme(strip.text = element_blank(),
axis.text.x = element_blank(),
axis.ticks = element_blank(),
panel.background = element_blank())
plot_grid(p1, axis, ncol = 1, axis = "lr", align = "v")
We can edit the text grobs after generating the plot, using library(grid).
g <- ggplot(data = diamonds, aes(cut_label, carat)) +
facet_grid(~cut, scales = "free_x") +
theme(axis.text.x = element_text(angle = 90, vjust = 0.5))
gt <- cowplot::as_gtable(g)
axis_grobs <- which(grepl("axis-b", gt$layout$name))
labs <- levels(factor(diamonds$cut_label))[order(levels(diamonds$cut))]
for (i in seq_along(axis_grobs)) {
gt$grobs[axis_grobs[i]][[1]] <-
textGrob(labs[i], y = unit(0, "npc"), just = "left", rot = 90, gp = gpar(fontsize = 9))
}
grid.draw(gt)

Adding Annotations to coord_polar

I'd like to place annotations in the bottom left corner of a plot with polar coordinates.
Is there a way of doing this by using coordinates that are not part of the circular coordinate system (as shown, which doesn't allow me to place them far out) but a normal x and y system?
I don't need the axes to show but have left them in to help show what's going on.
library(tidyverse)
iris2 <- iris %>%
mutate(id = row_number())
ggplot(iris2) +
theme_minimal() +
theme(
legend.position = "none",
axis.title = element_blank()
) +
coord_polar() +
geom_segment(aes(x = id, xend = id,
y = 10,
yend = 10 + Sepal.Length),
size = 1, inherit.aes = FALSE) +
annotate(geom="text", x = 90, y = 10, label="Annotation",
color="Red") +
annotate(geom="text", x = 90, y = 15, label="Annotation",
color="Blue")
You could use the cowplot package to make the desired plot. You can make two text_grob that will be the text labels and then add them on the lower and left side of the plot you already have (appears as p1 in the following code).
library(cowplot)
#X axis text
x <- textGrob(label = "X text",
gp=gpar(fontsize=12,
fontface = "bold"),
hjust = 0.1)
#Y axis text
y <- textGrob(label = "Y text",
#rotate it 90 degrees
rot = 90,
gp=gpar(fontsize=12,
fontface = "bold"))
#Get X axis text and p1 together in a 2 row array
p2<-plot_grid(p1,x,
nrow = 2,
rel_widths = c(4,1),
rel_heights = c(20,1))
#Get Y axis text and p2 together in 2 column array
p3<-plot_grid(y,p2,
ncol = 2,
rel_widths = c(1,20),
rel_heights = c(1,4))
p3
Based on #Aexman's helpful comment to try cowplot, I did and came up with the following. It means you can add two layers together, the second with an independent coordinate system.
Coordinates are between 0 and 1 for both axes, relative the bottom left corner. Absolute positioning is also available.
library(tidyverse)
library(cowplot)
iris2 <- iris %>%
mutate(id = row_number())
g <- ggplot(iris2) +
theme_minimal() +
theme(
legend.position = "none",
axis.title = element_blank()
) +
coord_polar() +
geom_segment(aes(x = id, xend = id,
y = 10,
yend = 10 + Sepal.Length),
size = 1, inherit.aes = FALSE)
ggdraw(g) +
draw_label("Annotation", x = 0.05, y = 0.05, hjust = 0, vjust = 0,
fontfamily = "", fontface = "plain", color = "black", size = 14,
colour = "Red")

Reduce white space margin in cowplot

Data and libraries used for this question:
library(tidyverse)
library(reshape2)
library(cowplot)
data("diamonds")
temp1_m <- temp2_m <- melt(diamonds[1:3])
temp1_m[3] <- temp2_m[3] <- NULL
colnames(temp1_m) <- colnames(temp2_m) <- c('Var1', 'Var2', 'value')
I'm trying to combine two geom_tile plots using the cowplot library.
The two individual plots are created using:
figMT <- ggplot(temp2_m, aes(Var1, Var2))+
geom_tile(aes(fill = value), colour = 'white')+
scale_fill_gradient(low = 'green', high = 'red', name = 'log fold change',
guide = guide_legend(title.vjust = 1, reverse = T))+
ggtitle('Mutant clusters')+
scale_x_discrete(expand = c(0, 0))+
scale_y_discrete(limits = rev(levels(temp2_m$Var2)))+
xlab('')+
ylab('')+
coord_equal()+
theme(axis.text.x = element_text(angle = 45, hjust = 1),
axis.text = element_text(size=12),plot.margin=grid::unit(c(0,0,0,0), "mm"))
and
figWT <- ggplot(temp1_m, aes(Var1, Var2))+
geom_tile(aes(fill = value), colour = 'white')+
scale_fill_gradient(low = 'green', high = 'red', name = 'log fold change',
guide = guide_legend(title.vjust = 1, reverse = T))+
ggtitle('WT clusters')+
scale_x_discrete(expand = c(0, 0))+
scale_y_discrete(limits = rev(levels(temp1_m$Var2)))+
xlab('')+
ylab('Cluster')+
coord_equal()+
theme(axis.text.x = element_text(angle = 45, hjust = 1),
axis.text = element_text(size=12),plot.margin=grid::unit(c(0,0,0,0), "mm"))
I then use cowplot to first make a title with:
title <- ggdraw()+
draw_label("Heatmaps over the average fold change of the clusters", fontface='bold')
and then to combine the title and the two plots with:
p <- plot_grid(figWT+ theme(plot.margin = unit(c(0, -10, 0, 0), "cm")),
figMT+ theme(plot.margin = unit(c(0, 0, 0, -5.1), "cm")),
labels=c('A', 'B'), hjust = c(-24, -.5))
plot_grid(title, p, nrow=2, rel_heights=c(0.1, 1))
I reduce a lot of whitespace between the two heatmaps after moving them closer together. But it creates a lot of whitespaces on the left and right margin which I don't manage to remove. That is, when I save the picture with:
ggsave('ex1.pdf', scale = 2)
Any suggestions?
By adding coord_equal(), you're setting a fixed-aspect-ratio coordinate system, which means you need to make the aspect ratio of the final output file match the aspect ratios of your underlying plots. By not specifying a width and a height in your ggsave() line you're essentially guaranteed to not get the right output. Also, if you find yourself setting large negative margins you know for sure you're doing something wrong.
Without knowing exactly what the intended output is, this seems reasonable to me:
p <- plot_grid(figWT, figMT, labels=c('A', 'B'))
plot_grid(title, p, nrow=2, rel_heights=c(0.1, 1))
ggsave("ex1.png", width = 8, height = 4.5, dpi = 300)

Resources