{"spec_id":"spectrogram-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// spectrogram-basic: Spectrogram Time-Frequency Heatmap\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-09\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\n// --- Signal (deterministic linear chirp + noise floor) ----------------------\nconst SAMPLE_RATE = 4000; // Hz\nconst DURATION = 2.5; // seconds\nconst N_SAMPLES = SAMPLE_RATE * DURATION; // 10000 samples\nconst F_START = 150; // Hz — chirp start frequency\nconst F_END = 1600; // Hz — chirp end frequency (within Nyquist)\n\n// Deterministic LCG seeded at 42 (no Math.random — must be reproducible)\nfunction makeLCG(seed) {\n  let s = seed >>> 0;\n  return () => {\n    s = ((s * 1664525) >>> 0) + 1013904223 >>> 0;\n    return s / 0x100000000;\n  };\n}\nconst rand = makeLCG(42);\n\nconst signal = new Float64Array(N_SAMPLES);\nfor (let n = 0; n < N_SAMPLES; n++) {\n  const time = n / SAMPLE_RATE;\n  const phase = 2 * Math.PI * (F_START * time + ((F_END - F_START) / (2 * DURATION)) * time * time);\n  signal[n] = Math.sin(phase) + (rand() - 0.5) * 0.08;\n}\n\n// --- Short-Time Fourier Transform (iterative radix-2 Cooley-Tukey) ----------\nconst WINDOW = 256; // samples per frame (power of 2)\nconst HOP = 64; // samples between frame starts (WINDOW / 4, for finer time resolution)\nconst FREQ_BINS = WINDOW / 2; // one-sided spectrum bins\nconst TIME_STEP = HOP / SAMPLE_RATE; // seconds per frame\nconst FREQ_STEP = SAMPLE_RATE / WINDOW; // Hz per bin\nconst N_FRAMES = Math.floor((N_SAMPLES - WINDOW) / HOP) + 1;\n\n// Hann window\nconst hann = Array.from({ length: WINDOW }, (_, n) => 0.5 - 0.5 * Math.cos((2 * Math.PI * n) / (WINDOW - 1)));\n\nfunction 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        curWi = curWr * wi + curWi * wr;\n        curWr = nextWr;\n      }\n    }\n  }\n}\n\n// power[frame][bin] in dB\nconst power = [];\nlet maxDb = -Infinity;\nfor (let f = 0; f < N_FRAMES; f++) {\n  const start = f * HOP;\n  const re = new Float64Array(WINDOW);\n  const im = new Float64Array(WINDOW);\n  for (let n = 0; n < WINDOW; n++) re[n] = signal[start + n] * hann[n];\n  fft(re, im);\n  const row = new Float64Array(FREQ_BINS);\n  for (let k = 0; k < FREQ_BINS; k++) {\n    const mag = (2 / WINDOW) * Math.sqrt(re[k] * re[k] + im[k] * im[k]);\n    const db = 20 * Math.log10(mag + 1e-9);\n    row[k] = db;\n    if (db > maxDb) maxDb = db;\n  }\n  power.push(row);\n}\nconst MIN_DB = maxDb - 60; // wide enough to keep noise-floor texture visible instead of clipping to solid green\nconst MAX_DB = maxDb;\nconst TOTAL_SEC = ((N_FRAMES - 1) * HOP + WINDOW) / SAMPLE_RATE;\n\n// Imprint sequential colormap: seq[0]=brand green → seq[1]=blue\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(norm) {\n  const v = Math.min(1, Math.max(0, norm));\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// Spectrogram cells, drawn as a filled time-frequency grid using MUI X scale hooks\nfunction SpectrogramCells() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n\n  return (\n    <>\n      {power.flatMap((row, f) =>\n        Array.from(row).map((db, k) => {\n          const norm = (db - MIN_DB) / (MAX_DB - MIN_DB);\n          const x0 = xScale(f * TIME_STEP);\n          const x1 = xScale((f + 1) * TIME_STEP);\n          const yTop = yScale((k + 1) * FREQ_STEP);\n          const yBottom = yScale(k * FREQ_STEP);\n          return (\n            <rect\n              key={`${f}-${k}`}\n              x={x0}\n              y={yTop}\n              width={x1 - x0 + 0.5}\n              height={yBottom - yTop + 0.5}\n              fill={seqColor(norm)}\n            />\n          );\n        })\n      )}\n    </>\n  );\n}\n\n// Colorbar gradient (power in dB) 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\">\n        {Math.round(MAX_DB)} dB\n      </text>\n      <text x={cbX + cbW / 2} y={top + gH + 16} textAnchor=\"middle\" fontSize={13} fill={t.inkSoft} fontFamily=\"Inter, system-ui, sans-serif\">\n        {Math.round(MIN_DB)} dB\n      </text>\n      <text\n        x={cbX + cbW + 18}\n        y={top + gH / 2}\n        textAnchor=\"middle\"\n        fontSize={14}\n        fill={t.inkSoft}\n        fontFamily=\"Inter, system-ui, sans-serif\"\n        transform={`rotate(90, ${cbX + cbW + 18}, ${top + gH / 2})`}\n      >\n        Power (dB)\n      </text>\n    </>\n  );\n}\n\nfunction ChartTitle() {\n  const { top } = useDrawingArea();\n  return (\n    <text x={width / 2} y={top - 46} textAnchor=\"middle\" fontSize={22} fontWeight={500} fill={t.ink} fontFamily=\"Inter, system-ui, sans-serif\">\n      spectrogram-basic · javascript · muix · anyplot.ai\n    </text>\n  );\n}\n\n// Custom y-axis label — drawn manually (instead of ChartsYAxis's built-in `label`)\n// so its fixed inner offset never collides with the 4-digit \"2000\"-style tick labels.\nfunction YAxisLabel() {\n  const { left, top, height: gH } = useDrawingArea();\n  const x = left - 68;\n  const y = top + gH / 2;\n  return (\n    <text x={x} y={y} textAnchor=\"middle\" fontSize={14} fill={t.inkSoft} fontFamily=\"Inter, system-ui, sans-serif\" transform={`rotate(-90, ${x}, ${y})`}>\n      Frequency (Hz)\n    </text>\n  );\n}\n\nexport default function Chart() {\n  return (\n    <ChartContainer\n      width={width}\n      height={height}\n      series={[]}\n      xAxis={[\n        {\n          scaleType: \"linear\",\n          min: 0,\n          max: TOTAL_SEC,\n          label: \"Time (s)\",\n          valueFormatter: (v) => `${v.toFixed(1)}`,\n        },\n      ]}\n      yAxis={[\n        {\n          scaleType: \"linear\",\n          min: 0,\n          max: FREQ_BINS * FREQ_STEP,\n          valueFormatter: (v) => `${Math.round(v)}`,\n        },\n      ]}\n      margin={{ left: 100, right: 90, top: 80, bottom: 70 }}\n    >\n      <ChartTitle />\n      <SpectrogramCells />\n      <Colorbar />\n      <ChartsXAxis />\n      <ChartsYAxis />\n      <YAxisLabel />\n    </ChartContainer>\n  );\n}\n"}