Phase Diagram (State Space Plot) — MUI X Charts

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

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// phase-diagram: Phase Diagram (State Space Plot)
// Library: muix 7.29.1 | JavaScript 22.23.2
// Quality: 93/100 | Created: 2026-09-05
import { ChartContainer } from "@mui/x-charts/ChartContainer";
import { LinePlot } from "@mui/x-charts/LineChart";
import { ChartsXAxis } from "@mui/x-charts/ChartsXAxis";
import { ChartsYAxis } from "@mui/x-charts/ChartsYAxis";
import { ChartsGrid } from "@mui/x-charts/ChartsGrid";
import { ChartsReferenceLine } from "@mui/x-charts/ChartsReferenceLine";
import { ChartsLegend } from "@mui/x-charts/ChartsLegend";
import { ChartsText } from "@mui/x-charts/ChartsText";
import { useXScale, useYScale } from "@mui/x-charts/hooks";

const t = window.ANYPLOT_TOKENS;

// --- Damped harmonic oscillator: x'' + 2*ZETA*OMEGA*x' + OMEGA^2*x = 0 ------
// A linear system has a closed-form underdamped solution, so every
// trajectory is generated analytically instead of via numeric integration.
const OMEGA = 1; // natural frequency, rad/s
const ZETA = 0.15; // damping ratio (< 1 -> underdamped, spirals to the origin)
const OMEGA_D = OMEGA * Math.sqrt(1 - ZETA * ZETA);
const STEPS_PER_TRAJECTORY = 360;
const T_MAX = (4.5 * 2 * Math.PI) / OMEGA_D; // ~4.5 decaying oscillation periods

function trajectory(x0, v0) {
  const A = x0;
  const B = (v0 + ZETA * OMEGA * A) / OMEGA_D;
  const x = [];
  const v = [];
  for (let i = 0; i <= STEPS_PER_TRAJECTORY; i++) {
    const time = (i / STEPS_PER_TRAJECTORY) * T_MAX;
    const decay = Math.exp(-ZETA * OMEGA * time);
    const cosTerm = Math.cos(OMEGA_D * time);
    const sinTerm = Math.sin(OMEGA_D * time);
    x.push(decay * (A * cosTerm + B * sinTerm));
    v.push(
      decay *
        (-ZETA * OMEGA * (A * cosTerm + B * sinTerm) +
          OMEGA_D * (-A * sinTerm + B * cosTerm)),
    );
  }
  return { x, v, x0, v0 };
}

// --- Data: four initial conditions spiralling into the same equilibrium —
// together they trace the shared basin of attraction. Each trajectory gets
// its own xAxisId because x is non-monotonic (a spiral revisits x values),
// which rules out a single shared/sorted xAxis. -----------------------------
const trajectories = [
  trajectory(2.2, 0),
  trajectory(-2.0, 0.6),
  trajectory(0, 2.6),
  trajectory(1.3, -2.0),
].map((traj, i) => ({
  ...traj,
  axisId: `x-axis-${i}`,
  color: t.palette[i],
  label: `x₀=${traj.x0}, v₀=${traj.v0}`,
}));

const allX = trajectories.flatMap((traj) => traj.x);
const allV = trajectories.flatMap((traj) => traj.v);
const xSpan = Math.max(...allX) - Math.min(...allX);
const vSpan = Math.max(...allV) - Math.min(...allV);
const X_MIN = Math.min(...allX) - xSpan * 0.12;
const X_MAX = Math.max(...allX) + xSpan * 0.12;
const V_MIN = Math.min(...allV) - vSpan * 0.12;
const V_MAX = Math.max(...allV) + vSpan * 0.12;

const TITLE =
  "Damped Oscillator · phase-diagram · javascript · muix · anyplot.ai";
const TITLE_HEIGHT = 70;
const TITLE_FONT_SIZE = Math.max(
  15,
  Math.round(22 * Math.min(1, 67 / TITLE.length)),
);

const MARGIN = { top: 30, bottom: 150, left: 130, right: 60 };

// --- Time-direction arrowheads: small triangles dropped at fixed points
// along each trajectory's arc (as fractions of its 0..STEPS_PER_TRAJECTORY
// index range), oriented along the local tangent. The spec explicitly
// suggests a direction cue since motion along a spiral is otherwise
// ambiguous in a static render. -------------------------------------------
const ARROW_FRACTIONS = [0.08, 0.24, 0.44, 0.68];
const ARROW_TANGENT_STEP = 2;
const ARROW_SIZE = 9;
const ARROW_SPREAD = 0.5; // radians between each back corner and the tip

function arrowPoints(cx, cy, angle, size) {
  const backAngle = angle + Math.PI;
  const tip = [cx + Math.cos(angle) * size, cy + Math.sin(angle) * size];
  const left = [
    cx + Math.cos(backAngle - ARROW_SPREAD) * size,
    cy + Math.sin(backAngle - ARROW_SPREAD) * size,
  ];
  const right = [
    cx + Math.cos(backAngle + ARROW_SPREAD) * size,
    cy + Math.sin(backAngle + ARROW_SPREAD) * size,
  ];
  return `${tip.join(",")} ${left.join(",")} ${right.join(",")}`;
}

// --- Custom overlay: the shared equilibrium, each trajectory's starting
// point, and direction-of-travel arrowheads. Community `@mui/x-charts/hooks`
// (useXScale/useYScale) map data coordinates to pixels so the markers stay
// aligned with the lines at any size. Hooks are called unconditionally, once
// per (fixed) trajectory. ---------------------------------------------------
function PhaseMarkers() {
  const yScale = useYScale();
  const xScale0 = useXScale(trajectories[0].axisId);
  const xScale1 = useXScale(trajectories[1].axisId);
  const xScale2 = useXScale(trajectories[2].axisId);
  const xScale3 = useXScale(trajectories[3].axisId);
  const xScales = [xScale0, xScale1, xScale2, xScale3];

  const origin = { x: xScale0(0), y: yScale(0) };

  return (
    <g>
      {trajectories.map((traj, i) => {
        const xScale = xScales[i];
        const start = { x: xScale(traj.x[0]), y: yScale(traj.v[0]) };
        return (
          <g key={traj.axisId}>
            {ARROW_FRACTIONS.map((frac) => {
              const idx = Math.round(frac * STEPS_PER_TRAJECTORY);
              const p0 = { x: xScale(traj.x[idx]), y: yScale(traj.v[idx]) };
              const p1 = {
                x: xScale(traj.x[idx + ARROW_TANGENT_STEP]),
                y: yScale(traj.v[idx + ARROW_TANGENT_STEP]),
              };
              const angle = Math.atan2(p1.y - p0.y, p1.x - p0.x);
              return (
                <polygon
                  key={`${traj.axisId}-arrow-${frac}`}
                  points={arrowPoints(p0.x, p0.y, angle, ARROW_SIZE)}
                  fill={traj.color}
                  stroke={t.pageBg}
                  strokeWidth={1}
                />
              );
            })}
            <circle
              cx={start.x}
              cy={start.y}
              r={10}
              fill={traj.color}
              stroke={t.pageBg}
              strokeWidth={2.5}
            />
          </g>
        );
      })}
      <circle
        cx={origin.x}
        cy={origin.y}
        r={13}
        fill={t.ink}
        stroke={t.pageBg}
        strokeWidth={3}
      />
      {/* Backing plate keeps the label legible over the densely wound spirals */}
      <rect
        x={origin.x + 18}
        y={origin.y - 38}
        width={190}
        height={28}
        rx={5}
        fill={t.elevatedBg}
        opacity={0.9}
      />
      <ChartsText
        x={origin.x + 26}
        y={origin.y - 24}
        text="Equilibrium (0, 0)"
        style={{ fontSize: 16, fill: t.inkSoft, dominantBaseline: "central" }}
      />
    </g>
  );
}

// --- Chart (default-exported component — the harness mounts it) -------------
export default function Chart() {
  return (
    <div
      style={{
        width: window.ANYPLOT_SIZE.width,
        height: window.ANYPLOT_SIZE.height,
        display: "flex",
        flexDirection: "column",
      }}
    >
      <div
        style={{
          height: TITLE_HEIGHT,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          fontSize: TITLE_FONT_SIZE,
          fontWeight: 500,
          color: t.ink,
        }}
      >
        {TITLE}
      </div>
      <ChartContainer
        width={window.ANYPLOT_SIZE.width}
        height={window.ANYPLOT_SIZE.height - TITLE_HEIGHT}
        margin={MARGIN}
        skipAnimation
        xAxis={trajectories.map((traj) => ({
          id: traj.axisId,
          scaleType: "linear",
          data: traj.x,
          min: X_MIN,
          max: X_MAX,
        }))}
        yAxis={[{ scaleType: "linear", min: V_MIN, max: V_MAX }]}
        series={trajectories.map((traj) => ({
          type: "line",
          data: traj.v,
          xAxisId: traj.axisId,
          color: traj.color,
          label: traj.label,
          curve: "linear",
          showMark: false,
        }))}
      >
        <ChartsGrid horizontal vertical />
        <ChartsReferenceLine
          x={0}
          lineStyle={{ stroke: t.grid, strokeWidth: 2 }}
        />
        <ChartsReferenceLine
          y={0}
          lineStyle={{ stroke: t.grid, strokeWidth: 2 }}
        />
        <LinePlot />
        <PhaseMarkers />
        <ChartsXAxis
          axisId={trajectories[0].axisId}
          label="Position x"
          labelStyle={{ fontSize: 16, fill: t.ink, fontWeight: 500 }}
          tickLabelStyle={{ fontSize: 14, fill: t.inkSoft }}
          stroke={t.inkSoft}
        />
        <ChartsYAxis
          label="Velocity dx/dt"
          labelStyle={{ fontSize: 16, fill: t.ink, fontWeight: 500 }}
          tickLabelStyle={{ fontSize: 14, fill: t.inkSoft }}
          stroke={t.inkSoft}
        />
        <ChartsLegend
          direction="row"
          position={{ vertical: "bottom", horizontal: "middle" }}
          slotProps={{
            legend: { labelStyle: { fontSize: 14, fill: t.inkSoft } },
          }}
        />
      </ChartContainer>
    </div>
  );
}

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/phase-diagram/muix/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": "muix",
  "page": "https://anyplot.ai/phase-diagram/javascript/muix",
  "hub": "https://anyplot.ai/phase-diagram",
  "code_json": "https://api.anyplot.ai/specs/phase-diagram/muix/code",
  "spec_json": "https://api.anyplot.ai/specs/phase-diagram",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/phase-diagram/javascript/muix/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/phase-diagram/javascript/muix/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/phase-diagram/javascript/muix/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/phase-diagram/javascript/muix/plot-dark.html",
  "quality_score": 93.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

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

Other implementations