Phase Diagram (State Space Plot) — ggplot2

A phase diagram (or state space plot) displays the trajectory of a dynamical system by plotting a variable against its derivative (x vs dx/dt). This visualization reveals the qualitative behavior of systems including fixed points, limit cycles, stability, and oscillation patterns. It is essential for analyzing differential equations without solving them explicitly.

Phase Diagram (State Space Plot) rendered with ggplot2

Renders

R source (ggplot2)

#' anyplot.ai
#' phase-diagram: Phase Diagram (State Space Plot)
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 89/100 | Created: 2026-09-05

library(ggplot2)
library(dplyr)
library(grid)
library(ragg)

set.seed(42)

# --- Theme tokens -----------------------------------------------------------
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"
IMPRINT_PALETTE <- c(
  "#009E73", # 1 - first categorical series (brand green)
  "#C475FD", # 2 - lavender
  "#4467A3"  # 3 - blue
)

# Blend grid color toward the page background so it recedes behind the data
# (more so in dark theme, where a flat INK grid reads too bright).
mix_color <- function(c1, c2, weight) {
  rgb_mix <- col2rgb(c1) * weight + col2rgb(c2) * (1 - weight)
  rgb(rgb_mix[1], rgb_mix[2], rgb_mix[3], maxColorValue = 255)
}
GRID_COLOR <- mix_color(INK, PAGE_BG, if (THEME == "light") 0.35 else 0.22)

# --- Data: damped harmonic oscillator state space ---------------------------
# dx/dt = v ; dv/dt = -omega^2 * x - 2 * zeta * omega * v (underdamped spiral)
omega    <- 1.5
zeta     <- 0.12
dt       <- 0.02
n_steps  <- 800

integrate_trajectory <- function(x0, v0) {
  x <- numeric(n_steps + 1)
  v <- numeric(n_steps + 1)
  x[1] <- x0
  v[1] <- v0
  for (i in seq_len(n_steps)) {
    ax <- v[i]
    av <- -omega^2 * x[i] - 2 * zeta * omega * v[i]
    x_mid <- x[i] + 0.5 * dt * ax
    v_mid <- v[i] + 0.5 * dt * av
    ax_mid <- v_mid
    av_mid <- -omega^2 * x_mid - 2 * zeta * omega * v_mid
    x[i + 1] <- x[i] + dt * ax_mid
    v[i + 1] <- v[i] + dt * av_mid
  }
  tibble::tibble(x = x, dx_dt = v)
}

initial_conditions <- tibble::tibble(
  x0    = c(2.5, -2.0, 1.0),
  v0    = c(0.0, 1.5, -2.2),
  label = c("x0 = 2.5, v0 = 0.0", "x0 = -2.0, v0 = 1.5", "x0 = 1.0, v0 = -2.2")
)

# Direction arrows: a few short tangent segments sampled along the early/mid
# part of each spiral (skipped near the fixed point, where segments would be
# too short to read) so a viewer can tell which way the system evolves.
make_direction_arrows <- function(traj, label, n_arrows = 4, step_ahead = 7) {
  n <- nrow(traj)
  idx <- unique(round(seq(0.05, 0.55, length.out = n_arrows) * n))
  idx <- idx[idx >= 1 & idx + step_ahead <= n]
  tibble::tibble(
    x     = traj$x[idx],
    y     = traj$dx_dt[idx],
    xend  = traj$x[idx + step_ahead],
    yend  = traj$dx_dt[idx + step_ahead],
    label = label
  )
}

trajectory_list <- lapply(seq_len(nrow(initial_conditions)), function(i) {
  cond <- initial_conditions[i, ]
  integrate_trajectory(cond$x0, cond$v0) |> mutate(label = cond$label)
})
trajectories <- bind_rows(trajectory_list)
trajectories$label <- factor(trajectories$label, levels = initial_conditions$label)

arrows <- bind_rows(lapply(seq_len(nrow(initial_conditions)), function(i) {
  make_direction_arrows(trajectory_list[[i]], initial_conditions$label[i])
}))
arrows$label <- factor(arrows$label, levels = initial_conditions$label)

start_points <- initial_conditions |>
  transmute(x = x0, dx_dt = v0, label = factor(label, levels = initial_conditions$label))

# Square the data domain so coord_fixed's 1:1 aspect fills the square canvas
# instead of letterboxing (the raw x/y ranges aren't symmetric).
half_extent <- max(abs(trajectories$x), abs(trajectories$dx_dt)) * 1.08

# --- Theme --------------------------------------------------------------
anyplot_theme <- 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.25),
    panel.grid.minor  = element_blank(),
    axis.title        = element_text(color = INK, size = 10),
    axis.text         = element_text(color = INK_SOFT, size = 8),
    axis.line         = element_line(color = INK_SOFT),
    axis.ticks        = element_blank(),
    plot.title        = element_text(color = INK, size = 12),
    legend.background = element_rect(fill = ELEVATED_BG, color = NA),
    legend.key        = element_rect(fill = ELEVATED_BG, color = NA),
    legend.text       = element_text(color = INK_SOFT, size = 8),
    legend.title      = element_text(color = INK, size = 10),
    legend.position   = "right"
  )

# --- Plot ---------------------------------------------------------------
p <- ggplot() +
  geom_hline(yintercept = 0, color = INK_SOFT, linewidth = 0.3, alpha = 0.4) +
  geom_vline(xintercept = 0, color = INK_SOFT, linewidth = 0.3, alpha = 0.4) +
  geom_path(
    data = trajectories,
    aes(x = x, y = dx_dt, color = label),
    linewidth = 0.85, alpha = 0.85, lineend = "round"
  ) +
  geom_segment(
    data = arrows,
    aes(x = x, y = y, xend = xend, yend = yend, color = label),
    linewidth = 0.85,
    arrow = arrow(length = unit(0.2, "cm"), type = "closed", angle = 24),
    show.legend = FALSE
  ) +
  geom_point(
    data = start_points,
    aes(x = x, y = dx_dt, color = label),
    size = 3.2, shape = 21, fill = PAGE_BG, stroke = 1.2
  ) +
  geom_point(
    aes(x = 0, y = 0),
    size = 3.5, shape = 4, stroke = 1.3, color = INK
  ) +
  scale_color_manual(values = IMPRINT_PALETTE) +
  scale_x_continuous(limits = c(-half_extent, half_extent), expand = c(0, 0)) +
  scale_y_continuous(limits = c(-half_extent, half_extent), expand = c(0, 0)) +
  coord_fixed(ratio = 1) +
  labs(
    title = "phase-diagram · r · ggplot2 · anyplot.ai",
    x = "Displacement x",
    y = expression(Velocity ~ dx/dt),
    color = "Initial condition"
  ) +
  anyplot_theme

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

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/phase-diagram/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": "phase-diagram",
  "language": "r",
  "library": "ggplot2",
  "page": "https://anyplot.ai/phase-diagram/r/ggplot2",
  "hub": "https://anyplot.ai/phase-diagram",
  "code_json": "https://api.anyplot.ai/specs/phase-diagram/ggplot2/code",
  "spec_json": "https://api.anyplot.ai/specs/phase-diagram",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/phase-diagram/r/ggplot2/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/phase-diagram/r/ggplot2/plot-dark.png",
  "quality_score": 89.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Phase Diagram (State Space Plot) on anyplot.ai.

Other implementations