library(tidyverse)
library(plotly)
library(swac)Interactivity
with plotly
Coming soon: This page is currently being updated with standardized datasets and parallel Python examples.
Interactivity provides additional opportunity to engage with and explain findings in data. With the plotly package and a little effort, nearly any visualization made with {ggplot2} can gain interactive tool tips and animation for online publication.
As always, start with loading packages:
Here’s an example of a bar chart. We’re saving it as an object to call on it later. Writing the object name displays its contents:
venues <-
gram19_football |>
count(team_venue) |>
ggplot(aes(y = team_venue,
x = n)) +
geom_col()
venues
Simple interactive visualizations can be made with plotly’s function ggplotly(). On a computer, try hovering over the bars below to see a popup window containing values for each bar:
ggplotly(venues)Pipe into ggplotly
To skip the step of naming an object, surround the code with {brackets} and use a pipe:
{gram19_football |>
count(team_venue) |>
ggplot(aes(
y = team_venue,
x = n)) +
geom_col()} |>
ggplotly()Customize popups
The popup windows can be further customized with two steps:
- Inside the
aes()function, define a parameter calledtext. For this step, it’s a good idea to combine values using something likepaste(). - In the
ggplotly()function, settooltip = "text".
{gram19_football |>
count(team_venue) |>
ggplot(aes(
y = team_venue,
x = n,
text = paste(n, team_venue, "games"))) +
geom_col()} |>
ggplotly(tooltip = "text")- 1
-
The
textparameter is ignored byggplot(), but it gets passed on toggplotly(). - 2
-
By default, the popup will show the X- and Y-axis coordinates. Defining it to
textwill hide these other values
The glue() function (from the glue package) offers another helpful interface if you prefer it:
library(glue)
{gram19_football |>
count(team_venue) |>
ggplot(aes(
y = team_venue,
x = n,
text = glue("{n} {team_venue} games"))) +
geom_col()} |>
ggplotly(tooltip = "text")- 1
-
The
glue()function accepts R code or variables enclosed by {curly brackets}.
These tooltips accept many HTML tags to format text, including <b> bold </b>, <i> italic </i>, and simple style definitions made within <span style="color: red;"> span </span>.
venues <-
gram19_football |>
count(team_venue) |>
ggplot(aes(
y = team_venue,
x = n,
text = glue("In 2019, Grambling played <span style = 'color: red;'>{n}</span> <i>{tolower(team_venue)}</i> games."))) +
geom_col()
ggplotly(venues, tooltip = "text")With a little work, it’s possible to make highly customized and useful popups that communicate key ideas:
Code
swac_timeline <-
membership |>
mutate(
current_name = current_name |>
str_replace_all(
"Prairie View A&M University",
"Prairie View A & M University") |>
str_replace_all(
"Alabama Agricultural and Mechanical University",
"Alabama A & M University") |>
str_replace_all(
"^Southern University",
"Southern University and A & M College") |>
str_replace_all(
"–",
"-") |>
str_replace_all(
"Wiley College",
"Wiley University")) |>
left_join(
universities |>
unnest(colors) |>
filter(color != "#000000") |>
rename(institution = name) |>
slice_head(n = 1, by = institution) |>
select(-note),
by = join_by(current_name == institution)) |>
mutate(
action_num = case_when(
action %in% c("founded", "joined") ~ 1,
TRUE ~ -1),
total = cumsum(action_num)) |>
group_by(year, action) |>
summarize(
total = last(total),
summary =
paste0("<b>", current_name, "</b>") |>
str_flatten_comma(last = ", and "),
colors = case_when(
n() > 1 & cur_group()$action == "joined" ~ first(color),
n() > 1 ~ "darkgray",
TRUE ~ str_flatten(color))
) |>
select(year, total, summary,
action, colors) |>
mutate(
summary = case_when(
n() == 1 ~ paste(summary, action, "the conference."),
action == first(action) ~ paste(summary, action, "the conference."),
TRUE ~ paste(summary, action, "it."))) |>
group_by(year) |>
summarize(
year = last(year),
total = max(total),
summary = paste(summary, collapse = " "),
colors = first(colors)
) |>
distinct() |>
ungroup()
numbers <-
c("one", "two", "three", "four",
"five", "six", "seven", "eight",
"nine", "ten", "eleven", "twelve")
{swac_timeline |>
mutate(
tooltip = glue("In {year}, {summary} This change brought the total number of members to <b>{numbers[total]}</b>.")) |>
ggplot(
aes(
x = year,
y = total,
color = colors,
text = tooltip |>
scales::label_wrap(35)()
)) +
geom_line(aes(group = 1),
color = "gray") +
geom_point(size = 4) +
scale_color_identity() +
labs(
x = NULL,
y = "members",
title = "Timeline of membership in the Southwest Athletic Conference<br><sup>From six members in 1920, the conference has grown to twelve institutions today.</sup>") +
theme_minimal() +
theme(
panel.grid.major.x = element_blank())} |>
ggplotly(
tooltip = c("text")) |>
style(showlegend = FALSE) |>
config(displayModeBar = FALSE)- 1
-
The
universitiesdata set fromswacincludes the colors from each institution. I want to use these to give the chart some flavor, but I need to be careful about making them usable later on. Each institution has two to four colors in a nested list. Un-nesting these colors withunnest()expands each member to have multiple rows, with one row per color, but we need to choose just one. Usingfilter()makes it possible to remove all instances of black, andslice_head()then selects the first row for each member institution. - 2
- These lines limit the color for each year and action to the first color in the list. A dark gray is assigned for the founding year.
- 3
-
When combining the “joined” and “left” groups, using
first()orlast()at this stage ensures that only one color gets selected. Since the “joined” group comes first alphabetically, usingfirst()here privileges new members over those who are leaving. - 4
-
Defining the popup by using
mutate()makes it easier to check all the text before charting it. - 5
-
Popups without word wrapping are hard to read, and those with inconsistent widths can look messy. The
scalespackage offers a function calledlabel_wrap()()—note the unusual use of two sets of parentheses—to automatically introduce line breaks between words. Inside the first set of parentheses, choose the number of characters for a maximum width. - 6
-
ggplotly()doesn’t understand subtitles, so we have to get creative. This approach of adding a line break and styling the subtitle text as superscript is suggested on Stack Overflow.
Adding animation
Simple animation along some column is also very easy. A standard chart without animation is easy to build in plotly when starting from {ggplot2}. Adding a frame definition (shown here in line 11) introduces animation:
{gram19_football |>
ungroup() |>
arrange(date) |>
mutate(
score = team_score - opponent_score,
win = score > 0,
game = row_number()
) |>
ggplot(aes(
x = date,
y = score,
frame = game
)) +
geom_hline(yintercept = 0) +
geom_point() +
labs(y = "Grambling's lead")} |>
ggplotly()- 1
-
The
framereally needs to be a numeric value, and a date doesn’t work. Therow_number()function here is a quick way to assign an ordered number to each row. - 2
-
Including the
frameparameter sets up animations automatically.
Caveats
The plotly package makes it easy to add interactivity and animation from visualizations made with {ggplot2}. As the following examples show, it’s not without limitations:
Code
fancy <-
football |>
mutate(
result = case_when(
team_score > opponent_score ~ "win",
team_score < opponent_score ~ "loss") |>
factor(levels = c("win", "loss")),
point_difference = team_score - opponent_score) |>
filter(team == "Grambling State",
!is.na(result)) |>
ggplot(aes(
x = season,
y = point_difference,
frame = season)) +
geom_abline(slope = 0, intercept = 0, linetype = "dashed") +
geom_smooth(method = "lm") +
geom_jitter(
aes(
size = abs(point_difference),
fill = result),
shape = 21,
color = "white",
show.legend = FALSE) +
labs(x = NULL,
y = "point difference",
title = "Grambling's lead per game has been dropping.") +
# polish it with changes to the theme
theme_minimal() +
theme(panel.grid.minor = element_blank()) +
scale_x_continuous(breaks = c(2015:2022)) +
# add secondary axis labels
scale_y_continuous(
sec.axis = sec_axis(~ ., breaks = c(-8, 0, 8), labels = c("Lose", "Tie", "Win")))
fancy
plotly version, shown below, adds animation and a popover, but these additions may not be worth it. Animation makes it hard to compare one season to the next, and the trend line is noticeably absent. On top of this, annotations on the secondary Y-axis have been replaced with an unnecessary legend.ggplotly(fancy)Highly customized visualizations will often become simplified when converted for plotly. Choose the result that makes the impact you prefer for te visualization you’re making.