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: plotnine 0.15.7 | Python 3.13.14
Quality: 86/100 | Updated: 2026-06-24
"""
import os
import numpy as np
import pandas as pd
from plotnine import (
aes,
element_blank,
element_rect,
element_text,
geom_text,
geom_tile,
geom_vline,
ggplot,
labs,
scale_alpha_identity,
scale_fill_gradient,
scale_x_continuous,
scale_y_discrete,
theme,
theme_minimal,
)
# Theme tokens (Imprint palette — see prompts/default-style-guide.md)
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
# Imprint sequential colormap — single-polarity energy data (brand green → blue)
SEQ_LOW = "#009E73" # Imprint position 1 — low energy
SEQ_HIGH = "#4467A3" # Imprint position 3 — high energy
# Data — C → G → Am → F chord progression chromagram
np.random.seed(42)
pitch_classes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
n_frames = 80
duration_sec = 8.0
time_step = duration_sec / n_frames
time_frames = np.linspace(0, duration_sec, n_frames)
chroma = np.random.uniform(0.02, 0.15, (12, n_frames))
chord_regions = [
(0, 20, [0, 4, 7], "C"), # C major: C-E-G
(20, 40, [7, 11, 2], "G"), # G major: G-B-D
(40, 60, [9, 0, 4], "Am"), # A minor: A-C-E
(60, 80, [5, 9, 0], "F"), # F major: F-A-C
]
for start, end, notes, _ in chord_regions:
for note in notes:
chroma[note, start:end] += np.random.uniform(0.55, 0.85, end - start)
for note in notes:
harmonic = (note + 7) % 12
chroma[harmonic, start:end] += np.random.uniform(0.1, 0.25, end - start)
chroma = chroma / chroma.max()
for boundary in [20, 40, 60]:
idx = max(0, boundary - 2)
end_idx = min(n_frames, boundary + 2)
for col in range(idx, end_idx):
blend = (col - idx) / (end_idx - idx)
chroma[:, col] = chroma[:, col] * (0.7 + 0.3 * blend)
time_idx, pitch_idx = np.meshgrid(np.arange(n_frames), np.arange(12))
df = pd.DataFrame(
{
"Time (s)": time_frames[time_idx.ravel()],
"Pitch Class": pd.Categorical(
[pitch_classes[i] for i in pitch_idx.ravel()], categories=pitch_classes[::-1], ordered=True
),
"Energy": chroma.ravel(),
}
)
chord_labels = pd.DataFrame(
{
"Time (s)": [1.0, 3.0, 5.0, 7.0],
"Pitch Class": pd.Categorical(["C"] * 4, categories=pitch_classes[::-1], ordered=True),
"label": ["C maj", "G maj", "A min", "F maj"],
"alpha": [0.9] * 4,
}
)
boundary_times = [time_frames[20], time_frames[40], time_frames[60]]
title = "heatmap-chromagram · python · plotnine · anyplot.ai"
# Plot
plot = (
ggplot(df, aes(x="Time (s)", y="Pitch Class", fill="Energy"))
+ geom_tile(width=time_step, height=1)
+ geom_vline(xintercept=boundary_times, linetype="dashed", color="#FFFFFF", alpha=0.6, size=0.8)
+ geom_text(
aes(x="Time (s)", y="Pitch Class", label="label", alpha="alpha"),
data=chord_labels,
inherit_aes=False,
color="#FFFFFF",
size=4.5,
fontweight="bold",
va="center",
)
+ scale_alpha_identity()
+ scale_fill_gradient(
low=SEQ_LOW,
high=SEQ_HIGH,
name="Energy",
breaks=[0.0, 0.25, 0.50, 0.75, 1.00],
labels=["0.00", "0.25", "0.50", "0.75", "1.00"],
)
+ scale_x_continuous(
expand=(0, 0),
breaks=np.arange(0, duration_sec + 0.5, 1.0),
labels=[f"{x:.0f}" for x in np.arange(0, duration_sec + 0.5, 1.0)],
)
+ scale_y_discrete(expand=(0, 0))
+ labs(
x="Time (s)",
y="Pitch Class",
title=title,
subtitle="Pitch class energy over time: C → G → Am → F chord progression",
)
+ theme_minimal()
+ theme(
figure_size=(8, 4.5),
text=element_text(family="sans-serif", color=INK),
plot_title=element_text(size=12, ha="center", weight="bold", color=INK, margin={"b": 4}),
plot_subtitle=element_text(size=9, ha="center", color=INK_SOFT, margin={"b": 6}),
axis_title_x=element_text(size=10, color=INK, margin={"t": 6}),
axis_title_y=element_text(size=10, color=INK, margin={"r": 4}),
axis_text_x=element_text(size=8, color=INK_SOFT),
axis_text_y=element_text(size=8, color=INK_SOFT, ha="right", margin={"r": 2}),
legend_title=element_text(size=8, weight="bold", color=INK),
legend_text=element_text(size=8, color=INK_SOFT),
legend_position="right",
legend_key_height=40,
legend_key_width=12,
legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),
panel_grid_major=element_blank(),
panel_grid_minor=element_blank(),
panel_background=element_blank(),
panel_border=element_rect(color=INK_SOFT, fill=None),
plot_background=element_rect(fill=PAGE_BG, color="none"),
plot_margin=0.05,
)
)
# Save
plot.save(f"plot-{THEME}.png", dpi=400, width=8, height=4.5, units="in", verbose=False)
Part of Music Chromagram (Pitch Class Distribution over Time) on anyplot.ai.