Root Locus Plot for Control Systems — Apache ECharts

A root locus plot traces how the closed-loop poles of a transfer function migrate through the complex plane as a system parameter (typically gain K) varies from 0 to infinity. It is a fundamental tool in classical control theory for analyzing system stability and designing controllers. The plot reveals critical information about pole trajectories, stability boundaries, and gain margins.

Root Locus Plot for Control Systems rendered with Apache ECharts

JavaScript source (Apache ECharts)

// anyplot.ai
// root-locus-basic: Root Locus Plot for Control Systems
// Library: echarts 5.5.1 | JavaScript 22.22.3
// Quality: 84/100 | Created: 2026-06-18
//# anyplot-orientation: square
// anyplot.ai
// root-locus-basic: Root Locus Plot for Control Systems
// Library: echarts 5.5.1 | JavaScript 22.22.3
// Quality: 78/100 | Created: 2026-06-18

const t = window.ANYPLOT_TOKENS;

// Open-loop: G(s) = K / [s(s+1)(s+3)] — poles {0, −1, −3}, no zeros
// Char eq: s³ + 4s² + 3s + K = 0
// Real-axis locus: (−∞, −3] ∪ [−1, 0]  |  Breakaway: s ≈ −0.4514 at K ≈ 0.631
// jω crossing: s = ±j√3 at K = 12  |  Asymptotes: 60°, 180°, 300° from (−4/3, 0)

// Parametric formulas for complex conjugate pair given real root σ_C (Vieta):
//   σ_AB = (−4 − σ_C)/2,   ω² = 3 − σ_AB² − 2·σ_C·σ_AB
function cplxPair(sigC) {
  const sig = (-4 - sigC) / 2;
  const omegaSq = 3 - sig * sig - 2 * sigC * sig;
  return { sig, omega: Math.sqrt(Math.max(0, omegaSq)) };
}

const BKWY = -0.4514;    // breakaway real coordinate
const SIG_C0 = -3.0972; // real root (branch C) at start of complex phase

// Locus path arrays: [real, imag] pairs
const bA = [], bB = [], bC = [];

// Phase 1: real-axis convergence toward breakaway (K: 0 → 0.631)
const N1 = 60;
for (let i = 0; i <= N1; i++) {
  const f = i / N1;
  bA.push([BKWY * f, 0]);                 // pole 0  → breakaway
  bB.push([-1 + 0.5486 * f, 0]);          // pole −1 → breakaway
  bC.push([-3 + (SIG_C0 + 3) * f, 0]);   // pole −3 → SIG_C0
}

// Phase 2: complex locus (K > 0.631), σ_C from SIG_C0 to −6
const N2 = 200;
for (let i = 1; i <= N2; i++) {
  const f = i / N2;
  const sigC = SIG_C0 + (-6 - SIG_C0) * f;
  const { sig, omega } = cplxPair(sigC);
  bA.push([sig, omega]);
  bB.push([sig, -omega]);
  bC.push([sigC, 0]);
}

// Constant damping-ratio reference lines: ζ = 0.5 (angle 60° from −Re axis)
// Direction: (cos 120°, ±sin 120°) = (−0.5, ±√3/2)
// Exits at y = ±4 when r = 4/(√3/2) ≈ 4.619, x = −4.619×0.5 ≈ −2.309
const zeta05up = [[0, 0], [-2.309, 4]];
const zeta05dn = [[0, 0], [-2.309, -4]];

// Natural frequency reference circle: ωn = 2 (full circle, r = 2)
const wnCircle = [];
for (let i = 0; i <= 360; i++) {
  const theta = (i / 180) * Math.PI;
  wnCircle.push([2 * Math.cos(theta), 2 * Math.sin(theta)]);
}

// Key markers
const poles = [[0, 0], [-1, 0], [-3, 0]];
const jwCross = [[0, Math.sqrt(3)], [0, -Math.sqrt(3)]]; // ±j√3, K=12

// Gain-direction arrows: symbolRotate = atan2(dx, dy) maps chart tangent to
// ECharts clockwise-from-up convention (right→90°, up→0°, left→-90°, down→180°)
function arrowAt(path, idx) {
  const i = Math.max(1, Math.min(path.length - 2, idx));
  const dx = path[i + 1][0] - path[i - 1][0];
  const dy = path[i + 1][1] - path[i - 1][1];
  return { value: path[i], symbolRotate: Math.atan2(dx, dy) * 180 / Math.PI };
}

const gainArrows = [
  arrowAt(bA, Math.floor(N1 * 0.70)),        // phase 1, bA: leftward (K↑ toward breakaway)
  arrowAt(bB, Math.floor(N1 * 0.70)),        // phase 1, bB: rightward (K↑ toward breakaway)
  arrowAt(bA, N1 + Math.floor(N2 * 0.45)),   // phase 2, bA: upper-right (K↑ along complex branch)
  arrowAt(bB, N1 + Math.floor(N2 * 0.45)),   // phase 2, bB: lower-right (K↑ along complex branch)
  arrowAt(bC, N1 + Math.floor(N2 * 0.55)),   // phase 2, bC: leftward (K↑ along real branch)
];

// ── Chart ─────────────────────────────────────────────────────────────────────
const chart = echarts.init(document.getElementById("container"));

chart.setOption({
  animation: false,
  color: t.palette,
  backgroundColor: "transparent",

  title: {
    text: "root-locus-basic · javascript · echarts · anyplot.ai",
    left: "center",
    top: 18,
    textStyle: { color: t.ink, fontSize: 22, fontWeight: "bold" },
  },

  legend: {
    bottom: 14,
    itemGap: 32,
    textStyle: { color: t.inkSoft, fontSize: 13 },
  },

  // Square grid (2200×2200 within 2400×2400 canvas) for equal axis scaling:
  // x range 8 units and y range 8 units → 275 px/unit on each axis
  grid: { left: 120, right: 80, top: 80, bottom: 120 },

  xAxis: {
    type: "value",
    name: "Real Axis",
    nameLocation: "middle",
    nameGap: 36,
    nameTextStyle: { color: t.inkSoft, fontSize: 14 },
    min: -6,
    max: 2,
    interval: 1,
    axisLabel: { color: t.inkSoft, fontSize: 12 },
    axisLine: { show: true, lineStyle: { color: t.inkSoft } },
    axisTick: { show: true, lineStyle: { color: t.inkSoft } },
    splitLine: { lineStyle: { color: t.grid, width: 1 } },
  },

  yAxis: {
    type: "value",
    name: "Imaginary Axis",
    nameLocation: "middle",
    nameGap: 56,
    nameTextStyle: { color: t.inkSoft, fontSize: 14 },
    min: -4,
    max: 4,
    interval: 1,
    axisLabel: { color: t.inkSoft, fontSize: 12 },
    axisLine: { show: true, lineStyle: { color: t.inkSoft } },
    axisTick: { show: true, lineStyle: { color: t.inkSoft } },
    splitLine: { lineStyle: { color: t.grid, width: 1 } },
  },

  series: [
    // ζ = 0.5 damping-ratio reference lines (subtle dashed, excluded from legend)
    {
      type: "line",
      data: zeta05up,
      showSymbol: false,
      lineStyle: { color: t.grid, width: 1.2, type: "dashed" },
      silent: true,
      legendHoverLink: false,
    },
    {
      type: "line",
      data: zeta05dn,
      showSymbol: false,
      lineStyle: { color: t.grid, width: 1.2, type: "dashed" },
      silent: true,
      legendHoverLink: false,
    },
    // ωn = 2 natural frequency reference circle (now renders as true circle)
    {
      type: "line",
      data: wnCircle,
      showSymbol: false,
      lineStyle: { color: t.grid, width: 1.2, type: "dashed" },
      silent: true,
      legendHoverLink: false,
    },
    // Root locus branches (A upper, B lower, C real — share one legend entry)
    {
      name: "Root Locus",
      type: "line",
      data: bA,
      showSymbol: false,
      lineStyle: { color: t.palette[0], width: 2.5 },
    },
    {
      name: "Root Locus",
      type: "line",
      data: bB,
      showSymbol: false,
      lineStyle: { color: t.palette[0], width: 2.5 },
    },
    {
      name: "Root Locus",
      type: "line",
      data: bC,
      showSymbol: false,
      lineStyle: { color: t.palette[0], width: 2.5 },
    },
    // Gain-direction arrows (excluded from legend — decorative overlay)
    {
      type: "scatter",
      data: gainArrows,
      symbol: "arrow",
      symbolSize: 14,
      itemStyle: { color: t.palette[0] },
      silent: true,
      legendHoverLink: false,
    },
    // Open-loop poles (× markers via rotated cross shape)
    {
      name: "Open-Loop Poles",
      type: "scatter",
      data: poles,
      symbol:
        "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",
      symbolSize: 20,
      symbolRotate: 45,
      itemStyle: { color: t.palette[4] },
    },
    // Stability boundary crossings ±j√3 at K = 12
    {
      name: "jω Crossings (K=12)",
      type: "scatter",
      data: jwCross,
      symbol: "diamond",
      symbolSize: 18,
      itemStyle: { color: t.palette[1] },
    },
  ],
});

Part of Root Locus Plot for Control Systems on anyplot.ai.

Other implementations