{"spec_id":"spectrum-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// spectrum-basic: Frequency Spectrum Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 90, right: 60, bottom: 90, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Synthetic audio spectrum: a 440 Hz fundamental (A4) with decaying harmonics\n// riding on a pink-noise-shaped floor, spanning the audible range 20 Hz-20 kHz.\nfunction mulberry32(seed) {\n  let a = seed;\n  return function () {\n    a |= 0;\n    a = (a + 0x6d2b79f5) | 0;\n    let x = Math.imul(a ^ (a >>> 15), 1 | a);\n    x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x;\n    return ((x ^ (x >>> 14)) >>> 0) / 4294967296;\n  };\n}\nconst rand = mulberry32(42);\n\nconst FUNDAMENTAL = 440;\nconst harmonics = d3.range(1, 7).map((n) => ({\n  freq: FUNDAMENTAL * n,\n  db: -5 - 7 * (n - 1),\n  widthOct: 0.02,\n}));\n\nconst N = 1024;\nconst fMin = 20;\nconst fMax = 20000;\nconst spectrum = d3.range(N).map((i) => {\n  const freq = fMin * Math.pow(fMax / fMin, i / (N - 1));\n\n  // Pink-noise-shaped floor: -70 dB at 20 Hz sloping to -95 dB at 20 kHz.\n  const octaveFrac =\n    (Math.log10(freq) - Math.log10(fMin)) /\n    (Math.log10(fMax) - Math.log10(fMin));\n  const floorDb = -70 - 25 * octaveFrac;\n  let linear = Math.pow(10, floorDb / 20);\n\n  for (const h of harmonics) {\n    const distOct = Math.log2(freq / h.freq);\n    linear +=\n      Math.pow(10, h.db / 20) *\n      Math.exp(-0.5 * Math.pow(distOct / h.widthOct, 2));\n  }\n\n  const ripple = 1 + (rand() - 0.5) * 0.35;\n  linear *= ripple;\n\n  return { freq, db: 20 * Math.log10(linear) };\n});\n\nconst peaks = harmonics.map((h) => {\n  const nearest = spectrum.reduce((best, d) =>\n    Math.abs(Math.log2(d.freq / h.freq)) <\n    Math.abs(Math.log2(best.freq / h.freq))\n      ? d\n      : best,\n  );\n  return nearest;\n});\n\n// Peak markers scale with harmonic prominence (linear amplitude, not dB) via a\n// sqrt scale, so marker *area* — not radius — tracks acoustic power. The\n// fundamental reads as the visually dominant peak; higher harmonics taper off.\nconst peakAmpLinear = harmonics.map((h) => Math.pow(10, h.db / 20));\nconst rScale = d3\n  .scaleSqrt()\n  .domain(d3.extent(peakAmpLinear))\n  .range([4.5, 10]);\n\n// --- SVG mount ----------------------------------------------------------------\nconst svg = d3\n  .select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\nconst g = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales ---------------------------------------------------------------\nconst x = d3.scaleLog().base(10).domain([fMin, fMax]).range([0, iw]);\nconst yMin = -100;\nconst yMax = 0;\nconst y = d3.scaleLinear().domain([yMin, yMax]).range([ih, 0]);\n\n// --- Gridlines (y-axis only, subtle) ---------------------------------------\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(y.ticks(6))\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", (d) => y(d))\n  .attr(\"y2\", (d) => y(d))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// --- Area + line -------------------------------------------------------------\nconst area = d3\n  .area()\n  .x((d) => x(d.freq))\n  .y0(y(yMin))\n  .y1((d) => y(d.db))\n  .curve(d3.curveMonotoneX);\n\nconst line = d3\n  .line()\n  .x((d) => x(d.freq))\n  .y((d) => y(d.db))\n  .curve(d3.curveMonotoneX);\n\ng.append(\"path\")\n  .datum(spectrum)\n  .attr(\"d\", area)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"fill-opacity\", 0.28);\n\ng.append(\"path\")\n  .datum(spectrum)\n  .attr(\"d\", line)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 3);\n\n// --- Harmonic peak markers --------------------------------------------------\ng.selectAll(\"circle\")\n  .data(peaks)\n  .join(\"circle\")\n  .attr(\"cx\", (d) => x(d.freq))\n  .attr(\"cy\", (d) => y(d.db))\n  .attr(\"r\", (d, i) => rScale(peakAmpLinear[i]))\n  .attr(\"fill\", t.palette[0])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2.5);\n\n// --- Harmonic peak labels ----------------------------------------------------\n// Direct numeric labels (\"440 Hz\") on each harmonic, positioned above its\n// marker and then corrected with getBBox — real measured layout from the\n// browser's text engine, not an estimate — so labels never clip the plot\n// edges or collide with a neighbor even as marker radius/label width vary.\nconst peakLabels = g\n  .selectAll(\".peak-label\")\n  .data(peaks)\n  .join(\"text\")\n  .attr(\"class\", \"peak-label\")\n  .attr(\"x\", (d) => x(d.freq))\n  .attr(\"y\", (d, i) => y(d.db) - rScale(peakAmpLinear[i]) - 10)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"13px\")\n  .style(\"font-weight\", \"600\")\n  .text((d, i) => `${harmonics[i].freq} Hz`);\n\nconst labelNodes = peakLabels.nodes();\nlabelNodes.forEach((node) => {\n  const bbox = node.getBBox();\n  if (bbox.x < 0) d3.select(node).attr(\"text-anchor\", \"start\").attr(\"x\", 0);\n  else if (bbox.x + bbox.width > iw)\n    d3.select(node).attr(\"text-anchor\", \"end\").attr(\"x\", iw);\n});\nfor (let i = 1; i < labelNodes.length; i++) {\n  const prev = labelNodes[i - 1].getBBox();\n  const cur = labelNodes[i].getBBox();\n  const overlapsX = cur.x < prev.x + prev.width && cur.x + cur.width > prev.x;\n  const overlapsY = Math.abs(cur.y - prev.y) < prev.height + 4;\n  if (overlapsX && overlapsY) {\n    const sel = d3.select(labelNodes[i]);\n    sel.attr(\"y\", parseFloat(sel.attr(\"y\")) - (prev.height + 4));\n  }\n}\n\n// --- Axes --------------------------------------------------------------------\nconst xTickValues = [20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000];\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(\n    d3\n      .axisBottom(x)\n      .tickValues(xTickValues)\n      .tickFormat((d) => (d >= 1000 ? `${d / 1000}k` : `${d}`)),\n  );\n\nconst yAxis = g\n  .append(\"g\")\n  .call(d3.axisLeft(y).tickValues(d3.range(yMin, yMax + 1, 20)));\n\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Axis labels ---------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 64)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .text(\"Frequency (Hz)\");\n\ng.append(\"text\")\n  .attr(\"transform\", `translate(${-78},${ih / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .text(\"Amplitude (dB)\");\n\n// --- Title ---------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 48)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"spectrum-basic · javascript · d3 · anyplot.ai\");\n"}