Suppose I have two plots, side by side, with the same y-axis, generated by the following R code:
df <- data.frame(x=c(5,2,7,3), y=c(11,3,5,6), facet=c(1,1,2,2))
ggplot(df, aes(x, y)) + facet_grid(~facet) + geom_point()
Is it possible to write the y axis text (e.g., 10.0, 7.5, 5.0) in the middle, between the two plots? (Preferentially the text should be centered.)
Here is a way (well almost) using Baptiste's answer from this SO post Display y-axis for each subplot when faceting. Not quite in the middle but its close
library(ggplot2)
library(gtable)
# your data
df <- data.frame(x=c(5,2,7,3), y=c(11,3,5,6), facet=c(1,1,2,2))
# First plot (a bit of extra space between facets)
p <- ggplot(df, aes(x, y)) + facet_grid(~facet) +
geom_point() +
theme(panel.margin = unit(1, "lines"),
axis.text.y = element_text( hjust=0))
# get y-axis labels
g <- ggplotGrob(p)
axis <- gtable_filter(g, "axis-l")[["grobs"]][[1]][["children"]][["axis"]][,1]
# remove axis
g[["grobs"]][[4]][["children"]][["axis"]] <- NULL
# build plot & add axis to LHS of left facet
panels <- subset(g$layout, name == "panel")
g <- gtable_add_grob(g, grobs=axis, t = unique(panels$t), l=tail(panels$l, -1)-1)
grid.newpage()
grid.draw(g)
Related
I am using the ndodge function explained by #jan-glx here;
https://stackoverflow.com/a/60650595/13399047
However I could not figure out how to align the axis ticks aligned as for example;
I should probably use theme(axis.ticks.length=) but I am not sure how to do it in an even/odd way.
Please help!
As far as I am aware there is no build in way to do this in ggplot, though that might change when they rewrite the guide system.
It is neither pretty nor easy, but here is an example how you could do it by messing around in the gtable / grid.
library(ggplot2)
library(grid)
data(diamonds)
diamonds$cut <- paste("Super Dee-Duper",as.character(diamonds$cut))
g <- ggplot(diamonds, aes(cut, carat)) +
geom_boxplot() +
scale_x_discrete(guide = guide_axis(n.dodge = 2))
# Convert to gtable
gt <- ggplotGrob(g)
# Grab bottom axis
is_axis <- grep("axis-b", gt$layout$name)
axisgrob <- gt$grobs[is_axis][[1]]
axis <- axisgrob$children$axis
# Grab tickmarks
is_ticks <- which(vapply(axis$grobs, inherits, logical(1), "polyline"))
ticks <- axis$grobs[[is_ticks]]
# Modify tickmarks
labelheight <- axis$heights[[2]] # First row of labels
modify <- which(seq_along(ticks$y) %% 4 == 0) - 1 # Change every the 3rd item in every quadruplet
ticks$y[modify] <- ticks$y[modify] - labelheight
# Insert ticks back into axis back into table
axis$grobs[[is_ticks]] <- ticks
axisgrob$children$axis <- axis
gt$grobs[[is_axis]] <- axisgrob
# Plot
grid.newpage()
grid.draw(gt)
Created on 2020-05-18 by the reprex package (v0.3.0)
Here is a solution using just ggplot2 stuff and not modifying any grobs. It requires ggplot2 3.0.0 and is based off https://stackoverflow.com/a/51312611/6615512
library(ggplot2)
data(diamonds)
diamonds$cut <- paste("Super Dee-Duper",as.character(diamonds$cut))
tick_min_pos_odd = -0.6
tick_min_pos_even = -0.4
custom_ticks = data.frame(cut = sort(unique(diamonds$cut)))
n_discrete_x_values = nrow(custom_ticks)
# Alternate tick lengths
custom_ticks$tick_min_pos = ifelse(1:n_discrete_x_values %% 2 == 0, tick_min_pos_odd, tick_min_pos_even)
ggplot(diamonds, aes(cut, carat)) +
geom_boxplot() +
scale_x_discrete(guide = guide_axis(n.dodge = 2)) +
geom_linerange(data = custom_ticks, # The custom tickmarks
aes(x=cut, ymax=-0.25, ymin=tick_min_pos),
size=0.5, color='black',
inherit.aes = F) +
coord_cartesian(clip='off', ylim=c(0,NA)) + # Clip off makes it so the geoms can be drawn outside the plot
# ylim sets the y-axis from 0 to the max.
theme(plot.margin = margin(0,0,20,0), # Add some whitespace to the bottom of the plot
axis.title.x = element_text(vjust=-1.5), # nudge the x-axis title and text down a tad
axis.text.x = element_text(vjust=-1.5))
Dummy data
d = data.frame(
x = factor(LETTERS[c(1,2,3,4,1,2,3,4,1,2,1,2,1,2,1,2)]),
y = c(100,80,70,60,130,90,65,60,2,3,3,3,2,2,1,2),
grid = rep(letters[1:2], each=8)
)
Issue
ggplot(d, aes(x=x, y=y)) + facet_grid(~grid, scales="free",space="free_x") + geom_point()
I like this graph. My only issue is that both grids use the same Y axis. So, I tried using facet_wrap instead of facet_grid and got
ggplot(d, aes(x=x, y=y)) + facet_wrap(~grid, scales="free") + geom_point()
But unfortunately, facet_wrap does not have a "space" parameter and as a result the right and the left graph are of the same width.
Question
How can I do so that the space between levels of the variable d$x is equal among both facets (leading to facets having different width) AND to have a separate Y axis for each facet. Of course, I would like to keep the facets to be aligned horizontally.
Use ggplot grob and modify the widths in the table
# Capture the plot
q = ggplot(d, aes(x=x, y=y)) + facet_grid(~grid, scales="free",space="free_x") + geom_point()
gt = ggplotGrob(q)
# Modify the widths
gt$widths[5] = unit(8, "cm")
gt$widths[9] = unit(4, "cm")
# Plot the graph
grid.newpage()
grid.draw(gt)
UPDATE: The question is moot. The vertical lines in the legend key are now default for geom_pointrange() in ggplot2.
For ggplot2 graphics that have a symbol for a point estimate and a vertical line representing a range about that estimate (95% confidence interval, Inter-quartile Range, Minimum and Maximum, etc) I cannot get the legend key to show the symbol with a vertical line. Since geom_pointrange() only has arguments for ymin and ymax, I would think the intended (default) functionality of geom_pointrange(show_guide=T) would be to have vertical lines (I say default because I understand that with coord_flip one could make horizontal lines in the plot). I also understand that having vertical lines in the legend key when the legend position is right or left will have the vertical lines "run together"...but for legends in the top or bottom having a vertical line through the symbol means that the key will match what appears in the plot.
Yet the approaches I've tried still put horizontal lines in the legend key:
## set up
library(ggplot2)
set.seed(123)
ru <- 2*runif(10) - 1
dt <- data.frame(x = 1:10,
y = rep(5,10)+ru,
ylo = rep(1,10)+ru,
yhi = rep(9,10)+ru,
s = rep(c("A","B"),each=5),
f = rep(c("facet1", "facet2"), each=5))
Default show_guide=T for geom_pointrange yields desired plot but has horizontal lines in legend key where vertical is desired (so as to match the plot):
ggplot(data=dt)+
geom_pointrange(aes(x = x,
y = y,
ymin = ylo,
ymax = yhi,
shape = s),
size=1.1,
show_guide=T)+
theme(legend.position="bottom")
An attempt with geom_point and geom_segment together yields desired plot but has horizontal lines in legend key where vertical is desired (so as to match the plot):
ggplot(data=dt)+
geom_point(aes( x = x,
y = y,
shape = s),
size=3,
show_guide=T)+
geom_segment(aes( x = x,
xend = x,
y = ylo,
yend = yhi),
show_guide=T)+
theme(legend.position="bottom")
An attempt with geom_point and geom_vline together yields desired legend key but does not respect the ymin and ymax values in the plot:
ggplot(data=dt)+
geom_point(aes(x=x, y=y, shape=s), show_guide=T, size=3)+
geom_vline(aes(xintercept=x, ymin=ylo, ymax=yhi ), show_guide=T)+
theme(legend.position="bottom")
How do I get the legend key of the 3rd graph but the plot of one of the first two?
My solution involves plotting a vertical line with geom_vline(show_guide=T) for an x-value that is out of the bounds of the displayed x-axis along with plotting geom_segment(show_guide=F):
ggplot(data=dt)+
geom_point(aes(x=x, y=y, shape=s), show_guide=T, size=3)+
geom_segment(aes(x=x, xend=x, y=ylo, yend=yhi), show_guide=F)+
geom_vline(xintercept=-1, show_guide=T)+
theme(legend.position="bottom")+
coord_cartesian(xlim=c(0.5,10.5))
The solution with coord_cartesian() for a numeric x axis is fine but facet_grid(scales='free_x') can be problematic:
# problem: coord_cartesian with numeric x and facetting with scales=free_x
ggplot(data=dt)+
geom_point(aes(x=x, y=y, shape=s), show_guide=T, size=3)+
geom_segment(aes(x=x, xend=x, y=ylo, yend=yhi), show_guide=F)+
geom_vline(xintercept=-1, show_guide=T)+
theme(legend.position="bottom")+
coord_cartesian(xlim=c(0.5,10.5))+
facet_grid(.~f, scales="free_x")
So in that situation, another solution that might not apply in every situation, but change x values to some meaningful character of factor and then adjust the xlim:
## hack solution: adjust xlim after change x to factor or character
## (carefully -- double check conversion):
dt$x <- factor(dt$x)
ggplot(data=dt)+
geom_point(aes(x=x, y=y, shape=s), show_guide=T, size=3)+
geom_segment(aes(x=x, xend=x, y=ylo, yend=yhi), show_guide=F)+
geom_vline(xintercept=-1, show_guide=T)+
theme(legend.position="bottom")+
coord_cartesian(xlim=c(0.5,5.5))+
facet_grid(.~f, scales="free_x")
If you don't mind having to use grid to draw the plot, you can manipulate the guide grobs directly:
library(grid)
library(gtable)
library(ggplot2)
set.seed(123)
ru <- 2*runif(10) - 1
dt <- data.frame(x = 1:10,
y = rep(5,10)+ru,
ylo = rep(1,10)+ru,
yhi = rep(9,10)+ru,
s = rep(c("A","B"),each=5),
f = rep(c("facet1", "facet2"), each=5))
ggplot(data=dt)+
geom_pointrange(aes(x = x,
y = y,
ymin = ylo,
ymax = yhi,
shape = s),
size=1.1,
show_guide=T)+
theme(legend.position="bottom") -> gg
gb <- ggplot_build(gg)
gt <- ggplot_gtable(gb)
seg <- grep("segments", names(gt$grobs[[8]]$grobs[[1]]$grobs[[4]]$children))
gt$grobs[[8]]$grobs[[1]]$grobs[[4]]$children[[seg]]$x0 <- unit(0.5, "npc")
gt$grobs[[8]]$grobs[[1]]$grobs[[4]]$children[[seg]]$x1 <- unit(0.5, "npc")
gt$grobs[[8]]$grobs[[1]]$grobs[[4]]$children[[seg]]$y0 <- unit(0.1, "npc")
gt$grobs[[8]]$grobs[[1]]$grobs[[4]]$children[[seg]]$y1 <- unit(0.9, "npc")
seg <- grep("segments", names(gt$grobs[[8]]$grobs[[1]]$grobs[[6]]$children))
gt$grobs[[8]]$grobs[[1]]$grobs[[6]]$children[[seg]]$x0 <- unit(0.5, "npc")
gt$grobs[[8]]$grobs[[1]]$grobs[[6]]$children[[seg]]$x1 <- unit(0.5, "npc")
gt$grobs[[8]]$grobs[[1]]$grobs[[6]]$children[[seg]]$y0 <- unit(0.1, "npc")
gt$grobs[[8]]$grobs[[1]]$grobs[[6]]$children[[seg]]$y1 <- unit(0.9, "npc")
grid.newpage()
grid.draw(gt)
How could I add box around legend key only for color not for size while keeping both legend keys. Code below add boxes to both mappings.
x<-1:6;
y<-factor(2:7);
z<-1:6;
df <- data.frame(x,y,z)
ggplot(df, aes(x,y)) +
geom_point(aes(colour=y, size = z) ) +
theme(legend.key = element_rect(colour = '#bdbdbd', size = 0.6))
Here is one approach using the ggplot layout and gtable. It extracts the color legend from the layout, draws boxes around each key, re-assambles the legend, then inserts the legend back into the ggplot layout.
library(ggplot2)
library(gtable)
library(grid)
x<-1:6;
y<-factor(2:7);
z<-1:6;
df <- data.frame(x,y,z)
p = ggplot(df, aes(x,y)) +
geom_point(aes(colour=y, size = z) )
# get ggplot grob
gt = ggplotGrob(p)
# Get the combined legend
leg = gtable_filter(gt, "guide-box")
# The legend has two parts.
# Get the second part - the color legend
leg2 = leg$grobs[[1]]$grobs[[2]]
# Get the locations of the top of each box containing the legend keys
# in this legend's layout
rects <- leg2$layout$t[grepl("bg", leg2$layout$name)]
# Draw boxes around each key
for(i in rects) leg2 = gtable_add_grob(leg2, grid.rect(gp = gpar(col = '#bdbdbd', fill = NA)), t = i, l = 2)
# Insert new color legend back into the combined legend
leg$grobs[[1]]$grobs[2][[1]] <- leg2
# Insert combined legend back into ggplot grob
gt$grobs[gt$layout$name == "guide-box"][[1]] <- leg
# Draw it
grid.newpage()
grid.draw(gt)
Here is a second approach (based on #Baptiste's answer here) that draw two plots: one containing the size legend, and the other containing the color legend (with boxes around the keys). It then extracts the legends from each plot's layout, combines the two legends into a single legend, then inserts the combined legend back into one of the layouts.
library(ggplot2)
library(gtable)
library(grid)
x<-1:6;
y<-factor(2:7);
z<-1:6;
df <- data.frame(x,y,z)
p1 = ggplot(df, aes(x,y)) +
geom_point(aes(colour=y, size = z) ) +
scale_colour_discrete(guide = "none")
p2 = ggplot(df, aes(x,y)) +
geom_point(aes(colour=y, size = z) ) +
scale_size(guide = "none") +
theme(legend.key = element_rect(colour = '#bdbdbd', size = 0.6))
# Get ggplot grobs
gt1 = ggplotGrob(p1)
gt2 = ggplotGrob(p2)
# Get the legends
leg1 = gtable_filter(gt1, "guide-box")
leg2 = gtable_filter(gt2, "guide-box")
# Combine the legends
leg <- rbind(leg1[["grobs"]][[1]], leg2[["grobs"]][[1]], size = "first")
# Insert legend into g1 (or g2)
gt1$grobs[gt1$layout$name == "guide-box"][[1]] <- leg
# Draw it
grid.newpage()
grid.draw(gt1)
I have the following dual plot (from another SO question):
Here's the code that generates the plot:
library(ggplot2)
library(gtable)
df <- data.frame(x=c(5,2,7,3),
y=c("asdasxfqwe","a","b","c"),
facet=c(1,1,2,2))
# First plot (a bit of extra space between facets)
p <- ggplot(df, aes(x, y)) + facet_grid(~facet) +
geom_point() +
theme(panel.margin = unit(4, "lines"),
axis.text.y = element_text( hjust=0.5))
# get y-axis labels
g <- ggplotGrob(p)
axis <- gtable_filter(g, "axis-l")[["grobs"]][[1]][["children"]][["axis"]][,1]
# remove axis
g[["grobs"]][[4]][["children"]][["axis"]] <- NULL
# build plot & add axis to LHS of left facet
panels <- subset(g$layout, name == "panel")
g <- gtable_add_grob(g, grobs=axis, t = unique(panels$t),
l=tail(panels$l, -1)-1)
grid.newpage()
grid.draw(g)
As I understand, the empty space on the left is where the y-axis text used to be before it was moved using the gtable code. How to get rid of this empty space?
Upgraded comment:
Since you're editing the gtable, you can set the relevant width to something smaller,
g[["widths"]][3] <- list(unit(1, "line"))