{"spec_id":"scatter-constellation-diagram","library":"d3","language":"javascript","code":"// anyplot.ai\n// scatter-constellation-diagram: Digital Modulation Constellation Diagram\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-18\n\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// Equal margins on all sides for square inner plot (equal aspect ratio)\nconst margin = { top: 80, right: 80, bottom: 100, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// Park-Miller LCG for reproducible deterministic data (no seeded RNG in browser)\nlet _s = 42;\nfunction rand() {\n  _s = (_s * 16807) % 2147483647;\n  return _s / 2147483647;\n}\nfunction randn() {\n  return Math.sqrt(-2 * Math.log(rand())) * Math.cos(2 * Math.PI * rand());\n}\n\n// 16-QAM ideal constellation points: 4×4 grid at ±1, ±3\nconst levels = [-3, -1, 1, 3];\nconst idealPoints = [];\nfor (const q of levels) {\n  for (const i of levels) {\n    idealPoints.push({ i, q });\n  }\n}\n\n// Received symbols: Gaussian noise (σ = 0.115 per dimension → EVM ≈ 5.2%)\nconst NOISE_STD = 0.115;\nconst symbols = [];\nfor (let k = 0; k < 800; k++) {\n  const idx = Math.floor(rand() * 16);\n  const ref = idealPoints[idx];\n  symbols.push({ i: ref.i + randn() * NOISE_STD, q: ref.q + randn() * NOISE_STD, idx });\n}\n\n// Compute EVM from generated data\nconst signalPower = idealPoints.reduce((s, p) => s + p.i * p.i + p.q * p.q, 0) / idealPoints.length;\nconst errorPower = symbols.reduce((s, sym) => {\n  const ref = idealPoints[sym.idx];\n  return s + (sym.i - ref.i) ** 2 + (sym.q - ref.q) ** 2;\n}, 0) / symbols.length;\nconst evmPct = (Math.sqrt(errorPower / signalPower) * 100).toFixed(1);\n\n// SVG mount\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\n\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// Clip path — keeps scatter symbols inside plot bounds\nsvg.append(\"defs\").append(\"clipPath\").attr(\"id\", \"clip\")\n  .append(\"rect\").attr(\"width\", iw).attr(\"height\", ih);\n\n// Scales — symmetric domain, equal aspect ratio (iw === ih for square canvas)\nconst EXTENT = 4.2;\nconst x = d3.scaleLinear().domain([-EXTENT, EXTENT]).range([0, iw]);\nconst y = d3.scaleLinear().domain([-EXTENT, EXTENT]).range([ih, 0]);\n\n// Decision boundary lines at 0, ±2 (midpoints between adjacent QAM symbol groups)\nconst boundaries = [-2, 0, 2];\nconst gBounds = g.append(\"g\").attr(\"clip-path\", \"url(#clip)\");\n\ngBounds.selectAll(\".bv\").data(boundaries).join(\"line\")\n  .attr(\"x1\", d => x(d)).attr(\"x2\", d => x(d))\n  .attr(\"y1\", 0).attr(\"y2\", ih)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-opacity\", 0.28)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"8,5\");\n\ngBounds.selectAll(\".bh\").data(boundaries).join(\"line\")\n  .attr(\"y1\", d => y(d)).attr(\"y2\", d => y(d))\n  .attr(\"x1\", 0).attr(\"x2\", iw)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-opacity\", 0.28)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"8,5\");\n\n// Received symbols — small semi-transparent circles (Imprint palette position 1)\ng.append(\"g\").attr(\"clip-path\", \"url(#clip)\")\n  .selectAll(\"circle\").data(symbols).join(\"circle\")\n  .attr(\"cx\", d => x(d.i))\n  .attr(\"cy\", d => y(d.q))\n  .attr(\"r\", 4)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"fill-opacity\", 0.45);\n\n// Ideal constellation points — red X crosses (matte red, Imprint position 5)\nconst gIdeal = g.append(\"g\");\nidealPoints.forEach(pt => {\n  const cx = x(pt.i);\n  const cy = y(pt.q);\n  const s = 14;\n  gIdeal.append(\"line\")\n    .attr(\"x1\", cx - s).attr(\"y1\", cy - s)\n    .attr(\"x2\", cx + s).attr(\"y2\", cy + s)\n    .attr(\"stroke\", t.palette[4])\n    .attr(\"stroke-width\", 3.5)\n    .attr(\"stroke-linecap\", \"round\");\n  gIdeal.append(\"line\")\n    .attr(\"x1\", cx + s).attr(\"y1\", cy - s)\n    .attr(\"x2\", cx - s).attr(\"y2\", cy + s)\n    .attr(\"stroke\", t.palette[4])\n    .attr(\"stroke-width\", 3.5)\n    .attr(\"stroke-linecap\", \"round\");\n});\n\n// Axes\nconst xAxisG = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).tickValues([-3, -1, 0, 1, 3]).tickSize(6).tickPadding(8));\n\nconst yAxisG = g.append(\"g\")\n  .call(d3.axisLeft(y).tickValues([-3, -1, 0, 1, 3]).tickSize(6).tickPadding(8));\n\nfor (const ax of [xAxisG, yAxisG]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\");\n  ax.selectAll(\".tick line\").attr(\"stroke\", t.inkSoft);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// Axis labels\nsvg.append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", height - 22)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"In-Phase (I)\");\n\nsvg.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -(margin.top + ih / 2))\n  .attr(\"y\", 26)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Quadrature (Q)\");\n\n// EVM annotation (bottom-right of plot area)\ng.append(\"text\")\n  .attr(\"x\", iw - 12)\n  .attr(\"y\", ih - 16)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"17px\")\n  .style(\"font-weight\", \"700\")\n  .text(`EVM = ${evmPct}%`);\n\n// Legend (upper right)\nconst legendItems = [\n  { label: \"Received symbols\", color: t.palette[0], shape: \"circle\" },\n  { label: \"Ideal points (16-QAM)\", color: t.palette[4], shape: \"cross\" },\n];\nconst lx = iw - 270;\nconst ly0 = 22;\nconst gLeg = g.append(\"g\");\n\nlegendItems.forEach((item, i) => {\n  const ly = ly0 + i * 38;\n  if (item.shape === \"circle\") {\n    gLeg.append(\"circle\")\n      .attr(\"cx\", lx + 12).attr(\"cy\", ly + 10)\n      .attr(\"r\", 7)\n      .attr(\"fill\", item.color)\n      .attr(\"fill-opacity\", 0.6);\n  } else {\n    const s = 9;\n    gLeg.append(\"line\")\n      .attr(\"x1\", lx + 3).attr(\"y1\", ly + 1)\n      .attr(\"x2\", lx + 21).attr(\"y2\", ly + 19)\n      .attr(\"stroke\", item.color).attr(\"stroke-width\", 3).attr(\"stroke-linecap\", \"round\");\n    gLeg.append(\"line\")\n      .attr(\"x1\", lx + 21).attr(\"y1\", ly + 1)\n      .attr(\"x2\", lx + 3).attr(\"y2\", ly + 19)\n      .attr(\"stroke\", item.color).attr(\"stroke-width\", 3).attr(\"stroke-linecap\", \"round\");\n  }\n  gLeg.append(\"text\")\n    .attr(\"x\", lx + 30).attr(\"y\", ly + 15)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"15px\")\n    .text(item.label);\n});\n\n// Title (60 chars — under 67 baseline, no font size scaling needed)\nsvg.append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"scatter-constellation-diagram · javascript · d3 · anyplot.ai\");\n"}