{"spec_id":"scatter-constellation-diagram","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// scatter-constellation-diagram: Digital Modulation Constellation Diagram\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 91/100 | Created: 2026-06-18\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Deterministic LCG (seed 42) — no Math.random() in the render harness\nlet seed = 42;\nfunction lcg() {\n  seed = ((1664525 * seed + 1013904223) >>> 0);\n  return seed / 0x100000000;\n}\nfunction randn() {\n  const u1 = Math.max(lcg(), 1e-10);\n  const u2 = lcg();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// 16-QAM ideal constellation — 4×4 grid at {-3,-1,+1,+3}²\nconst levels = [-3, -1, 1, 3];\nconst idealPoints = [];\nfor (const q of levels) {\n  for (const i of levels) {\n    idealPoints.push({ x: i, y: q });\n  }\n}\n\n// Received symbols — 800 samples with per-axis Gaussian noise (σ = 0.20)\nconst noiseSigma = 0.20;\nconst receivedSymbols = [];\nfor (let n = 0; n < 800; n++) {\n  const p = idealPoints[Math.floor(lcg() * 16)];\n  const noiseI = randn() * noiseSigma;\n  const noiseQ = randn() * noiseSigma;\n  receivedSymbols.push({ x: p.x + noiseI, y: p.y + noiseQ });\n}\n\n// Theoretical RMS EVM: sqrt(2σ² / P_avg) × 100%\n// P_avg = mean(|s|²) = mean(i²) + mean(q²) = 10 for this 16-QAM grid\nconst evmPct = (Math.sqrt(2 * noiseSigma * noiseSigma / 10) * 100).toFixed(1);\n\n// Decision boundary lines — dashed at {-2, 0, +2} on both axes\nconst mkBoundary = (axis, v) => ({\n  type: 'line',\n  label: '',\n  data: axis === 'v'\n    ? [{ x: v, y: -5 }, { x: v, y: 5 }]\n    : [{ x: -5, y: v }, { x: 5, y: v }],\n  borderColor: t.inkSoft,\n  borderWidth: 1.5,\n  borderDash: [8, 5],\n  pointRadius: 0,\n  fill: false,\n  tension: 0,\n});\nconst boundaries = [\n  ...[-2, 0, 2].map((v) => mkBoundary('v', v)),\n  ...[-2, 0, 2].map((v) => mkBoundary('h', v)),\n];\n\n// Mount\nconst canvas = document.createElement('canvas');\ndocument.getElementById('container').appendChild(canvas);\n\n// Chart\nnew Chart(canvas, {\n  type: 'scatter',\n  data: {\n    datasets: [\n      {\n        label: 'Received Symbols',\n        data: receivedSymbols,\n        backgroundColor: t.palette[0] + '80',\n        borderColor: t.inkSoft,\n        borderWidth: 0.5,\n        pointRadius: 5,\n        pointHoverRadius: 5,\n      },\n      {\n        label: 'Ideal Points (16-QAM)',\n        data: idealPoints,\n        backgroundColor: '#AE3030',\n        borderColor: '#AE3030',\n        pointStyle: 'cross',\n        pointRadius: 12,\n        pointBorderWidth: 4,\n        pointHoverRadius: 12,\n      },\n      ...boundaries,\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: 20 },\n    plugins: {\n      title: {\n        display: true,\n        text: 'scatter-constellation-diagram · javascript · chartjs · anyplot.ai',\n        color: t.ink,\n        font: { size: 22 },\n        padding: { top: 8, bottom: 14 },\n      },\n      legend: {\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          filter: (item) => item.text !== '',\n          usePointStyle: true,\n          pointStyleWidth: 14,\n          padding: 20,\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: 'linear',\n        min: -4.5,\n        max: 4.5,\n        ticks: { color: t.inkSoft, font: { size: 14 }, stepSize: 1 },\n        grid: { color: t.grid },\n        border: { display: false },\n        title: {\n          display: true,\n          text: 'In-Phase (I)',\n          color: t.ink,\n          font: { size: 16 },\n        },\n      },\n      y: {\n        type: 'linear',\n        min: -4.5,\n        max: 4.5,\n        ticks: { color: t.inkSoft, font: { size: 14 }, stepSize: 1 },\n        grid: { color: t.grid },\n        border: { display: false },\n        title: {\n          display: true,\n          text: 'Quadrature (Q)',\n          color: t.ink,\n          font: { size: 16 },\n        },\n      },\n    },\n  },\n  plugins: [\n    {\n      id: 'bgFill',\n      beforeDraw(chart) {\n        const { ctx } = chart;\n        ctx.save();\n        ctx.fillStyle = t.pageBg;\n        ctx.fillRect(0, 0, chart.width, chart.height);\n        ctx.restore();\n      },\n    },\n    {\n      // Force inner chartArea to a square so the I/Q geometry is preserved 1:1\n      id: 'squareAspect',\n      afterLayout(chart) {\n        const ca = chart.chartArea;\n        const w = ca.right - ca.left;\n        const h = ca.bottom - ca.top;\n        if (w > h) {\n          const d = (w - h) / 2;\n          ca.left += d;\n          ca.right -= d;\n        } else if (h > w) {\n          const d = (h - w) / 2;\n          ca.top += d;\n          ca.bottom -= d;\n        }\n      },\n    },\n    {\n      id: 'evmLabel',\n      afterDraw(chart) {\n        const { ctx, chartArea } = chart;\n        const fontFamily = Chart.defaults.font.family || 'sans-serif';\n        ctx.save();\n        ctx.font = `bold 17px ${fontFamily}`;\n        ctx.fillStyle = t.ink;\n        ctx.textAlign = 'right';\n        ctx.textBaseline = 'top';\n        ctx.fillText(`EVM = ${evmPct}%`, chartArea.right - 12, chartArea.top + 12);\n        ctx.restore();\n      },\n    },\n  ],\n});\n"}