On this page I’ll walk through the code used to produce the “spell dice” plot that I posted on social media. It’s not the most exciting code I’ve ever written but it was useful as a tiny side project that I could use to teach myself targets. Note that this post doesn’t use targets to render the plot, except in the trivial sense the entire blog is built with targets. Within the post, it’s just regular R code. Speaking of which, I suppose I’d best load the packages that the plot relies on:
library(ggplot2)
library(dplyr)
library(tidyr)
library(stringr)
library(tibble)
library(purrr)
library(readr)
library(forcats)
library(ggrepel)
The spells
data that I’m using here comes from the TidyTuesday D&D Spells data set. The data set was compiled by Jon Harmon, and originates in the recently released Dungeons & Dragons Free Rules (2024 edition). If you’ve played D&D before, this should be quite familiar:
spells <- read_csv("./data/spells.csv", show_col_types = FALSE)
print(spells)
#> # A tibble: 314 × 27
#> name level school bard cleric druid paladin ranger sorcerer warlock wizard
#> <chr> <dbl> <chr> <lgl> <lgl> <lgl> <lgl> <lgl> <lgl> <lgl> <lgl>
#> 1 Acid … 0 evoca… FALSE FALSE FALSE FALSE FALSE TRUE FALSE TRUE
#> 2 Aid 2 abjur… TRUE TRUE TRUE TRUE TRUE FALSE FALSE FALSE
#> 3 Alarm 1 abjur… FALSE FALSE FALSE FALSE TRUE FALSE FALSE TRUE
#> 4 Alter… 2 trans… FALSE FALSE FALSE FALSE FALSE TRUE FALSE TRUE
#> 5 Anima… 1 encha… TRUE FALSE TRUE FALSE TRUE FALSE FALSE FALSE
#> 6 Anima… 2 encha… TRUE FALSE TRUE FALSE TRUE FALSE FALSE FALSE
#> 7 Anima… 8 trans… FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE
#> 8 Anima… 3 necro… FALSE TRUE FALSE FALSE FALSE FALSE FALSE TRUE
#> 9 Anima… 5 trans… TRUE FALSE FALSE FALSE FALSE TRUE FALSE TRUE
#> 10 Antil… 5 abjur… FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE
#> # ℹ 304 more rows
#> # ℹ 16 more variables: casting_time <chr>, action <lgl>, bonus_action <lgl>,
#> # reaction <lgl>, ritual <lgl>, casting_time_long <chr>, trigger <chr>,
#> # range <chr>, range_type <chr>, verbal_component <lgl>,
#> # somatic_component <lgl>, material_component <lgl>,
#> # material_component_details <chr>, duration <chr>, concentration <lgl>,
#> # description <chr>
dice_dat <- spells |>
select(name, level, description) |>
mutate(
dice_txt = str_extract_all(description, "\\b\\d+d\\d+\\b"),
dice_txt = map(dice_txt, unique)
) |>
unnest_longer(
col = "dice_txt",
values_to = "dice_txt",
indices_to = "position"
) |>
mutate(
dice_num = dice_txt |> str_extract("\\d+(?=d)") |> as.numeric(),
dice_die = dice_txt |> str_extract("(?<=d)\\d+") |> as.numeric(),
dice_val = dice_num * (dice_die + 1)/2,
dice_txt = factor(dice_txt) |> fct_reorder(dice_val)
)
print(dice_dat)
#> # A tibble: 236 × 8
#> name level description dice_txt position dice_num dice_die dice_val
#> <chr> <dbl> <chr> <fct> <int> <dbl> <dbl> <dbl>
#> 1 Acid Splash 0 "You creat… 1d6 1 1 6 3.5
#> 2 Acid Splash 0 "You creat… 2d6 2 2 6 7
#> 3 Acid Splash 0 "You creat… 3d6 3 3 6 10.5
#> 4 Acid Splash 0 "You creat… 4d6 4 4 6 14
#> 5 Alter Self 2 "You alter… 1d6 1 1 6 3.5
#> 6 Animate Objec… 5 "Objects a… 1d4 1 1 4 2.5
#> 7 Animate Objec… 5 "Objects a… 1d6 2 1 6 3.5
#> 8 Animate Objec… 5 "Objects a… 1d12 3 1 12 6.5
#> 9 Animate Objec… 5 "Objects a… 2d6 4 2 6 7
#> 10 Animate Objec… 5 "Objects a… 2d12 5 2 12 13
#> # ℹ 226 more rows
palette <- hcl.colors(n = 10, palette = "PuOr")
labs <- dice_dat |>
summarise(
dice_txt = first(dice_txt),
count = n(),
.by = dice_txt
)
pic <- ggplot(
data = dice_dat,
mapping = aes(
x = dice_txt,
fill = factor(level)
)
) +
geom_bar(color = "#222") +
geom_label_repel(
data = labs,
mapping = aes(
x = dice_txt,
y = count,
label = dice_txt
),
size = 3,
direction = "y",
seed = 1,
nudge_y = 4,
color = "#ccc",
fill = "#222",
arrow = NULL,
inherit.aes = FALSE
) +
scale_fill_manual(
name = "Spell level",
values = palette
) +
scale_x_discrete(
name = "Increasing average outcome \u27a1",
breaks = NULL,
expand = expansion(.05)
) +
scale_y_continuous(name = NULL) +
labs(title = "Dice rolls in D&D spell descriptions by spell level") +
theme_void() +
theme(
plot.background = element_rect(fill = "#222"),
text = element_text(color = "#ccc"),
axis.text = element_text(color = "#ccc"),
axis.title = element_text(color = "#ccc"),
plot.margin = unit(c(1, 1, 1, 1), units = "cm"),
legend.position = "inside",
legend.position.inside = c(.3, .825),
legend.direction = "horizontal",
legend.title.position = "top",
legend.byrow = TRUE
)
plot(pic)