Skip to contents

Introduction and Main Motivation

Original from author: “Emilio Torres-Manzanera” pulled from https://github.com/cran/xkcd/

The XKCD web site is a web comic created by Randall Munroe, who described it as a web comic of romance, sarcasm, math, and language. He uses stick figures, very simple drawings composed of a few lines and curves, in his comics strips, that are available under the Creative Commons Attribution-NonCommercial 2.5 License.

Due to the popularity of Randall Munroe’s work, several questions about how to draw XKCD style graphs using different programming languages were posted in Stack Overflow. Many authors tried to replicate such style using different programming languages. For instance, there are implementations in Mathematica and Matlab using image processing distortion; a Python library focused on this style; and proposals in LaTeX.

The R language, despite being a software environment for statistical computing and graphics, lacked a specific package devoted to plot graphs in an XKCD style. To the best of our knowledge, there was not a satisfactory response to the question “How can we make xkcd style graphs in R?” published in Stack Overflow.

Fortunately, R is rich with facilities for creating and developing interesting graphics. In particular, the implementation of the grammar of graphics in R by ggplot2 allows the design of XKCD style graphs. The package xkcd presented in this paper develops several functions to plot statistical graphs in a freehand sketch style following the guidelines of the XKCD web site.

In this paper, an introduction to plot such graphs in R is detailed. The following section deals with installing xkcd fonts and saving graphs, a step that raised several questions among users. In the next section, some basic examples are explained, and the graph gallery section shows several complex plots. Finally, the article ends with a discussion concerning statistical information and creative drawing.

The XKCD Fonts

The package xkcd uses the XKCD fonts, so you must install them on your computer. An easy way to check whether these fonts are installed on the computer or not is typing the following code and comparing the results:

library(extrafont)
library(ggplot2)
if ('xkcd' %in% fonts()) {
  p <- ggplot() + geom_point(aes(x = mpg, y = wt), data = mtcars) +
    theme(text = element_text(size = 16, family = "xkcd"))
} else {
  warning("Not xkcd fonts installed!")
  p <- ggplot() + geom_point(aes(x = mpg, y = wt), data = mtcars)
}
p

  # Save initial font-check plot for README
  try({
    ggsave(filename = file.path("vignettes", "font_check.png"), plot = p, width = 6, height = 4)
  }, silent = TRUE)

Look at the labels and check the type of font that appears on them. In the case that you do not see the XKCD fonts, you should get and install them following the instructions of the next subsection.

Installing the XKCD Fonts in the Computer

If the XKCD fonts are not installed in the system, you should install them using the package extrafont. In particular, the function font_import() registers font files and assigns a family name, fonts() lists available type fonts, fonttable() lists available font families, and finally, loadfonts() registers the fonts in the Adobe font metric table with R’s PDF (portable document format) or PostScript output device.

library(extrafont)
tryCatch({
  download.file("https://github.com/ipython/xkcd-font/blob/master/xkcd.ttf?raw=true",
                dest = "xkcd.ttf", mode = "wb", timeout = 60)
}, error = function(e) {
  warning("Failed to download xkcd font. See https://github.com/ipython/xkcd-font")
})

# Create fonts directory
fonts_dir <- path.expand("~/.fonts")
if (!dir.exists(fonts_dir)) {
  dir.create(fonts_dir, recursive = TRUE)
}

# Copy font file
file.copy("xkcd.ttf", file.path(fonts_dir, "xkcd.ttf"), overwrite = TRUE)

# Import fonts
font_import(pattern = "[X/x]kcd", prompt = FALSE)
fonts()
fonttable()

# Load fonts for output
if (.Platform$OS.type != "unix") {
  # Register fonts for Windows bitmap output
  loadfonts(device = "win")
} else {
  loadfonts()
}

If you want to modify these fonts, you may use the spline font database file format (sfd), the official xkcd font created by the IPython project and distributed under a Creative Commons Attribution-NonCommercial 3.0 License, which can be opened with FontForge, an open-source font editor.

Saving the Graphs

ggsave() is the preferred function for saving a ggplot2 plot. For instance, the following instruction saves the graph as a bitmap file:

ggsave("gr1.png", p)

If you want to save this chart as PDF you should embed the fonts into the PDF file, using embed_fonts(). First, if you are running Windows, you may need to tell it where the Ghostscript program is, for embedding fonts.

ggsave("gr1.pdf", plot = p, width = 12, height = 4)
if (.Platform$OS.type != "unix") {
  ## Needed for Windows. Make sure you have the correct path
  Sys.setenv(R_GSCMD =
              "C:\\Program Files (x86)\\gs\\gs9.06\\bin\\gswin32c.exe")
}
embed_fonts("gr1.pdf")

See the development site of extrafont for additional instructions and examples of using fonts other than the standard PostScript fonts.

Installing xkcd

The package xkcd is available on CRAN. From within R, you can install it with the following instruction:

install.packages("xkcd", dependencies = TRUE)

Once the package has been installed, it can be loaded by typing:

# During local development prefer sourcing the package R/ files so vignettes
# can be rendered without installing the package. When the package is
# installed (e.g. CRAN/installed build) the code falls back to library(xkcd).
if (dir.exists("R") && file.exists("R/geom_xkcdpath.R")) {
  message("Sourcing local package R/ files for vignette (development mode)")
  files <- list.files("R", pattern = "\\.R$", full.names = TRUE)
  invisible(sapply(files, source))
} else {
  library(xkcd)
}

Automatically, it loads the packages ggplot2 and extrafont. The most relevant functions are xkcdaxis(), that plots axis in a handwritten style, xkcdman(), that draws stick figures, and xkcdrect(), that creates fuzzy rectangles. These functions are compatible with the grammar of graphics proposed by ggplot2.

From a technical point of view, lines plotted with the xkcd package are jittered with white noise and then smoothed using Bezier curves with the bezier() function from Hmisc.

Axis, Stick Figures, and Facets

In order to get a handmade drawing of the axis and an XKCD theme, a specific function based on the ggplot2 framework was created, xkcdaxis():

xrange <- range(mtcars$mpg)
yrange <- range(mtcars$wt)
set.seed(123)
p <- ggplot() + geom_point(aes(mpg, wt), data = mtcars) +
      xkcdaxis(xrange, yrange)
p

Due to the fact that white random noise is used in the picture, it is convenient to fix a seed for reproducing the same figure (set.seed()).

Maybe the most flashy function is xkcdman(), that draws a stick figure with different positions, angles, and lengths:

ightleg, angleleftleg = angleleftleg, angleofneck = angleofneck)

set.seed(123) p <- ggplot() + geom_point(aes(x = mpg, y = wt, colour = as.character(vs)), data = mtcars) + xkcdaxis(xrange, yrange) + xkcdman(mapping, dataman) p


Additionally, you may use the facet option of ggplot2 to split up your data by one or more variables and plot the subsets of data together.


``` r
p + facet_grid(. ~ vs)

Some Basic Examples

In this section, the code to plot a line graph and a bar chart is detailed. Remember to set the seed if you want to replicate your own figures.

Line chart with stick figures

volunteers <- data.frame(year = c(2007:2011),
                         number = c(56470, 56998, 59686, 61783, 64251))
xrange <- range(volunteers$year)
yrange <- range(volunteers$number)
ratioxy <- diff(xrange) / diff(yrange)

dataman <- data.frame(x = c(2008, 2010), 
                      y = c(63000, 58850),
                      scale = 1000,
                      ratioxy = ratioxy,
                      angleofspine = -pi / 2,
                      anglerighthumerus = c(-pi / 6, -pi / 6),
                      anglelefthumerus = c(-pi / 2 - pi / 6, -pi / 2 - pi / 6),
                      anglerightradius = c(pi / 5, -pi / 5),
                      angleleftradius = c(pi / 5, -pi / 5),
                      anglerightleg = 3 * pi / 2 - pi / 12,
                      angleleftleg = 3 * pi / 2 + pi / 12,
                      angleofneck = -pi / 2)

mapping <- aes(x = x, y = y, scale = scale, ratioxy = ratioxy, 
               angleofspine = angleofspine,
               anglerighthumerus = anglerighthumerus, 
               anglelefthumerus = anglelefthumerus,
               anglerightradius = anglerightradius, 
               angleleftradius = angleleftradius,
               anglerightleg = anglerightleg, 
               angleleftleg = angleleftleg, 
               angleofneck = angleofneck)

set.seed(123)
p <- ggplot() + 
  geom_smooth(mapping = aes(x = year, y = number),
              data = volunteers, method = "loess", se = FALSE) +
  xkcdaxis(xrange, yrange) +
  ylab("Volunteers at Caritas Spain") +
  xkcdman(mapping, dataman) +
  annotate("text", x = 2008.7, y = 63700,
           label = "We Need\nVolunteers!", family = "xkcd") +
  annotate("text", x = 2010.5, y = 60000,
           label = "Sure\nI can!", family = "xkcd")

p

# Save a representative plot for README
try({
  ggsave(filename = file.path("vignettes", "caritas_plot.png"), plot = p, width = 9, height = 5)
}, silent = TRUE)

One interesting property of these charts is that when xkcd lines intersect, there is a space in blank in the crossing, following XKCD style.

Mommy jittered-text example

# Adapted from a legacy example: draw jittered 'Mummy' labels inside
# time-of-day boxes, add small xkcd-style ticks and save the figure as a PNG
mommy <- read.table(sep=" ",text ="
8 100
9 0
10 0
11 0
12 0
13 0
14 100
15 100
16 500
17 420
18 75
19 50
20 100
21 40
22 0
")
names(mommy) <- c("hour","number")
data <- mommy
data$xmin <- data$hour - 0.25
data$xmax <- data$xmin + 1
data$ymin <- 0
data$ymax <- data$number
xrange <- range(8, 24)
yrange <- range(min(data$ymin) + 10 , max(data$ymax) + 200)
ratioxy <- diff(xrange)/diff(yrange)

timelabel <-  function(text,x,y) {
  te1 <- annotate("text", x=x, y = y + 65, label=text, size = 6,family ="xkcd")
  list(te1,
       geom_xkcdpath(mapping = aes(x = xbegin, y = ybegin, xend = xend, yend = yend),
                      data = data.frame(xbegin = x, ybegin = y + 50, xend = x, yend = y),
                      xjitteramount = 0.5, linewidth = 0.8, mask = FALSE))
}

n <- 1800
set.seed(123)
x <- runif(n, xrange[1],xrange[2] )
y <- runif(n, yrange[1],yrange[2] )
inside <- unlist(lapply(1:n, function(i) any(data$xmin <= x[i] & x[i] < data$xmax &
                            data$ymin <= y[i] & y[i] < data$ymax)))
x <- x[inside]
y <- y[inside]

nman <- length(x)
sizer <- round(runif(nman, 1, 10),0)
angler <- runif(nman, -10,10)

p <- ggplot() +
  geom_text(aes(x,y,label="Mummy",angle=angler,hjust=0, vjust=0),
            family="xkcd",size=sizer,alpha=0.3) +
  xkcdaxis(xrange,yrange) +
  annotate("text", x=16, y = 650,
           label="Happy Mother's day", size = 16,family ="xkcd") +
  xlab("daily schedule") +
  ylab("Number of times mothers are called on by their children") +
  timelabel("Wake up", 9, 125) + timelabel("School", 12.5, 90) +
  timelabel("Lunch", 15, 130) +
  timelabel("Homework", 18, 525) +
  timelabel("Bath", 21, 110) +
  timelabel("zzz", 23.5, 60)

print(p)

# Save a PNG into the vignette folder so it can be used in the README
out_png <- file.path("vignettes", "mommy_plot.png")
ggsave(filename = out_png, plot = p, width = 9, height = 6)

You may specify the width, the height, and colors of the bars.

This gallery provides a variety of complex charts designed to address different visualization needs.

Unemployment with Text Fill

library(zoo)
library(splancs)

mydatar <- read.table(text="
6.202
5.965
5.778
5.693
5.639
5.273
4.978
4.833
4.910
4.696
4.574
4.645
4.612
")

mydata1 <- mydatar[dim(mydatar)[1]:1,]
z <- zooreg(mydata1, end = as.yearqtr("2013-1"), frequency = 4)

mydata <- data.frame(parados = z)
mydata$year <- as.numeric(as.Date(as.yearqtr(rownames(mydata))))
mydata$label <- paste(substr(rownames(mydata), 3, 4), substr(rownames(mydata), 6, 7), sep = "")

xrange <- range(mydata$year)
yrange <- range(mydata$parados) + c(-0.3, 0.3)
ratioxy <- diff(xrange) / diff(yrange)

set.seed(123)
n <- 3200
poligono <- mydata[, c("year", "parados")]
names(poligono) <- c("x", "y")
poligono <- rbind(poligono, c(max(poligono$x), 4.4))
poligono <- rbind(poligono, c(min(poligono$x), 4.4))
points <- data.frame(x = runif(n, range(poligono$x)[1], range(poligono$x)[2]),
                     y = runif(n, range(poligono$y)[1], range(poligono$y)[2]))
kk <- inout(points, poligono)
points <- points[kk, ]
points <- rbind(points, poligono)

x <- points$x
y <- points$y
nman <- length(x)
sizer <- runif(nman, 4, 6)

n <- 2
dataman <- data.frame(
  x = c(15600, 14800),
  y = c(5.3, 5.7),
  scale = 0.2,
  ratioxy = ratioxy,
  angleofspine = runif(n, -pi / 2 - pi / 10, -pi / 2 + pi / 10),
  anglerighthumerus = runif(n, -pi / 6 - pi / 10, -pi / 6 + pi / 10),
  anglelefthumerus = runif(n, pi + pi / 6 - pi / 10, pi + pi / 6 + pi / 10),
  anglerightradius = runif(n, -pi / 4, pi / 4),
  angleleftradius = runif(n, pi - pi / 4, pi + pi / 4),
  anglerightleg = runif(n, 3 * pi / 2 + pi / 12, 3 * pi / 2 + pi / 12 + pi / 10),
  angleleftleg = runif(n, 3 * pi / 2 - pi / 12 - pi / 10, 3 * pi / 2 - pi / 12),
  angleofneck = runif(n, -pi / 2 - pi / 10, -pi / 2 + pi / 10)
)

mapping <- aes(x = x, y = y, scale = scale, ratioxy = ratioxy,
               angleofspine = angleofspine,
               anglerighthumerus = anglerighthumerus,
               anglelefthumerus = anglelefthumerus,
               anglerightradius = anglerightradius,
               angleleftradius = angleleftradius,
               anglerightleg = anglerightleg,
               angleleftleg = angleleftleg,
               angleofneck = angleofneck)

set.seed(123)
p1 <- ggplot() + 
  geom_text(aes(x = x, y = y, label = "0"), 
            data = data.frame(x = x, y = y), family = "xkcd", alpha = 0.4, size = sizer) +
  xkcdaxis(xrange, yrange) +
  ylab("Unemployed persons (millions)") + 
  xlab("Date") +
  annotate("text", x = 15250, y = 5.95, label = "Help!", family = "xkcd", size = 7) +
  geom_xkcdpath(mapping = aes(x = xbegin, y = ybegin, xend = xend, yend = yend, group = 1),
                data = data.frame(xbegin = 15600, ybegin = 5.42, xend = 15250, yend = 5.902, group = 1),
                xjitteramount = 200) + 
  theme(legend.position = "none")

p2 <- p1 + scale_x_continuous(breaks = as.numeric(mydata$year), label = mydata$label)

p2 + xkcdman(mapping, dataman)

# Grouping example: provide a `group` aesthetic to `geom_xkcdpath()` when
# drawing multiple segments/paths. Use `linewidth` to control stroke thickness.
df <- data.frame(x = c(1, 3), y = c(1, 1), xend = c(2, 4), yend = c(1.2, 0.8), group = 1)
ggplot() +
  geom_xkcdpath(aes(x = x, y = y, xend = xend, yend = yend, group = group),
                data = df, linewidth = 1, xjitteramount = 0.05, yjitteramount = 0.05) +
  xkcdaxis(c(0,5), c(0,2)) + theme_xkcd()

Economic Projections

library(reshape)

mydata <- data.frame(
  year = c(2013, 2014, 2015),
  ministerio = c(2, 2.1, 2.2),
  banco = c(1.95, 1.97, 2.05),
  fmi = c(1.96, 1.93, 1.90),
  homo = c(1.94, 1.88, 1.87)
)

mydatalong <- melt(mydata, id = "year", 
                   measure.vars = c("ministerio", "banco", "fmi", "homo"))

xrange <- c(2013, 2015)
yrange <- c(1.86, 2.21)

set.seed(123)
p <- ggplot() + 
  geom_smooth(aes(x = year, y = value, group = variable, linetype = variable), 
              data = mydatalong, color = "black", se = FALSE) + 
  theme(legend.position = "none") + 
  xkcdaxis(xrange, yrange) +
  ylab("Change in real GDP (%)") + 
  xlab("Economic Projections of several Institutes") + 
  scale_x_continuous(breaks = c(2013, 2014, 2015), labels = c(2013, 2014, 2015))

datalabel <- data.frame(
  x = 2014.95,
  y = c(mydata[mydata$year == 2015, "ministerio"],
        mydata[mydata$year == 2015, "banco"],
        mydata[mydata$year == 2015, "fmi"],
        mydata[mydata$year == 2015, "homo"]),
  label = c("Ministry of Economy", "National Bank", 
            "International Monetary Fund", "Homo Sapiens Sapiens*")
)

p2 <- p + 
  geom_text(aes(x = x, y = y, label = label), 
            data = datalabel, hjust = 1, vjust = 1, family = "xkcd", size = 3) +
  annotate("text", x = 2013.4, y = 1.852, 
           label = "*Homo Sapiens Sapiens = Doubly Wise Man", 
           family = "xkcd", size = 2.5)

p2

Regional Migration Pyramid

set.seed(130613)

resumen <- structure(list(
  tonombre = structure(c(1L, 2L, 3L, 11L, 4L, 5L, 8L, 6L, 7L, 9L, 10L, 14L, 12L, 13L, 15L), 
                      .Label = c("Andalucía", "Aragón", "Asturias", "Canarias", "Cantabria", 
                                "C-LaMancha", "CyLeón", "Cataluña", "Extremadura", "Galicia", 
                                "Baleares", "Madrid", "Murcia", "La Rioja", "Valencia"), 
                      class = "factor"),
  persons = c(2743706L, 515772L, 364410L, 399963L, 699410L, 212737L, 2847377L, 717874L, 894946L, 371502L, 942277L, 119341L, 2561918L, 493833L, 1661613L),
  frompersons = c(14266L, 3910L, 3214L, 3283L, 4371L, 1593L, 10912L, 8931L, 9566L, 3231L, 5407L, 940L, 21289L, 3202L, 9939L),
  topersons = c(10341L, 3805L, 2523L, 4039L, 3911L, 1524L, 12826L, 10897L, 7108L, 2312L, 4522L, 1066L, 26464L, 3529L, 9187L),
  llegan = c(0.38, 0.74, 0.69, 1.01, 0.56, 0.72, 0.45, 1.52, 0.79, 0.62, 0.48, 0.89, 1.03, 0.71, 0.55),
  sevan = c(0.52, 0.76, 0.88, 0.82, 0.62, 0.75, 0.38, 1.24, 1.07, 0.87, 0.57, 0.79, 0.83, 0.65, 0.60)
), class = "data.frame", row.names = c(NA, -15L))

library(reshape)
resumenlargo <- melt(resumen[, c("tonombre", "llegan", "sevan")])

oo <- order(resumen$llegan)
nombreordenados <- resumen$tonombre[oo]
resumenlargo$tonombre <- factor(resumenlargo$tonombre, levels = nombreordenados, ordered = TRUE)

xrange <- c(1, 15)
yrange <- c(-1.3, 1.6)
ratioxy <- diff(xrange) / diff(yrange)

kk <- ggplot() +
  geom_bar(aes(y = value, x = tonombre, fill = variable), 
           data = resumenlargo[resumenlargo$variable == "llegan", ], stat = "identity") +
  geom_bar(aes(y = (-1) * value, x = tonombre, fill = variable), 
           data = resumenlargo[resumenlargo$variable == "sevan", ], stat = "identity") +
  scale_y_continuous(breaks = seq(-1.2, 1.5, 0.3), labels = abs(seq(-1.2, 1.5, 0.3))) +
  ylab("Movilidad de los asalariados (% sobre asalariados residentes)") +
  coord_flip() +
  theme_xkcd() + 
  xlab("") + 
  theme(axis.ticks.y = element_blank(), axis.text.y = element_blank())

kk2 <- kk +
  geom_text(aes(x = tonombre, y = 0, label = tonombre), 
            data = resumenlargo[resumenlargo$variable == "llegan", ], family = "xkcd", size = 2)

lleganespana <- sum(resumen$topersons) * 100 / sum(resumen$persons)
sevanespana <- sum(resumen$frompersons) * 100 / sum(resumen$persons)

kk3 <- kk2 + 
  scale_fill_discrete(name = "",
                      breaks = c("llegan", "sevan"),
                      labels = c("Llegan", "Se van")) + 
  theme(legend.justification = c(0, 0), legend.position = c(0, 0))

dataman <- data.frame(
  x = 7,
  y = 1.5,
  scale = 0.35,
  ratioxy = ratioxy,
  angleofspine = runif(1, -pi / 2 - pi / 2 - pi / 10, -pi / 2 - pi / 2 + pi / 10),
  anglerighthumerus = runif(1, -pi / 2 - pi / 6 - pi / 10, -pi / 2 - pi / 6 + pi / 10),
  anglelefthumerus = runif(1, -pi / 2 - pi / 2 - pi / 10, -pi / 2 - pi / 2 + pi / 10),
  anglerightradius = runif(1, -pi / 2 - pi / 5 - pi / 10, -pi / 2 - pi / 5 + pi / 10),
  angleleftradius = runif(1, -pi / 2 - pi / 5 - pi / 10, -pi / 2 - pi / 5 + pi / 10),
  angleleftleg = runif(1, -pi / 2 + 3 * pi / 2 + pi / 12 - pi / 20, -pi / 2 + 3 * pi / 2 + pi / 12 + pi / 20),
  anglerightleg = runif(1, -pi / 2 + 3 * pi / 2 - pi / 12 - pi / 20, -pi / 2 + 3 * pi / 2 - pi / 12 + pi / 20),
  angleofneck = runif(1, -pi / 2 + 3 * pi / 2 - pi / 10, -pi / 2 + 3 * pi / 2 + pi / 10)
)

mapping <- aes(x = x, y = y, scale = scale, ratioxy = ratioxy,
               angleofspine = angleofspine,
               anglerighthumerus = anglerighthumerus,
               anglelefthumerus = anglelefthumerus,
               anglerightradius = anglerightradius,
               angleleftradius = angleleftradius,
               anglerightleg = anglerightleg,
               angleleftleg = angleleftleg,
               angleofneck = angleofneck)

p1 <- xkcdman(mapping, dataman)

kk4 <- kk3 + 
  annotate("text", x = 9.3, y = 1.3, label = "Unos vienen, otros se van", family = "xkcd") +
  annotate("text", x = 1, y = c(lleganespana, -sevanespana), 
           label = "Media de España", hjust = c(-0.11, -0.11), 
           vjust = c(-0.1, 0.1), family = "xkcd", angle = 90)

kk4 + xkcdman(mapping, dataman)

Mother’s Day Schedule

set.seed(123)

mommy <- data.frame(
  hour = c(8, 9, 10, 14, 15, 16, 17, 18, 19, 20, 21, 22),
  number = c(100, 0, 0, 100, 100, 500, 420, 75, 50, 100, 40, 0)
)

data <- mommy
data$xmin <- data$hour - 0.25
data$xmax <- data$xmin + 1
data$ymin <- 0
data$ymax <- data$number

xrange <- c(8, 24)
yrange <- c(0, 600)
ratioxy <- diff(xrange) / diff(yrange)

mapping <- aes(xmin = xmin, ymin = ymin, xmax = xmax, ymax = ymax)

p <- ggplot() + 
  xkcdrect(mapping, data, fillcolour = "pink", borderlinewidth = 1) +
  xkcdaxis(xrange, yrange) +
  annotate("text", x = 16, y = 550,
           label = "Happy Mother's day", size = 6, family = "xkcd") +
  xlab("Daily schedule") +
  ylab("Times mothers are called by children")

p

Discussion

Visualizing the data is an essential part of any data analysis, but the role graphics play to enlighten the audience means different things to different people. On the one hand, statisticians argue that graphic display needs to be clear, concise, and accurate; on the other hand, artists say that to be effective, it needs to be eye-catching, engaging, and innovative.

The book “The Visual Display of Quantitative Information” by Edward Tufte is a reference in the field of visual communication of information. Tufte coined the word “chartjunk” to refer to useless, non-informative, or information-obscuring elements of quantitative information displays and argues against using excessive decoration. Overloading your chart with XKCD elements may lead to loss of accuracy and the data visualization might become chartjunk.

In order to avoid this error, you must think about what the consumers of your graphs are looking for. Charts designed for business decisions and those designed for entertainment or education should not be the same. If you are trying to provoke or entertain, the XKCD style graphs may be suitable for your purpose and meaningful to your audience.

In summary, the package xkcd is a ggplot2 wrapper that makes graphs look like they came from an XKCD comic and it gives a satisfactory answer to the question “How can we make XKCD style graphs in R?”

Package Dependencies

The xkcd package relies on several key dependencies:

  • extrafont: Manages font installation and registration for use with R graphics
  • Hmisc: Provides Bezier curve interpolation for smoothing jittered lines
  • ggplot2: The graphics framework underlying all XKCD plotting functions
  • grid: Low-level graphics primitives for creating custom geoms
  • stats: Statistical functions for random number generation and utilities
  • rlang: Tools for tidy evaluation of aesthetic mappings

Key Functions Reference

xkcdaxis()

Plots axes in XKCD style with hand-drawn appearance.

xkcdaxis(xrange, yrange, ...)

Arguments: - xrange: Range of the X axis (vector of length 2) - yrange: Range of the Y axis (vector of length 2) - ...: Additional arguments passed to geom_xkcdpath

Returns: A list of layers including axes, coordinate system, and theme

xkcdman()

Draws a stick figure with customizable position and angles.

xkcdman(mapping, data, ...)

Arguments: - mapping: Aesthetic mapping generated by aes() with required aesthetics: x, y, scale, ratioxy, angleofspine, and angles for limbs and neck - data: Data frame containing figure parameters - ...: Additional arguments passed to geom_xkcdpath

Returns: A list of layers forming the stick figure

xkcdrect()

Draws fuzzy rectangles with hand-drawn borders.

xkcdrect(mapping, data, fillcolour = "grey90", bordercolour = "black",
         borderlinewidth = 0.5, borderxjitteramount = 0.005,
         borderyjitteramount = 0.005)

Arguments: - mapping: Aesthetic mapping with required aesthetics: xmin, xmax, ymin, ymax - data: Data frame with rectangle coordinates - fillcolour: Fill color of rectangles - bordercolour: Color of border lines - borderlinewidth: Width of border lines - borderxjitteramount: Horizontal jitter amount for borders - borderyjitteramount: Vertical jitter amount for borders

Returns: A list of layers with fill and border

geom_xkcdpath()

The core geom for drawing lines, segments, and circles in XKCD style.

geom_xkcdpath(mapping = NULL, data = NULL, stat = "identity", 
              position = "identity", ...,
              xjitteramount = NULL, yjitteramount = NULL, npoints = NULL, 
              ratioxy = NULL, bezier = FALSE, mask = FALSE, 
              na.rm = FALSE, show.legend = NA, inherit.aes = TRUE)

Key Arguments: - xjitteramount: Horizontal jitter amount for path points - yjitteramount: Vertical jitter amount for path points - npoints: Number of interpolation points - ratioxy: Ratio of x range to y range (for circles) - bezier: Apply Bezier smoothing - mask: Draw white mask underneath for hand-drawn effect - linewidth: Line stroke width (use this instead of the deprecated size aesthetic for lines) - group: Required aesthetic to properly group observations

Required aesthetics depend on geometry: - Segments: x, y, xend, yend - Circles: x, y, diameter - Paths: x, y

theme_xkcd()

Applies XKCD theme to ggplot2 plots.

Returns: A ggplot2 theme object with XKCD styling

Tips and Tricks

  1. Always use set.seed() before creating plots with XKCD elements, as the jitter is random
  2. Include group aesthetics when using geom_xkcdpath() to avoid grouping warnings
  3. Adjust ratioxy parameter when drawing circles to ensure they appear circular
  4. Use mask = TRUE for segments to create the hand-drawn outline effect
  5. Consider readability when using XKCD style for business or academic presentations
  6. Embed fonts in PDF output using extrafont::embed_fonts() for portable documents