---
title: "ggnext: every function, plot and option"
subtitle: "A worked reference for the whole package"
author: "Generated with ggnext"
date: "`r Sys.Date()`"
output:
  html_document:
    toc: true
    toc_float: true
    toc_depth: 2
    theme: null
    css: null
    self_contained: true
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
  echo = TRUE, message = FALSE, warning = FALSE, comment = "#>"
)
library(ggnext)
set.seed(1)
```

```{css, echo = FALSE}
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica,
       Arial, sans-serif; max-width: 1000px; margin: 0 auto; padding: 0 24px;
       color: #16181D; background: #fff; line-height: 1.6; }
h1 { border-bottom: 2px solid #E3E5EA; padding-bottom: 8px; margin-top: 48px; }
h2 { margin-top: 36px; color: #1A6E55; }
h3 { margin-top: 26px; font-size: 17px; }
pre { background: #F5F6F8; border: 1px solid #E3E5EA; border-radius: 8px;
      padding: 12px 14px; font-size: 13px; }
svg { display: block; margin: 14px 0 22px; border: 1px solid #E3E5EA;
      border-radius: 8px; max-width: 100%; height: auto; }
table { border-collapse: collapse; font-size: 14px; margin: 12px 0; }
th, td { border-bottom: 1px solid #E3E5EA; padding: 5px 10px; text-align: left; }
.ok { color: #1A6E55; font-weight: 600; }
```

This document exercises **every exported function** of ggnext and **every
option** that changes what is drawn. It doubles as a regression check: if
any layer, scale, coordinate system or theme setting breaks, this document
fails to knit.

Writing a plot object in a chunk renders it inline — ggnext registers a
`knit_print()` method, so no `results='asis'` boilerplate is needed.

```{r}
packageVersion("ggnext")
length(getNamespaceExports("ggnext"))
```

# 1. The grammar

## 1.1 Minimal plot

A plot is data, an aesthetic mapping, and at least one layer.

```{r}
ggnext(cars, aes(speed, dist)) + geom_point()
```

## 1.2 Plots are immutable values

`+` returns a *new* plot, so a partial specification is a reusable template.

```{r}
base <- ggnext(iris, aes(Sepal.Length, Sepal.Width))
length(base@layers)            # still empty
length((base + geom_point())@layers)
length(base@layers)            # base is unchanged
```

## 1.3 `aes()` — mapping vs setting

```{r}
aes(speed, dist, color = gear)
```

A constant inside `aes()` that names a real colour is honoured literally
rather than being treated as a one-level category.

```{r}
ggnext(cars, aes(speed, dist, color = "steelblue")) + geom_point(size = 4)
```

## 1.4 Layer-level data and mapping

A layer can override both, which is how you annotate one plot with a
second dataset.

```{r}
means <- aggregate(Sepal.Length ~ Species, iris, mean)

ggnext(iris, aes(Species, Sepal.Length)) +
  geom_jitter(alpha = 0.3) +
  geom_point(aes(Species, Sepal.Length), data = means,
             color = "#C1462F", size = 7)
```

## 1.5 Output size

```{r}
p_wide <- ggnext(cars, aes(speed, dist), width = 900, height = 260) +
  geom_point()
p_wide
```

`plot_size()` does the same thing after the fact.

```{r}
b <- ggnext:::build_geometry(ggnext(cars, aes(speed, dist)) +
                                geom_point() + plot_size(800, 600))
c(width = b$width, height = b$height)
```

# 2. Essential geoms

## 2.1 geom_point

```{r}
ggnext(cars, aes(speed, dist)) + geom_point(color = "#2B6BE0", size = 4,
                                             alpha = 0.7)
```

Size and colour can be mapped instead of set:

```{r}
ggnext(iris, aes(Sepal.Length, Sepal.Width,
                  color = Species, size = Petal.Length)) +
  geom_point(alpha = 0.75)
```

## 2.2 geom_jitter

```{r}
ggnext(iris, aes(Species, Sepal.Width, color = Species)) +
  geom_jitter(alpha = 0.7) +
  theme(legend_position = "none")
```

Jitter is seeded, so the same plot renders identically every time — and it
restores the caller's random stream:

```{r}
set.seed(42); before <- .Random.seed
invisible(render(ggnext(iris, aes(Species, Sepal.Width)) + geom_jitter()))
identical(.Random.seed, before)
```

## 2.3 geom_line, geom_path, geom_step

```{r}
ts <- data.frame(
  t = rep(1:12, 2),
  v = c(cumsum(rnorm(12, 2)), cumsum(rnorm(12, 1))),
  g = rep(c("A", "B"), each = 12)
)
ggnext(ts, aes(t, v, color = g)) + geom_line(linewidth = 2) + theme_minimal()
```

`geom_path()` follows data order rather than x order; `geom_step()` holds
each value until the next observation.

```{r}
steps <- data.frame(t = 1:8, v = c(2, 2, 3, 3, 5, 4, 4, 6))
ggnext(steps, aes(t, v)) + geom_step(color = "#12A594", linewidth = 2) +
  geom_point() + theme_minimal()
```

Dashed lines via `dash` (an SVG dash pattern):

```{r}
ggnext(steps, aes(t, v)) + geom_line(dash = "6,4") + theme_minimal()
```

## 2.4 geom_area and geom_ribbon

```{r}
ggnext(data.frame(t = 1:10, v = c(2, 4, 3, 6, 8, 7, 9, 8, 11, 13)),
        aes(t, v)) +
  geom_area(alpha = 0.45) + theme_minimal()
```

```{r}
band <- data.frame(t = 1:12, lo = (1:12) * 0.8, hi = (1:12) * 1.4)
ggnext(band, aes(t, ymin = lo, ymax = hi)) +
  geom_ribbon(alpha = 0.4) + theme_minimal()
```

## 2.5 geom_segment and geom_dumbbell

```{r}
seg <- data.frame(x = 1:4, y = c(2, 4, 3, 5),
                  xe = (1:4) + 0.7, ye = c(4, 6, 2, 7))
ggnext(seg, aes(x, y, xend = xe, yend = ye)) +
  geom_segment(linewidth = 2) + theme_minimal()
```

```{r}
db <- data.frame(g = c("North", "South", "East", "West"),
                 before = c(12, 18, 9, 14), after = c(19, 21, 15, 13))
ggnext(db, aes(before, xend = after, y = g)) +
  geom_dumbbell() + theme_minimal()
```

## 2.6 Reference lines

`geom_hline()` and `geom_vline()` span the panel and ignore the plot's
`aes()`, so they never disturb the data mapping.

```{r}
ggnext(cars, aes(speed, dist)) +
  geom_point(alpha = 0.6) +
  geom_hline(mean(cars$dist), dash = "5,4", color = "#C1462F") +
  geom_vline(mean(cars$speed), dash = "5,4", color = "#C1462F") +
  theme_minimal()
```

## 2.7 geom_text

```{r}
lab <- data.frame(x = c(1, 2, 3), y = c(3, 1, 2),
                  l = c("alpha", "beta", "gamma"))
ggnext(lab, aes(x, y, label = l)) +
  geom_point(size = 6, alpha = 0.25) + geom_text() + theme_minimal()
```

## 2.8 geom_tile

```{r}
grid <- expand.grid(x = 1:10, y = 1:8)
grid$z <- as.vector(outer(1:10, 1:8, function(a, b) sin(a / 2) + cos(b / 2)))
ggnext(grid, aes(x, y, color = z)) + geom_tile() + theme_minimal()
```

# 3. Distributions

## 3.1 geom_bar and geom_col

`geom_bar()` counts rows; `geom_col()` takes the height from `y`.

```{r}
cnt <- data.frame(g = c("a", "a", "a", "b", "b", "c"))
ggnext(cnt, aes(g)) + geom_bar() + theme_minimal()
```

```{r}
vals <- data.frame(g = c("alpha", "beta", "gamma", "delta"),
                   v = c(12, 27, 19, 8))
ggnext(vals, aes(g, v, color = g)) + geom_col() +
  theme(legend_position = "none")
```

## 3.2 Position adjustments

```{r}
grp <- data.frame(g = rep(c("Q1", "Q2", "Q3"), each = 3),
                  grp = rep(c("a", "b", "c"), 3),
                  v = c(4, 6, 3, 7, 4, 5, 5, 8, 2))
ggnext(grp, aes(g, v, color = grp)) + geom_col(position = "stack") +
  labs(title = 'position = "stack" (default)') + theme_minimal()
```

```{r}
ggnext(grp, aes(g, v, color = grp)) + geom_col(position = "dodge") +
  labs(title = 'position = "dodge"') + theme_minimal()
```

```{r}
ggnext(grp, aes(g, v, color = grp)) + geom_col(position = "fill") +
  labs(title = 'position = "fill" (proportions)') + theme_minimal()
```

Stacked totals train the axis correctly — the domain covers the stack, not
the tallest single bar:

```{r}
stacked <- ggnext:::build_geometry(
  ggnext(grp, aes(g, v, color = grp)) + geom_col(position = "stack")
)
stacked$panels[[1]]$y$domain
```

## 3.3 geom_histogram

`bins = n` yields exactly `n` bins, and counts sum to the row count.

```{r}
ggnext(cars, aes(speed)) + geom_histogram(bins = 8) + theme_minimal()
```

```{r}
h <- plot_data(ggnext(cars, aes(speed)) + geom_histogram(bins = 5))
h
c(bins = nrow(h), total = sum(h$y), rows = nrow(cars))
```

`binwidth` overrides `bins`:

```{r}
ggnext(cars, aes(speed)) + geom_histogram(binwidth = 5) + theme_minimal()
```

## 3.4 geom_density

```{r}
ggnext(iris, aes(Sepal.Length, color = Species)) +
  geom_density(alpha = 0.45) + theme_minimal()
```

The estimate integrates to 1:

```{r}
dd <- compute_stat(stat_density(), list(x = rnorm(500),
                                        group = rep("all", 500)))
round(sum(dd$y) * diff(dd$x[1:2]), 3)
```

## 3.5 geom_boxplot

```{r}
ggnext(iris, aes(Species, Sepal.Length, color = Species)) +
  geom_boxplot() + theme(legend_position = "none")
```

Quartiles match `stats::quantile()`:

```{r}
bx <- plot_data(ggnext(iris, aes(Species, Sepal.Length)) + geom_boxplot())
setosa <- iris$Sepal.Length[iris$Species == "setosa"]
c(computed = bx$middle[1], base_r = median(setosa))
```

## 3.6 geom_violin

```{r}
ggnext(iris, aes(Species, Sepal.Width, color = Species)) +
  geom_violin() + theme(legend_position = "none")
```

## 3.7 Layered distribution view

Layers draw in the order added: shape underneath, summary, raw data on top.

```{r}
ggnext(iris, aes(Species, Sepal.Length, color = Species)) +
  geom_violin(alpha = 0.25) +
  geom_boxplot() +
  geom_jitter(alpha = 0.4) +
  theme(legend_position = "none")
```

## 3.8 geom_ridgeline

```{r}
ggnext(iris, aes(Sepal.Length, Species)) +
  geom_ridgeline() + theme_minimal()
```

```{r}
ggnext(iris, aes(Sepal.Length, Species)) +
  geom_ridgeline(scale = 3, alpha = 0.55) +
  labs(title = "scale = 3 makes ridges overlap") + theme_minimal()
```

## 3.9 geom_smooth

```{r}
ggnext(cars, aes(speed, dist)) +
  geom_point(alpha = 0.6) + geom_smooth(method = "lm") + theme_minimal()
```

The linear fit matches `stats::lm()`:

```{r}
sm <- compute_stat(stat_smooth(method = "lm"),
                   list(x = cars$speed, y = cars$dist,
                        group = rep("all", nrow(cars))))
fit <- lm(dist ~ speed, cars)
max(abs(sm$y - unname(predict(fit, data.frame(speed = sm$x)))))
```

```{r}
ggnext(cars, aes(speed, dist)) +
  geom_point(alpha = 0.6) + geom_smooth(method = "loess", se = FALSE) +
  theme_minimal()
```

## 3.10 Error bars and point ranges

```{r}
eb <- data.frame(g = c("A", "B", "C", "D"), m = c(5, 7, 4, 8),
                 lo = c(4, 6.2, 3.1, 7.1), hi = c(6, 7.8, 4.9, 8.9))
ggnext(eb, aes(g, m, ymin = lo, ymax = hi)) +
  geom_errorbar() + theme_minimal()
```

```{r}
ggnext(eb, aes(g, m, ymin = lo, ymax = hi)) +
  geom_pointrange() + theme_minimal()
```

## 3.11 geom_waterfall

```{r}
wf <- data.frame(
  step = factor(c("Start", "Sales", "Costs", "Tax", "End"),
                levels = c("Start", "Sales", "Costs", "Tax", "End")),
  v = c(100, 45, -30, -12, 0)
)
ggnext(wf, aes(step, v)) + geom_waterfall() + theme_minimal()
```

# 4. Scales and axes

## 4.1 Axis titles, breaks, labels, padding

```{r}
ggnext(cars, aes(speed, dist)) +
  geom_point() +
  scale_x_continuous(
    name = "Speed (mph)",
    breaks = c(5, 10, 15, 20, 25),
    expand = 0
  ) +
  scale_y_continuous(
    name = "Stopping distance",
    labels = function(v) paste0(v, " ft")
  ) +
  theme_minimal()
```

`expand = 0` makes the domain exactly the data range:

```{r}
tight <- ggnext:::build_geometry(
  ggnext(cars, aes(speed, dist)) + geom_point() +
    scale_x_continuous(expand = 0)
)
rbind(domain = tight$panels[[1]]$x$domain, data = range(cars$speed))
```

## 4.2 Limits

```{r}
ggnext(cars, aes(speed, dist)) + geom_point() +
  xlim(0, 30) + ylim(0, 150) + theme_minimal()
```

`lims()` sets both at once. Limits set the *domain*, not a crop — data
outside still goes through the stats and is clipped when drawn.

```{r}
ggnext(cars, aes(speed, dist)) + geom_point() +
  lims(x = c(10, 20)) +
  geom_smooth(method = "lm") +
  labs(subtitle = "the fit still uses all 50 rows") + theme_minimal()
```

## 4.3 Transforms

```{r}
lg <- data.frame(x = 10^(1:5), y = 1:5)
ggnext(lg, aes(x, y)) + geom_point(size = 5) + scale_x_log10() +
  theme_minimal()
```

Positions are the log of the data; labels stay in original units.

```{r}
lb <- ggnext:::build_geometry(
  ggnext(lg, aes(x, y)) + geom_point() + scale_x_log10()
)
unlist(lb$panels[[1]]$x$ticks$labels)
```

```{r}
sq <- data.frame(x = c(1, 4, 9, 16, 25), y = 1:5)
ggnext(sq, aes(x, y)) + geom_point(size = 5) + scale_x_sqrt() +
  theme_minimal()
```

```{r}
ggnext(sq, aes(x, y)) + geom_point(size = 5) + scale_y_reverse() +
  labs(title = "scale_y_reverse()") + theme_minimal()
```

## 4.4 Discrete scales

Explicit level order — how you sort bars by size rather than alphabetically.

```{r}
srt <- data.frame(name = c("delta", "alpha", "gamma", "beta"),
                  value = c(8, 12, 19, 27))
srt <- srt[order(-srt$value), ]
ggnext(srt, aes(name, value)) + geom_col() +
  scale_x_discrete(limits = srt$name) + theme_minimal()
```

## 4.5 Colour scales

```{r}
ggnext(iris, aes(Sepal.Length, Sepal.Width, color = Species)) +
  geom_point(size = 3) +
  scale_color_manual(c("#2B6BE0", "#E05A2B", "#12A594")) +
  theme_minimal()
```

```{r}
ggnext(iris, aes(Sepal.Length, Sepal.Width, color = Petal.Length)) +
  geom_point(size = 3) +
  scale_color_gradient(low = "#FFF3B0", high = "#9E2A2B") +
  labs(color = "Petal length") + theme_minimal()
```

# 5. Coordinates

## 5.1 coord_flip

```{r}
ggnext(vals, aes(g, v, color = g)) + geom_col() + coord_flip() +
  theme(legend_position = "none")
```

## 5.2 coord_polar

```{r}
days <- data.frame(d = c("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"),
                   v = c(4, 7, 6, 9, 12, 15, 11))
ggnext(days, aes(d, v, color = d)) + geom_col() + coord_polar() +
  theme(legend_position = "none")
```

Donut hole, direction and start angle:

```{r}
ggnext(days, aes(d, v, color = d)) + geom_col() +
  coord_polar(inner = 0.35, direction = -1, start = pi / 4) +
  theme(legend_position = "none")
```

Categories wrap evenly around the full turn:

```{r}
pol <- ggnext:::build_geometry(
  ggnext(days, aes(d, v)) + geom_col() + coord_polar()
)
unlist(pol$panels[[1]]$polar$spokes)
```

# 6. Facets

## 6.1 facet_wrap

```{r}
ggnext(iris, aes(Sepal.Length, Sepal.Width, color = Species)) +
  geom_point() + facet_wrap(Species) +
  theme(legend_position = "none")
```

## 6.2 Grid shape and free scales

```{r}
ggnext(iris, aes(Sepal.Length, Sepal.Width, color = Species)) +
  geom_point() + facet_wrap(Species, ncol = 2, scales = "free") +
  theme(legend_position = "none")
```

Fixed scales share one domain; free scales train per panel.

```{r}
fixed <- ggnext:::build_geometry(
  ggnext(iris, aes(Sepal.Length, Sepal.Width)) + geom_point() +
    facet_wrap(Species))
free <- ggnext:::build_geometry(
  ggnext(iris, aes(Sepal.Length, Sepal.Width)) + geom_point() +
    facet_wrap(Species, scales = "free"))
rbind(
  fixed_p1 = fixed$panels[[1]]$x$domain,
  fixed_p3 = fixed$panels[[3]]$x$domain,
  free_p1  = free$panels[[1]]$x$domain,
  free_p3  = free$panels[[3]]$x$domain
)
```

## 6.3 facet_grid

```{r}
mt <- transform(mtcars, cyl = factor(cyl), am = factor(am))
ggnext(mt, aes(disp, mpg)) + geom_point() + facet_grid(am, cyl) +
  theme_minimal()
```

## 6.4 A layer without the faceting variable repeats in every panel

```{r}
ggnext(iris, aes(Sepal.Length, Sepal.Width)) +
  geom_point(alpha = 0.6) +
  geom_hline(3, color = "#C1462F", dash = "4,3") +
  facet_wrap(Species) + theme_minimal()
```

# 7. Titles, labels and themes

## 7.1 The title block

```{r}
ggnext(cars, aes(speed, dist)) +
  geom_point() +
  labs(
    title    = "Stopping distance rises with speed",
    subtitle = "1920s road tests, 50 observations",
    caption  = "Source: datasets::cars",
    tag      = "A",
    x = "Speed (mph)", y = "Distance (ft)"
  ) +
  theme_minimal()
```

`labs()` merges across repeated calls; `ggtitle()`, `xlab()`, `ylab()` are
shorthands.

```{r}
lab_plot <- ggnext(cars, aes(speed, dist)) + geom_point() +
  labs(title = "first") + labs(subtitle = "second") + xlab("X") + ylab("Y")
b <- ggnext:::build_geometry(lab_plot)
unlist(b$labels[c("title", "subtitle")])
```

## 7.2 The six presets

```{r}
theme_demo <- function(th, nm) {
  ggnext(iris, aes(Sepal.Length, Sepal.Width, color = Species)) +
    geom_point(alpha = 0.85) + labs(title = nm) + th
}
theme_demo(theme_ggnext(), "theme_ggnext()")
```

```{r}
theme_demo(theme_minimal(), "theme_minimal()")
```

```{r}
theme_demo(theme_classic(), "theme_classic()")
```

```{r}
theme_demo(theme_modern(), "theme_modern()")
```

```{r}
theme_demo(theme_dark(), "theme_dark()")
```

```{r}
theme_demo(theme_void(), "theme_void()")
```

## 7.3 Customising a theme

`base` picks the preset to start from; everything else applies over it.

```{r}
ggnext(iris, aes(Sepal.Length, Sepal.Width, color = Species)) +
  geom_point(size = 3) +
  labs(title = "Custom theme") +
  theme(
    base            = theme_minimal(),
    panel_fill      = "#FFF8F0",
    grid_major_x    = FALSE,
    grid_color      = "#E0D5C8",
    plot_title_size = 22,
    legend_position = "bottom",
    point_palette   = c("#2B6BE0", "#E05A2B", "#12A594")
  )
```

## 7.4 All 35 theme settings

```{r}
defaults <- ggnext:::THEME_DEFAULTS
data.frame(
  setting = names(defaults),
  default = vapply(defaults, function(v) {
    if (length(v) == 0) "—" else paste(format(v), collapse = ", ")
  }, character(1)),
  row.names = NULL
)
```

Individual toggles, verified against the rendered SVG:

```{r}
count_tag <- function(p, tag) {
  svg <- render(p)
  length(gregexpr(tag, svg, fixed = TRUE)[[1]]) *
    (!grepl("^\\s*$", svg)) * as.integer(grepl(tag, svg, fixed = TRUE))
}
p0 <- ggnext(cars, aes(speed, dist)) + geom_point()
data.frame(
  setting = c("grid on (default)", "grid_major_x = FALSE",
              "grid_color = ''", "axis_text_x = FALSE"),
  gridlines_or_labels = c(
    count_tag(p0, "stroke=\"#FFFFFF\""),
    count_tag(p0 + theme(grid_major_x = FALSE), "stroke=\"#FFFFFF\""),
    count_tag(p0 + theme(grid_color = ""), "stroke=\"#FFFFFF\""),
    count_tag(p0 + theme(axis_text_x = FALSE), "text-anchor=\"middle\"")
  )
)
```

## 7.5 Legends

```{r}
ggnext(iris, aes(Sepal.Length, Sepal.Width, color = Species)) +
  geom_point() + theme(legend_position = "bottom")
```

```{r}
ggnext(iris, aes(Sepal.Length, Sepal.Width, color = Species)) +
  geom_point() + theme(legend_position = "none")
```

# 8. Layout geoms

## 8.1 geom_radar

```{r}
radar <- data.frame(
  axis  = rep(c("Speed", "Power", "Range", "Cost", "Safety"), 2),
  value = c(8, 6, 7, 4, 9, 5, 9, 4, 8, 6),
  model = rep(c("A", "B"), each = 5)
)
ggnext(radar, aes(axis, value, color = model)) +
  geom_radar() + coord_polar() + theme_minimal()
```

## 8.2 geom_treemap

```{r}
tm <- data.frame(region = c("North", "South", "East", "West", "Central"),
                 revenue = c(52, 38, 27, 19, 11))
ggnext(tm, aes(size = revenue, label = region, color = region)) +
  geom_treemap() + theme(legend_position = "none")
```

## 8.3 geom_sankey

```{r}
flows <- data.frame(
  from = c("Visited", "Visited", "Signed up", "Signed up"),
  to   = c("Signed up", "Left", "Purchased", "Churned"),
  n    = c(400, 600, 150, 250)
)
ggnext(flows, aes(x = from, xend = to, y = n)) + geom_sankey()
```

## 8.4 geom_network

```{r}
net <- data.frame(from = c("A", "A", "B", "C", "D", "E", "B"),
                  to   = c("B", "C", "C", "D", "E", "A", "E"))
ggnext(net, aes(x = from, xend = to)) + geom_network()
```

## 8.5 geom_chord

```{r}
ch <- data.frame(from = c("A", "A", "B", "C"), to = c("B", "C", "C", "A"),
                 n = c(5, 3, 7, 2))
ggnext(ch, aes(x = from, xend = to, y = n)) + geom_chord()
```

## 8.6 geom_stream

```{r}
st <- data.frame(
  t = rep(1:8, 3),
  v = c(2, 4, 6, 5, 3, 2, 4, 5, 1, 3, 5, 8, 6, 4, 3, 2, 5, 4, 3, 4, 6, 7, 5, 4),
  grp = rep(c("a", "b", "c"), each = 8)
)
ggnext(st, aes(t, v, color = grp)) + geom_stream() + theme_minimal()
```

## 8.7 geom_bump

```{r}
bump <- data.frame(
  year = rep(2020:2024, 4),
  rank = c(1, 2, 3, 3, 2, 2, 1, 1, 2, 1, 3, 3, 2, 1, 3, 4, 4, 4, 4, 4),
  team = rep(c("A", "B", "C", "D"), each = 5)
)
ggnext(bump, aes(year, rank, color = team)) +
  geom_bump() + scale_y_reverse() + theme_minimal()
```

## 8.8 geom_funnel

```{r}
fn <- data.frame(
  stage = factor(c("Visits", "Signups", "Trials", "Paid"),
                 levels = c("Visits", "Signups", "Trials", "Paid")),
  n = c(10000, 3200, 1100, 420)
)
ggnext(fn, aes(stage, n, color = stage)) + geom_funnel() +
  theme(legend_position = "none")
```

## 8.9 geom_parallel

```{r}
sub <- iris[c(1, 20, 60, 80, 110, 140), ]
par_d <- data.frame(
  id  = rep(rownames(sub), 4),
  var = rep(c("SL", "SW", "PL", "PW"), each = nrow(sub)),
  val = c(sub$Sepal.Length, sub$Sepal.Width, sub$Petal.Length, sub$Petal.Width),
  sp  = rep(as.character(sub$Species), 4)
)
ggnext(par_d, aes(var, val, group = id, color = sp)) +
  geom_parallel() + theme_minimal()
```

## 8.10 geom_upset

```{r}
us <- data.frame(sets = c("A", "A&B", "B", "A&B&C", "C", "A&B",
                          "A", "B&C", "A&C", "A&B"))
ggnext(us, aes(label = sets)) + geom_upset()
```

# 9. Machine-learning geoms

## 9.1 geom_shap

```{r}
shap <- data.frame(
  feature = rep(c("age", "income", "tenure"), each = 40),
  shap    = c(rnorm(40, 0.3, 0.2), rnorm(40, -0.1, 0.3), rnorm(40, 0, 0.15)),
  value   = runif(120)
)
ggnext(shap, aes(shap, feature, color = value)) +
  geom_shap() + geom_vline(0, dash = "3,3") +
  labs(x = "SHAP value", y = NULL) + theme_minimal()
```

## 9.2 geom_partial_dependence

```{r}
pdp <- data.frame(
  x = rep(1:10, 8), id = rep(1:8, each = 10),
  pred = as.vector(sapply(1:8, function(i) (1:10) * 0.1 * i + rnorm(10, 0, .15)))
)
ggnext(pdp, aes(x, pred, group = id)) +
  geom_partial_dependence() + theme_minimal()
```

## 9.3 geom_roc

```{r}
sc <- runif(300)
roc_d <- data.frame(score = sc, truth = rbinom(300, 1, sc))
ggnext(roc_d, aes(score = score, truth = truth)) +
  geom_roc() + theme_minimal()
```

A perfect classifier gives an exact staircase:

```{r}
perfect <- compute_stat(stat_roc(), list(
  truth = c(1, 1, 0, 0), score = c(0.9, 0.8, 0.2, 0.1),
  group = rep("all", 4)
))
data.frame(fpr = perfect$x, tpr = perfect$y)
```

## 9.4 geom_calibration

```{r}
pr <- runif(400)
cal <- data.frame(pred = pr, obs = rbinom(400, 1, pr^1.3))
ggnext(cal, aes(pred, obs)) + geom_calibration() + theme_minimal()
```

## 9.5 geom_lift_gain

```{r}
s2 <- runif(250)
lg_d <- data.frame(score = s2, y = rbinom(250, 1, s2))
ggnext(lg_d, aes(score = score, truth = y)) +
  geom_lift_gain(type = "gain") + theme_minimal()
```

```{r}
ggnext(lg_d, aes(score = score, truth = y)) +
  geom_lift_gain(type = "lift") + theme_minimal()
```

## 9.6 geom_confusion_matrix

```{r}
cm <- data.frame(
  predicted = c(rep("cat", 14), rep("dog", 9), rep("bird", 6)),
  actual = c(rep("cat", 11), rep("dog", 3), rep("dog", 7),
             rep("cat", 2), rep("bird", 5), "cat")
)
ggnext(cm, aes(predicted, actual)) + geom_confusion_matrix() +
  theme(legend_position = "none")
```

## 9.7 geom_residual

```{r}
m <- lm(dist ~ speed, cars)
res <- data.frame(fitted = fitted(m), resid = resid(m))
ggnext(res, aes(fitted, resid)) + geom_residual() + theme_minimal()
```

## 9.8 geom_learning_curve

```{r}
lc <- data.frame(
  n = rep(c(50, 100, 200, 400, 800), 2),
  score = c(.72, .80, .85, .88, .90, .66, .75, .81, .85, .88),
  split = rep(c("train", "validation"), each = 5)
)
ggnext(lc, aes(n, score, color = split)) +
  geom_learning_curve() + theme_minimal()
```

## 9.9 geom_silhouette

```{r}
sil <- data.frame(
  cluster = rep(c("1", "2", "3"), each = 25),
  width = c(runif(25, .3, .9), runif(25, .1, .7), runif(25, -.1, .6))
)
ggnext(sil, aes(width, cluster, color = cluster)) +
  geom_silhouette() + theme(legend_position = "none")
```

## 9.10 geom_embedding

```{r}
emb <- data.frame(d1 = c(rnorm(40), rnorm(40, 4)),
                  d2 = c(rnorm(40), rnorm(40, 3)),
                  cluster = rep(c("a", "b"), each = 40))
ggnext(emb, aes(d1, d2, color = cluster)) +
  geom_embedding() + theme_minimal()
```

## 9.11 geom_decision_boundary

```{r}
gr <- expand.grid(x = seq(0, 1, 0.04), y = seq(0, 1, 0.04))
gr$cls <- ifelse(gr$x + gr$y > 1, "a", "b")
ggnext(gr, aes(x, y, color = cls)) +
  geom_decision_boundary() + theme_minimal()
```

## 9.12 geom_forecast_band

```{r}
fc <- data.frame(
  t = 1:12, v = c(3, 4, 4, 5, 6, 6, 7, 8, 9, 10, 11, 12),
  lo = c(rep(NA, 7), 6.5, 7, 7.5, 8, 8.5),
  hi = c(rep(NA, 7), 9.5, 11, 12.5, 14, 15.5),
  part = rep(c("actual", "forecast"), c(7, 5))
)
ggnext(fc, aes(t, v, ymin = lo, ymax = hi, group = part)) +
  geom_forecast_band() + theme_minimal()
```

# 10. Clinical geoms

## 10.1 geom_km

```{r}
km_d <- data.frame(
  t = c(rexp(60, 0.08), rexp(60, 0.14)),
  ev = rbinom(120, 1, 0.75),
  arm = rep(c("Treatment", "Control"), each = 60)
)
ggnext(km_d, aes(time = t, status = ev, color = arm)) +
  geom_km() + theme_minimal()
```

The product-limit estimate, checked by hand on four subjects — events at
t = 1, 2, 4 and a censoring at t = 3, so S(1) = 3/4, S(2) = 1/2, S(4) = 0:

```{r}
km_check <- compute_stat(stat_km(), list(
  time = c(1, 2, 3, 4), status = c(1, 1, 0, 1), group = rep("all", 4)
))
curve <- which(km_check$role == "curve")
vapply(c(1, 2, 4),
       function(tt) min(km_check$y[curve][km_check$x[curve] == tt]),
       numeric(1))
```

## 10.2 geom_cuminc

```{r}
ci <- data.frame(t = rexp(150, 0.1),
                 ev = sample(0:2, 150, replace = TRUE, prob = c(.4, .35, .25)))
ggnext(ci, aes(time = t, status = ev)) + geom_cuminc() + theme_minimal()
```

## 10.3 geom_forest

```{r}
fp <- data.frame(
  study = c("Trial A", "Trial B", "Trial C", "Trial D", "Pooled"),
  hr = c(0.82, 0.71, 0.95, 0.88, 0.83),
  lo = c(0.65, 0.52, 0.78, 0.70, 0.74),
  hi = c(1.03, 0.97, 1.16, 1.10, 0.93),
  weight = c(30, 22, 28, 20, 100)
)
ggnext(fp, aes(hr, study, ymin = lo, ymax = hi, size = weight)) +
  geom_forest() + labs(x = "Hazard ratio (95% CI)", y = NULL) +
  theme_minimal()
```

## 10.4 geom_swimmer

```{r}
sw <- data.frame(
  subject = paste0("S", 1:8),
  months = c(4, 9, 14, 6, 20, 11, 17, 7),
  response = c("PR", "CR", "CR", "SD", "PR", "SD", "CR", "PD"),
  ongoing = c(FALSE, FALSE, TRUE, FALSE, TRUE, FALSE, TRUE, FALSE)
)
ggnext(sw, aes(months, subject, color = response, label = ongoing)) +
  geom_swimmer() + labs(x = "Months", y = NULL) + theme_minimal()
```

## 10.5 geom_spider_response

```{r}
sp <- data.frame(
  month = rep(c(0, 2, 4, 6, 8), 4),
  pct = c(0, -20, -35, -40, -42, 0, 10, 25, 40, 55,
          0, -5, -10, -8, -12, 0, -30, -45, -50, -48),
  subject = rep(c("S1", "S2", "S3", "S4"), each = 5)
)
ggnext(sp, aes(month, pct, color = subject)) +
  geom_spider_response() +
  labs(x = "Month", y = "% change from baseline") + theme_minimal()
```

## 10.6 geom_waterfall_response

```{r}
wr <- data.frame(subject = paste0("S", 1:24),
                 pct = sort(runif(24, -78, 48), decreasing = TRUE))
ggnext(wr, aes(subject, pct)) + geom_waterfall_response() +
  labs(y = "% change from baseline", x = NULL) +
  theme(axis_text_x = FALSE)
```

## 10.7 geom_spaghetti

```{r}
sg <- data.frame(
  week = rep(0:5, 10), id = rep(1:10, each = 6),
  score = as.vector(sapply(1:10, function(i) 50 + i + (0:5) * 2 + rnorm(6, 0, 3)))
)
ggnext(sg, aes(week, score, group = id)) + geom_spaghetti() + theme_minimal()
```

## 10.8 geom_bland_altman

```{r}
a <- rnorm(80, 100, 12)
ba <- data.frame(method_a = a, method_b = a + rnorm(80, 2, 5))
ggnext(ba, aes(method_a, method_b)) + geom_bland_altman() + theme_minimal()
```

## 10.9 geom_dose_response

```{r}
dr <- data.frame(
  dose = rep(c(0.1, 1, 10, 100, 1000), each = 3),
  resp = c(5, 7, 6, 18, 22, 20, 52, 48, 55, 82, 79, 85, 95, 97, 93)
)
ggnext(dr, aes(dose, resp)) + geom_dose_response() + scale_x_log10() +
  theme_minimal()
```

## 10.10 geom_ae_heatmap

```{r}
ae <- expand.grid(arm = c("Placebo", "Low", "High"),
                  ae = c("Nausea", "Fatigue", "Headache", "Rash"))
ae$pct <- c(5, 12, 22, 8, 15, 26, 3, 6, 11, 2, 9, 17)
ggnext(ae, aes(arm, ae, size = pct)) + geom_ae_heatmap() +
  theme(legend_position = "none")
```

## 10.11 geom_shift

```{r}
sh <- data.frame(
  baseline = c("G0", "G0", "G1", "G1", "G2", "G0", "G1", "G0"),
  followup = c("G0", "G1", "G1", "G2", "G2", "G0", "G0", "G1")
)
ggnext(sh, aes(baseline, followup)) + geom_shift() +
  theme(legend_position = "none")
```

## 10.12 geom_consort

```{r}
cs <- data.frame(
  stage = c("Assessed for eligibility", "Randomised",
            "Received allocation", "Completed follow-up", "Analysed"),
  n = c(420, 300, 291, 276, 271)
)
ggnext(cs, aes(label = stage, size = n)) + geom_consort()
```

# 11. Interactivity and animation

Plots are static first. `render(p)` gives an SVG; `+ interact()` switches
the default target to a self-contained HTML page.

```{r}
p_int <- ggnext(iris, aes(Sepal.Length, Sepal.Width, color = Species)) +
  geom_point()

nchar(render(p_int))                       # static SVG
nchar(render(p_int + interact()))          # interactive HTML
substr(render(p_int + interact()), 1, 15)
```

Interaction flags travel in the buffer:

```{r}
opts <- ggnext:::build_geometry(
  p_int + interact(tooltip = c("Species"), zoom = FALSE, brush = TRUE)
)$interaction
unlist(opts)
```

Tooltips can be built from named data columns:

```{r}
html <- render(p_int + interact(tooltip = c("Species", "Petal.Length")))
grepl("Species: setosa", html, fixed = TRUE)
```

Both targets consume the same geometry buffer, so mark counts always agree:

```{r}
svg  <- render(p_int)
html <- render(p_int, target = "interactive")
c(
  svg_points  = length(gregexpr("<circle ", svg, fixed = TRUE)[[1]]),
  html_points = length(gregexpr("\"type\":\"circle\"", html, fixed = TRUE)[[1]]),
  data_rows   = nrow(iris)
)
```

Animation reruns the pipeline per level of the transition variable, with
axes fixed across frames:

```{r}
anim_d <- data.frame(
  x = rep(1:5, 3), y = c(1:5, (1:5) * 2, (1:5) * 3),
  step = rep(c(1, 2, 3), each = 5)
)
p_anim <- ggnext(anim_d, aes(x, y)) + geom_point(size = 5) +
  animate(step, duration = 600)
ab <- ggnext:::build_animation(p_anim, ggnext:::build_geometry(p_anim))
c(frames = length(ab$animation$frames),
  labels = paste(unlist(ab$animation$labels), collapse = ","),
  marks_in_frame_1 = length(ab$animation$frames[[1]][[1]][[1]]$marks))
```

An animated plot still renders statically:

```{r}
p_anim
```

# 12. Exact data export

`plot_data()` returns precisely the values drawn — post-stat,
post-position, post-facet, in data units.

```{r}
head(plot_data(ggnext(cars, aes(speed, dist)) + geom_point()))
```

For a stat-backed layer it reflects what was computed, not the input:

```{r}
plot_data(ggnext(iris, aes(Species, Sepal.Length)) + geom_boxplot())
```

Facets add a panel column:

```{r}
fd <- plot_data(ggnext(iris, aes(Sepal.Length, Sepal.Width)) +
                  geom_point() + facet_wrap(Species))
head(fd)
table(fd$panel)
```

Selecting layers and panels:

```{r}
multi <- ggnext(cars, aes(speed, dist)) + geom_point() + geom_smooth()
length(plot_data(multi))                    # one table per layer
nrow(plot_data(multi, layer = 1))
nrow(plot_data(multi, panel = 1))
```

Writing it out alongside the figure:

```{r}
csv <- file.path(tempdir(), "figure-data.csv")
write_plot_data(ggnext(cars, aes(speed, dist)) + geom_point(), csv)
head(read.csv(csv), 3)
```

# 13. Rendering and the logo

```{r}
p <- ggnext(cars, aes(speed, dist)) + geom_point()
svg_file  <- file.path(tempdir(), "plot.svg")
html_file <- file.path(tempdir(), "plot.html")
render(p, file = svg_file)
render(p + interact(), file = html_file)
file.exists(c(svg_file, html_file))
```

`render()` output prints as a summary rather than dumping markup:

```{r}
render(p)
```

The hex sticker is drawn by the package's own SVG writer:

```{r results = "asis"}
cat(ggnext_logo(width = 260))
```

```{r results = "asis"}
cat(ggnext_logo(width = 260, style = "monogram"))
```

# 14. Extending the package

A geom is an S7 subclass plus one `build_marks()` method returning
primitives in normalized panel coordinates. Both renderers consume those
primitives, so a new geom needs no renderer changes.

```{r}
GeomCross <- S7::new_class("GeomCross", parent = Geom,
  constructor = function() {
    S7::new_object(Geom(name = "cross",
                        default_params = list(size = 6, alpha = 1,
                                              color = "#C1462F")))
  }
)

S7::method(build_marks, GeomCross) <- function(geom, scaled) {
  unlist(lapply(seq_along(scaled$x), function(i) {
    r <- scaled$size[[i]] / 400
    list(
      ggnext:::mk_line(c(scaled$x[[i]] - r, scaled$x[[i]] + r),
                        rep(scaled$y[[i]], 2), scaled$color[[i]], width = 2),
      ggnext:::mk_line(rep(scaled$x[[i]], 2),
                        c(scaled$y[[i]] - r, scaled$y[[i]] + r),
                        scaled$color[[i]], width = 2)
    )
  }), recursive = FALSE)
}

geom_cross <- function(mapping = NULL, data = NULL, ...) {
  ggnext:::layer_new(GeomCross(), stat_identity(), mapping, data, list(...))
}

ggnext(cars, aes(speed, dist)) + geom_cross() + theme_minimal()
```

The custom geom works with every other part of the grammar — facets,
scales, themes and the interactive target — without further work.

```{r}
ggnext(iris, aes(Sepal.Length, Sepal.Width)) +
  geom_cross(size = 4) + facet_wrap(Species) + theme_minimal()
```

# 15. Error handling

Every failure mode below raises a clear, actionable message.

```{r}
show_error <- function(expr) {
  tryCatch({ force(expr); "no error" },
           error = function(e) conditionMessage(e))
}
```

```{r}
# A name that is not a column, where R finds a function of that name.
show_error(render(ggnext(mtcars, aes(disp, hp, color = class)) + geom_point()))
```

```{r}
# Missing required aesthetics.
show_error(render(ggnext(cars, aes(speed)) + geom_point()))
show_error(render(ggnext(cars, aes(speed, dist)) + geom_sankey()))
```

```{r}
# No layers, no data.
show_error(render(ggnext(cars, aes(speed, dist))))
show_error(render(ggnext() + geom_point()))
```

```{r}
# Impossible scales.
show_error(render(ggnext(data.frame(x = c(0, 1, 10), y = 1:3), aes(x, y)) +
                    geom_point() + scale_x_log10()))
show_error(scale_x_continuous(limits = c(10, 1)))
```

```{r}
# Unsupported aesthetics and theme settings.
show_error(aes(x, y, shape = z))
show_error(theme(not_a_setting = 1))
show_error(ggnext(cars, aes(speed, dist)) + geom_point() + 42)
```

```{r}
# Faceting on a variable that does not exist.
show_error(render(ggnext(iris, aes(Sepal.Length, Sepal.Width)) +
                    geom_point() + facet_wrap(NotAColumn)))
```

# 16. Edge cases

```{r}
edge <- function(label, expr) {
  out <- tryCatch({ nchar(render(expr)); "rendered" },
                  error = function(e) paste("ERROR:", conditionMessage(e)))
  data.frame(case = label, result = out)
}

do.call(rbind, list(
  edge("single row",
       ggnext(data.frame(x = 1, y = 1), aes(x, y)) + geom_point()),
  edge("two rows",
       ggnext(data.frame(x = 1:2, y = 1:2), aes(x, y)) + geom_point()),
  edge("zero variance in x",
       ggnext(data.frame(x = rep(5, 10), y = rnorm(10)), aes(x, y)) +
         geom_point()),
  edge("zero variance in both",
       ggnext(data.frame(x = rep(1, 5), y = rep(2, 5)), aes(x, y)) +
         geom_point()),
  edge("NA in y",
       ggnext(data.frame(x = 1:5, y = c(1, NA, 3, NA, 5)), aes(x, y)) +
         geom_point()),
  edge("very large values",
       ggnext(data.frame(x = c(1e9, 2e9, 3e9), y = 1:3), aes(x, y)) +
         geom_point()),
  edge("very small values",
       ggnext(data.frame(x = c(1e-9, 2e-9, 3e-9), y = 1:3), aes(x, y)) +
         geom_point()),
  edge("unicode in labels",
       ggnext(cars, aes(speed, dist)) + geom_point() +
         labs(title = "éàü 中文 — dash")),
  edge("XML-special characters in labels",
       ggnext(cars, aes(speed, dist)) + geom_point() +
         labs(title = "a < b & c > d")),
  edge("single facet level",
       ggnext(iris[iris$Species == "setosa", ],
               aes(Sepal.Length, Sepal.Width)) +
         geom_point() + facet_wrap(Species)),
  edge("many categories (palette recycles)",
       ggnext(data.frame(g = letters[1:12], v = 1:12),
               aes(g, v, color = g)) + geom_col())
))
```

# 17. Session information

```{r}
sessionInfo()
```
