{"spec_id":"spectrum-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' spectrum-basic: Frequency Spectrum Plot\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 88/100 | Created: 2026-09-09\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(scales)\nlibrary(ragg)\n\nset.seed(42)\n\n# --- Theme tokens -----------------------------------------------------------\nTHEME       <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG     <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                     \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\nBRAND <- IMPRINT_PALETTE[1]\n\n# --- Data --------------------------------------------------------------------\n# Synthetic audio signal: a fundamental tone at 440 Hz (concert A4) plus two\n# decaying harmonics and a higher resonance, buried in noise. FFT recovers the\n# frequency content, mirroring a real spectrum-analyzer workflow.\nn  <- 4096   # FFT size (samples)\nfs <- 8000   # sampling rate, Hz\nt  <- (0:(n - 1)) / fs\npeak_freqs <- c(440, 880, 1320, 2150)\n\nsignal <- 1.00 * sin(2 * pi * peak_freqs[1] * t) +\n          0.55 * sin(2 * pi * peak_freqs[2] * t) +\n          0.30 * sin(2 * pi * peak_freqs[3] * t) +\n          0.15 * sin(2 * pi * peak_freqs[4] * t) +\n          rnorm(n, mean = 0, sd = 0.05)\n\nspectrum <- fft(signal)\nmagnitude <- Mod(spectrum[1:(n / 2)]) * 2 / n\nfrequency <- (0:(n / 2 - 1)) * fs / n\n\ndf <- tibble::tibble(frequency = frequency, magnitude = magnitude) %>%\n  filter(frequency > 0) %>%\n  mutate(amplitude_db = 20 * log10(magnitude + 1e-6))\n\n# Noise floor: median amplitude across all bins. Used both as a reference\n# line and as the ribbon baseline so the fill emphasizes height *above* the\n# floor at each peak instead of washing every bin down to 0 dB.\nnoise_floor <- median(df$amplitude_db)\n\ndf <- df %>%\n  mutate(fill_min = pmin(amplitude_db, noise_floor),\n         fill_max = pmax(amplitude_db, noise_floor))\n\n# Callout labels for the fundamental + harmonics, using the nearest FFT bin's\n# actual recovered amplitude.\npeak_labels <- lapply(peak_freqs, function(target) {\n  idx <- which.min(abs(df$frequency - target))\n  tibble::tibble(\n    frequency    = df$frequency[idx],\n    amplitude_db = df$amplitude_db[idx],\n    label        = paste0(target, \" Hz\")\n  )\n}) %>% bind_rows()\n\n# --- Plot ---------------------------------------------------------------------\np <- ggplot(df, aes(x = frequency, y = amplitude_db)) +\n  geom_ribbon(aes(ymin = fill_min, ymax = fill_max), fill = BRAND, alpha = 0.25) +\n  geom_hline(yintercept = noise_floor, color = INK_SOFT, linewidth = 0.4, linetype = \"dashed\") +\n  geom_line(color = BRAND, linewidth = 0.7, alpha = 0.9) +\n  geom_point(\n    data = peak_labels, aes(x = frequency, y = amplitude_db),\n    color = BRAND, size = 2.5, inherit.aes = FALSE\n  ) +\n  geom_text(\n    data = peak_labels, aes(x = frequency, y = amplitude_db, label = label),\n    color = INK, size = 3, vjust = -0.9, inherit.aes = FALSE\n  ) +\n  scale_x_log10(\n    breaks = c(20, 50, 100, 200, 500, 1000, 2000, 4000),\n    labels = label_comma()\n  ) +\n  scale_y_continuous(expand = expansion(mult = c(0.05, 0.12))) +\n  labs(\n    x     = \"Frequency (Hz)\",\n    y     = \"Amplitude (dB)\",\n    title = \"spectrum-basic · r · ggplot2 · anyplot.ai\"\n  ) +\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.x = element_blank(),\n    panel.grid.minor.x = element_blank(),\n    panel.grid.minor.y = element_blank(),\n    panel.grid.major.y = element_line(color = INK, linewidth = 0.3),\n    panel.border      = element_blank(),\n    axis.line         = element_line(color = INK_SOFT),\n    axis.ticks        = element_blank(),\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  )\n\n# --- Save ----------------------------------------------------------------------\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot     = p,\n  device   = ragg::agg_png,\n  width    = 8,\n  height   = 4.5,\n  units    = \"in\",\n  dpi      = 400\n)\n"}