--- title: "Palmer Penguins with xkcd" author: "ToledoEM" date: "`r Sys.Date()`" output: rmarkdown::html_vignette: code_folding: hide vignette: > %\VignetteIndexEntry{Palmer Penguins with xkcd} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set( echo = TRUE, message = FALSE, warning = FALSE, fig.width = 8, fig.height = 5 ) ``` ## Overview This vignette demonstrates every function in the **xkcd** package using the [Palmer Penguins](https://allisonhorst.github.io/palmerpenguins/) dataset — a fun alternative to `mtcars` featuring size measurements of three penguin species observed on islands near Palmer Station, Antarctica. ```{r libraries} library(xkcd) library(dplyr) library(palmerpenguins) # Drop rows with missing values for cleaner plots penguins <- na.omit(penguins) ``` > **Reproducibility note:** All plots use `set.seed()` because xkcd lines are > drawn with random jitter — fix the seed to get the same figure every time. --- ## 1. `theme_xkcd()` — The XKCD Look `theme_xkcd()` applies a hand-drawn feel to any ggplot2 chart: no grid lines, black axis ticks, and — if the xkcd font is installed — the iconic comic font. ```{r theme_xkcd} set.seed(123456) ggplot(penguins, aes(flipper_length_mm, body_mass_g, colour = species)) + geom_point(size = 2, alpha = 0.7) + labs( title = "Flipper length vs body mass", x = "Flipper length mm", y = "Body mass g", colour = "Species" ) + theme_xkcd() ``` `theme_xkcd()` returns a standard ggplot2 `theme` object, so you can layer additional `theme()` calls on top of it. --- ## 2. `xkcdaxis()` — Hand-Drawn Axes `xkcdaxis()` replaces the default ggplot2 axis lines with wobbly, hand-drawn ones. Pass the x and y ranges of your data and it adds jittered axis arrows, a clipped coordinate system, and calls `theme_xkcd()` internally. ```{r xkcdaxis} xrange <- range(penguins$bill_length_mm) yrange <- range(penguins$bill_depth_mm) set.seed(7) ggplot() + geom_point( aes(bill_length_mm, bill_depth_mm, colour = species), data = penguins, size = 2, alpha = 0.8 ) + xkcdaxis(xrange, yrange) + labs( x = "Bill length mm", y = "Bill depth mm", colour = "Species", title = "Bill dimensions by species" ) ``` `xkcdaxis()` returns a list of ggplot2 layers — just `+` it onto any plot. --- ## 3. `geom_xkcdpath()` — Wobbly Lines and Segments `geom_xkcdpath()` is the low-level building block used by the other functions. It draws jittered, Bezier-smoothed line **segments** (using `x`, `y`, `xend`, `yend`) or fuzzy **circles** (using `x`, `y`, `diameter`). ### 3a. Annotating a trend with a segment ```{r geom_xkcdpath_segment} # Gentoo penguins — add an arrow-like segment pointing at the cluster xrange <- range(penguins$flipper_length_mm) yrange <- range(penguins$body_mass_g) arrow_df <- data.frame( x = 228, y = 4200, xend = 220, yend = 5300 ) set.seed(99) ggplot() + geom_point( aes(flipper_length_mm, body_mass_g, colour = species), data = penguins, size = 2, alpha = 0.7 ) + geom_xkcdpath( mapping = aes(x = x, y = y, xend = xend, yend = yend), data = arrow_df, linewidth = 1, xjitteramount = 1, yjitteramount = 60, mask = TRUE ) + annotate("text", x = 230, y = 4100, label = "Big Gentoos!", family = "xkcd", size = 5) + xkcdaxis(xrange, yrange) + labs(x = "Flipper length mm", y = "Body mass g", colour = "Species") ``` ### 3b. Drawing a circle Use `diameter` instead of `xend`/`yend` to draw a fuzzy circle. The `ratioxy` aesthetic keeps the circle from looking like an ellipse when x and y have different scales. ```{r geom_xkcdpath_circle} xrange <- c(160, 240) yrange <- c(2500, 6500) ratioxy <- diff(xrange) / diff(yrange) # diameter is in x-axis units; ratioxy corrects for the different x/y scales # so the circle appears round on screen circle_df <- data.frame(x = 200, y = 4000, diameter = 20) set.seed(5) ggplot() + geom_point( aes(flipper_length_mm, body_mass_g, colour = species), data = penguins, size = 2, alpha = 0.7 ) + geom_xkcdpath( aes(x = x, y = y, diameter = diameter), data = circle_df, linewidth = 1.2, colour = "firebrick", ratioxy = ratioxy, mask = FALSE ) + annotate("text", x = 200, y = 3600, label = "Overlap zone", family = "xkcd", size = 4, colour = "firebrick") + xkcdaxis(xrange, yrange) + labs(x = "Flipper length mm", y = "Body mass g", colour = "Species", title = "A fuzzy circle highlights the overlap zone") ``` --- ## 4. `xkcdrect()` — Fuzzy Rectangles `xkcdrect()` draws filled rectangles with wobbly hand-drawn borders, perfect for bar-chart-style plots. Required aesthetics: `xmin`, `xmax`, `ymin`, `ymax`. ```{r xkcdrect} # Average body mass per species as a bar chart using fuzzy rectangles avg_mass <- penguins |> group_by(species) |> summarise(mean_mass = mean(body_mass_g), .groups = "drop") |> mutate( xmin = as.numeric(species) - 0.35, xmax = as.numeric(species) + 0.35, ymin = 0, ymax = mean_mass ) xrange <- c(0.5, 3.5) yrange <- c(0, max(avg_mass$mean_mass) + 300) set.seed(11) ggplot() + xkcdrect( aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax), data = avg_mass, fill = c("#f28e2b", "#4e79a7", "#59a14f"), colour = "black", linewidth = 1 ) + annotate("text", x = 1:3, y = avg_mass$mean_mass + 150, label = levels(penguins$species), family = "xkcd", size = 5) + xkcdaxis(xrange, yrange) + scale_x_continuous(breaks = 1:3, labels = levels(penguins$species)) + labs( x = "Species", y = "Mean body mass g", title = "Average penguin weight" ) ``` --- ## 5. `xkcdman()` — Stick Figures `xkcdman()` draws a customisable stick figure. Every body part (spine, arms, legs, neck) is controlled by an angle. The key parameters are: | Aesthetic | Meaning | |-----------|---------| | `x`, `y` | Head position | | `scale` | Overall size | | `ratioxy` | x/y scale ratio (keeps figure from being distorted) | | `angleofspine` | Spine angle (−π/2 = upright) | | `anglerighthumerus` / `anglelefthumerus` | Upper arm angles | | `anglerightradius` / `angleleftradius` | Lower arm angles | | `anglerightleg` / `angleleftleg` | Leg angles | | `angleofneck` | Neck angle | ### 5a. Two penguin researchers The key to well-proportioned stick figures is `scale` and `ratioxy`. `scale` should be ~10–15% of `diff(yrange)` so the figure is visible. `ratioxy = diff(xrange) / diff(yrange)` corrects for axis distortion so limbs don't look stretched. Place figures **above** the data cloud, inside the plot limits, and expand `yrange` to make room. ```{r xkcdman_basic} xrange <- range(penguins$flipper_length_mm) # Expand y upward to give room for figures above the data yrange <- c(min(penguins$body_mass_g) - 200, max(penguins$body_mass_g) + 1200) ratioxy <- diff(xrange) / diff(yrange) # scale ≈ 10% of yrange so figures are clearly visible scale_val <- diff(yrange) * 0.10 dataman <- data.frame( x = c(178, 228), y = c(max(penguins$body_mass_g) + 500, min(penguins$body_mass_g) + 1500), scale = scale_val, 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(22) ggplot() + geom_point( aes(flipper_length_mm, body_mass_g, colour = species), data = penguins, size = 2, alpha = 0.7 ) + xkcdaxis(xrange, yrange) + xkcdman(mapping, dataman) + annotate("text", x = 174, y = max(penguins$body_mass_g) + 1050, label = "Small\nones!", family = "xkcd", size = 4) + annotate("text", x = 234, y = max(penguins$body_mass_g) - 1050, label = "Big\nones!", family = "xkcd", size = 4) + labs(x = "Flipper length mm", y = "Body mass g", colour = "Species", title = "Two researchers discuss the data") ``` ### 5b. One stick figure per island One figure stands at the centroid of each island's data. `runif()` gives each figure a slightly different pose. ```{r xkcdman_per_island} island_means <- penguins |> group_by(island) |> summarise( mx = mean(flipper_length_mm), my = mean(body_mass_g), .groups = "drop" ) xrange <- range(penguins$flipper_length_mm) yrange <- c(min(penguins$body_mass_g) - 200, max(penguins$body_mass_g) + 1400) ratioxy <- diff(xrange) / diff(yrange) scale_val <- diff(yrange) * 0.10 set.seed(33) dataman <- data.frame( x = island_means$mx, y = island_means$my + 800, scale = scale_val, ratioxy = ratioxy, angleofspine = -pi / 2, anglerighthumerus = runif(3, -pi / 6 - pi / 10, -pi / 6 + pi / 10), anglelefthumerus = runif(3, -pi / 2 - pi / 6 - pi / 10, -pi / 2 - pi / 6 + pi / 10), anglerightradius = runif(3, pi / 5 - pi / 10, pi / 5 + pi / 10), angleleftradius = runif(3, pi / 5 - pi / 10, pi / 5 + pi / 10), 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(33) ggplot() + geom_point( aes(flipper_length_mm, body_mass_g, colour = island), data = penguins, size = 2, alpha = 0.7 ) + xkcdaxis(xrange, yrange) + xkcdman(mapping, dataman) + annotate("text", x = island_means$mx, y = island_means$my + 1350, label = island_means$island, family = "xkcd", size = 4) + labs(x = "Flipper length mm", y = "Body mass g", colour = "Island", title = "One researcher per island",caption = "Trogersen and Dream Island overlap!!") ``` --- ## 6. Putting It All Together A single plot that uses every function: `theme_xkcd()`, `xkcdaxis()`, `xkcdrect()`, `xkcdman()`, and `geom_xkcdpath()`. ```{r full_example} # Yearly penguin count as fuzzy bars + a stick figure + annotation arrow counts <- penguins |> group_by(year, species) |> summarise(n = n(), .groups = "drop") |> group_by(year) |> summarise(total = sum(n), .groups = "drop") |> mutate( xmin = year - 0.35, xmax = year + 0.35, ymin = 0, ymax = total ) xrange <- c(2006.5, 2009.5) # Expand y to give the figure room above the tallest bar yrange <- c(0, max(counts$total) + 60) ratioxy <- diff(xrange) / diff(yrange) scale_val <- diff(yrange) * 0.12 # ~12% of y range = clearly visible # Figure stands above the 2009 bar (tallest), pointing left dataman <- data.frame( x = 2009, y = min(counts$total) - 30, scale = scale_val, ratioxy = ratioxy, angleofspine = -pi / 2, anglerighthumerus = -pi / 6, anglelefthumerus = -pi / 2 - pi / 6, anglerightradius = pi / 5, angleleftradius = pi / 5, anglerightleg = 3 * pi / 2 - pi / 12, angleleftleg = 3 * pi / 2 + pi / 12, angleofneck = -pi / 2 ) man_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 ) # Arrow from annotation label to 2009 bar top arrow_df <- data.frame( x = 2007.8, y = max(counts$total) + 48, xend = 2008.6, yend = max(counts$total) + 10 ) set.seed(55) ggplot() + xkcdrect( aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax), data = counts, fill = "#aecbfa", colour = "black", linewidth = 1 ) + geom_xkcdpath( aes(x = x, y = y, xend = xend, yend = yend), data = arrow_df, linewidth = 1, xjitteramount = 0.03, yjitteramount = 3, mask = TRUE ) + xkcdman(man_mapping, dataman,color="white") + xkcdaxis(xrange, yrange) + annotate("text", x = 2007.5, y = max(counts$total) + 48, label = "More penguins\nevery year!", family = "xkcd", size = 4) + annotate("text", x = counts$year, y = counts$total + 8, label = counts$total, family = "xkcd", size = 5) + scale_x_continuous(breaks = c(2007, 2008, 2009)) + labs(x = "Year", y = "Penguins observed", title = "Palmer penguins surveyed per year") ``` --- ## Function Quick Reference | Function | What it does | |---|---| | `theme_xkcd()` | Applies XKCD theme (no grid, comic font if available) | | `xkcdaxis(xrange, yrange)` | Draws wobbly hand-drawn axes | | `geom_xkcdpath()` | Draws jittered segments or circles | | `xkcdrect()` | Draws fuzzy filled rectangles | | `xkcdman()` | Draws a customisable stick figure | All functions are ggplot2-compatible and can be combined freely with standard `geom_*`, `annotate()`, `scale_*`, and `facet_*` calls.