{"spec_id":"heatmap-chromagram","library":"echarts","language":"javascript","code":"// anyplot.ai\n// heatmap-chromagram: Music Chromagram (Pitch Class Distribution over Time)\n// Library: echarts 5.5.1 | JavaScript 22.22.3\n// Quality: 89/100 | Created: 2026-06-24\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Pitch classes ordered C to B\nconst pitchClasses = [\"C\", \"C#\", \"D\", \"D#\", \"E\", \"F\", \"F#\", \"G\", \"G#\", \"A\", \"A#\", \"B\"];\nconst numFrames = 80;\nconst framesPerChord = 20;\n\n// C–G–Am–F progression (indices into pitchClasses)\n// C major: C(0), E(4), G(7)\n// G major: D(2), G(7), B(11)\n// A minor: C(0), E(4), A(9)\n// F major: C(0), F(5), A(9)\nconst progression = [\n  [0, 4, 7],\n  [2, 7, 11],\n  [0, 4, 9],\n  [0, 5, 9],\n];\n\n// Deterministic LCG for reproducible noise (seed 42)\nlet seed = 42;\nconst lcg = () => {\n  seed = (seed * 1664525 + 1013904223) >>> 0;\n  return seed / 4294967296;\n};\n\n// Build chromagram: [frameIdx, pitchClassIdx, energy]\nconst chromaData = [];\nfor (let f = 0; f < numFrames; f++) {\n  const chordTones = progression[Math.floor(f / framesPerChord)];\n  for (let p = 0; p < 12; p++) {\n    const isChordTone = chordTones.includes(p);\n    const energy = isChordTone ? 0.55 + lcg() * 0.45 : lcg() * 0.18;\n    chromaData.push([f, p, parseFloat(energy.toFixed(3))]);\n  }\n}\n\n// X-axis labels: show seconds only at chord boundaries (every 20 frames = 1 second)\nconst timeLabels = Array.from({ length: numFrames }, (_, i) =>\n  i % 20 === 0 ? `${(i * 0.05).toFixed(1)}s` : \"\"\n);\n\n// Chord section annotations — graphic positions in 1200×1200 CSS-px space\n// Grid: left 82, right 118 (from right edge), top 112, bottom 80\n// Grid area: x [82, 1082], y [112, 1120], width 1000, height 1008\nconst gLeft = 82;\nconst gTop = 112;\nconst gBottom = 1120;\nconst sectionW = 1000 / 4; // 250px per chord section\n\nconst chordNames = [\"C\", \"G\", \"Am\", \"F\"];\nconst labelGraphics = chordNames.map((name, i) => ({\n  type: \"text\",\n  style: {\n    text: name,\n    fill: t.ink,\n    fontSize: 12,\n    fontWeight: \"bold\",\n    textAlign: \"center\",\n    textVerticalAlign: \"bottom\",\n  },\n  x: gLeft + sectionW * i + sectionW / 2,\n  y: gTop - 5,\n}));\n\n// Vertical dashed lines at chord boundaries (between sections)\nconst boundaryGraphics = [1, 2, 3].map((i) => ({\n  type: \"line\",\n  shape: {\n    x1: gLeft + sectionW * i,\n    y1: gTop,\n    x2: gLeft + sectionW * i,\n    y2: gBottom,\n  },\n  style: { stroke: t.inkSoft, lineWidth: 1, lineDash: [4, 3], opacity: 0.55 },\n}));\n\nconst chart = echarts.init(document.getElementById(\"container\"));\n\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n\n  title: {\n    text: \"heatmap-chromagram · javascript · echarts · anyplot.ai\",\n    subtext: \"C–G–Am–F chord progression  |  4 seconds, 80 analysis frames\",\n    left: \"center\",\n    top: 20,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: \"bold\" },\n    subtextStyle: { color: t.inkSoft, fontSize: 14 },\n  },\n\n  grid: { left: 82, right: 118, top: 112, bottom: 80 },\n\n  xAxis: {\n    type: \"category\",\n    data: timeLabels,\n    name: \"Time (seconds)\",\n    nameLocation: \"middle\",\n    nameGap: 44,\n    nameTextStyle: { color: t.inkSoft, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 13, interval: 0 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n\n  yAxis: {\n    type: \"category\",\n    data: pitchClasses,\n    name: \"Pitch Class\",\n    nameLocation: \"middle\",\n    nameGap: 58,\n    nameTextStyle: { color: t.inkSoft, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n\n  visualMap: {\n    min: 0,\n    max: 1,\n    calculable: false,\n    orient: \"vertical\",\n    right: 18,\n    top: \"center\",\n    itemWidth: 22,\n    itemHeight: 220,\n    inRange: { color: t.seq },\n    text: [\"High\", \"Low\"],\n    textGap: 8,\n    textStyle: { color: t.inkSoft, fontSize: 14 },\n    outOfRange: { color: t.pageBg },\n  },\n\n  graphic: [...labelGraphics, ...boundaryGraphics],\n\n  series: [\n    {\n      type: \"heatmap\",\n      data: chromaData,\n      itemStyle: {\n        borderWidth: 0.5,\n        borderColor: t.pageBg,\n      },\n      emphasis: { disabled: true },\n    },\n  ],\n});\n"}