Phase Diagram (State Space Plot) — Apache ECharts

A phase diagram (or state space plot) displays the trajectory of a dynamical system by plotting a variable against its derivative (x vs dx/dt). This visualization reveals the qualitative behavior of systems including fixed points, limit cycles, stability, and oscillation patterns. It is essential for analyzing differential equations without solving them explicitly.

Phase Diagram (State Space Plot) rendered with Apache ECharts

Renders

JavaScript source (Apache ECharts)

// anyplot.ai
// phase-diagram: Phase Diagram (State Space Plot)
// Library: echarts 6.1.0 | JavaScript 22.23.2
// Quality: 88/100 | Created: 2026-09-05
//# anyplot-orientation: square

const t = window.ANYPLOT_TOKENS;

// --- Data (in-memory, deterministic) ----------------------------------------
// Linear damped oscillator: theta'' + 2*zeta*omega*theta' + omega^2*theta = 0.
// This linearized model captures the phase-plane structure common to many damped
// systems (spring-mass-damper, RLC circuit, weakly-driven pendulum). Three initial
// conditions all spiral into the same fixed point (theta=0, dtheta/dt=0), revealing
// the shared basin of attraction of the damped system.
const OMEGA = 2.2; // natural frequency (rad/s)
const ZETA = 0.12; // damping ratio (underdamped -> spiral, not overdamped decay)
const DT = 0.02;
const STEPS = 480;

function derivative(theta, omega_) {
  return [omega_, -2 * ZETA * OMEGA * omega_ - OMEGA * OMEGA * theta];
}

function integrate(theta0, omega0) {
  const points = [[theta0, omega0]];
  let theta = theta0;
  let omega_ = omega0;
  for (let i = 0; i < STEPS; i++) {
    const k1 = derivative(theta, omega_);
    const k2 = derivative(theta + (DT / 2) * k1[0], omega_ + (DT / 2) * k1[1]);
    const k3 = derivative(theta + (DT / 2) * k2[0], omega_ + (DT / 2) * k2[1]);
    const k4 = derivative(theta + DT * k3[0], omega_ + DT * k3[1]);
    theta += (DT / 6) * (k1[0] + 2 * k2[0] + 2 * k3[0] + k4[0]);
    omega_ += (DT / 6) * (k1[1] + 2 * k2[1] + 2 * k3[1] + k4[1]);
    points.push([theta, omega_]);
  }
  return points;
}

const trajectories = [
  { name: "θ₀ = 2.5 rad, ω₀ = 0", points: integrate(2.5, 0) },
  { name: "θ₀ = -1.5 rad, ω₀ = 2.0 rad/s", points: integrate(-1.5, 2.0) },
  { name: "θ₀ = 0.3 rad, ω₀ = -3.0 rad/s", points: integrate(0.3, -3.0) },
];

const allPoints = trajectories.flatMap((traj) => traj.points);
const thetaBound = Math.ceil(Math.max(...allPoints.map((p) => Math.abs(p[0]))) * 1.15 * 10) / 10;
const omegaBound = Math.ceil(Math.max(...allPoints.map((p) => Math.abs(p[1]))) * 1.15 * 10) / 10;

// Pick the quadrant corner farthest from every trajectory point, so the
// "Equilibrium" callout label never lands on top of the converging spirals.
const labelCandidates = [
  [0.62 * thetaBound, 0.62 * omegaBound],
  [-0.62 * thetaBound, 0.62 * omegaBound],
  [-0.62 * thetaBound, -0.62 * omegaBound],
  [0.62 * thetaBound, -0.62 * omegaBound],
];
const [labelX, labelY] = labelCandidates.reduce((best, candidate) => {
  const minDist = (pt) => Math.min(...allPoints.map((p) => Math.hypot(p[0] - pt[0], p[1] - pt[1])));
  return minDist(candidate) > minDist(best) ? candidate : best;
});

// --- Init -------------------------------------------------------------------
const chart = echarts.init(document.getElementById("container"));

// --- Option -----------------------------------------------------------------
chart.setOption({
  animation: false,
  color: t.palette,
  backgroundColor: "transparent",
  title: {
    text: "phase-diagram · javascript · echarts · anyplot.ai",
    left: "center",
    textStyle: { color: t.ink, fontSize: 22 },
  },
  legend: {
    bottom: 10,
    textStyle: { color: t.inkSoft, fontSize: 14 },
  },
  grid: { left: 90, right: 60, top: 90, bottom: 110, containLabel: true },
  xAxis: {
    type: "value",
    name: "Angular displacement θ (rad)",
    nameLocation: "middle",
    nameGap: 45,
    nameTextStyle: { color: t.ink, fontSize: 16 },
    min: -thetaBound,
    max: thetaBound,
    axisLabel: { color: t.inkSoft, fontSize: 14 },
    axisLine: { lineStyle: { color: t.inkSoft } },
    splitLine: { lineStyle: { color: t.grid } },
  },
  yAxis: {
    type: "value",
    name: "Angular velocity dθ/dt (rad/s)",
    nameLocation: "middle",
    nameGap: 60,
    nameTextStyle: { color: t.ink, fontSize: 16 },
    min: -omegaBound,
    max: omegaBound,
    axisLabel: { color: t.inkSoft, fontSize: 14 },
    axisLine: { lineStyle: { color: t.inkSoft } },
    splitLine: { lineStyle: { color: t.grid } },
  },
  series: [
    ...trajectories.map((traj, i) => ({
      name: traj.name,
      type: "line",
      data: traj.points,
      showSymbol: false,
      smooth: false,
      lineStyle: { width: 3, color: t.palette[i] },
      markPoint: {
        symbol: "circle",
        symbolSize: 16,
        itemStyle: { color: t.palette[i], borderColor: t.pageBg, borderWidth: 2 },
        label: { show: false },
        data: [{ coord: traj.points[0], name: "start" }],
      },
      // nullclines theta=0 / dtheta/dt=0 -- attached once, to the first trajectory
      markLine:
        i === 0
          ? {
              symbol: "none",
              silent: true,
              lineStyle: { type: "dashed", width: 1.5, color: t.inkSoft, opacity: 0.4 },
              label: { show: false },
              data: [{ xAxis: 0 }, { yAxis: 0 }],
            }
          : undefined,
    })),
    {
      name: "Equilibrium (fixed point)",
      type: "scatter",
      data: [[0, 0]],
      symbol: "diamond",
      symbolSize: 18,
      itemStyle: { color: t.ink, borderColor: t.pageBg, borderWidth: 2 },
      label: { show: false },
      z: 10,
    },
    // Leader line from the true fixed point to the callout label, placed in
    // whichever corner sits farthest from the converging trajectories.
    {
      type: "line",
      data: [
        [0, 0],
        [labelX, labelY],
      ],
      showSymbol: false,
      silent: true,
      lineStyle: { type: "dashed", width: 1.5, color: t.inkSoft, opacity: 0.6 },
      z: 9,
    },
    {
      type: "scatter",
      data: [[labelX, labelY]],
      symbol: "circle",
      symbolSize: 6,
      itemStyle: { color: t.ink },
      label: {
        show: true,
        formatter: "Equilibrium\n(θ=0, dθ/dt=0)",
        position: [labelX >= 0 ? 10 : -10, labelY >= 0 ? -10 : 10],
        align: labelX >= 0 ? "left" : "right",
        verticalAlign: labelY >= 0 ? "bottom" : "top",
        color: t.ink,
        fontSize: 14,
      },
      z: 10,
    },
  ],
});

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/phase-diagram/echarts/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "phase-diagram",
  "language": "javascript",
  "library": "echarts",
  "page": "https://anyplot.ai/phase-diagram/javascript/echarts",
  "hub": "https://anyplot.ai/phase-diagram",
  "code_json": "https://api.anyplot.ai/specs/phase-diagram/echarts/code",
  "spec_json": "https://api.anyplot.ai/specs/phase-diagram",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/phase-diagram/javascript/echarts/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/phase-diagram/javascript/echarts/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/phase-diagram/javascript/echarts/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/phase-diagram/javascript/echarts/plot-dark.html",
  "quality_score": 88.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Phase Diagram (State Space Plot) on anyplot.ai.

Other implementations