{"spec_id":"heatmap-chromagram","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' heatmap-chromagram: Music Chromagram (Pitch Class Distribution over Time)\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 91/100 | Created: 2026-06-24\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 chroma features for a I-vi-IV-V chord progression in C major\nn_frames    <- 80\ntime_sec    <- seq(0, 8, length.out = n_frames)\npitch_names <- c(\"C\", \"C#\", \"D\", \"D#\", \"E\", \"F\", \"F#\", \"G\", \"G#\", \"A\", \"A#\", \"B\")\n\n# Chord pitch-class indices (1-based): C=1, C#=2, D=3, ..., B=12\nchords <- list(\n    c(1, 5, 8),   # C major: C, E, G\n    c(10, 1, 5),  # A minor: A, C, E\n    c(6, 10, 1),  # F major: F, A, C\n    c(8, 12, 3)   # G major: G, B, D\n)\nchord_seq <- c(rep(1, 20), rep(2, 20), rep(3, 20), rep(4, 20))\n\n# Build 12 x n_frames energy matrix: background noise + chord energy peaks\nenergy_mat <- matrix(pmax(0, rnorm(12 * n_frames, mean = 0.05, sd = 0.04)), nrow = 12)\nfor (f in seq_len(n_frames)) {\n    active <- chords[[chord_seq[f]]]\n    energy_mat[active, f] <- pmin(1, pmax(0, rnorm(length(active), mean = 0.78, sd = 0.07)))\n}\n\n# Long-format data frame using integer frame indices for clean tile placement\ndf <- data.frame(\n    frame       = rep(seq_len(n_frames), each = 12),\n    pitch_class = factor(rep(pitch_names, times = n_frames), levels = pitch_names),\n    energy      = as.vector(energy_mat)\n)\n\n# Primary x-axis: time labels at 2-second intervals\nlabel_frames <- c(1, 21, 41, 61, 80)\nlabel_times  <- round(time_sec[label_frames])\n\n# Secondary x-axis: chord names at midpoints of each 20-frame region\nchord_midpoints <- c(10.5, 30.5, 50.5, 70.5)\nchord_labels    <- c(\"C\", \"Am\", \"F\", \"G\")\n\n# Plot\nplot_title <- \"heatmap-chromagram · r · ggplot2 · anyplot.ai\"\n\np <- ggplot(df, aes(x = frame, y = pitch_class, fill = energy)) +\n    geom_tile() +\n    # Thin dashed separators at chord-change boundaries\n    geom_vline(\n        xintercept = c(20.5, 40.5, 60.5),\n        color = INK_SOFT, linewidth = 0.4, linetype = \"dashed\"\n    ) +\n    scale_fill_gradient(\n        low    = \"#009E73\",\n        high   = \"#4467A3\",\n        name   = \"Energy\",\n        limits = c(0, 1),\n        breaks = c(0, 0.25, 0.5, 0.75, 1.0)\n    ) +\n    # Primary axis: seconds; secondary axis: chord name per region\n    scale_x_continuous(\n        breaks = label_frames,\n        labels = paste0(label_times, \"s\"),\n        expand = c(0, 0),\n        sec.axis = sec_axis(\n            ~ .,\n            breaks = chord_midpoints,\n            labels = chord_labels,\n            name   = \"Chord\"\n        )\n    ) +\n    scale_y_discrete(expand = c(0, 0)) +\n    # coord_fixed: lock panel to square so the plot fills the square canvas\n    coord_fixed(ratio = n_frames / 12) +\n    labs(\n        title = plot_title,\n        x     = \"Time (seconds)\",\n        y     = \"Pitch Class\"\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  = element_blank(),\n        panel.grid.minor  = element_blank(),\n        panel.border      = element_rect(color = INK_SOFT, fill = NA, linewidth = 0.5),\n        axis.title        = element_text(color = INK,      size = 10),\n        axis.text         = element_text(color = INK_SOFT, size = 8),\n        axis.text.x.top   = element_text(color = INK,      size = 9, face = \"bold\"),\n        axis.title.x.top  = element_text(color = INK_SOFT, size = 9),\n        axis.ticks        = element_line(color = INK_SOFT, linewidth = 0.3),\n        plot.title        = element_text(color = INK, size = 12),\n        plot.margin       = margin(12, 12, 12, 12, \"pt\"),\n        legend.background = element_rect(fill = ELEVATED_BG, color = NA),\n        legend.text       = element_text(color = INK_SOFT, size = 8),\n        legend.title      = element_text(color = INK, size = 10),\n        legend.position   = \"right\",\n        legend.key.height = unit(1.5, \"in\"),\n        legend.key.width  = unit(0.25, \"in\")\n    )\n\n# Save — square canvas: 6 x 6 in at 400 dpi = 2400 x 2400 px\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"}