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