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.

# anyplot.ai
# heatmap-chromagram: Music Chromagram (Pitch Class Distribution over Time)
# Library: makie 0.22.10 | Julia 1.11.9
# Quality: 88/100 | Created: 2026-06-24
using CairoMakie
using Colors
using Random
Random.seed!(42)
# Theme tokens
const THEME = get(ENV, "ANYPLOT_THEME", "light")
const PAGE_BG = THEME == "light" ? colorant"#FAF8F1" : colorant"#1A1A17"
const ELEVATED_BG = THEME == "light" ? colorant"#FFFDF6" : colorant"#242420"
const INK = THEME == "light" ? colorant"#1A1A17" : colorant"#F0EFE8"
const INK_SOFT = THEME == "light" ? colorant"#4A4A44" : colorant"#B8B7B0"
# Imprint sequential colormap for energy (single-polarity continuous data)
const ANYPLOT_SEQ = cgrad([colorant"#009E73", colorant"#4467A3"])
# Data: chromagram of a I-V-vi-IV progression in C major
const PITCH_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
const N_PITCH = 12
const N_FRAMES = 120 # 12 seconds at 10 fps
time_seconds = collect(range(0.0, 12.0, length=N_FRAMES))
# Chord tones (1-based pitch indices)
# C major: C(1), E(5), G(8) | G major: D(3), G(8), B(12)
# A minor: C(1), E(5), A(10) | F major: C(1), F(6), A(10)
const CHORD_TONES = [
[1, 5, 8],
[3, 8, 12],
[1, 5, 10],
[1, 6, 10],
]
frames_per_chord = N_FRAMES ÷ 4
# Build chromagram matrix: chroma[frame, pitch_class]
chroma = zeros(N_FRAMES, N_PITCH)
for f in 1:N_FRAMES
chord_idx = min(4, div(f - 1, frames_per_chord) + 1)
tones = CHORD_TONES[chord_idx]
for p in 1:N_PITCH
if p in tones
chroma[f, p] = 0.60 + 0.40 * rand()
else
circ_dist = minimum(min(abs(p - t), 12 - abs(p - t)) for t in tones)
chroma[f, p] = 0.20 * exp(-0.9 * circ_dist) + 0.06 * rand()
end
end
end
# Plot
fig = Figure(
size = (1200, 1200),
fontsize = 14,
backgroundcolor = PAGE_BG,
)
ax = Axis(
fig[1, 1];
title = "heatmap-chromagram · julia · makie · anyplot.ai",
titlesize = 20,
titlecolor = INK,
xlabel = "Time (seconds)",
ylabel = "Pitch Class",
xlabelsize = 16,
ylabelsize = 16,
xticklabelsize = 14,
yticklabelsize = 14,
xlabelcolor = INK,
ylabelcolor = INK,
xticklabelcolor = INK_SOFT,
yticklabelcolor = INK_SOFT,
xtickcolor = INK_SOFT,
ytickcolor = INK_SOFT,
backgroundcolor = PAGE_BG,
topspinevisible = false,
rightspinevisible = false,
leftspinecolor = INK_SOFT,
bottomspinecolor = INK_SOFT,
xgridvisible = false,
ygridvisible = false,
)
hm = heatmap!(ax, time_seconds, collect(1:N_PITCH), chroma; colormap = ANYPLOT_SEQ)
ax.yticks = (collect(1:N_PITCH), PITCH_NAMES)
# Chord boundary markers and region labels
vlines!(ax, [3.0, 6.0, 9.0]; color = INK_SOFT, linewidth = 1.5, linestyle = :dash)
for (xc, label) in zip([1.5, 4.5, 7.5, 10.5], ["C", "G", "Am", "F"])
text!(ax, xc, 12.7; text = label, color = INK, fontsize = 14, align = (:center, :bottom))
end
ylims!(ax, 0.35, 13.6)
cb = Colorbar(
fig[1, 2], hm;
label = "Energy",
labelsize = 16,
ticklabelsize = 13,
labelcolor = INK,
ticklabelcolor = INK_SOFT,
tickcolor = INK_SOFT,
width = 25,
)
colgap!(fig.layout, 15)
# Save
save("plot-$(THEME).png", fig; px_per_unit = 2)
Part of Music Chromagram (Pitch Class Distribution over Time) on anyplot.ai.