Music Chromagram (Pitch Class Distribution over Time) — Seaborn

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 Seaborn

Python source (Seaborn)

""" anyplot.ai
heatmap-chromagram: Music Chromagram (Pitch Class Distribution over Time)
Library: seaborn 0.13.2 | Python 3.13.14
Quality: 88/100 | Updated: 2026-06-24
"""

import os

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from matplotlib.colors import LinearSegmentedColormap


# Theme tokens
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"

sns.set_theme(
    style="ticks",
    rc={
        "figure.facecolor": PAGE_BG,
        "axes.facecolor": PAGE_BG,
        "axes.edgecolor": INK_SOFT,
        "axes.labelcolor": INK,
        "text.color": INK,
        "xtick.color": INK_SOFT,
        "ytick.color": INK_SOFT,
        "grid.color": INK,
        "grid.alpha": 0.15,
        "legend.facecolor": ELEVATED_BG,
        "legend.edgecolor": INK_SOFT,
    },
)

# Imprint sequential colormap for single-polarity energy data
imprint_seq = LinearSegmentedColormap.from_list("imprint_seq", ["#009E73", "#4467A3"])

# Data
np.random.seed(42)
pitch_classes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
n_frames = 160
duration = 8.0
time_seconds = np.linspace(0, duration, n_frames)

chromagram = np.random.uniform(0.03, 0.15, size=(12, n_frames))

# Chord progression: C major (0-2s, 6-7s), G major (2-4s), Am (4-6s), F major (7-8s)
for t_idx in range(n_frames):
    t = time_seconds[t_idx]
    if (0 <= t < 2) or (6 <= t < 7):
        chromagram[0, t_idx] += 0.75  # C
        chromagram[4, t_idx] += 0.55  # E
        chromagram[7, t_idx] += 0.50  # G
    elif 2 <= t < 4:
        chromagram[7, t_idx] += 0.75  # G
        chromagram[11, t_idx] += 0.55  # B
        chromagram[2, t_idx] += 0.50  # D
    elif 4 <= t < 6:
        chromagram[9, t_idx] += 0.70  # A
        chromagram[0, t_idx] += 0.50  # C
        chromagram[4, t_idx] += 0.55  # E
    elif 7 <= t <= 8:
        chromagram[5, t_idx] += 0.70  # F
        chromagram[9, t_idx] += 0.50  # A
        chromagram[0, t_idx] += 0.45  # C

# Passing tones near chord transitions
for t_idx in range(n_frames):
    t = time_seconds[t_idx]
    if 1.8 <= t < 2.2:
        chromagram[1, t_idx] += 0.2  # C# passing tone
    if 3.8 <= t < 4.2:
        chromagram[6, t_idx] += 0.2  # F# passing tone
    if 5.8 <= t < 6.2:
        chromagram[10, t_idx] += 0.15  # A# passing tone

# Smooth transitions with convolution
for row in range(12):
    kernel = np.array([0.05, 0.15, 0.3, 0.3, 0.15, 0.05])
    chromagram[row] = np.convolve(chromagram[row], kernel, mode="same")

chromagram = chromagram / chromagram.max()
df_chroma = pd.DataFrame(chromagram, index=pitch_classes)

tick_times = np.arange(0, int(duration) + 1)
tick_positions = [t / duration * n_frames for t in tick_times]
tick_labels = [str(int(t)) for t in tick_times]

# Plot — square canvas for heatmap (2400×2400)
fig, ax = plt.subplots(figsize=(6, 6), dpi=400, facecolor=PAGE_BG)
ax.set_facecolor(PAGE_BG)

sns.heatmap(
    df_chroma,
    ax=ax,
    cmap=imprint_seq,
    vmin=0,
    vmax=1,
    cbar_kws={"label": "Energy", "shrink": 0.82, "aspect": 22, "pad": 0.02},
    linewidths=0,
    rasterized=True,
    xticklabels=False,
)

# Spine styling
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["bottom"].set_color(INK_SOFT)
ax.spines["left"].set_color(INK_SOFT)

# Axis labels and title
ax.set_xlabel("Time (seconds)", fontsize=10, color=INK, labelpad=8)
ax.set_ylabel("Pitch Class", fontsize=10, color=INK, labelpad=8)
ax.set_title("heatmap-chromagram · python · seaborn · anyplot.ai", fontsize=12, fontweight="medium", color=INK, pad=12)

ax.set_xticks(tick_positions)
ax.set_xticklabels(tick_labels, rotation=0, ha="center")
ax.tick_params(axis="both", labelsize=8, length=0, colors=INK_SOFT)
ax.set_yticklabels(ax.get_yticklabels(), rotation=0, fontsize=8)

# Colorbar styling
cbar = ax.collections[0].colorbar
cbar.ax.set_facecolor(PAGE_BG)
cbar.ax.tick_params(labelsize=8, colors=INK_SOFT)
cbar.set_label("Energy", fontsize=10, color=INK)
cbar.outline.set_edgecolor(INK_SOFT)

# Chord transition annotations — vertical dashed lines + chord labels above plot
chord_bounds = [0, 40, 80, 120, 140, 160]
chord_names = ["C", "G", "Am", "C", "F"]
for bound in chord_bounds[1:-1]:
    ax.axvline(x=bound, color=INK_SOFT, linewidth=0.8, linestyle="--", alpha=0.65, zorder=5)
for start, end, name in zip(chord_bounds[:-1], chord_bounds[1:], chord_names, strict=False):
    center_frac = ((start + end) / 2) / n_frames
    ax.text(center_frac, 1.012, name, ha="center", va="bottom", fontsize=7.5, color=INK_SOFT, transform=ax.transAxes)

fig.tight_layout()
plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG)

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

Other implementations