{"spec_id":"nyquist-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' nyquist-basic: Nyquist Plot for Control Systems\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 90/100 | Created: 2026-06-17\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(ragg)\nlibrary(grid)\n\n# Theme tokens (Imprint palette, theme-adaptive chrome)\nTHEME       <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG     <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nELEVATED_BG <- if (THEME == \"light\") \"#FFFDF6\" else \"#242420\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nINK_MUTED   <- if (THEME == \"light\") \"#6B6A63\" else \"#A8A79F\"\n\n# Imprint palette — brand green always first\nIMPRINT_PALETTE <- c(\n  \"#009E73\",  # 1 — brand green (first series)\n  \"#C475FD\",  # 2 — lavender\n  \"#4467A3\",  # 3 — blue\n  \"#BD8233\",  # 4 — ochre\n  \"#AE3030\",  # 5 — matte red (semantic anchor: critical/error)\n  \"#2ABCCD\",  # 6 — cyan\n  \"#954477\",  # 7 — rose\n  \"#99B314\"   # 8 — lime\n)\n\n# Data: open-loop transfer function G(s) = 2 / (s*(s+1)*(s+2))\n# Phase crossover at ω = sqrt(2) ≈ 1.414 rad/s, where G(jω) = -1/3 (stable)\n# Gain crossover (|G| = 1) near ω ≈ 0.77 rad/s\nset.seed(42)\nn_pts <- 800\nomega <- exp(seq(log(0.05), log(100), length.out = n_pts))\n\nG_jw <- function(w) {\n  s <- 1i * w\n  2 / (s * (s + 1) * (s + 2))\n}\n\nGjw     <- G_jw(omega)\nre_vals <- Re(Gjw)\nim_vals <- Im(Gjw)\n\ndf <- data.frame(re = re_vals, im = im_vals, omega = omega)\n\n# Unit circle (reference: gain = 1 boundary)\ntheta       <- seq(0, 2 * pi, length.out = 300)\nunit_circle <- data.frame(x = cos(theta), y = sin(theta))\n\n# Frequency annotation points: key control-theory landmarks\n# pc = phase crossover (Im = 0, curve crosses real axis at ω = sqrt(2))\nlabel_omegas  <- c(0.3, 0.7, 1.0, sqrt(2), 2.5)\nlabel_idx     <- sapply(label_omegas, function(w) which.min(abs(omega - w)))\nlabel_df      <- data.frame(\n  re    = re_vals[label_idx],\n  im    = im_vals[label_idx],\n  label = c(\"ω=0.3\", \"ω=0.7\", \"ω=1.0\", \"ω=√2 (pc)\", \"ω=2.5\"),\n  nx    = c( 0.10,    0.10,    0.10,    -0.28,         0.20),\n  ny    = c( 0.13,    0.13,   -0.18,     0.20,          0.16)\n)\n\n# Directional arrows showing direction of increasing ω along the curve\narrow_steps <- c(185, 255, 330, 415, 515)\narrow_end   <- arrow_steps + 22\narrow_df    <- data.frame(\n  x    = re_vals[arrow_steps],\n  y    = im_vals[arrow_steps],\n  xend = re_vals[arrow_end],\n  yend = im_vals[arrow_end]\n)\n# Keep only arrows within the display window\nvisible  <- abs(arrow_df$x) < 3.2 & abs(arrow_df$y) < 3.2 &\n            abs(arrow_df$xend) < 3.2 & abs(arrow_df$yend) < 3.2\narrow_df <- arrow_df[visible, ]\n\n# Title\nplot_title <- \"nyquist-basic · r · ggplot2 · anyplot.ai\"\n\n# Plot\np <- ggplot() +\n  # Unit circle: gain = 1 reference\n  geom_path(\n    data = unit_circle, aes(x = x, y = y),\n    color = INK_MUTED, linewidth = 0.55, linetype = \"dashed\"\n  ) +\n  # Reference axes through origin\n  geom_hline(yintercept = 0, color = INK_SOFT, linewidth = 0.35) +\n  geom_vline(xintercept = 0, color = INK_SOFT, linewidth = 0.35) +\n  # Nyquist curve (brand green — first series)\n  geom_path(\n    data = df, aes(x = re, y = im),\n    color = IMPRINT_PALETTE[1], linewidth = 1.15\n  ) +\n  # Directional arrows showing increasing ω\n  geom_segment(\n    data  = arrow_df,\n    aes(x = x, y = y, xend = xend, yend = yend),\n    color = IMPRINT_PALETTE[1], linewidth = 1.15,\n    arrow = arrow(length = unit(0.22, \"cm\"), type = \"closed\", angle = 22)\n  ) +\n  # Frequency annotation dots (blue — second categorical series)\n  geom_point(\n    data = label_df, aes(x = re, y = im),\n    color = IMPRINT_PALETTE[3], size = 2.5, shape = 19\n  ) +\n  geom_text(\n    data = label_df, aes(x = re, y = im, label = label),\n    color = INK_SOFT, size = 2.7, nudge_x = label_df$nx, nudge_y = label_df$ny\n  ) +\n  # Critical point (-1, 0): matte red X (semantic anchor for instability boundary)\n  annotate(\n    \"point\", x = -1, y = 0,\n    color = IMPRINT_PALETTE[5], size = 6, shape = 4, stroke = 2.8\n  ) +\n  annotate(\n    \"text\", x = -1.0, y = -0.24, label = \"(-1, 0)\",\n    color = IMPRINT_PALETTE[5], size = 3.0, hjust = 0.5\n  ) +\n  # Axis labels and title\n  labs(x = \"Real\", y = \"Imaginary\", title = plot_title) +\n  # 1:1 aspect ratio — required so unit circle appears circular\n  coord_fixed(ratio = 1, xlim = c(-3, 1.5), ylim = c(-3, 1.5)) +\n  theme_minimal(base_size = 8) +\n  theme(\n    plot.background  = element_rect(fill = PAGE_BG, color = PAGE_BG),\n    panel.background = element_rect(fill = PAGE_BG, color = NA),\n    panel.grid.major = element_line(color = INK_MUTED, linewidth = 0.2),\n    panel.grid.minor = element_blank(),\n    panel.border     = element_rect(color = INK_MUTED, fill = NA, linewidth = 0.3),\n    axis.title       = element_text(color = INK, size = 10),\n    axis.text        = element_text(color = INK_SOFT, size = 8),\n    plot.title       = element_text(color = INK, size = 12),\n    plot.margin      = margin(20, 20, 20, 20, unit = \"pt\"),\n    legend.position  = \"none\"\n  )\n\n# Save — square canvas (2400 × 2400 px: width=6, height=6 in, dpi=400)\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot     = p,\n  device   = ragg::agg_png,\n  width    = 6,\n  height   = 6,\n  units    = \"in\",\n  dpi      = 400\n)\n"}