{"spec_id":"nyquist-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// nyquist-basic: Nyquist Plot for Control Systems\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 88/100 | Created: 2026-06-17\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// G(s) = 48/((s+1)(s+2)(s+3)): Re[G(jω)] = 48rd/d², Im[G(jω)] = −48id/d²\n// where rd = 6−6ω², id = ω(11−ω²), d² = rd²+id²\n\n// 600 log-spaced frequencies ω ∈ [0.03, 30]\nconst N = 600;\nconst wMin = 0.03, wMax = 30;\nconst omegas = Array.from({ length: N }, (_, i) =>\n  wMin * Math.pow(wMax / wMin, i / (N - 1))\n);\nconst pos = omegas.map((w) => {\n  const rd = 6 - 6 * w * w, id = w * (11 - w * w), d2 = rd * rd + id * id;\n  return { re: 48 * rd / d2, im: -48 * id / d2, w };\n});\n// Negative-frequency mirror: reflect Im across real axis, reverse so it joins at ω→−∞\nconst neg = [...pos].reverse().map((p) => ({ re: p.re, im: -p.im }));\n\n// Layout — square inner plot centred in landscape canvas\nconst margin = { top: 72, right: 50, bottom: 80, left: 80 };\nconst iw = width  - margin.left - margin.right;\nconst ih = height - margin.top  - margin.bottom;\nconst ps = Math.min(iw, ih); // square side (height-limited: 748 px at 1600×900)\nconst ox = margin.left + (iw - ps) / 2; // left edge of square plot\nconst oy = margin.top;                   // top  edge of square plot\n\n// Equal-range scales for 1:1 aspect ratio — unit circle stays circular\nconst domRange = 12.5;\nconst xMid = 3.25, yMid = 0;\nconst xDom = [xMid - domRange / 2, xMid + domRange / 2]; // [−3, 9.5]\nconst yDom = [yMid - domRange / 2, yMid + domRange / 2]; // [−6.25, 6.25]\nconst xSc  = d3.scaleLinear().domain(xDom).range([0, ps]);\nconst ySc  = d3.scaleLinear().domain(yDom).range([ps, 0]);\n\n// SVG root\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width).attr(\"height\", height);\n\n// Defs: clip path + arrow marker\nconst defs = svg.append(\"defs\");\ndefs.append(\"clipPath\").attr(\"id\", \"nyq-clip\")\n  .append(\"rect\").attr(\"width\", ps).attr(\"height\", ps);\ndefs.append(\"marker\")\n  .attr(\"id\", \"arr\")\n  .attr(\"viewBox\", \"0 -5 10 10\")\n  .attr(\"refX\", 8).attr(\"refY\", 0)\n  .attr(\"markerWidth\", 7).attr(\"markerHeight\", 7)\n  .attr(\"orient\", \"auto\")\n  .append(\"path\").attr(\"d\", \"M0,-5L10,0L0,5Z\")\n  .attr(\"fill\", t.palette[0]);\n\nconst root = svg.append(\"g\").attr(\"transform\", `translate(${ox},${oy})`);\n\n// Plot area background must come BEFORE the clipped group so it stays behind the data\nroot.append(\"rect\").attr(\"width\", ps).attr(\"height\", ps)\n  .attr(\"fill\", t.pageBg);\n\nconst clipped = root.append(\"g\").attr(\"clip-path\", \"url(#nyq-clip)\");\n\n// Grid lines\n[-3,-2,-1,0,1,2,3,4,5,6,7,8,9].forEach((v) => {\n  clipped.append(\"line\")\n    .attr(\"x1\", xSc(v)).attr(\"x2\", xSc(v)).attr(\"y1\", 0).attr(\"y2\", ps)\n    .attr(\"stroke\", t.grid).attr(\"stroke-width\", 1);\n});\n[-6,-4,-2,0,2,4,6].forEach((v) => {\n  clipped.append(\"line\")\n    .attr(\"x1\", 0).attr(\"x2\", ps).attr(\"y1\", ySc(v)).attr(\"y2\", ySc(v))\n    .attr(\"stroke\", t.grid).attr(\"stroke-width\", 1);\n});\n\n// Zero-axes (real and imaginary)\nclipped.append(\"line\")\n  .attr(\"x1\", 0).attr(\"x2\", ps).attr(\"y1\", ySc(0)).attr(\"y2\", ySc(0))\n  .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1.5).attr(\"opacity\", 0.45);\nclipped.append(\"line\")\n  .attr(\"x1\", xSc(0)).attr(\"x2\", xSc(0)).attr(\"y1\", 0).attr(\"y2\", ps)\n  .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1.5).attr(\"opacity\", 0.45);\n\n// Unit circle (dashed reference)\nclipped.append(\"circle\")\n  .attr(\"cx\", xSc(0)).attr(\"cy\", ySc(0))\n  .attr(\"r\", xSc(1) - xSc(0))\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 2.5)\n  .attr(\"stroke-dasharray\", \"7,5\").attr(\"opacity\", 0.65);\n\n// Line generator\nconst lineGen = d3.line().x((d) => xSc(d.re)).y((d) => ySc(d.im));\n\n// Mirror curve — ω < 0 (dashed, semi-transparent)\nclipped.append(\"path\")\n  .datum(neg)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 2)\n  .attr(\"stroke-dasharray\", \"9,6\")\n  .attr(\"opacity\", 0.38)\n  .attr(\"d\", lineGen);\n\n// Main curve — ω > 0 (solid)\nclipped.append(\"path\")\n  .datum(pos)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 3.5)\n  .attr(\"d\", lineGen);\n\n// Direction arrows showing increasing frequency\n[70, 105, 185, 305, 435].forEach((i) => {\n  if (i + 4 >= pos.length) return;\n  const p1 = pos[i], p2 = pos[i + 4];\n  clipped.append(\"line\")\n    .attr(\"x1\", xSc(p1.re)).attr(\"y1\", ySc(p1.im))\n    .attr(\"x2\", xSc(p2.re)).attr(\"y2\", ySc(p2.im))\n    .attr(\"stroke\", t.palette[0]).attr(\"stroke-width\", 4.5)\n    .attr(\"marker-end\", \"url(#arr)\");\n});\n\n// Frequency-annotation dots and labels\n// Key ω values: 0.3, 1.0, 2.0, √11 (phase crossover)\nconst phaseXover = Math.sqrt(11); // ≈ 3.317 rad/s\nconst annotPts = [\n  { omega: 0.3,         label: \"ω = 0.3\",                   dx:  14, dy: -10, anchor: \"start\" },\n  { omega: 1.0,         label: \"ω = 1.0\",                   dx: -14, dy:  20, anchor: \"end\"   },\n  { omega: 2.0,         label: \"ω = 2.0\",                   dx: -14, dy:  20, anchor: \"end\"   },\n  { omega: phaseXover,  label: \"ω = 3.32 (phase crossover)\", dx:  12, dy:  38, anchor: \"start\" },\n];\nannotPts.forEach(({ omega, label, dx, dy, anchor }) => {\n  const rd = 6 - 6 * omega * omega, id = omega * (11 - omega * omega), d2 = rd * rd + id * id;\n  const p = { re: 48 * rd / d2, im: -48 * id / d2 };\n  clipped.append(\"circle\")\n    .attr(\"cx\", xSc(p.re)).attr(\"cy\", ySc(p.im)).attr(\"r\", 7)\n    .attr(\"fill\", t.palette[2]).attr(\"stroke\", t.pageBg).attr(\"stroke-width\", 2.5);\n  clipped.append(\"text\")\n    .attr(\"x\", xSc(p.re) + dx).attr(\"y\", ySc(p.im) + dy)\n    .attr(\"fill\", t.ink).style(\"font-size\", \"13px\").style(\"font-weight\", \"500\")\n    .attr(\"text-anchor\", anchor)\n    .text(label);\n});\n\n// Critical point (−1, 0) — red × marker\nconst cpx = xSc(-1), cpy = ySc(0), cs = 12;\n[[-cs,-cs,cs,cs],[cs,-cs,-cs,cs]].forEach(([x1,y1,x2,y2]) => {\n  clipped.append(\"line\")\n    .attr(\"x1\", cpx + x1).attr(\"y1\", cpy + y1)\n    .attr(\"x2\", cpx + x2).attr(\"y2\", cpy + y2)\n    .attr(\"stroke\", \"#AE3030\").attr(\"stroke-width\", 4)\n    .attr(\"stroke-linecap\", \"round\");\n});\nclipped.append(\"text\")\n  .attr(\"x\", cpx + 18).attr(\"y\", cpy - 14)\n  .attr(\"fill\", \"#AE3030\").style(\"font-size\", \"13px\").style(\"font-weight\", \"600\")\n  .attr(\"text-anchor\", \"start\")\n  .text(\"(−1, 0)\");\n\n// Starting-point dot (DC gain: ω → 0⁺)\nclipped.append(\"circle\")\n  .attr(\"cx\", xSc(pos[0].re)).attr(\"cy\", ySc(pos[0].im)).attr(\"r\", 8)\n  .attr(\"fill\", t.palette[0]).attr(\"stroke\", t.pageBg).attr(\"stroke-width\", 2.5);\n\n// Axes\nconst xAxisG = root.append(\"g\").attr(\"transform\", `translate(0,${ps})`).call(\n  d3.axisBottom(xSc).ticks(8).tickSizeOuter(0)\n);\nconst yAxisG = root.append(\"g\").call(\n  d3.axisLeft(ySc).ticks(7).tickSizeOuter(0)\n);\nfor (const ax of [xAxisG, yAxisG]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.inkSoft).attr(\"opacity\", 0.6);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft).attr(\"opacity\", 0.6);\n}\n\n// Axis labels\nroot.append(\"text\")\n  .attr(\"x\", ps / 2).attr(\"y\", ps + 58)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\").style(\"font-weight\", \"500\")\n  .text(\"Real Axis\");\n\nroot.append(\"text\")\n  .attr(\"transform\", `translate(-56,${ps / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\").style(\"font-weight\", \"500\")\n  .text(\"Imaginary Axis\");\n\n// Legend (right of square plot)\nconst lx  = ox + ps + 28;\nconst ly0 = oy + 30;\nconst legendItems = [\n  { color: t.palette[0], label: \"G(jω), ω > 0\", solid: true,  opacity: 1    },\n  { color: t.palette[0], label: \"G(jω), ω < 0\", solid: false, opacity: 0.5  },\n  { color: t.inkSoft,    label: \"Unit circle\",             solid: false, opacity: 0.65 },\n  { color: \"#AE3030\",    label: \"Critical (−1, 0)\",   type: \"x\"                   },\n  { color: t.palette[2], label: \"Annotated ω\",        type: \"dot\"                 },\n];\nlegendItems.forEach(({ color, label, solid, opacity, type }, i) => {\n  const ly = ly0 + i * 32;\n  if (type === \"x\") {\n    const cx2 = lx + 11, cy2 = ly;\n    [[-7,-7,7,7],[7,-7,-7,7]].forEach(([x1,y1,x2,y2]) => {\n      svg.append(\"line\")\n        .attr(\"x1\", cx2+x1).attr(\"y1\", cy2+y1)\n        .attr(\"x2\", cx2+x2).attr(\"y2\", cy2+y2)\n        .attr(\"stroke\", color).attr(\"stroke-width\", 3).attr(\"stroke-linecap\", \"round\");\n    });\n  } else if (type === \"dot\") {\n    svg.append(\"circle\").attr(\"cx\", lx + 11).attr(\"cy\", ly).attr(\"r\", 6)\n      .attr(\"fill\", color).attr(\"stroke\", t.pageBg).attr(\"stroke-width\", 2);\n  } else {\n    const leg = svg.append(\"line\")\n      .attr(\"x1\", lx).attr(\"y1\", ly).attr(\"x2\", lx + 22).attr(\"y2\", ly)\n      .attr(\"stroke\", color).attr(\"stroke-width\", solid ? 3.5 : 2)\n      .attr(\"opacity\", opacity);\n    if (!solid) leg.attr(\"stroke-dasharray\", \"6,4\");\n  }\n  svg.append(\"text\").attr(\"x\", lx + 30).attr(\"y\", ly + 5)\n    .attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\")\n    .text(label);\n});\n\n// Transfer function note below legend\nsvg.append(\"text\")\n  .attr(\"x\", lx).attr(\"y\", ly0 + legendItems.length * 32 + 24)\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"12px\").style(\"font-style\", \"italic\")\n  .text(\"G(s) = 48 /\");\nsvg.append(\"text\")\n  .attr(\"x\", lx).attr(\"y\", ly0 + legendItems.length * 32 + 40)\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"12px\").style(\"font-style\", \"italic\")\n  .text(\"[(s+1)(s+2)(s+3)]\");\n\n// Title\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\").style(\"font-weight\", \"600\")\n  .text(\"nyquist-basic · javascript · d3 · anyplot.ai\");\n"}