{"spec_id":"heatmap-chromagram","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nheatmap-chromagram: Music Chromagram (Pitch Class Distribution over Time)\nLibrary: matplotlib 3.11.0 | Python 3.13.14\nQuality: 93/100 | Updated: 2026-06-24\n\"\"\"\n\nimport os\n\nimport matplotlib.colors as mcolors\nimport matplotlib.pyplot as plt\nimport matplotlib.ticker as mticker\nimport numpy as np\n\n\n# Theme-adaptive chrome\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint sequential colormap (green → blue) for single-polarity energy heatmap\nimprint_seq = mcolors.LinearSegmentedColormap.from_list(\"imprint_seq\", [\"#009E73\", \"#4467A3\"])\n\n# Data — simulate a chromagram for a short musical passage\n# Classic I-V-vi-IV pop chord progression over 8 seconds\nnp.random.seed(42)\n\npitch_classes = [\"C\", \"C#\", \"D\", \"D#\", \"E\", \"F\", \"F#\", \"G\", \"G#\", \"A\", \"A#\", \"B\"]\nn_pitches = len(pitch_classes)\nn_frames = 120\ntime_seconds = np.linspace(0, 8, n_frames)\n\n# Low background energy across all pitches\nchroma = np.random.uniform(0.02, 0.12, (n_pitches, n_frames))\n\n# Chord regions with realistic harmonic energy\nchord_regions = [\n    (0, 30, \"C maj\", {\"C\": 0.9, \"E\": 0.75, \"G\": 0.8}),\n    (30, 60, \"G maj\", {\"G\": 0.92, \"B\": 0.78, \"D\": 0.72}),\n    (60, 90, \"A min\", {\"A\": 0.88, \"C\": 0.76, \"E\": 0.82}),\n    (90, 120, \"F maj\", {\"F\": 0.85, \"A\": 0.74, \"C\": 0.80}),\n]\n\nfor start, end, _, notes in chord_regions:\n    for note, energy in notes.items():\n        idx = pitch_classes.index(note)\n        chroma[idx, start:end] = energy + np.random.normal(0, 0.05, end - start)\n        # Harmonic bleeding at chord boundaries for realism\n        if start > 0:\n            chroma[idx, start - 3 : start] = np.linspace(0.1, energy * 0.7, 3)\n        if end < n_frames:\n            tail = min(3, n_frames - end)\n            chroma[idx, end : end + tail] = np.linspace(energy * 0.7, 0.1, tail)\n\nchroma = np.clip(chroma, 0, 1)\n\n# PowerNorm enhances perceptual contrast between quiet and active pitch regions\nnorm = mcolors.PowerNorm(gamma=0.6, vmin=0, vmax=1)\n\n# Canvas — square (2400×2400) for symmetric heatmap\nfig, ax = plt.subplots(figsize=(6, 6), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\ntime_edges = np.linspace(0, 8, n_frames + 1)\npitch_edges = np.arange(n_pitches + 1) - 0.5\n\nim = ax.pcolormesh(time_edges, pitch_edges, chroma, cmap=imprint_seq, norm=norm, shading=\"flat\", rasterized=True)\n\n# Chord region labels and subtle dividers for harmonic storytelling\nfor start, end, label, _ in chord_regions:\n    t_mid = (time_seconds[start] + time_seconds[min(end - 1, n_frames - 1)]) / 2\n    ax.text(\n        t_mid,\n        n_pitches - 0.2,\n        label,\n        ha=\"center\",\n        va=\"top\",\n        fontsize=7,\n        fontstyle=\"italic\",\n        color=INK_SOFT,\n        fontweight=\"medium\",\n    )\n    if start > 0:\n        t_boundary = time_seconds[start]\n        ax.axvline(t_boundary, color=INK_MUTED, linewidth=0.6, linestyle=\"--\", alpha=0.5)\n\n# Axes\nax.set_yticks(np.arange(n_pitches))\nax.set_yticklabels(pitch_classes, fontsize=8, fontfamily=\"monospace\", color=INK_SOFT)\nax.set_xlabel(\"Time (seconds)\", fontsize=10, color=INK, labelpad=8)\nax.set_ylabel(\"Pitch Class\", fontsize=10, color=INK, labelpad=8)\n\ntitle = \"heatmap-chromagram · python · matplotlib · anyplot.ai\"\ntitle_fontsize = max(8, round(12 * 67 / len(title))) if len(title) > 67 else 12\nax.set_title(title, fontsize=title_fontsize, fontweight=\"medium\", color=INK, pad=12)\n\nax.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT, labelcolor=INK_SOFT)\nax.xaxis.set_major_locator(mticker.MultipleLocator(1))\nax.xaxis.set_minor_locator(mticker.MultipleLocator(0.25))\nax.tick_params(axis=\"x\", which=\"minor\", length=2, color=INK_MUTED)\n\nfor spine in ax.spines.values():\n    spine.set_visible(False)\n\n# Colorbar with theme-adaptive styling\ncbar = fig.colorbar(im, ax=ax, fraction=0.02, pad=0.02, aspect=30)\ncbar.set_label(\"Energy\", fontsize=10, labelpad=10, color=INK)\ncbar.ax.tick_params(labelsize=8, colors=INK_SOFT, labelcolor=INK_SOFT)\ncbar.outline.set_visible(False)\ncbar.set_ticks([0, 0.25, 0.5, 0.75, 1.0])\ncbar.ax.set_yticklabels([\"0.0\", \"0.25\", \"0.5\", \"0.75\", \"1.0\"], color=INK_SOFT, fontsize=8)\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\nplt.close()\n"}