{"spec_id":"spectrogram-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// spectrogram-basic: Spectrogram Time-Frequency Heatmap\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Signal (deterministic linear chirp, no RNG needed) --------------------\nconst sampleRate = 8000; // Hz\nconst duration = 3.0; // seconds\nconst numSamples = Math.round(sampleRate * duration);\nconst startFreq = 200; // Hz\nconst endFreq = 3000; // Hz\nconst chirpRate = (endFreq - startFreq) / duration;\n\n// Small fixed-seed LCG for reproducible broadband background noise (no Math.random).\nlet noiseSeed = 42;\nfunction nextNoise() {\n  noiseSeed = (noiseSeed * 1103515245 + 12345) & 0x7fffffff;\n  return (noiseSeed / 0x7fffffff) * 2 - 1;\n}\n\nconst noiseAmplitude = 0.05;\nconst signal = new Float64Array(numSamples);\nfor (let n = 0; n < numSamples; n++) {\n  const time = n / sampleRate;\n  const phase = 2 * Math.PI * (startFreq * time + (chirpRate * time * time) / 2);\n  signal[n] = Math.sin(phase) + noiseAmplitude * nextNoise();\n}\n\n// --- Short-time Fourier transform --------------------------------------------\nconst windowSize = 256; // must be a power of two for the FFT below\nconst hopSize = 200;\nconst numFreqBins = windowSize / 2 + 1;\nconst numFrames = Math.floor((numSamples - windowSize) / hopSize) + 1;\n\nconst hannWindow = new Float64Array(windowSize);\nfor (let i = 0; i < windowSize; i++) {\n  hannWindow[i] = 0.5 - 0.5 * Math.cos((2 * Math.PI * i) / (windowSize - 1));\n}\n\n// In-place iterative radix-2 Cooley-Tukey FFT (real/imag arrays, length = power of two).\nfunction fft(real, imag) {\n  const n = real.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      [real[i], real[j]] = [real[j], real[i]];\n      [imag[i], imag[j]] = [imag[j], imag[i]];\n    }\n  }\n  for (let len = 2; len <= n; len <<= 1) {\n    const angle = (-2 * Math.PI) / len;\n    const wr = Math.cos(angle);\n    const wi = Math.sin(angle);\n    for (let i = 0; i < n; i += len) {\n      let curWr = 1;\n      let curWi = 0;\n      for (let k = 0; k < len / 2; k++) {\n        const evenR = real[i + k];\n        const evenI = imag[i + k];\n        const oddR = real[i + k + len / 2] * curWr - imag[i + k + len / 2] * curWi;\n        const oddI = real[i + k + len / 2] * curWi + imag[i + k + len / 2] * curWr;\n        real[i + k] = evenR + oddR;\n        imag[i + k] = evenI + oddI;\n        real[i + k + len / 2] = evenR - oddR;\n        imag[i + k + len / 2] = evenI - oddI;\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 timeLabels = [];\nconst freqLabels = [];\nfor (let f = 0; f < numFreqBins; f++) {\n  freqLabels.push(Math.round((f * sampleRate) / windowSize));\n}\n\nlet peakDb = -Infinity;\nconst powerDb = [];\nfor (let frame = 0; frame < numFrames; frame++) {\n  const start = frame * hopSize;\n  timeLabels.push((start / sampleRate).toFixed(2));\n\n  const real = new Float64Array(windowSize);\n  const imag = new Float64Array(windowSize);\n  for (let i = 0; i < windowSize; i++) {\n    real[i] = signal[start + i] * hannWindow[i];\n  }\n  fft(real, imag);\n\n  for (let f = 0; f < numFreqBins; f++) {\n    const magnitude = Math.sqrt(real[f] * real[f] + imag[f] * imag[f]) / windowSize;\n    const db = 20 * Math.log10(magnitude + 1e-8);\n    if (db > peakDb) peakDb = db;\n    powerDb.push([frame, f, db]);\n  }\n}\n\n// Normalize to dB relative to the signal's peak, floored at -80 dB.\nconst heatmapData = powerDb.map(([frame, f, db]) => [frame, f, Math.round(Math.max(db - peakDb, -80) * 10) / 10]);\n\n// --- Init ---------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option ---------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"spectrogram-basic · javascript · echarts · anyplot.ai\",\n    subtext: \"Linear chirp 200 Hz → 3000 Hz rising through a broadband noise floor\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22 },\n    subtextStyle: { color: t.inkSoft, fontSize: 14 },\n  },\n  tooltip: {\n    formatter: (params) => `${params.value[2]} dB`,\n  },\n  grid: { left: 100, right: 110, top: 110, bottom: 90 },\n  xAxis: {\n    type: \"category\",\n    data: timeLabels,\n    name: \"Time (s)\",\n    nameLocation: \"middle\",\n    nameGap: 45,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 13, interval: Math.round(numFrames / 8) },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"category\",\n    data: freqLabels,\n    name: \"Frequency (Hz)\",\n    nameLocation: \"middle\",\n    nameGap: 65,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 13, interval: Math.round(numFreqBins / 8) },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  visualMap: {\n    min: -80,\n    max: 0,\n    calculable: false,\n    orient: \"vertical\",\n    itemWidth: 16,\n    itemHeight: 220,\n    right: 20,\n    top: \"middle\",\n    text: [\"0 dB\", \"-80 dB\"],\n    textStyle: { color: t.inkSoft, fontSize: 13 },\n    inRange: { color: t.seq },\n  },\n  series: [\n    {\n      type: \"heatmap\",\n      data: heatmapData,\n      itemStyle: { borderWidth: 0 },\n      progressive: 4000,\n    },\n  ],\n});\n"}