{"spec_id":"heatmap-chromagram","library":"muix","language":"javascript","code":"// anyplot.ai\n// heatmap-chromagram: Music Chromagram (Pitch Class Distribution over Time)\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// Quality: 89/100 | Created: 2026-06-24\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { useDrawingArea, useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\nconst PITCH_CLASSES = [\"C\", \"C#\", \"D\", \"D#\", \"E\", \"F\", \"F#\", \"G\", \"G#\", \"A\", \"A#\", \"B\"];\nconst N_FRAMES = 80;\nconst SEC_PER_FRAME = 0.05;\nconst TOTAL_SEC = N_FRAMES * SEC_PER_FRAME;\n\nconst CHORDS = [\n  { label: \"C major\", pcs: [0, 4, 7],  start: 0,  end: 20 },\n  { label: \"G major\", pcs: [7, 11, 2], start: 20, end: 40 },\n  { label: \"A minor\", pcs: [9, 0, 4],  start: 40, end: 60 },\n  { label: \"F major\", pcs: [5, 9, 0],  start: 60, end: 80 },\n];\n\n// Deterministic LCG seeded at 42\nfunction makeLCG(seed) {\n  let s = seed >>> 0;\n  return () => { s = (((s * 1664525) >>> 0) + 1013904223) >>> 0; return s / 0x100000000; };\n}\nconst rand = makeLCG(42);\n\n// 12 × 80 chroma matrix; chord tones high energy, overtones medium, rest noise\nconst chroma = Array.from({ length: 12 }, (_, pc) =>\n  Array.from({ length: N_FRAMES }, (_, f) => {\n    const chord = CHORDS.find(c => f >= c.start && f < c.end);\n    const active   = chord ? chord.pcs.includes(pc) : false;\n    const overtone = chord ? chord.pcs.map(p => (p + 7) % 12).includes(pc) : false;\n    const base = active   ? 0.65 + rand() * 0.30\n               : overtone ? 0.22 + rand() * 0.18\n               :             rand() * 0.16;\n    return Math.min(1.0, base);\n  })\n);\n\n// Imprint sequential colormap: seq[0]=#009E73 → seq[1]=#4467A3\nfunction hexRgb(h) {\n  return [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16)];\n}\nfunction seqColor(v) {\n  const [r1, g1, b1] = hexRgb(t.seq[0]);\n  const [r2, g2, b2] = hexRgb(t.seq[1]);\n  return `rgb(${Math.round(r1 + (r2 - r1) * v)},${Math.round(g1 + (g2 - g1) * v)},${Math.round(b1 + (b2 - b1) * v)})`;\n}\n\n// Heatmap cells, chord dividers, and chord labels — drawn using MUI X scale hooks\nfunction HeatmapCells() {\n  const { top, height: gH } = useDrawingArea();\n  const xScale = useXScale();\n  // Use exact gH/12 step to fill rows without gaps regardless of band scale padding\n  const step = gH / 12;\n  const divColor = window.ANYPLOT_THEME === \"dark\"\n    ? \"rgba(240,239,232,0.4)\"\n    : \"rgba(26,26,23,0.4)\";\n\n  return (\n    <>\n      {chroma.flatMap((row, pc) =>\n        row.map((val, f) => (\n          <rect\n            key={`${pc}-${f}`}\n            x={xScale(f * SEC_PER_FRAME)}\n            y={top + (11 - pc) * step}\n            width={xScale((f + 1) * SEC_PER_FRAME) - xScale(f * SEC_PER_FRAME) + 0.5}\n            height={step + 0.5}\n            fill={seqColor(val)}\n          />\n        ))\n      )}\n      {/* Chord-change dividers at 1s / 2s / 3s — semi-transparent in both themes */}\n      {[1, 2, 3].map(sec => (\n        <line\n          key={sec}\n          x1={xScale(sec)} y1={top}\n          x2={xScale(sec)} y2={top + gH}\n          stroke={divColor} strokeWidth={2.5}\n        />\n      ))}\n      {CHORDS.map(c => (\n        <text\n          key={c.label}\n          x={xScale((c.start + (c.end - c.start) / 2) * SEC_PER_FRAME)}\n          y={top - 10}\n          textAnchor=\"middle\" fontSize={13} fill={t.inkSoft}\n          fontFamily=\"Inter, system-ui, sans-serif\"\n        >\n          {c.label}\n        </text>\n      ))}\n    </>\n  );\n}\n\n// Colorbar gradient using drawing-area position from MUI X context\nfunction Colorbar() {\n  const { left, top, width: gW, height: gH } = useDrawingArea();\n  const cbX = left + gW + 22;\n  const cbW = 20;\n\n  return (\n    <>\n      <defs>\n        <linearGradient id=\"cbGrad\" x1=\"0\" y1=\"1\" x2=\"0\" y2=\"0\">\n          <stop offset=\"0%\" stopColor={t.seq[0]} />\n          <stop offset=\"100%\" stopColor={t.seq[1]} />\n        </linearGradient>\n      </defs>\n      <rect x={cbX} y={top} width={cbW} height={gH} fill=\"url(#cbGrad)\" />\n      <text x={cbX + cbW / 2} y={top - 6} textAnchor=\"middle\" fontSize={13} fill={t.inkSoft} fontFamily=\"Inter, system-ui, sans-serif\">1.0</text>\n      <text x={cbX + cbW / 2} y={top + gH + 16} textAnchor=\"middle\" fontSize={13} fill={t.inkSoft} fontFamily=\"Inter, system-ui, sans-serif\">0.0</text>\n      <text\n        x={cbX + cbW + 16} y={top + gH / 2}\n        textAnchor=\"middle\" fontSize={14} fill={t.inkSoft}\n        fontFamily=\"Inter, system-ui, sans-serif\"\n        transform={`rotate(90, ${cbX + cbW + 16}, ${top + gH / 2})`}\n      >\n        Energy\n      </text>\n    </>\n  );\n}\n\nfunction ChartTitle() {\n  const { top } = useDrawingArea();\n  return (\n    <text\n      x={width / 2} y={top - 46}\n      textAnchor=\"middle\" fontSize={22} fontWeight={500}\n      fill={t.ink} fontFamily=\"Inter, system-ui, sans-serif\"\n    >\n      heatmap-chromagram · javascript · muix · anyplot.ai\n    </text>\n  );\n}\n\nexport default function Chart() {\n  return (\n    <ChartContainer\n      width={width}\n      height={height}\n      series={[]}\n      xAxis={[{\n        scaleType: \"linear\",\n        min: 0,\n        max: TOTAL_SEC,\n        label: \"Time (seconds)\",\n        valueFormatter: (v) => `${v.toFixed(1)}s`,\n      }]}\n      yAxis={[{\n        scaleType: \"band\",\n        data: [...PITCH_CLASSES].reverse(),\n        label: \"Pitch Class\",\n      }]}\n      margin={{ left: 70, right: 90, top: 80, bottom: 70 }}\n    >\n      <ChartTitle />\n      <HeatmapCells />\n      <Colorbar />\n      <ChartsXAxis />\n      <ChartsYAxis />\n    </ChartContainer>\n  );\n}\n"}