{"spec_id":"scatter-complex-plane","library":"d3","language":"javascript","code":"// anyplot.ai\n// scatter-complex-plane: Complex Plane Visualization (Argand Diagram)\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 87/100 | Created: 2026-08-26\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// 5th roots of unity: z_k = e^(i*2*pi*k/5) — foundational for FFT / number theory,\n// all sitting exactly on the unit circle by construction.\nconst subscripts = [\"₀\", \"₁\", \"₂\", \"₃\", \"₄\"];\nconst rootsOfUnity = d3.range(5).map((k) => {\n  const theta = (2 * Math.PI * k) / 5;\n  return {\n    label: `z${subscripts[k]}`,\n    re: Math.cos(theta),\n    im: Math.sin(theta),\n    category: \"5th roots of unity\",\n  };\n});\n\n// A few arbitrary complex numbers, well outside the unit circle and spaced\n// away from the roots' angles so vectors and annotations stay uncluttered\nconst arbitraryPoints = [\n  { label: \"w₁\", re: 1.88, im: 0.68, category: \"Arbitrary points\" },\n  { label: \"w₂\", re: -0.36, im: -2.07, category: \"Arbitrary points\" },\n  { label: \"w₃\", re: 1.72, im: -0.8, category: \"Arbitrary points\" },\n];\n\nconst points = [...rootsOfUnity, ...arbitraryPoints];\n\nconst categories = [\"5th roots of unity\", \"Arbitrary points\"];\nconst color = d3.scaleOrdinal().domain(categories).range([t.palette[0], t.palette[1]]);\nconst shapeOf = { \"5th roots of unity\": \"circle\", \"Arbitrary points\": \"square\" };\n\n// --- Layout: force a true square plot area so the unit circle stays circular\nconst margin = { top: 160, right: 110, bottom: 90, left: 110 };\nconst availW = width - margin.left - margin.right;\nconst availH = height - margin.top - margin.bottom;\nconst side = Math.min(availW, availH);\nconst plotX = margin.left + (availW - side) / 2;\nconst plotY = margin.top + (availH - side) / 2;\n\nconst maxAbs = d3.max(points, (d) => Math.hypot(d.re, d.im));\nconst lim = Math.ceil(maxAbs * 1.35 * 2) / 2; // round outward to nearest 0.5\nconst gridTicks = d3.range(-Math.floor(lim), Math.floor(lim) + 1);\n\nconst xScale = d3.scaleLinear().domain([-lim, lim]).range([0, side]);\nconst yScale = d3.scaleLinear().domain([-lim, lim]).range([side, 0]);\n\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${plotX},${plotY})`);\n\n// --- Faint reference grid at integer real/imaginary values -------------------\nfor (const gt of gridTicks) {\n  if (gt === 0) continue;\n  g.append(\"line\")\n    .attr(\"x1\", xScale(gt))\n    .attr(\"x2\", xScale(gt))\n    .attr(\"y1\", 0)\n    .attr(\"y2\", side)\n    .attr(\"stroke\", t.grid)\n    .attr(\"stroke-width\", 1);\n  g.append(\"line\")\n    .attr(\"x1\", 0)\n    .attr(\"x2\", side)\n    .attr(\"y1\", yScale(gt))\n    .attr(\"y2\", yScale(gt))\n    .attr(\"stroke\", t.grid)\n    .attr(\"stroke-width\", 1);\n}\n\n// --- Unit circle reference (dashed) -------------------------------------------\ng.append(\"circle\")\n  .attr(\"cx\", xScale(0))\n  .attr(\"cy\", yScale(0))\n  .attr(\"r\", xScale(1) - xScale(0))\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 2)\n  .attr(\"stroke-dasharray\", \"8,7\")\n  .attr(\"opacity\", 0.6);\n\n// --- Real / imaginary axes through the origin, with labeled ticks ------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${yScale(0)})`)\n  .call(\n    d3\n      .axisBottom(xScale)\n      .tickValues(gridTicks.filter((v) => v !== 0))\n      .tickSize(8)\n  );\nxAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\");\nxAxis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1.5);\n\nconst yAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(${xScale(0)},0)`)\n  .call(\n    d3\n      .axisLeft(yScale)\n      .tickValues(gridTicks.filter((v) => v !== 0))\n      .tickFormat((d) => `${d}i`)\n      .tickSize(8)\n  );\nyAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\");\nyAxis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nyAxis.select(\".domain\").attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1.5);\n\n// Axis end labels + origin marker\ng.append(\"text\")\n  .attr(\"x\", side + 16)\n  .attr(\"y\", yScale(0) + 6)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Re\");\ng.append(\"text\")\n  .attr(\"x\", xScale(0))\n  .attr(\"y\", -18)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Im\");\ng.append(\"text\")\n  .attr(\"x\", xScale(0) - 12)\n  .attr(\"y\", yScale(0) + 22)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"0\");\n\n// --- Arrowhead marker defs (one per category color) ---------------------------\nconst defs = svg.append(\"defs\");\ncategories.forEach((cat, i) => {\n  defs\n    .append(\"marker\")\n    .attr(\"id\", `arrow-cat-${i}`)\n    .attr(\"viewBox\", \"0 0 10 10\")\n    .attr(\"refX\", 9)\n    .attr(\"refY\", 5)\n    .attr(\"markerWidth\", 7)\n    .attr(\"markerHeight\", 7)\n    .attr(\"orient\", \"auto-start-reverse\")\n    .append(\"path\")\n    .attr(\"d\", \"M0,0 L10,5 L0,10 Z\")\n    .attr(\"fill\", color(cat));\n});\n\n// --- Vectors from the origin to each complex number ---------------------------\nconst markerRadius = 13;\nconst ox = xScale(0);\nconst oy = yScale(0);\n\npoints.forEach((d) => {\n  const px = xScale(d.re);\n  const py = yScale(d.im);\n  const dist = Math.hypot(px - ox, py - oy);\n  const shrink = dist > 0 ? (dist - markerRadius - 5) / dist : 0;\n  const catIndex = categories.indexOf(d.category);\n  g.append(\"line\")\n    .attr(\"x1\", ox)\n    .attr(\"y1\", oy)\n    .attr(\"x2\", ox + (px - ox) * shrink)\n    .attr(\"y2\", oy + (py - oy) * shrink)\n    .attr(\"stroke\", color(d.category))\n    .attr(\"stroke-width\", 3)\n    .attr(\"opacity\", 0.85)\n    .attr(\"marker-end\", `url(#arrow-cat-${catIndex})`);\n});\n\n// --- Points + rectangular/polar annotations ------------------------------------\nconst labelOffset = 56;\npoints.forEach((d) => {\n  const px = xScale(d.re);\n  const py = yScale(d.im);\n  const dirLen = Math.hypot(px - ox, py - oy) || 1;\n  const ux = (px - ox) / dirLen;\n  const uy = (py - oy) / dirLen;\n\n  const marker = g.append(\"g\").attr(\"transform\", `translate(${px},${py})`);\n  if (shapeOf[d.category] === \"circle\") {\n    marker\n      .append(\"circle\")\n      .attr(\"r\", markerRadius)\n      .attr(\"fill\", color(d.category))\n      .attr(\"stroke\", t.pageBg)\n      .attr(\"stroke-width\", 2.5);\n  } else {\n    const s = markerRadius * 1.7;\n    marker\n      .append(\"rect\")\n      .attr(\"x\", -s / 2)\n      .attr(\"y\", -s / 2)\n      .attr(\"width\", s)\n      .attr(\"height\", s)\n      .attr(\"fill\", color(d.category))\n      .attr(\"stroke\", t.pageBg)\n      .attr(\"stroke-width\", 2.5);\n  }\n\n  const r = Math.hypot(d.re, d.im);\n  const thetaDeg = (Math.atan2(d.im, d.re) * 180) / Math.PI;\n  const sign = d.im >= 0 ? \"+\" : \"−\";\n  const rectForm = `${d.label}  ${d.re.toFixed(2)}${sign}${Math.abs(d.im).toFixed(2)}i`;\n  const polarForm = `r=${r.toFixed(2)}, θ=${thetaDeg.toFixed(0)}°`;\n  const anchor = ux >= 0 ? \"start\" : \"end\";\n  // Near-horizontal vectors (point sits close to the real axis) always go up:\n  // the x-axis tick labels live in the band just below the axis, so a\n  // downward annotation would crowd them (e.g. z0 lands on the \"2.0\" tick).\n  const goesDown = Math.abs(uy) < 0.15 ? false : uy >= 0;\n\n  const label = g\n    .append(\"g\")\n    .attr(\"transform\", `translate(${px + ux * labelOffset},${py + uy * labelOffset})`);\n  label\n    .append(\"text\")\n    .attr(\"text-anchor\", anchor)\n    .attr(\"dy\", goesDown ? \"0.4em\" : \"-1.75em\")\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", \"16px\")\n    .style(\"font-weight\", \"600\")\n    .text(rectForm);\n  label\n    .append(\"text\")\n    .attr(\"text-anchor\", anchor)\n    .attr(\"dy\", goesDown ? \"1.75em\" : \"-0.4em\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"13px\")\n    .style(\"font-style\", \"italic\")\n    .text(polarForm);\n});\n\n// --- Legend --------------------------------------------------------------------\nconst legend = svg.append(\"g\").attr(\"transform\", `translate(${plotX},${margin.top - 68})`);\nlet legendX = 0;\ncategories.forEach((cat, i) => {\n  const item = legend.append(\"g\").attr(\"transform\", `translate(${legendX},0)`);\n  if (shapeOf[cat] === \"circle\") {\n    item.append(\"circle\").attr(\"r\", 9).attr(\"cy\", -5).attr(\"fill\", color(cat));\n  } else {\n    item.append(\"rect\").attr(\"x\", -9).attr(\"y\", -14).attr(\"width\", 18).attr(\"height\", 18).attr(\"fill\", color(cat));\n  }\n  const label = item\n    .append(\"text\")\n    .attr(\"x\", 22)\n    .attr(\"y\", 0)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"15px\")\n    .text(cat);\n  legendX += label.node().getBBox().width + 60;\n});\n\n// --- Title -----------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"20px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"5th Roots of Unity · scatter-complex-plane · javascript · d3 · anyplot.ai\");\n"}