{"spec_id":"nyquist-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// nyquist-basic: Nyquist Plot for Control Systems\n// Library: highcharts 12.6.0 | JavaScript 22.22.3\n// Quality: 87/100 | Created: 2026-06-17\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: G(s) = 30 / ((s+1)(s+2)(s+3)) ---\n// G(jw) = 30 / ((6 - 6w²) + j(11w - w³))\n// Phase crossover at w = √11 ≈ 3.317 rad/s: G(j√11) = -0.5 (gain margin = 2 = 6 dB)\nfunction g(w) {\n  const dr = 6 - 6 * w * w;\n  const di = 11 * w - w * w * w;\n  const d2 = dr * dr + di * di;\n  return { re: 30 * dr / d2, im: -30 * di / d2 };\n}\n\n// Gain crossover frequency: binary search for |G(jω)| = 1\n// At ω=1: |G|≈3 > 1;  at ω=4: |G|≈0.33 < 1  →  root lies in [1, 4]\nlet wgcLow = 1.0, wgcHigh = 4.0;\nfor (let i = 0; i < 50; i++) {\n  const wm = (wgcLow + wgcHigh) / 2;\n  const pm = g(wm);\n  const mag2 = pm.re * pm.re + pm.im * pm.im;\n  if (mag2 > 1) wgcLow = wm; else wgcHigh = wm;\n}\nconst wgc = (wgcLow + wgcHigh) / 2;\nconst pgc = g(wgc);\n// Phase margin = 180° + ∠G(jω_gc)  [∠G is negative for this system]\nconst phaseGc = Math.atan2(pgc.im, pgc.re) * 180 / Math.PI;\nconst pmDeg = Math.round(180 + phaseGc);\n\n// Logarithmically spaced frequencies: ω ∈ [0.01, 100] rad/s\nconst N = 400;\nconst posData = [];\nfor (let i = 0; i <= N; i++) {\n  const w = Math.exp(Math.log(0.01) + (Math.log(100) - Math.log(0.01)) * i / N);\n  const p = g(w);\n  posData.push([p.re, p.im]);\n}\n// Negative frequencies: conjugate of positive, traversed in reverse\nconst negData = posData.slice().reverse().map(([r, im]) => [r, -im]);\n\n// Unit circle centered at origin (reference)\nconst unitCircle = [];\nfor (let i = 0; i <= 200; i++) {\n  const th = 2 * Math.PI * i / 200;\n  unitCircle.push([Math.cos(th), Math.sin(th)]);\n}\n\n// Frequency annotation points — ω_pc handled separately with below-axis label\n// to avoid crowding with the (−1, 0) label above the real axis\nconst annotPoints = [\n  { w: 0.3, label: \"0.3 rad/s\" },\n  { w: 1.0, label: \"1 rad/s\" },\n  { w: 2.0, label: \"2 rad/s\" }\n].map(f => {\n  const p = g(f.w);\n  return { x: p.re, y: p.im, name: f.label };\n});\n\n// ω_pc sits on the real axis at (−0.5, 0) — place label below to clear the\n// (−1, 0) label above the axis\nconst wpc = Math.sqrt(11);\nconst ppc = g(wpc);\nconst wpcPoint = {\n  x: ppc.re, y: ppc.im, name: \"ω_pc ≈ 3.32 rad/s\",\n  dataLabels: { y: 18, x: 4 }\n};\n\n// Frequencies at which to place direction arrows (ω > 0 curve and its mirror)\nconst arrowFreqs = [0.5, 2.0];\n\n// Title and subtitle conveying the transfer function and stability margins\nconst title = \"nyquist-basic · javascript · highcharts · anyplot.ai\";\nconst subtitle = `G(s) = 30/((s+1)(s+2)(s+3))  ·  Gain margin: 6 dB  ·  Phase margin: ${pmDeg}°`;\n\n// --- Chart ---\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"line\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    events: {\n      render() {\n        // Remove previously added SVG elements (prevents stacking on re-render)\n        if (this._svgExtra) {\n          this._svgExtra.forEach(el => el.destroy());\n        }\n        this._svgExtra = [];\n\n        const ax = this.xAxis[0];\n        const ay = this.yAxis[0];\n\n        // Draw \"×\" at critical point (-1, 0) using SVG renderer\n        const cpx = ax.toPixels(-1);\n        const cpy = ay.toPixels(0);\n        const xs = 11;\n        this._svgExtra.push(\n          this.renderer.path([\n            \"M\", cpx - xs, cpy - xs, \"L\", cpx + xs, cpy + xs,\n            \"M\", cpx + xs, cpy - xs, \"L\", cpx - xs, cpy + xs\n          ]).attr({\n            stroke: t.palette[4],\n            \"stroke-width\": 3.5,\n            zIndex: 6\n          }).add()\n        );\n\n        // Direction arrows on positive-frequency curve (solid) and its mirror (dashed)\n        arrowFreqs.forEach(w => {\n          const dw = 0.06;\n          const pa = g(w), pb = g(w + dw);\n\n          // Positive curve: arrow pointing from pa → pb\n          const x1 = ax.toPixels(pa.re), y1 = ay.toPixels(pa.im);\n          const x2 = ax.toPixels(pb.re), y2 = ay.toPixels(pb.im);\n          const ang = Math.atan2(y2 - y1, x2 - x1);\n          const cx = (x1 + x2) / 2, cy = (y1 + y2) / 2;\n          const hs = 10;\n\n          this._svgExtra.push(\n            this.renderer.path([\n              \"M\", cx + hs * Math.cos(ang), cy + hs * Math.sin(ang),\n              \"L\", cx - hs * Math.cos(ang - 0.55), cy - hs * Math.sin(ang - 0.55),\n              \"L\", cx - hs * Math.cos(ang + 0.55), cy - hs * Math.sin(ang + 0.55),\n              \"Z\"\n            ]).attr({ fill: t.palette[0], zIndex: 5 }).add()\n          );\n\n          // Negative-frequency mirror: reversed direction, conjugate position\n          const x1n = ax.toPixels(pb.re), y1n = ay.toPixels(-pb.im);\n          const x2n = ax.toPixels(pa.re), y2n = ay.toPixels(-pa.im);\n          const angn = Math.atan2(y2n - y1n, x2n - x1n);\n          const cxn = (x1n + x2n) / 2, cyn = (y1n + y2n) / 2;\n\n          this._svgExtra.push(\n            this.renderer.path([\n              \"M\", cxn + hs * Math.cos(angn), cyn + hs * Math.sin(angn),\n              \"L\", cxn - hs * Math.cos(angn - 0.55), cyn - hs * Math.sin(angn - 0.55),\n              \"L\", cxn - hs * Math.cos(angn + 0.55), cyn - hs * Math.sin(angn + 0.55),\n              \"Z\"\n            ]).attr({ fill: t.palette[0], zIndex: 5, opacity: 0.6 }).add()\n          );\n        });\n      }\n    }\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: title,\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" }\n  },\n  subtitle: {\n    text: subtitle,\n    style: { color: t.inkSoft, fontSize: \"14px\" }\n  },\n  xAxis: {\n    type: \"linear\",\n    title: { text: \"Real\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    min: -2, max: 6,\n    tickInterval: 1,\n    gridLineColor: t.grid,\n    gridLineWidth: 1,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    plotLines: [{ value: 0, color: t.inkSoft, width: 1, zIndex: 2 }]\n  },\n  yAxis: {\n    title: { text: \"Imaginary\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    min: -4, max: 4,\n    tickInterval: 1,\n    gridLineColor: t.grid,\n    gridLineWidth: 1,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    plotLines: [{ value: 0, color: t.inkSoft, width: 1, zIndex: 2 }]\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"13px\" },\n    itemHoverStyle: { color: t.ink }\n  },\n  plotOptions: {\n    series: { animation: false },\n    line: {\n      marker: { enabled: false },\n      states: { hover: { lineWidthPlus: 0 } }\n    },\n    scatter: {\n      states: { hover: { enabled: false } }\n    }\n  },\n  series: [\n    {\n      name: \"G(jω), ω > 0\",\n      type: \"line\",\n      data: posData,\n      color: t.palette[0],\n      lineWidth: 2.5\n    },\n    {\n      name: \"G(jω), ω < 0\",\n      type: \"line\",\n      data: negData,\n      color: t.palette[0],\n      lineWidth: 2.5,\n      dashStyle: \"ShortDash\",\n      opacity: 0.6\n    },\n    {\n      name: \"Unit circle\",\n      type: \"line\",\n      data: unitCircle,\n      color: t.inkSoft,\n      lineWidth: 1,\n      dashStyle: \"Dash\",\n      enableMouseTracking: false\n    },\n    {\n      name: \"Critical point (−1, 0)\",\n      type: \"scatter\",\n      data: [{ x: -1, y: 0 }],\n      color: t.palette[4],\n      marker: { symbol: \"circle\", radius: 8 },\n      dataLabels: {\n        enabled: true,\n        format: \"(−1, 0)\",\n        style: { color: t.palette[4], fontSize: \"13px\", fontWeight: \"600\" },\n        x: 6, y: -28\n      }\n    },\n    {\n      name: \"Frequency markers\",\n      type: \"scatter\",\n      data: [...annotPoints, wpcPoint],\n      color: t.palette[2],\n      marker: { symbol: \"circle\", radius: 5 },\n      dataLabels: {\n        enabled: true,\n        format: \"{point.name}\",\n        style: { color: t.inkSoft, fontSize: \"12px\", fontWeight: \"normal\" },\n        allowOverlap: false\n      },\n      showInLegend: false\n    }\n  ]\n});\n"}