{"spec_id":"heatmap-chromagram","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// heatmap-chromagram: Music Chromagram (Pitch Class Distribution over Time)\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-24\n\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (deterministic chord simulation) ---\nconst PITCH_CLASSES = [\"C\", \"C#\", \"D\", \"D#\", \"E\", \"F\", \"F#\", \"G\", \"G#\", \"A\", \"A#\", \"B\"];\nconst NUM_FRAMES = 80;\nconst CHORD_PROFILES = [\n  [0.90, 0.05, 0.05, 0.05, 0.76, 0.05, 0.05, 0.82, 0.05, 0.05, 0.05, 0.05], // C major: C E G\n  [0.05, 0.05, 0.76, 0.05, 0.05, 0.05, 0.05, 0.90, 0.05, 0.05, 0.05, 0.79], // G major: G B D\n  [0.70, 0.05, 0.05, 0.05, 0.73, 0.05, 0.05, 0.05, 0.05, 0.88, 0.05, 0.05], // A minor: A C E\n  [0.66, 0.05, 0.05, 0.05, 0.05, 0.88, 0.05, 0.05, 0.05, 0.74, 0.05, 0.05], // F major: F A C\n];\nconst CHORD_LABELS = [\"C maj\", \"G maj\", \"A min\", \"F maj\"];\n\nlet seed = 42;\nconst lcg = () => { seed = (seed * 1664525 + 1013904223) & 0xffffffff; return (seed >>> 0) / 4294967295; };\n\nconst energy = Array.from({ length: 12 }, () => new Array(NUM_FRAMES).fill(0));\nfor (let f = 0; f < NUM_FRAMES; f++) {\n  const profile = CHORD_PROFILES[Math.floor(f / 20) % 4];\n  for (let p = 0; p < 12; p++) {\n    energy[p][f] = Math.min(1, Math.max(0, profile[p] + (lcg() - 0.5) * 0.10));\n  }\n}\n\n// --- Color (Imprint sequential: t.seq[0]=#009E73 → t.seq[1]=#4467A3) ---\nconst [sR, sG, sB] = t.seq[0].match(/[\\da-f]{2}/gi).map(h => parseInt(h, 16));\nconst [eR, eG, eB] = t.seq[1].match(/[\\da-f]{2}/gi).map(h => parseInt(h, 16));\nfunction energyToColor(v) {\n  v = Math.min(1, Math.max(0, v));\n  return `rgb(${Math.round(sR + (eR - sR) * v)},${Math.round(sG + (eG - sG) * v)},${Math.round(sB + (eB - sB) * v)})`;\n}\n\n// --- Plugin: heatmap cells, separators, chord markers + labels, colorbar ---\nconst chromagramPlugin = {\n  id: \"chromagram\",\n  afterDraw(chart) {\n    const ctx = chart.ctx;\n    const { x: xScale, y: yScale } = chart.scales;\n\n    // Heatmap cells\n    for (let f = 0; f < NUM_FRAMES; f++) {\n      const xL = xScale.getPixelForValue(f);\n      const xR = xScale.getPixelForValue(f + 1);\n      for (let p = 0; p < 12; p++) {\n        const yT = yScale.getPixelForValue(p + 1);\n        const yB = yScale.getPixelForValue(p);\n        ctx.fillStyle = energyToColor(energy[p][f]);\n        ctx.fillRect(xL, yT, xR - xL + 0.5, yB - yT + 0.5);\n      }\n    }\n\n    // Thin row separators (page bg color delineates pitch classes)\n    ctx.strokeStyle = t.pageBg;\n    ctx.lineWidth = 0.8;\n    for (let p = 1; p < 12; p++) {\n      const y = yScale.getPixelForValue(p);\n      ctx.beginPath();\n      ctx.moveTo(xScale.left, y);\n      ctx.lineTo(xScale.right, y);\n      ctx.stroke();\n    }\n\n    // Chord-change markers at 2s / 4s / 6s\n    ctx.globalAlpha = 0.55;\n    ctx.strokeStyle = t.pageBg;\n    ctx.lineWidth = 2;\n    for (let f = 20; f < NUM_FRAMES; f += 20) {\n      const x = xScale.getPixelForValue(f);\n      ctx.beginPath();\n      ctx.moveTo(x, yScale.top);\n      ctx.lineTo(x, yScale.bottom);\n      ctx.stroke();\n    }\n    ctx.globalAlpha = 1;\n\n    // Chord section labels above each section\n    ctx.fillStyle = t.ink;\n    ctx.font = \"bold 14px system-ui, sans-serif\";\n    ctx.textAlign = \"center\";\n    for (let c = 0; c < 4; c++) {\n      const cx = (xScale.getPixelForValue(c * 20) + xScale.getPixelForValue((c + 1) * 20)) / 2;\n      ctx.fillText(CHORD_LABELS[c], cx, yScale.top - 8);\n    }\n\n    // Colorbar\n    const cbX = xScale.right + 22;\n    const cbY = yScale.top;\n    const cbH = yScale.bottom - yScale.top;\n    const cbW = 16;\n    const grad = ctx.createLinearGradient(0, cbY + cbH, 0, cbY);\n    for (let i = 0; i <= 20; i++) grad.addColorStop(i / 20, energyToColor(i / 20));\n    ctx.fillStyle = grad;\n    ctx.fillRect(cbX, cbY, cbW, cbH);\n    ctx.strokeStyle = t.inkSoft;\n    ctx.lineWidth = 0.5;\n    ctx.strokeRect(cbX, cbY, cbW, cbH);\n\n    // Colorbar tick labels\n    ctx.fillStyle = t.inkSoft;\n    ctx.font = \"12px system-ui, sans-serif\";\n    ctx.textAlign = \"left\";\n    for (let i = 0; i <= 4; i++) {\n      const v = i / 4;\n      ctx.fillText(v.toFixed(2), cbX + cbW + 4, cbY + cbH - v * cbH + 4);\n    }\n\n    // Colorbar axis label (rotated)\n    ctx.save();\n    ctx.fillStyle = t.ink;\n    ctx.font = \"13px system-ui, sans-serif\";\n    ctx.textAlign = \"center\";\n    ctx.translate(cbX + cbW + 46, cbY + cbH / 2);\n    ctx.rotate(-Math.PI / 2);\n    ctx.fillText(\"Energy\", 0, 0);\n    ctx.restore();\n  },\n};\n\n// --- Mount ---\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: { datasets: [] },\n  plugins: [chromagramPlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: {\n      padding: { top: 10, right: 110, bottom: 10, left: 10 },\n    },\n    plugins: {\n      title: {\n        display: true,\n        text: \"heatmap-chromagram · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"bold\" },\n        padding: { bottom: 28 },\n      },\n      legend: { display: false },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: 0,\n        max: NUM_FRAMES,\n        title: {\n          display: true,\n          text: \"Time (seconds)\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n        afterBuildTicks(scale) {\n          scale.ticks = [0, 10, 20, 30, 40, 50, 60, 70, 80].map((v) => ({ value: v }));\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (v) => `${(v * 0.1).toFixed(1)}s`,\n        },\n        grid: { display: false },\n        border: { display: false },\n      },\n      y: {\n        type: \"linear\",\n        min: 0,\n        max: 12,\n        title: {\n          display: true,\n          text: \"Pitch Class\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n        afterBuildTicks(scale) {\n          scale.ticks = PITCH_CLASSES.map((_, i) => ({ value: i + 0.5 }));\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (v) => PITCH_CLASSES[Math.floor(v)] ?? \"\",\n        },\n        grid: { display: false },\n        border: { display: false },\n      },\n    },\n  },\n});\n"}