Andrews Curves for Multivariate Data — ggplot2

Andrews curves visualization transforms multivariate observations into smooth Fourier series curves. Each data point is represented as a continuous function where variable values become coefficients in a Fourier expansion, producing distinctive wave patterns. This technique enables visual comparison of multivariate patterns, cluster identification, and outlier detection—observations with similar values across variables produce similar curves, while outliers appear as distinctly different patterns.

Andrews Curves for Multivariate Data rendered with ggplot2

Renders

R source (ggplot2)

#' anyplot.ai
#' andrews-curves: Andrews Curves for Multivariate Data
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 89/100 | Created: 2026-09-02

library(ggplot2)
library(dplyr)
library(tidyr)
library(scales)
library(ragg)

set.seed(42)

# --- Theme tokens -------------------------------------------------------
THEME       <- Sys.getenv("ANYPLOT_THEME", "light")
PAGE_BG     <- if (THEME == "light") "#FAF8F1" else "#1A1A17"
INK         <- if (THEME == "light") "#1A1A17" else "#F0EFE8"
INK_SOFT    <- if (THEME == "light") "#4A4A44" else "#B8B7B0"
GRID_COLOR  <- scales::alpha(INK, 0.15)
IMPRINT_PALETTE <- c("#009E73", "#C475FD", "#4467A3", "#BD8233",
                     "#AE3030", "#2ABCCD", "#954477", "#99B314")

# --- Data -----------------------------------------------------------------
# Iris sepal/petal measurements, z-score normalized so no single dimension
# dominates the Fourier expansion (see specification "Notes").
measurements <- iris %>%
  select(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width) %>%
  scale() %>%
  as.matrix()

species <- iris$Species
n_vars  <- ncol(measurements)

# Andrews curve Fourier basis:
# f(t) = x1/sqrt(2) + x2 sin(t) + x3 cos(t) + x4 sin(2t) + x5 cos(2t) + ...
t_vals <- seq(-pi, pi, length.out = 200)
basis  <- matrix(0, nrow = length(t_vals), ncol = n_vars)
basis[, 1] <- 1 / sqrt(2)
for (k in 2:n_vars) {
  harmonic   <- k %/% 2
  basis[, k] <- if (k %% 2 == 0) sin(harmonic * t_vals) else cos(harmonic * t_vals)
}

curve_values <- measurements %*% t(basis) # observations x t_vals

curves_df <- as.data.frame(curve_values) %>%
  setNames(as.character(t_vals)) %>%
  mutate(obs_id = row_number(), species = species) %>%
  pivot_longer(-c(obs_id, species), names_to = "t", values_to = "f_t") %>%
  mutate(t = as.numeric(t))

# --- Plot -------------------------------------------------------------------
# facet_wrap(~species) splits the overlaid Fourier bands into per-species
# small multiples, so cluster shape is legible even where the pooled overlay
# is densest (t ~= 0) — a ggplot2-idiomatic alternative to a single hairball.
p <- ggplot(curves_df, aes(x = t, y = f_t, group = obs_id, color = species)) +
  geom_line(linewidth = 0.5, alpha = 0.3) +
  facet_wrap(~species, nrow = 1) +
  scale_color_manual(values = IMPRINT_PALETTE[1:3], name = "Species") +
  scale_x_continuous(breaks = c(-pi, -pi / 2, 0, pi / 2, pi),
                      labels = c("-π", "-π/2", "0", "π/2", "π")) +
  labs(title = "andrews-curves · r · ggplot2 · anyplot.ai",
       x = "t", y = "f(t)") +
  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  = element_line(color = GRID_COLOR, linewidth = 0.3),
    panel.grid.minor  = element_blank(),
    panel.border      = element_blank(),
    panel.spacing     = unit(1.2, "lines"),
    strip.background  = element_blank(),
    strip.text        = element_text(color = INK, size = 9, face = "bold"),
    axis.title        = element_text(color = INK, size = 10),
    axis.text         = element_text(color = INK_SOFT, size = 8),
    axis.line         = element_line(color = INK_SOFT),
    plot.title        = element_text(color = INK, size = 12),
    legend.text       = element_text(color = INK_SOFT, size = 8),
    legend.title      = element_text(color = INK, size = 10),
    legend.background = element_blank(),
    legend.key        = element_blank()
  )

# --- 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/andrews-curves/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": "andrews-curves",
  "language": "r",
  "library": "ggplot2",
  "page": "https://anyplot.ai/andrews-curves/r/ggplot2",
  "hub": "https://anyplot.ai/andrews-curves",
  "code_json": "https://api.anyplot.ai/specs/andrews-curves/ggplot2/code",
  "spec_json": "https://api.anyplot.ai/specs/andrews-curves",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/andrews-curves/r/ggplot2/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/andrews-curves/r/ggplot2/plot-dark.png",
  "quality_score": 89.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Andrews Curves for Multivariate Data on anyplot.ai.

Other implementations