{"spec_id":"nyquist-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// nyquist-basic: Nyquist Plot for Control Systems\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// Quality: 86/100 | Created: 2026-06-17\n//# anyplot-orientation: square\n// anyplot.ai\n// nyquist-basic: Nyquist Plot for Control Systems\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-06-17\n\nimport { ScatterChart } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// G(jω) = 1/(jω(1+jω)(1+0.5jω)); denominator = -1.5ω² + jω(1-0.5ω²)\n// Re(G) = -1.5ω²/|d|²,  Im(G) = -ω(1-0.5ω²)/|d|²\nconst nyquistXY = (omega) => {\n  const dre = -1.5 * omega * omega;\n  const dim = omega * (1 - 0.5 * omega * omega);\n  const m2 = dre * dre + dim * dim;\n  return [dre / m2, -dim / m2];\n};\n\n// Nyquist curve: 600 log-spaced points ω ∈ [0.4, 25] rad/s\nconst nyquistData = Array.from({ length: 600 }, (_, k) => {\n  const omega = 0.4 * Math.pow(62.5, k / 599);\n  const [x, y] = nyquistXY(omega);\n  return { x, y, id: k };\n});\n\n// Unit circle reference\nconst circleData = Array.from({ length: 601 }, (_, k) => {\n  const theta = (2 * Math.PI * k) / 600;\n  return { x: Math.cos(theta), y: Math.sin(theta), id: 1000 + k };\n});\n\n// Key frequency markers: gain crossover, phase crossover, high-freq\nconst KEY_FREQS = [\n  { omega: 0.75, label: \"ω=0.75 rad/s\" },\n  { omega: Math.SQRT2, label: \"ω=√2 rad/s\" },\n  { omega: 5, label: \"ω=5 rad/s\" },\n];\nconst keyFreqData = KEY_FREQS.map(({ omega }, i) => {\n  const [x, y] = nyquistXY(omega);\n  return { x, y, id: 2000 + i };\n});\n\n// Indices along Nyquist curve where direction arrows are drawn\nconst ARROW_INDICES = [50, 160, 290];\n\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const margin = { left: 80, right: 35, top: 35, bottom: 80 };\n  const titleH = 52;\n  const chartSize = Math.min(width - 8, height - titleH - 14);\n\n  // Data-to-SVG-pixel coordinate transform for the plot area\n  const plotW = chartSize - margin.left - margin.right;\n  const plotH = chartSize - margin.top - margin.bottom;\n  const [xMin, xMax, yMin, yMax] = [-2.0, 1.5, -2.0, 1.5];\n  const toPixX = (xv) => margin.left + ((xv - xMin) / (xMax - xMin)) * plotW;\n  const toPixY = (yv) => margin.top + ((yMax - yv) / (yMax - yMin)) * plotH;\n\n  // Direction arrows: tangent computed in pixel space\n  const arrows = ARROW_INDICES.map((idx) => {\n    const p0 = nyquistData[idx];\n    const p1 = nyquistData[idx + 10];\n    const px0 = toPixX(p0.x), py0 = toPixY(p0.y);\n    const px1 = toPixX(p1.x), py1 = toPixY(p1.y);\n    const dpx = px1 - px0, dpy = py1 - py0;\n    const len = Math.sqrt(dpx * dpx + dpy * dpy);\n    const nx = dpx / len, ny = dpy / len;\n    const cx = (px0 + px1) / 2, cy = (py0 + py1) / 2;\n    const AL = 26;\n    return {\n      x1: cx - (nx * AL) / 2,\n      y1: cy - (ny * AL) / 2,\n      x2: cx + (nx * AL) / 2,\n      y2: cy + (ny * AL) / 2,\n    };\n  });\n\n  // Text label anchor offsets relative to each key frequency marker\n  const labelOffsets = [\n    { dx: 10, dy: -10 }, // ω=0.75: lower-left quadrant → label right+up\n    { dx: 10, dy: -12 }, // ω=√2: on real axis → label right+up\n    { dx: 8, dy: -10 },  // ω=5: near origin → label right+up\n  ];\n\n  return (\n    <div\n      style={{\n        width,\n        height,\n        backgroundColor: t.pageBg,\n        display: \"flex\",\n        flexDirection: \"column\",\n        alignItems: \"center\",\n        padding: \"14px 4px 4px\",\n        boxSizing: \"border-box\",\n        fontFamily: \"sans-serif\",\n      }}\n    >\n      <div\n        style={{\n          fontSize: \"22px\",\n          fontWeight: 500,\n          color: t.ink,\n          marginBottom: \"10px\",\n          textAlign: \"center\",\n          letterSpacing: \"0.01em\",\n        }}\n      >\n        nyquist-basic · javascript · muix · anyplot.ai\n      </div>\n      <div style={{ position: \"relative\", width: chartSize, height: chartSize }}>\n        <ScatterChart\n          width={chartSize}\n          height={chartSize}\n          skipAnimation\n          series={[\n            {\n              data: nyquistData,\n              label: \"G(jω) — Open-loop Response\",\n              color: t.palette[0],\n              markerSize: 3,\n            },\n            {\n              data: circleData,\n              label: \"Unit Circle\",\n              color: t.inkSoft,\n              markerSize: 2,\n            },\n            {\n              data: keyFreqData,\n              label: \"Key Frequencies (ω_gc, ω_pc, ω=5)\",\n              color: t.palette[1],\n              markerSize: 7,\n            },\n            {\n              data: [{ x: -1, y: 0, id: 9999 }],\n              label: \"Critical Point (−1, 0)\",\n              color: \"#AE3030\",\n              markerSize: 10,\n            },\n          ]}\n          xAxis={[{ label: \"Real\", min: xMin, max: xMax, showGrid: true }]}\n          yAxis={[{ label: \"Imaginary\", min: yMin, max: yMax, showGrid: true }]}\n          margin={margin}\n          sx={{\n            \"& .MuiChartsAxis-tickLabel\": { fontSize: \"14px\" },\n            \"& .MuiChartsAxis-label\": { fontSize: \"16px\" },\n            \"& .MuiChartsLegend-label\": { fontSize: \"14px\" },\n            \"& .MuiChartsGrid-line\": {\n              stroke: t.inkSoft,\n              strokeOpacity: 0.2,\n            },\n          }}\n        >\n          {/* Vertical reference line through the critical point x=-1 */}\n          <ChartsReferenceLine\n            x={-1}\n            lineStyle={{\n              stroke: \"#AE3030\",\n              strokeDasharray: \"5 3\",\n              strokeOpacity: 0.45,\n              strokeWidth: 1,\n            }}\n          />\n        </ScatterChart>\n        {/* SVG overlay: direction-of-increasing-ω arrows + frequency labels */}\n        <svg\n          style={{\n            position: \"absolute\",\n            top: 0,\n            left: 0,\n            width: chartSize,\n            height: chartSize,\n            pointerEvents: \"none\",\n            overflow: \"visible\",\n          }}\n        >\n          <defs>\n            <marker\n              id=\"arr\"\n              markerWidth=\"7\"\n              markerHeight=\"7\"\n              refX=\"5\"\n              refY=\"3.5\"\n              orient=\"auto\"\n            >\n              <path d=\"M0,0 L7,3.5 L0,7 Z\" fill={t.palette[0]} />\n            </marker>\n          </defs>\n          {arrows.map((a, i) => (\n            <line\n              key={i}\n              x1={a.x1}\n              y1={a.y1}\n              x2={a.x2}\n              y2={a.y2}\n              stroke={t.palette[0]}\n              strokeWidth={2.5}\n              markerEnd=\"url(#arr)\"\n            />\n          ))}\n          {KEY_FREQS.map(({ omega, label }, i) => {\n            const [xv, yv] = nyquistXY(omega);\n            const px = toPixX(xv);\n            const py = toPixY(yv);\n            const { dx, dy } = labelOffsets[i];\n            return (\n              <text\n                key={i}\n                x={px + dx}\n                y={py + dy}\n                fill={t.ink}\n                fontSize=\"13\"\n                fontFamily=\"sans-serif\"\n                fontWeight=\"500\"\n              >\n                {label}\n              </text>\n            );\n          })}\n        </svg>\n      </div>\n    </div>\n  );\n}\n"}