{"spec_id":"spectrogram-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// spectrogram-basic: Spectrogram Time-Frequency Heatmap\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Signal: linear chirp with mild deterministic noise ---------------------\nconst sampleRate = 4000;\nconst duration = 3;\nconst numSamples = sampleRate * duration;\nconst freqStart = 200;\nconst freqEnd = 1500;\n\nlet seed = 42;\nconst noise = () => {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff - 0.5;\n};\n\nconst signal = new Float64Array(numSamples);\nfor (let i = 0; i < numSamples; i++) {\n  const time = i / sampleRate;\n  const phase = freqStart * time + ((freqEnd - freqStart) * time * time) / (2 * duration);\n  signal[i] = Math.sin(2 * Math.PI * phase) + 0.08 * noise();\n}\n\n// --- STFT: Hann-windowed frames, radix-2 FFT ---------------------------------\nconst winSize = 256;\nconst hop = 128;\nconst numBins = winSize / 2 + 1;\nconst numFrames = Math.floor((numSamples - winSize) / hop) + 1;\n\nconst hann = new Float64Array(winSize);\nfor (let i = 0; i < winSize; i++) {\n  hann[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / (winSize - 1)));\n}\n\nconst fft = (re, im) => {\n  const n = re.length;\n  for (let i = 1, j = 0; i < n; i++) {\n    let bit = n >> 1;\n    for (; j & bit; bit >>= 1) j ^= bit;\n    j ^= bit;\n    if (i < j) {\n      [re[i], re[j]] = [re[j], re[i]];\n      [im[i], im[j]] = [im[j], im[i]];\n    }\n  }\n  for (let len = 2; len <= n; len <<= 1) {\n    const ang = -(2 * Math.PI) / len;\n    const wr = Math.cos(ang);\n    const wi = Math.sin(ang);\n    for (let i = 0; i < n; i += len) {\n      let curWr = 1;\n      let curWi = 0;\n      for (let j = 0; j < len / 2; j++) {\n        const ur = re[i + j];\n        const ui = im[i + j];\n        const vr = re[i + j + len / 2] * curWr - im[i + j + len / 2] * curWi;\n        const vi = re[i + j + len / 2] * curWi + im[i + j + len / 2] * curWr;\n        re[i + j] = ur + vr;\n        im[i + j] = ui + vi;\n        re[i + j + len / 2] = ur - vr;\n        im[i + j + len / 2] = ui - vi;\n        const nextWr = curWr * wr - curWi * wi;\n        const nextWi = curWr * wi + curWi * wr;\n        curWr = nextWr;\n        curWi = nextWi;\n      }\n    }\n  }\n};\n\nconst magnitudeDb = [];\nlet maxDb = -Infinity;\nfor (let f = 0; f < numFrames; f++) {\n  const start = f * hop;\n  const re = new Float64Array(winSize);\n  const im = new Float64Array(winSize);\n  for (let i = 0; i < winSize; i++) re[i] = signal[start + i] * hann[i];\n  fft(re, im);\n  const frameDb = new Float64Array(numBins);\n  for (let k = 0; k < numBins; k++) {\n    const magnitude = Math.hypot(re[k], im[k]) / winSize;\n    const db = 20 * Math.log10(magnitude + 1e-9);\n    frameDb[k] = db;\n    if (db > maxDb) maxDb = db;\n  }\n  magnitudeDb.push(frameDb);\n}\n\nconst dbFloor = -70;\nconst cells = [];\nfor (let f = 0; f < numFrames; f++) {\n  for (let k = 0; k < numBins; k++) {\n    cells.push({ frame: f, bin: k, db: Math.max(dbFloor, magnitudeDb[f][k] - maxDb) });\n  }\n}\n\n// --- Layout -------------------------------------------------------------------\nconst margin = { top: 90, right: 170, bottom: 80, left: 90 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\nconst dt = hop / sampleRate;\nconst df = sampleRate / winSize;\nconst totalTime = (numFrames - 1) * dt + winSize / sampleRate;\nconst nyquist = sampleRate / 2;\n\nconst xScale = d3.scaleLinear().domain([0, totalTime]).range([0, iw]);\nconst yScale = d3.scaleLinear().domain([0, nyquist]).range([ih, 0]);\nconst cellWidth = xScale(dt) - xScale(0);\nconst cellHeight = yScale(0) - yScale(df);\n\nconst color = d3.scaleSequential(d3.interpolateRgbBasis(t.seq)).domain([dbFloor, 0]);\n\n// --- SVG mount ------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Heatmap cells ----------------------------------------------------------\n// A small sub-pixel overlap (plus crispEdges) avoids the anti-aliasing seam\n// that otherwise appears as hairlines between adjacent rects.\nconst heatmap = g.append(\"g\").attr(\"shape-rendering\", \"crispEdges\");\nheatmap\n  .selectAll(\"rect\")\n  .data(cells)\n  .join(\"rect\")\n  .attr(\"x\", (d) => xScale(d.frame * dt))\n  .attr(\"y\", (d) => yScale((d.bin + 1) * df))\n  .attr(\"width\", cellWidth + 0.75)\n  .attr(\"height\", cellHeight + 0.75)\n  .attr(\"fill\", (d) => color(d.db));\n\n// --- Axes ---------------------------------------------------------------------\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(d3.axisBottom(xScale).ticks(8));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(yScale).ticks(8));\nfor (const axisGroup of [xAxis, yAxis]) {\n  axisGroup.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  axisGroup.selectAll(\"line\").attr(\"stroke\", t.grid);\n  axisGroup.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Time (s)\");\n\ng.append(\"text\")\n  .attr(\"transform\", `translate(${-64}, ${ih / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Frequency (Hz)\");\n\n// --- Colorbar (power, dB relative to peak) -----------------------------------\nconst barWidth = 24;\nconst barX = iw + 50;\nconst defs = svg.append(\"defs\");\nconst gradient = defs\n  .append(\"linearGradient\")\n  .attr(\"id\", \"spectrogram-db-gradient\")\n  .attr(\"x1\", \"0\").attr(\"y1\", \"1\").attr(\"x2\", \"0\").attr(\"y2\", \"0\");\ngradient.append(\"stop\").attr(\"offset\", \"0%\").attr(\"stop-color\", t.seq[0]);\ngradient.append(\"stop\").attr(\"offset\", \"100%\").attr(\"stop-color\", t.seq[1]);\n\nconst bar = g.append(\"g\").attr(\"transform\", `translate(${barX},0)`);\nbar\n  .append(\"rect\")\n  .attr(\"width\", barWidth)\n  .attr(\"height\", ih)\n  .attr(\"fill\", \"url(#spectrogram-db-gradient)\");\n\nconst barScale = d3.scaleLinear().domain([dbFloor, 0]).range([ih, 0]);\nconst barAxis = bar\n  .append(\"g\")\n  .attr(\"transform\", `translate(${barWidth},0)`)\n  .call(d3.axisRight(barScale).ticks(5).tickFormat((d) => `${d} dB`));\nbarAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nbarAxis.selectAll(\"line\").attr(\"stroke\", t.grid);\nbarAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\nbar\n  .append(\"text\")\n  .attr(\"transform\", `translate(${barWidth + 58}, ${ih / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(\"Power (dB)\");\n\n// --- Title --------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"spectrogram-basic · javascript · d3 · anyplot.ai\");\n"}