Epidemic Curve (Epi Curve) — ggplot2

An epidemic curve (epi curve) is a histogram showing the number of new disease cases over time, plotted by date of symptom onset. The shape of the curve reveals the outbreak pattern: a sharp peak indicates a point source, successive waves suggest propagated transmission, and a plateau indicates continuous exposure. It is fundamental to epidemiological investigation and public health surveillance.

Epidemic Curve (Epi Curve) rendered with ggplot2

Renders

R source (ggplot2)

#' anyplot.ai
#' histogram-epidemic: Epidemic Curve (Epi Curve)
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 89/100 | Created: 2026-06-02

library(ggplot2)
library(scales)
library(ragg)

set.seed(42)

# Theme tokens (Imprint palette, theme-adaptive chrome)
THEME       <- Sys.getenv("ANYPLOT_THEME", "light")
PAGE_BG     <- if (THEME == "light") "#FAF8F1" else "#1A1A17"
ELEVATED_BG <- if (THEME == "light") "#FFFDF6" else "#242420"
INK         <- if (THEME == "light") "#1A1A17" else "#F0EFE8"
INK_SOFT    <- if (THEME == "light") "#4A4A44" else "#B8B7B0"
INK_MUTED   <- if (THEME == "light") "#6B6A63" else "#A8A79F"

IMPRINT_PALETTE <- c(
    "#009E73",  # 1 - confirmed (brand green, always first)
    "#C475FD",  # 2 - probable (lavender)
    "#4467A3"   # 3 - suspect (blue)
)

# Data: simulated two-wave influenza outbreak (90 days, Jan-Apr 2024)
n_days     <- 90
start_date <- as.Date("2024-01-15")
dates      <- seq(start_date, by = "day", length.out = n_days)
days       <- seq_len(n_days)

# Two-wave epidemic curve (Gaussian mixture)
primary   <- 200 * exp(-((days - 28)^2) / (2 * 10^2))
secondary <-  80 * exp(-((days - 58)^2) / (2 *  8^2))
lambda    <- pmax(primary + secondary, 0.5)
total     <- rpois(n_days, lambda = lambda)

# Split into case classifications
confirmed <- rbinom(n_days, size = total, prob = 0.62)
remaining <- total - confirmed
probable  <- rbinom(n_days, size = remaining, prob = 0.70)
suspect   <- remaining - probable

df <- data.frame(
    date      = rep(dates, 3),
    cases     = c(confirmed, probable, suspect),
    case_type = factor(
        rep(c("Confirmed", "Probable", "Suspect"), each = n_days),
        levels = c("Confirmed", "Probable", "Suspect")
    )
)

# Public health intervention events
events <- data.frame(
    date  = as.Date(c("2024-02-05", "2024-02-20")),
    label = c("School\nclosures", "Vaccination\ncampaign")
)

y_max <- max(tapply(df$cases, df$date, sum), na.rm = TRUE)

# Cumulative case burden for secondary y-axis
total_by_date <- tapply(df$cases, df$date, sum)
cum_df <- data.frame(
    date  = as.Date(names(total_by_date)),
    daily = as.numeric(total_by_date)
)
cum_df        <- cum_df[order(cum_df$date), ]
cum_df$cumulative <- cumsum(cum_df$daily)
cum_max      <- max(cum_df$cumulative)
scale_factor <- (y_max * 1.15) / cum_max  # scale cumulative to primary axis range

# Plot
p <- ggplot(df, aes(x = date, y = cases, fill = case_type)) +
    geom_col(width = 1, position = "stack") +
    geom_line(
        data        = cum_df,
        aes(x = date, y = cumulative * scale_factor),
        color       = INK_MUTED,
        linewidth   = 0.9,
        linetype    = "solid",
        inherit.aes = FALSE
    ) +
    geom_vline(
        data     = events,
        aes(xintercept = date),
        color    = INK_SOFT,
        linewidth = 0.7,
        linetype = "dashed"
    ) +
    geom_text(
        data        = events,
        aes(x = date, y = y_max * 0.97, label = label),
        color       = INK_MUTED,
        size        = 2.8,
        hjust       = -0.12,
        lineheight  = 0.9,
        inherit.aes = FALSE
    ) +
    scale_fill_manual(
        values = IMPRINT_PALETTE,
        name   = "Case classification"
    ) +
    scale_x_date(
        date_breaks = "2 weeks",
        date_labels = "%b %d",
        expand      = expansion(mult = c(0.01, 0.02))
    ) +
    scale_y_continuous(
        expand   = expansion(mult = c(0, 0.15)),
        labels   = label_comma(),
        sec.axis = sec_axis(
            ~ . / scale_factor,
            name   = "Cumulative cases",
            labels = label_comma()
        )
    ) +
    labs(
        x     = "Date of symptom onset",
        y     = "New cases",
        title = "histogram-epidemic · r · ggplot2 · anyplot.ai"
    ) +
    theme_minimal(base_size = 8) +
    theme(
        plot.background    = element_rect(fill = PAGE_BG,     color = PAGE_BG),
        panel.background   = element_rect(fill = PAGE_BG,     color = NA),
        panel.grid.major.y = element_line(color = INK_MUTED,  linewidth = 0.25),
        panel.grid.major.x = element_blank(),
        panel.grid.minor   = element_blank(),
        axis.title         = element_text(color = INK,        size = 10),
        axis.title.y.right = element_text(color = INK_MUTED,  size = 9),
        axis.text          = element_text(color = INK_SOFT,   size = 8),
        axis.text.x        = element_text(angle = 30,         hjust = 1),
        axis.text.y.right  = element_text(color = INK_MUTED,  size = 7),
        axis.line.x        = element_line(color = INK_SOFT,   linewidth = 0.4),
        plot.title         = element_text(color = INK,        size = 12, face = "bold"),
        legend.background  = element_rect(fill = ELEVATED_BG, color = INK_SOFT, linewidth = 0.3),
        legend.text        = element_text(color = INK_SOFT,   size = 8),
        legend.title       = element_text(color = INK,        size = 10),
        legend.position    = "top",
        legend.key.size    = unit(0.45, "cm"),
        plot.margin        = margin(t = 10, r = 15, b = 5, l = 5, unit = "pt")
    )

# Save
ggsave(
    filename = sprintf("plot-%s.png", THEME),
    plot     = p,
    device   = ragg::agg_png,
    width    = 8,
    height   = 4.5,
    units    = "in",
    dpi      = 400
)

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/histogram-epidemic/ggplot2/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "histogram-epidemic",
  "language": "r",
  "library": "ggplot2",
  "page": "https://anyplot.ai/histogram-epidemic/r/ggplot2",
  "hub": "https://anyplot.ai/histogram-epidemic",
  "code_json": "https://api.anyplot.ai/specs/histogram-epidemic/ggplot2/code",
  "spec_json": "https://api.anyplot.ai/specs/histogram-epidemic",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/histogram-epidemic/r/ggplot2/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/histogram-epidemic/r/ggplot2/plot-dark.png",
  "quality_score": 89.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Epidemic Curve (Epi Curve) on anyplot.ai.

Other implementations