{"spec_id":"root-locus-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// root-locus-basic: Root Locus Plot for Control Systems\n// Library: echarts 5.5.1 | JavaScript 22.22.3\n// Quality: 84/100 | Created: 2026-06-18\n//# anyplot-orientation: square\n// anyplot.ai\n// root-locus-basic: Root Locus Plot for Control Systems\n// Library: echarts 5.5.1 | JavaScript 22.22.3\n// Quality: 78/100 | Created: 2026-06-18\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Open-loop: G(s) = K / [s(s+1)(s+3)] — poles {0, −1, −3}, no zeros\n// Char eq: s³ + 4s² + 3s + K = 0\n// Real-axis locus: (−∞, −3] ∪ [−1, 0]  |  Breakaway: s ≈ −0.4514 at K ≈ 0.631\n// jω crossing: s = ±j√3 at K = 12  |  Asymptotes: 60°, 180°, 300° from (−4/3, 0)\n\n// Parametric formulas for complex conjugate pair given real root σ_C (Vieta):\n//   σ_AB = (−4 − σ_C)/2,   ω² = 3 − σ_AB² − 2·σ_C·σ_AB\nfunction cplxPair(sigC) {\n  const sig = (-4 - sigC) / 2;\n  const omegaSq = 3 - sig * sig - 2 * sigC * sig;\n  return { sig, omega: Math.sqrt(Math.max(0, omegaSq)) };\n}\n\nconst BKWY = -0.4514;    // breakaway real coordinate\nconst SIG_C0 = -3.0972; // real root (branch C) at start of complex phase\n\n// Locus path arrays: [real, imag] pairs\nconst bA = [], bB = [], bC = [];\n\n// Phase 1: real-axis convergence toward breakaway (K: 0 → 0.631)\nconst N1 = 60;\nfor (let i = 0; i <= N1; i++) {\n  const f = i / N1;\n  bA.push([BKWY * f, 0]);                 // pole 0  → breakaway\n  bB.push([-1 + 0.5486 * f, 0]);          // pole −1 → breakaway\n  bC.push([-3 + (SIG_C0 + 3) * f, 0]);   // pole −3 → SIG_C0\n}\n\n// Phase 2: complex locus (K > 0.631), σ_C from SIG_C0 to −6\nconst N2 = 200;\nfor (let i = 1; i <= N2; i++) {\n  const f = i / N2;\n  const sigC = SIG_C0 + (-6 - SIG_C0) * f;\n  const { sig, omega } = cplxPair(sigC);\n  bA.push([sig, omega]);\n  bB.push([sig, -omega]);\n  bC.push([sigC, 0]);\n}\n\n// Constant damping-ratio reference lines: ζ = 0.5 (angle 60° from −Re axis)\n// Direction: (cos 120°, ±sin 120°) = (−0.5, ±√3/2)\n// Exits at y = ±4 when r = 4/(√3/2) ≈ 4.619, x = −4.619×0.5 ≈ −2.309\nconst zeta05up = [[0, 0], [-2.309, 4]];\nconst zeta05dn = [[0, 0], [-2.309, -4]];\n\n// Natural frequency reference circle: ωn = 2 (full circle, r = 2)\nconst wnCircle = [];\nfor (let i = 0; i <= 360; i++) {\n  const theta = (i / 180) * Math.PI;\n  wnCircle.push([2 * Math.cos(theta), 2 * Math.sin(theta)]);\n}\n\n// Key markers\nconst poles = [[0, 0], [-1, 0], [-3, 0]];\nconst jwCross = [[0, Math.sqrt(3)], [0, -Math.sqrt(3)]]; // ±j√3, K=12\n\n// Gain-direction arrows: symbolRotate = atan2(dx, dy) maps chart tangent to\n// ECharts clockwise-from-up convention (right→90°, up→0°, left→-90°, down→180°)\nfunction arrowAt(path, idx) {\n  const i = Math.max(1, Math.min(path.length - 2, idx));\n  const dx = path[i + 1][0] - path[i - 1][0];\n  const dy = path[i + 1][1] - path[i - 1][1];\n  return { value: path[i], symbolRotate: Math.atan2(dx, dy) * 180 / Math.PI };\n}\n\nconst gainArrows = [\n  arrowAt(bA, Math.floor(N1 * 0.70)),        // phase 1, bA: leftward (K↑ toward breakaway)\n  arrowAt(bB, Math.floor(N1 * 0.70)),        // phase 1, bB: rightward (K↑ toward breakaway)\n  arrowAt(bA, N1 + Math.floor(N2 * 0.45)),   // phase 2, bA: upper-right (K↑ along complex branch)\n  arrowAt(bB, N1 + Math.floor(N2 * 0.45)),   // phase 2, bB: lower-right (K↑ along complex branch)\n  arrowAt(bC, N1 + Math.floor(N2 * 0.55)),   // phase 2, bC: leftward (K↑ along real branch)\n];\n\n// ── Chart ─────────────────────────────────────────────────────────────────────\nconst chart = echarts.init(document.getElementById(\"container\"));\n\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n\n  title: {\n    text: \"root-locus-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 18,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: \"bold\" },\n  },\n\n  legend: {\n    bottom: 14,\n    itemGap: 32,\n    textStyle: { color: t.inkSoft, fontSize: 13 },\n  },\n\n  // Square grid (2200×2200 within 2400×2400 canvas) for equal axis scaling:\n  // x range 8 units and y range 8 units → 275 px/unit on each axis\n  grid: { left: 120, right: 80, top: 80, bottom: 120 },\n\n  xAxis: {\n    type: \"value\",\n    name: \"Real Axis\",\n    nameLocation: \"middle\",\n    nameGap: 36,\n    nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n    min: -6,\n    max: 2,\n    interval: 1,\n    axisLabel: { color: t.inkSoft, fontSize: 12 },\n    axisLine: { show: true, lineStyle: { color: t.inkSoft } },\n    axisTick: { show: true, lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid, width: 1 } },\n  },\n\n  yAxis: {\n    type: \"value\",\n    name: \"Imaginary Axis\",\n    nameLocation: \"middle\",\n    nameGap: 56,\n    nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n    min: -4,\n    max: 4,\n    interval: 1,\n    axisLabel: { color: t.inkSoft, fontSize: 12 },\n    axisLine: { show: true, lineStyle: { color: t.inkSoft } },\n    axisTick: { show: true, lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid, width: 1 } },\n  },\n\n  series: [\n    // ζ = 0.5 damping-ratio reference lines (subtle dashed, excluded from legend)\n    {\n      type: \"line\",\n      data: zeta05up,\n      showSymbol: false,\n      lineStyle: { color: t.grid, width: 1.2, type: \"dashed\" },\n      silent: true,\n      legendHoverLink: false,\n    },\n    {\n      type: \"line\",\n      data: zeta05dn,\n      showSymbol: false,\n      lineStyle: { color: t.grid, width: 1.2, type: \"dashed\" },\n      silent: true,\n      legendHoverLink: false,\n    },\n    // ωn = 2 natural frequency reference circle (now renders as true circle)\n    {\n      type: \"line\",\n      data: wnCircle,\n      showSymbol: false,\n      lineStyle: { color: t.grid, width: 1.2, type: \"dashed\" },\n      silent: true,\n      legendHoverLink: false,\n    },\n    // Root locus branches (A upper, B lower, C real — share one legend entry)\n    {\n      name: \"Root Locus\",\n      type: \"line\",\n      data: bA,\n      showSymbol: false,\n      lineStyle: { color: t.palette[0], width: 2.5 },\n    },\n    {\n      name: \"Root Locus\",\n      type: \"line\",\n      data: bB,\n      showSymbol: false,\n      lineStyle: { color: t.palette[0], width: 2.5 },\n    },\n    {\n      name: \"Root Locus\",\n      type: \"line\",\n      data: bC,\n      showSymbol: false,\n      lineStyle: { color: t.palette[0], width: 2.5 },\n    },\n    // Gain-direction arrows (excluded from legend — decorative overlay)\n    {\n      type: \"scatter\",\n      data: gainArrows,\n      symbol: \"arrow\",\n      symbolSize: 14,\n      itemStyle: { color: t.palette[0] },\n      silent: true,\n      legendHoverLink: false,\n    },\n    // Open-loop poles (× markers via rotated cross shape)\n    {\n      name: \"Open-Loop Poles\",\n      type: \"scatter\",\n      data: poles,\n      symbol:\n        \"path://M-1,-4 L1,-4 L1,-1 L4,-1 L4,1 L1,1 L1,4 L-1,4 L-1,1 L-4,1 L-4,-1 L-1,-1 Z\",\n      symbolSize: 20,\n      symbolRotate: 45,\n      itemStyle: { color: t.palette[4] },\n    },\n    // Stability boundary crossings ±j√3 at K = 12\n    {\n      name: \"jω Crossings (K=12)\",\n      type: \"scatter\",\n      data: jwCross,\n      symbol: \"diamond\",\n      symbolSize: 18,\n      itemStyle: { color: t.palette[1] },\n    },\n  ],\n});\n"}