{"spec_id":"spectrogram-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' spectrogram-basic: Spectrogram Time-Frequency Heatmap\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 87/100 | Created: 2026-09-09\n\nlibrary(ggplot2)\nlibrary(ragg)\n\nset.seed(42)\n\n# --- Theme tokens -----------------------------------------------------------\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\"\n\n# --- Data: synthetic linear chirp signal ------------------------------------\nsample_rate <- 4000\nduration <- 4\nn_samples <- sample_rate * duration\nt <- seq_len(n_samples) / sample_rate\n\nfreq_start <- 100\nfreq_end <- 800\ninstantaneous_phase <- 2 * pi * (freq_start * t + (freq_end - freq_start) * t^2 / (2 * duration))\nsignal <- sin(instantaneous_phase) + 0.15 * rnorm(n_samples)\n\n# --- Short-time Fourier transform (Hann window, 75% overlap) ---------------\nwindow_size <- 512\nhop <- 128\nhann_window <- 0.5 - 0.5 * cos(2 * pi * (0:(window_size - 1)) / (window_size - 1))\n\nn_windows <- floor((n_samples - window_size) / hop) + 1\nn_freq_bins <- window_size %/% 2 + 1\n\nfreqs <- (0:(n_freq_bins - 1)) * sample_rate / window_size\ntimes <- ((0:(n_windows - 1)) * hop + window_size / 2) / sample_rate\n\npower_db <- matrix(0, nrow = n_windows, ncol = n_freq_bins)\nfor (i in seq_len(n_windows)) {\n  start <- (i - 1) * hop + 1\n  segment <- signal[start:(start + window_size - 1)] * hann_window\n  spectrum <- fft(segment)[1:n_freq_bins]\n  power_db[i, ] <- 20 * log10(Mod(spectrum) + 1e-6)\n}\n\n# Light separable 3-tap smoothing (time then frequency) quiets salt-and-pepper\n# noise-floor speckle without blurring the ridge, which spans many bins.\nsmooth_1d <- function(v) {\n  n <- length(v)\n  c(v[1], (v[1:(n - 2)] + v[2:(n - 1)] + v[3:n]) / 3, v[n])\n}\npower_db <- t(apply(power_db, 1, smooth_1d))\npower_db <- apply(power_db, 2, smooth_1d)\n\ndynamic_range_db <- 48\npower_db <- pmax(power_db, max(power_db) - dynamic_range_db)\n\nmax_freq_display <- 1000\nfreq_display_idx <- which(freqs <= max_freq_display)\n\nspec_df <- data.frame(\n  time = rep(times, times = length(freq_display_idx)),\n  frequency = rep(freqs[freq_display_idx], each = n_windows),\n  power = as.vector(power_db[, freq_display_idx])\n)\n\n# Ridge trace: the peak-power frequency at each time step, from the real STFT\n# data (not a fitted curve) — a second ggplot2 layer beyond the raster.\nridge_df <- data.frame(\n  time = times,\n  frequency = freqs[freq_display_idx][apply(power_db[, freq_display_idx, drop = FALSE], 1, which.max)]\n)\n\noverlap_pct <- round((window_size - hop) / window_size * 100)\n\n# --- Plot ---------------------------------------------------------------\ntitle_str <- \"spectrogram-basic · r · ggplot2 · anyplot.ai\"\ntitle_fontsize <- round(12 * min(1.0, 67 / nchar(title_str)))\nsubtitle_str <- sprintf(\n  \"Hann window, %d samples · %d-sample hop (%d%% overlap) · %d dB dynamic range\",\n  window_size, hop, overlap_pct, dynamic_range_db\n)\n\np <- ggplot(spec_df, aes(x = time, y = frequency, fill = power)) +\n  geom_raster() +\n  geom_line(\n    data = ridge_df, aes(x = time, y = frequency),\n    inherit.aes = FALSE, color = INK, linewidth = 0.35, alpha = 0.55\n  ) +\n  scale_fill_gradient(\n    low = \"#009E73\", high = \"#4467A3\",\n    name = \"Power (dB)\",\n    guide = guide_colorbar(barwidth = unit(0.35, \"cm\"), barheight = unit(6, \"cm\"))\n  ) +\n  scale_x_continuous(expand = c(0, 0), breaks = seq(0, duration, 1)) +\n  scale_y_continuous(expand = c(0, 0), breaks = seq(0, max_freq_display, 200)) +\n  labs(x = \"Time (s)\", y = \"Frequency (Hz)\", title = title_str, subtitle = subtitle_str) +\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        = element_blank(),\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 = title_fontsize),\n    plot.subtitle     = element_text(color = INK_SOFT, size = 8),\n    legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT),\n    legend.text       = element_text(color = INK_SOFT, size = 8),\n    legend.title      = element_text(color = INK, size = 10),\n    plot.margin       = margin(10, 10, 10, 10)\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"}