{"spec_id":"heatmap-chromagram","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nheatmap-chromagram: Music Chromagram (Pitch Class Distribution over Time)\nLibrary: seaborn 0.13.2 | Python 3.13.14\nQuality: 88/100 | Updated: 2026-06-24\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nfrom matplotlib.colors import LinearSegmentedColormap\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.15,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# Imprint sequential colormap for single-polarity energy data\nimprint_seq = LinearSegmentedColormap.from_list(\"imprint_seq\", [\"#009E73\", \"#4467A3\"])\n\n# Data\nnp.random.seed(42)\npitch_classes = [\"C\", \"C#\", \"D\", \"D#\", \"E\", \"F\", \"F#\", \"G\", \"G#\", \"A\", \"A#\", \"B\"]\nn_frames = 160\nduration = 8.0\ntime_seconds = np.linspace(0, duration, n_frames)\n\nchromagram = np.random.uniform(0.03, 0.15, size=(12, n_frames))\n\n# Chord progression: C major (0-2s, 6-7s), G major (2-4s), Am (4-6s), F major (7-8s)\nfor t_idx in range(n_frames):\n    t = time_seconds[t_idx]\n    if (0 <= t < 2) or (6 <= t < 7):\n        chromagram[0, t_idx] += 0.75  # C\n        chromagram[4, t_idx] += 0.55  # E\n        chromagram[7, t_idx] += 0.50  # G\n    elif 2 <= t < 4:\n        chromagram[7, t_idx] += 0.75  # G\n        chromagram[11, t_idx] += 0.55  # B\n        chromagram[2, t_idx] += 0.50  # D\n    elif 4 <= t < 6:\n        chromagram[9, t_idx] += 0.70  # A\n        chromagram[0, t_idx] += 0.50  # C\n        chromagram[4, t_idx] += 0.55  # E\n    elif 7 <= t <= 8:\n        chromagram[5, t_idx] += 0.70  # F\n        chromagram[9, t_idx] += 0.50  # A\n        chromagram[0, t_idx] += 0.45  # C\n\n# Passing tones near chord transitions\nfor t_idx in range(n_frames):\n    t = time_seconds[t_idx]\n    if 1.8 <= t < 2.2:\n        chromagram[1, t_idx] += 0.2  # C# passing tone\n    if 3.8 <= t < 4.2:\n        chromagram[6, t_idx] += 0.2  # F# passing tone\n    if 5.8 <= t < 6.2:\n        chromagram[10, t_idx] += 0.15  # A# passing tone\n\n# Smooth transitions with convolution\nfor row in range(12):\n    kernel = np.array([0.05, 0.15, 0.3, 0.3, 0.15, 0.05])\n    chromagram[row] = np.convolve(chromagram[row], kernel, mode=\"same\")\n\nchromagram = chromagram / chromagram.max()\ndf_chroma = pd.DataFrame(chromagram, index=pitch_classes)\n\ntick_times = np.arange(0, int(duration) + 1)\ntick_positions = [t / duration * n_frames for t in tick_times]\ntick_labels = [str(int(t)) for t in tick_times]\n\n# Plot — square canvas for heatmap (2400×2400)\nfig, ax = plt.subplots(figsize=(6, 6), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\nsns.heatmap(\n    df_chroma,\n    ax=ax,\n    cmap=imprint_seq,\n    vmin=0,\n    vmax=1,\n    cbar_kws={\"label\": \"Energy\", \"shrink\": 0.82, \"aspect\": 22, \"pad\": 0.02},\n    linewidths=0,\n    rasterized=True,\n    xticklabels=False,\n)\n\n# Spine styling\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nax.spines[\"bottom\"].set_color(INK_SOFT)\nax.spines[\"left\"].set_color(INK_SOFT)\n\n# Axis labels and title\nax.set_xlabel(\"Time (seconds)\", fontsize=10, color=INK, labelpad=8)\nax.set_ylabel(\"Pitch Class\", fontsize=10, color=INK, labelpad=8)\nax.set_title(\"heatmap-chromagram · python · seaborn · anyplot.ai\", fontsize=12, fontweight=\"medium\", color=INK, pad=12)\n\nax.set_xticks(tick_positions)\nax.set_xticklabels(tick_labels, rotation=0, ha=\"center\")\nax.tick_params(axis=\"both\", labelsize=8, length=0, colors=INK_SOFT)\nax.set_yticklabels(ax.get_yticklabels(), rotation=0, fontsize=8)\n\n# Colorbar styling\ncbar = ax.collections[0].colorbar\ncbar.ax.set_facecolor(PAGE_BG)\ncbar.ax.tick_params(labelsize=8, colors=INK_SOFT)\ncbar.set_label(\"Energy\", fontsize=10, color=INK)\ncbar.outline.set_edgecolor(INK_SOFT)\n\n# Chord transition annotations — vertical dashed lines + chord labels above plot\nchord_bounds = [0, 40, 80, 120, 140, 160]\nchord_names = [\"C\", \"G\", \"Am\", \"C\", \"F\"]\nfor bound in chord_bounds[1:-1]:\n    ax.axvline(x=bound, color=INK_SOFT, linewidth=0.8, linestyle=\"--\", alpha=0.65, zorder=5)\nfor start, end, name in zip(chord_bounds[:-1], chord_bounds[1:], chord_names, strict=False):\n    center_frac = ((start + end) / 2) / n_frames\n    ax.text(center_frac, 1.012, name, ha=\"center\", va=\"bottom\", fontsize=7.5, color=INK_SOFT, transform=ax.transAxes)\n\nfig.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}