Music Chromagram (Pitch Class Distribution over Time) in ggplot2 (R)

The same plot in 14 other libraries — Python: Altair, Bokeh, lets-plot, Matplotlib, Plotly, plotnine, Pygal, Seaborn; Julia: Makie.jl; JavaScript: Chart.js, D3.js, Apache ECharts, Highcharts, MUI X Charts. Compare all 15 side by side: Music Chromagram (Pitch Class Distribution over Time) in Python, R, Julia and JavaScript.

A heatmap-style visualization showing the energy distribution across the 12 pitch classes (C, C#, D, D#, E, F, F#, G, G#, A, A#, B) over time. Each column represents a time frame and each row a pitch class, with color intensity indicating the energy or magnitude at that pitch-time point. Widely used in music information retrieval to analyze harmonic content, detect chords, estimate musical key, and study tonal progressions in audio signals.

Music Chromagram (Pitch Class Distribution over Time) rendered with ggplot2

Renders

R source (ggplot2)

#' anyplot.ai
#' heatmap-chromagram: Music Chromagram (Pitch Class Distribution over Time)
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 91/100 | Created: 2026-06-24

library(ggplot2)
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"

# Data: synthetic chroma features for a I-vi-IV-V chord progression in C major
n_frames    <- 80
time_sec    <- seq(0, 8, length.out = n_frames)
pitch_names <- c("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B")

# Chord pitch-class indices (1-based): C=1, C#=2, D=3, ..., B=12
chords <- list(
    c(1, 5, 8),   # C major: C, E, G
    c(10, 1, 5),  # A minor: A, C, E
    c(6, 10, 1),  # F major: F, A, C
    c(8, 12, 3)   # G major: G, B, D
)
chord_seq <- c(rep(1, 20), rep(2, 20), rep(3, 20), rep(4, 20))

# Build 12 x n_frames energy matrix: background noise + chord energy peaks
energy_mat <- matrix(pmax(0, rnorm(12 * n_frames, mean = 0.05, sd = 0.04)), nrow = 12)
for (f in seq_len(n_frames)) {
    active <- chords[[chord_seq[f]]]
    energy_mat[active, f] <- pmin(1, pmax(0, rnorm(length(active), mean = 0.78, sd = 0.07)))
}

# Long-format data frame using integer frame indices for clean tile placement
df <- data.frame(
    frame       = rep(seq_len(n_frames), each = 12),
    pitch_class = factor(rep(pitch_names, times = n_frames), levels = pitch_names),
    energy      = as.vector(energy_mat)
)

# Primary x-axis: time labels at 2-second intervals
label_frames <- c(1, 21, 41, 61, 80)
label_times  <- round(time_sec[label_frames])

# Secondary x-axis: chord names at midpoints of each 20-frame region
chord_midpoints <- c(10.5, 30.5, 50.5, 70.5)
chord_labels    <- c("C", "Am", "F", "G")

# Plot
plot_title <- "heatmap-chromagram · r · ggplot2 · anyplot.ai"

p <- ggplot(df, aes(x = frame, y = pitch_class, fill = energy)) +
    geom_tile() +
    # Thin dashed separators at chord-change boundaries
    geom_vline(
        xintercept = c(20.5, 40.5, 60.5),
        color = INK_SOFT, linewidth = 0.4, linetype = "dashed"
    ) +
    scale_fill_gradient(
        low    = "#009E73",
        high   = "#4467A3",
        name   = "Energy",
        limits = c(0, 1),
        breaks = c(0, 0.25, 0.5, 0.75, 1.0)
    ) +
    # Primary axis: seconds; secondary axis: chord name per region
    scale_x_continuous(
        breaks = label_frames,
        labels = paste0(label_times, "s"),
        expand = c(0, 0),
        sec.axis = sec_axis(
            ~ .,
            breaks = chord_midpoints,
            labels = chord_labels,
            name   = "Chord"
        )
    ) +
    scale_y_discrete(expand = c(0, 0)) +
    # coord_fixed: lock panel to square so the plot fills the square canvas
    coord_fixed(ratio = n_frames / 12) +
    labs(
        title = plot_title,
        x     = "Time (seconds)",
        y     = "Pitch Class"
    ) +
    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_blank(),
        panel.grid.minor  = element_blank(),
        panel.border      = element_rect(color = INK_SOFT, fill = NA, linewidth = 0.5),
        axis.title        = element_text(color = INK,      size = 10),
        axis.text         = element_text(color = INK_SOFT, size = 8),
        axis.text.x.top   = element_text(color = INK,      size = 9, face = "bold"),
        axis.title.x.top  = element_text(color = INK_SOFT, size = 9),
        axis.ticks        = element_line(color = INK_SOFT, linewidth = 0.3),
        plot.title        = element_text(color = INK, size = 12),
        plot.margin       = margin(12, 12, 12, 12, "pt"),
        legend.background = 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",
        legend.key.height = unit(1.5, "in"),
        legend.key.width  = unit(0.25, "in")
    )

# Save — square canvas: 6 x 6 in at 400 dpi = 2400 x 2400 px
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/heatmap-chromagram/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": "heatmap-chromagram",
  "language": "r",
  "library": "ggplot2",
  "page": "https://anyplot.ai/heatmap-chromagram/r/ggplot2",
  "hub": "https://anyplot.ai/heatmap-chromagram",
  "code_json": "https://api.anyplot.ai/specs/heatmap-chromagram/ggplot2/code",
  "spec_json": "https://api.anyplot.ai/specs/heatmap-chromagram",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/heatmap-chromagram/r/ggplot2/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/heatmap-chromagram/r/ggplot2/plot-dark.png",
  "quality_score": 91.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Music Chromagram (Pitch Class Distribution over Time) on anyplot.ai.

Other implementations