Piecing Plots Together
Piecing Plots Together
Packages
Code
Custom Theme:
Code
my_theme <- function(){
theme_classic() +
theme(
legend.position = "bottom"
, legend.title = element_text(face = "bold", size = rel(1))
, legend.text = element_text(face = "italic", size = rel(1))
, axis.text = element_text(face = "bold", size = rel(1.1), color = "black")
, axis.title = element_text(face = "bold", size = rel(1.2))
, plot.title = element_text(face = "bold", size = rel(1.2), hjust = .5)
, plot.subtitle = element_text(face = "italic", size = rel(1.2), hjust = .5)
, strip.text = element_text(face = "bold", size = rel(1.1), color = "white")
, strip.background = element_rect(fill = "black")
)
}
Review
- Over the last several weeks, we have talked about:
- tidying data
-
ggplot2
logic - visualizing proportions
- visualizing differences
- visualizing time series
- visualizing uncertainty
- For the rest of the course, we will pivot to taking everything we’ve learning and piecing it all together
- Today: Piecing visualizations together
- Next week: Polishing visualizations **
-
11/21: Interactive Visualizations (
shiny
)
Today
- There are lots of packages for piecing visualizations together
- I have used lots and the only one that I can say I’ve actually liked in
cowplot
, so I’m going to teach you that - There are other more specialized packages worth mentioning
- Here is a short list of some core
ggplot2
extensions: https://exts.ggplot2.tidyverse.org/gallery/ - We’ll cover:
ggExtra
-
cowplot
(and lots of assortments) patchwork
ggExtra
- We’ll start with
ggExtra
because it will help us create plots with distributions in the margins. - After, we’ll move to cowplot, where there will be lots of little odds and ends to step through
- Remember these data?
Code
Let’s plot the association between conscientiousness and self-rated health across genders in Study 1:
Code
p <- pred_data %>%
filter(study == "Study1") %>%
ggplot(aes(x = p_value, y = SRhealth, color = gender, fill = gender)) +
geom_point(
, color = "grey20"
, shape = 21, size = 3
) +
scale_fill_manual(
values = c("cornflowerblue", "coral")
, labels = c("Male", "Female")
) +
labs(
x = "Conscientiousness (POMP, 0-10)"
, y = "Self-Rated Health (POMP, 0-10)"
, fill = "Gender"
) +
my_theme()
p
And add in a smoothed association and custom colors and labels
Code
To get marginal distributions, we can just use ggExtra::ggMarginal()
This is fine, but we can do better!
Let’s try color
Let’s try fill
Let’s try a histogram
And group-based fill and color
This is not expected behavior, so let’s move on to patchwork
and cowplot
where we can make these much more flexibly with just a few extra lines of code
cowplot
+ pathwork
- Why
cowplot
orpatchwork
?- figure alignment
- easier to choose relative values and layouts
- can mix base
R
plots andggplot2
plots - allows you to annotate plots (including stacking, as opposed to layering)
- shared legends!
- includes the themes from his book
Piecing the Plots Together
First, let’s build the x
and y
marignals
Code
px <- pred_data %>%
filter(study == "Study1") %>%
ggplot(aes(x = p_value, fill = gender, color = gender)) +
geom_density(alpha = .5) +
scale_color_manual(
values = c("cornflowerblue", "coral")
, labels = c("Male", "Female")
) +
scale_fill_manual(
values = c("cornflowerblue", "coral")
, labels = c("Male", "Female")
) +
labs(fill = "Gender", color = "Gender") +
theme_void()
px
Code
py <- pred_data %>%
filter(study == "Study1") %>%
ggplot(aes(x = SRhealth, fill = gender, color = gender)) +
geom_density(alpha = .5) +
scale_color_manual(
values = c("cornflowerblue", "coral")
, labels = c("Male", "Female")
) +
scale_fill_manual(
values = c("cornflowerblue", "coral")
, labels = c("Male", "Female")
) +
labs(fill = "Gender", color = "Gender") +
coord_flip() +
theme_void()
py
With patchwork
, we can use the +
and /
operators to arrange them:
That arrangement isn’t quite right, so let’s use plot_layout()
to create a custom layout:
Code
Those legends are messing us up! Let’s use guides = "collect"
within plot_layout()
. Then we’ll use the &
to add a theme to the whole plot:
Code
Honestly, we don’t need the marginal legend
So let’s remove it:
Code
Let’s do the same thing but with geom_boxplot()
and geom_jitter()
Code
px <- pred_data %>%
filter(study == "Study1") %>%
ggplot(aes(x = p_value, y = gender, fill = gender, color = gender)) +
geom_boxplot(alpha = .5) +
geom_jitter(aes(y = gender), alpha = .5) +
scale_color_manual(
values = c("cornflowerblue", "coral")
, labels = c("Male", "Female")
) +
scale_fill_manual(
values = c("cornflowerblue", "coral")
, labels = c("Male", "Female")
) +
labs(fill = "Gender", color = "Gender") +
theme_void() +
theme(legend.position = "none")
px
Code
py <- pred_data %>%
filter(study == "Study1") %>%
ggplot(aes(x = SRhealth, y = gender, fill = gender, color = gender)) +
geom_boxplot(alpha = .5) +
geom_jitter(aes(y = gender), alpha = .5) +
scale_color_manual(
values = c("cornflowerblue", "coral")
, labels = c("Male", "Female")
) +
scale_fill_manual(
values = c("cornflowerblue", "coral")
, labels = c("Male", "Female")
) +
labs(fill = "Gender", color = "Gender") +
coord_flip() +
theme_void() +
theme(legend.position = "none")
py
And let’s put it back together
Advanced Piecing Plots Together
- Marginal plots are great for lots of reasons
- But when it comes to piecing plots together, we are often interested for bringing together different kinds of figures together because you can’t bring them together with facets or other ways
cowplot
Let me show you a couple of examples from my work that has used cowplot
From Beck & Jackson (2020):
From Beck et al. (under review):
From Beck & Jackson (2022):
Example: Forest Plots
- Let’s build up our use cases incrementally!
- But first, we need some plots to plot!
Models
And remember these models?
Code
Let’s do one small change
Code
m_fun <- function(d) {
glm(o_value ~ p_value + married + married:p_value
, data = d
, family = binomial(link = "logit"))
}
tidy_ci <- function(m) tidy(m, conf.int = T) %>% mutate(df.resid = m$df.residual, n = nrow(m$data))
nested_m <- pred_data %>%
group_by(study) %>%
nest() %>%
ungroup() %>%
mutate(
m = map(data, m_fun)
, tidy = map(m, tidy_ci)
)
nested_m
Here’s our unnested model terms
Code
But maybe we are particularly interested in the interaction between marital status and personality in predicting mortality, which we want to plot as a forest plot
Code
- We could hack our way to a forest plot in a single figure, but it never looks as nice as if we do it in two
- the forest plot itself
- the table of values
- the forest plot itself
Forest Plot
Code
Let’s add our point estimates and uncertainty intervals
Code
But we want to order the terms by their effect size:
Code
p1 <- nested_m %>% select(study, tidy) %>%
unnest(tidy) %>%
mutate_at(vars(estimate, conf.low, conf.high), exp) %>%
filter(term == "p_value:married1")
p1 <- p1 %>%
mutate(study = factor(study, (p1 %>% arrange(desc(estimate)))$study)) %>%
ggplot(aes(x = estimate, y = study)) +
labs(
x = "Model Estimated OR (CI)"
, y = NULL
) +
my_theme()
p1
Now, let’s re-add our point estimates and uncertainty intervals
Code
And add a vertical line at one (no higher or lower odds)
Forest Plot Table
In a forest plot, we don’t just show estimates, we print them with the sample size
Code
p2 <- nested_m %>% select(study, tidy) %>%
unnest(tidy) %>%
mutate_at(vars(estimate, conf.low, conf.high), exp) %>%
filter(term == "p_value:married1")
stdy_levs <- tibble(num = 1:6, new = (p2 %>% arrange(desc(estimate)))$study)
p2 <- p2 %>%
arrange(desc(estimate)) %>%
mutate(study = factor(study, stdy_levs$new)
, study2 = 1:n()) %>%
mutate_at(vars(estimate, conf.low, conf.high), ~sprintf("%.2f", .)) %>%
mutate(est = sprintf("%s [%s, %s]", estimate, conf.low, conf.high)
, n = as.character(n)) %>%
select(study, study2, estimate, n, est) %>%
pivot_longer(
cols = c(est, n)
, values_to = "lab"
, names_to = "est"
)
p2
Add labels and themes:
Add the label text:
Change to theme_void()
Add a top line and re-add the bottom axis line:
Code
And annotate()
the column levels back in
Code
Change the scale limits to improve the figure
Back to the Forest Plot
We added an extra row at the top of the table, so we need to do that for the forest plot, too
Code
p1 <- nested_m %>% select(study, tidy) %>%
unnest(tidy) %>%
mutate_at(vars(estimate, conf.low, conf.high), exp) %>%
filter(term == "p_value:married1")
stdy_levs <- tibble(num = 1:6, new = (p1 %>% arrange(desc(estimate)))$study)
p1 <- p1 %>%
arrange(desc(estimate)) %>%
mutate(study = factor(study, stdy_levs$new)
, study2 = 1:n()) %>%
ggplot(aes(x = estimate, y = study2)) +
labs(
x = "Model Estimated OR (CI)"
, y = NULL
) +
my_theme()
p1
Add our point estimates and uncertainty intervals, along with the vertical line at OR = 1
Code
Change the y scale back to match the study labels
Code
Add in that top bar to match the table
Remove the y axis line
And let’s block out where the dashed line touches the top:
patchwork
Piecing the Plots Together
- I know that was a lot, but such is the reality of ggplot – we have to hack it!
-
annotate()
is a great tool for this - so are our
scale_[map]_[type]
functions, especially given the labels can be anything we want! - and our
theme
elements also let us hack many more parts!
-
- The biggest trick to
ggplot2
is simply having lots of tricks up your sleeve, which come from knowledge (and StackOverflow) -
patchwork
is great, and a little more intuitive for simple use cases - (We’ll still talk some about cowplot and a more full demo of it is at the end of the slides and in the workbook)
-
patchwork
allows you to use the+
to piece plots together and makes a lot of default assumptions about alignment - It also let’s you continue to layer on top of figures that are pieced together, which
cowplot
doesn’t do (easily)
We can just use the +
operator!
We can also add rows using the /
And change their arrangement using plot_layout()
And change their arrangement using plot_layout()
We can add titles using plot_annotation()
Code
We can add labels to plot using plot_annotation()
Code
And control additional elements with tag_prefix
and tag_suffix()
Example Setup: Simple Effects
But maybe we want to add the simple effects along with the forest plots of the interaction. Let’s set that up.
Code
We also want to bring the residual degrees of freedom in so that we can use them later in stat_lineribbon()
:
Code
Let’s save that and set up the basic core of the plot
Code
p3 <- nested_m %>%
mutate(df.resid = map_dbl(m, df.residual)) %>%
select(study, pred, df.resid) %>%
unnest(pred) %>%
mutate(married = factor(married, c(0,1), c("Never Married", "Married"))) %>%
ggplot(aes(x = p_value, y = .fitted, fill = study, color = study)) +
labs(x = "Conscientiousness (POMP, 0-10)"
, y = "Predicted Odds Ratio\nof Mortality (95% CI)"
, fill = NULL
, color = NULL) +
facet_grid(~married) +
my_theme()
Now let’s use stat_lineribbon()
and add in the color palettes for fill
and color
Code
We can then use +
to bring together the forest plot and table on the same row and /
to put the simple effects plot on the next row.
And use plot annotation to add in the title
and subtitle
Code
Let’s collect the legend to the bottom of the plot
cowplot
cowplot
provides lots of tools for sprucing up these plots.
New grobs for drawing on our plots
- Relative to
patchwork
,cowplot
also adds some other new tools to our repertoire:ggdraw()
draw_label()
draw_plot_label()
draw_grob()
draw_image()
ggdraw()
+ draw_label()
*
ggdraw()is more or a setup function that allows us to add grobs on top * We'll use it with
draw_label()` to make our title (just some text to put on the plot)
It’d be nice if the title was centered, right?
Code
We could use draw_label()
to add a title and subtitle to our plot:
Code
title <- ggdraw() +
draw_label(
"Mortality Odds"
, fontface = 'bold'
, x = .5
, hjust = .5
, y = .8
) +
draw_label(
"Conscientiousness x Marital Status"
, fontface = 'italic'
, x = .5
, hjust = .5
, y = .2
) +
theme(
# add margin on the left of the drawing canvas,
# so title is aligned with left edge of first plot
plot.margin = margin(0, 0, 0, 7)
)
title
Now we can add that title in using patchwork
(see below fo the cowplot
version)
-
draw_label()
is meant to be a better wrapper forgeom_text()
that requires less customization
- Say for example, we want to put a wordmark on our plots (there are journals that require this!)
- Doing this with
geom_text()
would require 10+ arguments and has no easy application to figures put together with cowplot (or other packages for doing so)
- Imagine you want to put a plot inside of another
- First let’s set up the examples
Code
inset <-
pred_data %>%
filter(study == "Study1") %>%
ggplot(aes(y = gender, x = SRhealth, fill = gender)) +
scale_fill_manual(values = c("cornflowerblue", "coral")) +
scale_y_discrete(labels = c("Male", "Female")) +
stat_halfeye(alpha = .8) +
my_theme() +
theme(legend.position = "none") + theme_half_open(12)
p4 <- pred_data %>%
filter(study == "Study1") %>%
ggplot(aes(x = p_value, SRhealth, fill = gender)) +
geom_point(shape = 21, color = "grey20", size = 3) +
scale_fill_manual(values = c("cornflowerblue", "coral"), labels = c("Male", "Female")) +
my_theme()
And add the inset plot on (note, this is not a great example)
We can also add images!
Extra Slides: cowplot::plot_grid()
plot_grid()
- The core function of
cowplot
isplot_grid()
, which allows us to place differnt figures within the same figure in a grid, and it has a lot of useful arguments - It’s the alternative to
+
,/
in `patchwork
plotlist = NULL
align = c("none", "h", "v", "hv")
axis = c("none", "l", "r", "t", "b", "lr", "tb", "tblr")
nrow = NULL
ncol = NULL
rel_widths = 1
rel_heights = 1
labels = NULL
label_size = 14
label_fontfamily = NULL
label_fontface = "bold"
label_colour = NULL
label_x = 0
label_y = 1
hjust = -0.5
vjust = 1.5
scale = 1
greedy = TRUE
byrow = TRUE
cols = NULL
rows = NULL
Let’s revisit how to put together our forest plot using cowplot
instead of patchwork
:
Not bad, but we want to align our plots. We can do that with either align
or axis
:
align
Similar behavior, but "hv"
leads to odd spacing
axis
Doesn’t properly align our bottom because it’s not optimized for labels.
Now let’s change the widths of the plots:
Let our interval estimates shine
We wouldn’t do this, but note that when we have rows, we use rel_heights
plot_grid()
: Labels
We can do lots with labels using cowplot::plot_grid()
Let’s have some fun and add some chaos: label_size
, label_fontface
, label_fontfamily
, and label_colour
(note the spelling)
Code
And set the location using label_x
and label_y
Code
And let’s move them some more