{"spec_id":"root-locus-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// root-locus-basic: Root Locus Plot for Control Systems\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// Quality: 85/100 | Created: 2026-06-18\n\nimport { ScatterChart } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsReferenceLine } from \"@mui/x-charts\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst tok = window.ANYPLOT_TOKENS;\n\n// Root locus for G(s) = K / [s(s+2)(s+4)], K ∈ [0, 90]\n// Characteristic equation: s³ + 6s² + 8s + K = 0\n// Open-loop poles: s = 0, −2, −4\n// Breakaway at s ≈ −0.845 (K ≈ 3.08); jω crossing at K = 48 (s ≈ ±j2.83)\n\nfunction polyVal(r, K) {\n  return r * r * r + 6 * r * r + 8 * r + K;\n}\nfunction polyDeriv(r) {\n  return 3 * r * r + 12 * r + 8;\n}\nfunction findRealRoot(K, guess) {\n  let r = guess;\n  for (let it = 0; it < 80; it++) {\n    const fp = polyDeriv(r);\n    if (Math.abs(fp) < 1e-15) break;\n    const delta = polyVal(r, K) / fp;\n    r -= delta;\n    if (Math.abs(delta) < 1e-12) break;\n  }\n  return r;\n}\nfunction complexPair(r3) {\n  const b = 6 + r3;\n  const c = 8 + r3 * b;\n  const disc = b * b - 4 * c;\n  if (disc >= 0) {\n    const sq = Math.sqrt(disc);\n    return [{ re: (-b + sq) / 2, im: 0 }, { re: (-b - sq) / 2, im: 0 }];\n  }\n  const sq = Math.sqrt(-disc);\n  return [{ re: -b / 2, im: sq / 2 }, { re: -b / 2, im: -sq / 2 }];\n}\n\nconst N_K = 500;\nconst K_MAX = 90;\nconst branchA = [], branchB = [], branchC = [];\nlet r3 = -4.001;\n\nfor (let i = 0; i <= N_K; i++) {\n  const K = (i / N_K) * K_MAX;\n  r3 = findRealRoot(K, r3);\n  const pair = complexPair(r3);\n  branchA.push({ x: pair[0].re, y: pair[0].im, id: i });\n  branchB.push({ x: pair[1].re, y: pair[1].im, id: i + N_K + 1 });\n  branchC.push({ x: r3, y: 0, id: i + 2 * (N_K + 1) });\n}\n\n// Open-loop poles (× markers at K=0)\nconst poleData = [\n  { x: 0, y: 0, id: 9000 },\n  { x: -2, y: 0, id: 9001 },\n  { x: -4, y: 0, id: 9002 },\n];\n\n// Constant damping-ratio lines: straight rays from origin for ζ = 0.3, 0.5, 0.7\n// Each ray: x = -r·ζ, y = ±r·√(1−ζ²)\nconst dampingData = [];\nlet dId = 10000;\nfor (const zeta of [0.3, 0.5, 0.7]) {\n  const sinT = Math.sqrt(1 - zeta * zeta);\n  for (let k = 1; k <= 28; k++) {\n    const r = k * 0.3;\n    dampingData.push({ x: -r * zeta, y: r * sinT, id: dId++ });\n    dampingData.push({ x: -r * zeta, y: -r * sinT, id: dId++ });\n  }\n}\n\n// Gain-direction arrows at branch midpoints (K ≈ 45, index = 250)\nconst MID = 250;\nconst STEP = 15;\nfunction dirAngle(pts) {\n  const dx = pts[Math.min(MID + STEP, N_K)].x - pts[MID].x;\n  const dy = pts[Math.min(MID + STEP, N_K)].y - pts[MID].y;\n  return Math.atan2(dy, dx) * (180 / Math.PI);\n}\nconst ARROW_ANGLES = {\n  arrowA: dirAngle(branchA),\n  arrowB: dirAngle(branchB),\n  arrowC: dirAngle(branchC),\n};\nconst arrowData = [\n  { x: branchA[MID].x, y: branchA[MID].y, id: 20000 },\n  { x: branchB[MID].x, y: branchB[MID].y, id: 20001 },\n  { x: branchC[MID].x, y: branchC[MID].y, id: 20002 },\n];\n\n// ─── Custom mark ────────────────────────────────────────────────────────────\n// × for poles, ▶ (rotated) for gain arrows, tiny dot for damping, circle otherwise\nconst CustomMark = ({ x, y, color, seriesId }) => {\n  if (seriesId === \"poles\") {\n    const sz = 7;\n    return (\n      <g>\n        <line\n          x1={x - sz} y1={y - sz} x2={x + sz} y2={y + sz}\n          stroke={color} strokeWidth={2.5} strokeLinecap=\"round\"\n        />\n        <line\n          x1={x + sz} y1={y - sz} x2={x - sz} y2={y + sz}\n          stroke={color} strokeWidth={2.5} strokeLinecap=\"round\"\n        />\n      </g>\n    );\n  }\n  if (seriesId in ARROW_ANGLES) {\n    const angle = ARROW_ANGLES[seriesId];\n    return (\n      <g transform={`translate(${x},${y}) rotate(${angle})`}>\n        <polygon points=\"-7,-5 8,0 -7,5\" fill={color} opacity={0.85} />\n      </g>\n    );\n  }\n  if (seriesId === \"damping\") {\n    return <circle cx={x} cy={y} r={1.5} fill={color} opacity={0.35} />;\n  }\n  return <circle cx={x} cy={y} r={2.5} fill={color} />;\n};\n\nconst TITLE_H = 58;\n\nexport default function Chart() {\n  return (\n    <Box\n      sx={{\n        width: window.ANYPLOT_SIZE.width,\n        height: window.ANYPLOT_SIZE.height,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <Typography\n        sx={{\n          color: \"text.primary\",\n          fontWeight: 500,\n          fontSize: 22,\n          textAlign: \"center\",\n          lineHeight: `${TITLE_H}px`,\n          height: TITLE_H,\n          flexShrink: 0,\n        }}\n      >\n        root-locus-basic · javascript · muix · anyplot.ai\n      </Typography>\n      <ScatterChart\n        width={window.ANYPLOT_SIZE.width}\n        height={window.ANYPLOT_SIZE.height - TITLE_H}\n        skipAnimation\n        slots={{ mark: CustomMark }}\n        series={[\n          {\n            id: \"branchA\",\n            label: \"Branch A  (s=0 → upper half-plane)\",\n            data: branchA,\n            color: tok.palette[0],\n            markerSize: 3,\n          },\n          {\n            id: \"branchB\",\n            label: \"Branch B  (s=−2 → lower half-plane)\",\n            data: branchB,\n            color: tok.palette[1],\n            markerSize: 3,\n          },\n          {\n            id: \"branchC\",\n            label: \"Branch C  (s=−4 → −∞)\",\n            data: branchC,\n            color: tok.palette[2],\n            markerSize: 3,\n          },\n          {\n            id: \"poles\",\n            label: \"Open-loop poles  (K=0)\",\n            data: poleData,\n            color: tok.ink,\n            markerSize: 10,\n          },\n          {\n            id: \"damping\",\n            label: \"Const. damping ratio  ζ=0.3, 0.5, 0.7\",\n            data: dampingData,\n            color: tok.inkSoft,\n            markerSize: 2,\n          },\n          {\n            id: \"arrowA\",\n            data: [arrowData[0]],\n            color: tok.palette[0],\n            markerSize: 12,\n          },\n          {\n            id: \"arrowB\",\n            data: [arrowData[1]],\n            color: tok.palette[1],\n            markerSize: 12,\n          },\n          {\n            id: \"arrowC\",\n            data: [arrowData[2]],\n            color: tok.palette[2],\n            markerSize: 12,\n          },\n        ]}\n        xAxis={[\n          {\n            label: \"Real Axis  σ\",\n            min: -9,\n            max: 3,\n            tickLabelStyle: { fontSize: 14 },\n            labelStyle: { fontSize: 16 },\n          },\n        ]}\n        yAxis={[\n          {\n            label: \"Imaginary Axis  jω\",\n            min: -6,\n            max: 6,\n            tickLabelStyle: { fontSize: 14 },\n            labelStyle: { fontSize: 16 },\n          },\n        ]}\n        slotProps={{\n          legend: { labelStyle: { fontSize: 13 } },\n        }}\n      >\n        <ChartsReferenceLine\n          x={0}\n          label=\"Stability boundary (K=48)\"\n          lineStyle={{\n            stroke: tok.amber,\n            strokeDasharray: \"8 5\",\n            strokeWidth: 2,\n          }}\n          labelStyle={{ fontSize: 12, fill: tok.amber, dy: -10 }}\n        />\n      </ScatterChart>\n    </Box>\n  );\n}\n"}