Root Locus Plot for Control Systems — MUI X Charts

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 MUI X Charts

JavaScript source (MUI X Charts)

// anyplot.ai
// root-locus-basic: Root Locus Plot for Control Systems
// Library: muix 7.29.1 | JavaScript 22.22.3
// Quality: 85/100 | Created: 2026-06-18

import { ScatterChart } from "@mui/x-charts/ScatterChart";
import { ChartsReferenceLine } from "@mui/x-charts";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";

const tok = window.ANYPLOT_TOKENS;

// Root locus for G(s) = K / [s(s+2)(s+4)], K ∈ [0, 90]
// Characteristic equation: s³ + 6s² + 8s + K = 0
// Open-loop poles: s = 0, −2, −4
// Breakaway at s ≈ −0.845 (K ≈ 3.08); jω crossing at K = 48 (s ≈ ±j2.83)

function polyVal(r, K) {
  return r * r * r + 6 * r * r + 8 * r + K;
}
function polyDeriv(r) {
  return 3 * r * r + 12 * r + 8;
}
function findRealRoot(K, guess) {
  let r = guess;
  for (let it = 0; it < 80; it++) {
    const fp = polyDeriv(r);
    if (Math.abs(fp) < 1e-15) break;
    const delta = polyVal(r, K) / fp;
    r -= delta;
    if (Math.abs(delta) < 1e-12) break;
  }
  return r;
}
function complexPair(r3) {
  const b = 6 + r3;
  const c = 8 + r3 * b;
  const disc = b * b - 4 * c;
  if (disc >= 0) {
    const sq = Math.sqrt(disc);
    return [{ re: (-b + sq) / 2, im: 0 }, { re: (-b - sq) / 2, im: 0 }];
  }
  const sq = Math.sqrt(-disc);
  return [{ re: -b / 2, im: sq / 2 }, { re: -b / 2, im: -sq / 2 }];
}

const N_K = 500;
const K_MAX = 90;
const branchA = [], branchB = [], branchC = [];
let r3 = -4.001;

for (let i = 0; i <= N_K; i++) {
  const K = (i / N_K) * K_MAX;
  r3 = findRealRoot(K, r3);
  const pair = complexPair(r3);
  branchA.push({ x: pair[0].re, y: pair[0].im, id: i });
  branchB.push({ x: pair[1].re, y: pair[1].im, id: i + N_K + 1 });
  branchC.push({ x: r3, y: 0, id: i + 2 * (N_K + 1) });
}

// Open-loop poles (× markers at K=0)
const poleData = [
  { x: 0, y: 0, id: 9000 },
  { x: -2, y: 0, id: 9001 },
  { x: -4, y: 0, id: 9002 },
];

// Constant damping-ratio lines: straight rays from origin for ζ = 0.3, 0.5, 0.7
// Each ray: x = -r·ζ, y = ±r·√(1−ζ²)
const dampingData = [];
let dId = 10000;
for (const zeta of [0.3, 0.5, 0.7]) {
  const sinT = Math.sqrt(1 - zeta * zeta);
  for (let k = 1; k <= 28; k++) {
    const r = k * 0.3;
    dampingData.push({ x: -r * zeta, y: r * sinT, id: dId++ });
    dampingData.push({ x: -r * zeta, y: -r * sinT, id: dId++ });
  }
}

// Gain-direction arrows at branch midpoints (K ≈ 45, index = 250)
const MID = 250;
const STEP = 15;
function dirAngle(pts) {
  const dx = pts[Math.min(MID + STEP, N_K)].x - pts[MID].x;
  const dy = pts[Math.min(MID + STEP, N_K)].y - pts[MID].y;
  return Math.atan2(dy, dx) * (180 / Math.PI);
}
const ARROW_ANGLES = {
  arrowA: dirAngle(branchA),
  arrowB: dirAngle(branchB),
  arrowC: dirAngle(branchC),
};
const arrowData = [
  { x: branchA[MID].x, y: branchA[MID].y, id: 20000 },
  { x: branchB[MID].x, y: branchB[MID].y, id: 20001 },
  { x: branchC[MID].x, y: branchC[MID].y, id: 20002 },
];

// ─── Custom mark ────────────────────────────────────────────────────────────
// × for poles, ▶ (rotated) for gain arrows, tiny dot for damping, circle otherwise
const CustomMark = ({ x, y, color, seriesId }) => {
  if (seriesId === "poles") {
    const sz = 7;
    return (
      <g>
        <line
          x1={x - sz} y1={y - sz} x2={x + sz} y2={y + sz}
          stroke={color} strokeWidth={2.5} strokeLinecap="round"
        />
        <line
          x1={x + sz} y1={y - sz} x2={x - sz} y2={y + sz}
          stroke={color} strokeWidth={2.5} strokeLinecap="round"
        />
      </g>
    );
  }
  if (seriesId in ARROW_ANGLES) {
    const angle = ARROW_ANGLES[seriesId];
    return (
      <g transform={`translate(${x},${y}) rotate(${angle})`}>
        <polygon points="-7,-5 8,0 -7,5" fill={color} opacity={0.85} />
      </g>
    );
  }
  if (seriesId === "damping") {
    return <circle cx={x} cy={y} r={1.5} fill={color} opacity={0.35} />;
  }
  return <circle cx={x} cy={y} r={2.5} fill={color} />;
};

const TITLE_H = 58;

export default function Chart() {
  return (
    <Box
      sx={{
        width: window.ANYPLOT_SIZE.width,
        height: window.ANYPLOT_SIZE.height,
        display: "flex",
        flexDirection: "column",
      }}
    >
      <Typography
        sx={{
          color: "text.primary",
          fontWeight: 500,
          fontSize: 22,
          textAlign: "center",
          lineHeight: `${TITLE_H}px`,
          height: TITLE_H,
          flexShrink: 0,
        }}
      >
        root-locus-basic · javascript · muix · anyplot.ai
      </Typography>
      <ScatterChart
        width={window.ANYPLOT_SIZE.width}
        height={window.ANYPLOT_SIZE.height - TITLE_H}
        skipAnimation
        slots={{ mark: CustomMark }}
        series={[
          {
            id: "branchA",
            label: "Branch A  (s=0 → upper half-plane)",
            data: branchA,
            color: tok.palette[0],
            markerSize: 3,
          },
          {
            id: "branchB",
            label: "Branch B  (s=−2 → lower half-plane)",
            data: branchB,
            color: tok.palette[1],
            markerSize: 3,
          },
          {
            id: "branchC",
            label: "Branch C  (s=−4 → −∞)",
            data: branchC,
            color: tok.palette[2],
            markerSize: 3,
          },
          {
            id: "poles",
            label: "Open-loop poles  (K=0)",
            data: poleData,
            color: tok.ink,
            markerSize: 10,
          },
          {
            id: "damping",
            label: "Const. damping ratio  ζ=0.3, 0.5, 0.7",
            data: dampingData,
            color: tok.inkSoft,
            markerSize: 2,
          },
          {
            id: "arrowA",
            data: [arrowData[0]],
            color: tok.palette[0],
            markerSize: 12,
          },
          {
            id: "arrowB",
            data: [arrowData[1]],
            color: tok.palette[1],
            markerSize: 12,
          },
          {
            id: "arrowC",
            data: [arrowData[2]],
            color: tok.palette[2],
            markerSize: 12,
          },
        ]}
        xAxis={[
          {
            label: "Real Axis  σ",
            min: -9,
            max: 3,
            tickLabelStyle: { fontSize: 14 },
            labelStyle: { fontSize: 16 },
          },
        ]}
        yAxis={[
          {
            label: "Imaginary Axis  jω",
            min: -6,
            max: 6,
            tickLabelStyle: { fontSize: 14 },
            labelStyle: { fontSize: 16 },
          },
        ]}
        slotProps={{
          legend: { labelStyle: { fontSize: 13 } },
        }}
      >
        <ChartsReferenceLine
          x={0}
          label="Stability boundary (K=48)"
          lineStyle={{
            stroke: tok.amber,
            strokeDasharray: "8 5",
            strokeWidth: 2,
          }}
          labelStyle={{ fontSize: 12, fill: tok.amber, dy: -10 }}
        />
      </ScatterChart>
    </Box>
  );
}

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

Other implementations