{"spec_id":"nyquist-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// nyquist-basic: Nyquist Plot for Control Systems\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 84/100 | Created: 2026-06-17\n\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Complex arithmetic ---\nfunction cmul(a, b) { return { re: a.re * b.re - a.im * b.im, im: a.re * b.im + a.im * b.re }; }\nfunction cdiv(a, b) {\n  const d = b.re * b.re + b.im * b.im;\n  return { re: (a.re * b.re + a.im * b.im) / d, im: (a.im * b.re - a.re * b.im) / d };\n}\n\n// G(s) = 6 / ((s+1)(s+2)(s+3)) — stable third-order plant\n// DC gain = 1.0, phase crossover ω_pc ≈ 3.32 rad/s, gain margin = 10 (20 dB)\nfunction evalG(w) {\n  return cdiv(\n    { re: 6, im: 0 },\n    cmul(cmul({ re: 1, im: w }, { re: 2, im: w }), { re: 3, im: w })\n  );\n}\n\n// --- Data ---\n// 600 log-spaced frequencies: 0.01 to 100 rad/s\nconst N = 600;\nconst posFreqData = Array.from({ length: N }, (_, i) => {\n  const w = Math.pow(10, -2 + (i * 4) / (N - 1));\n  const G = evalG(w);\n  return { x: G.re, y: G.im, w };\n});\n// Negative-frequency curve is the complex conjugate (mirror about real axis)\nconst negFreqData = [...posFreqData].reverse().map(p => ({ x: p.x, y: -p.y }));\n\n// Unit circle for stability reference\nconst unitCircle = Array.from({ length: 101 }, (_, i) => {\n  const theta = (2 * Math.PI * i) / 100;\n  return { x: Math.cos(theta), y: Math.sin(theta) };\n});\n\n// Key frequency annotation points\nconst annotPts = [\n  { w: 0.5,           label: \"ω = 0.5\"      },\n  { w: 1.0,           label: \"ω = 1\"         },\n  { w: 2.0,           label: \"ω = 2\"         },\n  { w: Math.sqrt(11), label: \"ω ≈ 3.32 (φ×)\" },\n].map(({ w, label }) => {\n  const G = evalG(w);\n  return { x: G.re, y: G.im, label };\n});\n\n// Indices along the positive-freq curve where direction arrows are drawn\nconst arrowIndices = [35, 100, 175, 265, 370];\n\n// --- Background plugin ---\nconst bgPlugin = {\n  id: \"bg\",\n  beforeDraw(chart) {\n    const ctx = chart.ctx;\n    ctx.save();\n    ctx.fillStyle = t.pageBg;\n    ctx.fillRect(0, 0, chart.width, chart.height);\n    ctx.restore();\n  },\n};\n\n// --- Arrows + frequency-label plugin ---\nconst nyquistPlugin = {\n  id: \"nyquist\",\n  afterDraw(chart) {\n    const ctx = chart.ctx;\n    const xs = chart.scales.x;\n    const ys = chart.scales.y;\n    const toPx = (x, y) => ({ cx: xs.getPixelForValue(x), cy: ys.getPixelForValue(y) });\n\n    ctx.save();\n\n    // Direction arrows on positive-frequency curve\n    ctx.fillStyle = t.palette[0];\n    for (const idx of arrowIndices) {\n      if (idx + 8 >= posFreqData.length) continue;\n      const { cx: x0, cy: y0 } = toPx(posFreqData[idx].x, posFreqData[idx].y);\n      const { cx: x1, cy: y1 } = toPx(posFreqData[idx + 8].x, posFreqData[idx + 8].y);\n      const dx = x1 - x0, dy = y1 - y0;\n      const len = Math.hypot(dx, dy);\n      if (len < 1) continue;\n      const nx = dx / len, ny = dy / len;\n      const mx = (x0 + x1) / 2, my = (y0 + y1) / 2;\n      const as = 13; // arrowhead half-size in logical canvas px\n      ctx.beginPath();\n      ctx.moveTo(mx + nx * as,                      my + ny * as);\n      ctx.lineTo(mx - ny * (as * 0.5) - nx * (as * 0.5), my + nx * (as * 0.5) - ny * (as * 0.5));\n      ctx.lineTo(mx + ny * (as * 0.5) - nx * (as * 0.5), my - nx * (as * 0.5) - ny * (as * 0.5));\n      ctx.closePath();\n      ctx.fill();\n    }\n\n    // Frequency annotation dots and labels\n    ctx.font = \"bold 15px sans-serif\";\n    for (const { x, y, label } of annotPts) {\n      const { cx, cy } = toPx(x, y);\n      // Ochre dot at the annotated point\n      ctx.beginPath();\n      ctx.arc(cx, cy, 5, 0, 2 * Math.PI);\n      ctx.fillStyle = t.palette[3]; // Imprint ochre\n      ctx.fill();\n      // Label — offset away from curve\n      const offX = x < 0.05 ? -10 : 10;\n      const offY = y <= 0 ? 18 : -8;\n      ctx.fillStyle = t.inkSoft;\n      ctx.textAlign = x < 0.05 ? \"right\" : \"left\";\n      ctx.textBaseline = \"middle\";\n      ctx.fillText(label, cx + offX, cy + offY);\n    }\n\n    ctx.restore();\n  },\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: \"Unit circle\",\n        data: unitCircle,\n        borderColor: t.inkSoft,\n        backgroundColor: \"transparent\",\n        showLine: true,\n        borderWidth: 1.5,\n        borderDash: [4, 6],\n        pointRadius: 0,\n        tension: 0,\n      },\n      {\n        label: \"G(jω), ω < 0\",\n        data: negFreqData,\n        borderColor: t.palette[0],\n        backgroundColor: \"transparent\",\n        showLine: true,\n        borderWidth: 2,\n        borderDash: [6, 5],\n        pointRadius: 0,\n        tension: 0,\n      },\n      {\n        label: \"G(jω), ω ≥ 0\",\n        data: posFreqData,\n        borderColor: t.palette[0],\n        backgroundColor: \"transparent\",\n        showLine: true,\n        borderWidth: 2.5,\n        pointRadius: 0,\n        tension: 0,\n      },\n      {\n        label: \"Critical point (−1, 0)\",\n        data: [{ x: -1, y: 0 }],\n        borderColor: \"#AE3030\",\n        backgroundColor: \"#AE3030\",\n        pointStyle: \"crossRot\",\n        pointRadius: 12,\n        pointBorderWidth: 3,\n        showLine: false,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"nyquist-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { bottom: 14 },\n      },\n      legend: {\n        labels: { color: t.ink, font: { size: 14 }, usePointStyle: true },\n      },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        title: { display: true, text: \"Real\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 13 } },\n        grid: { color: t.grid },\n        border: { display: false },\n        min: -1.5,\n        max: 1.5,\n      },\n      y: {\n        type: \"linear\",\n        title: { display: true, text: \"Imaginary\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 13 } },\n        grid: { color: t.grid },\n        border: { display: false },\n        min: -1.5,\n        max: 1.5,\n      },\n    },\n  },\n  plugins: [bgPlugin, nyquistPlugin],\n});\n"}