{"spec_id":"phase-diagram","library":"echarts","language":"javascript","code":"// anyplot.ai\n// phase-diagram: Phase Diagram (State Space Plot)\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-05\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Linear damped oscillator: theta'' + 2*zeta*omega*theta' + omega^2*theta = 0.\n// This linearized model captures the phase-plane structure common to many damped\n// systems (spring-mass-damper, RLC circuit, weakly-driven pendulum). Three initial\n// conditions all spiral into the same fixed point (theta=0, dtheta/dt=0), revealing\n// the shared basin of attraction of the damped system.\nconst OMEGA = 2.2; // natural frequency (rad/s)\nconst ZETA = 0.12; // damping ratio (underdamped -> spiral, not overdamped decay)\nconst DT = 0.02;\nconst STEPS = 480;\n\nfunction derivative(theta, omega_) {\n  return [omega_, -2 * ZETA * OMEGA * omega_ - OMEGA * OMEGA * theta];\n}\n\nfunction integrate(theta0, omega0) {\n  const points = [[theta0, omega0]];\n  let theta = theta0;\n  let omega_ = omega0;\n  for (let i = 0; i < STEPS; i++) {\n    const k1 = derivative(theta, omega_);\n    const k2 = derivative(theta + (DT / 2) * k1[0], omega_ + (DT / 2) * k1[1]);\n    const k3 = derivative(theta + (DT / 2) * k2[0], omega_ + (DT / 2) * k2[1]);\n    const k4 = derivative(theta + DT * k3[0], omega_ + DT * k3[1]);\n    theta += (DT / 6) * (k1[0] + 2 * k2[0] + 2 * k3[0] + k4[0]);\n    omega_ += (DT / 6) * (k1[1] + 2 * k2[1] + 2 * k3[1] + k4[1]);\n    points.push([theta, omega_]);\n  }\n  return points;\n}\n\nconst trajectories = [\n  { name: \"θ₀ = 2.5 rad, ω₀ = 0\", points: integrate(2.5, 0) },\n  { name: \"θ₀ = -1.5 rad, ω₀ = 2.0 rad/s\", points: integrate(-1.5, 2.0) },\n  { name: \"θ₀ = 0.3 rad, ω₀ = -3.0 rad/s\", points: integrate(0.3, -3.0) },\n];\n\nconst allPoints = trajectories.flatMap((traj) => traj.points);\nconst thetaBound = Math.ceil(Math.max(...allPoints.map((p) => Math.abs(p[0]))) * 1.15 * 10) / 10;\nconst omegaBound = Math.ceil(Math.max(...allPoints.map((p) => Math.abs(p[1]))) * 1.15 * 10) / 10;\n\n// Pick the quadrant corner farthest from every trajectory point, so the\n// \"Equilibrium\" callout label never lands on top of the converging spirals.\nconst labelCandidates = [\n  [0.62 * thetaBound, 0.62 * omegaBound],\n  [-0.62 * thetaBound, 0.62 * omegaBound],\n  [-0.62 * thetaBound, -0.62 * omegaBound],\n  [0.62 * thetaBound, -0.62 * omegaBound],\n];\nconst [labelX, labelY] = labelCandidates.reduce((best, candidate) => {\n  const minDist = (pt) => Math.min(...allPoints.map((p) => Math.hypot(p[0] - pt[0], p[1] - pt[1])));\n  return minDist(candidate) > minDist(best) ? candidate : best;\n});\n\n// --- Init -------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option -----------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"phase-diagram · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22 },\n  },\n  legend: {\n    bottom: 10,\n    textStyle: { color: t.inkSoft, fontSize: 14 },\n  },\n  grid: { left: 90, right: 60, top: 90, bottom: 110, containLabel: true },\n  xAxis: {\n    type: \"value\",\n    name: \"Angular displacement θ (rad)\",\n    nameLocation: \"middle\",\n    nameGap: 45,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    min: -thetaBound,\n    max: thetaBound,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Angular velocity dθ/dt (rad/s)\",\n    nameLocation: \"middle\",\n    nameGap: 60,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    min: -omegaBound,\n    max: omegaBound,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    ...trajectories.map((traj, i) => ({\n      name: traj.name,\n      type: \"line\",\n      data: traj.points,\n      showSymbol: false,\n      smooth: false,\n      lineStyle: { width: 3, color: t.palette[i] },\n      markPoint: {\n        symbol: \"circle\",\n        symbolSize: 16,\n        itemStyle: { color: t.palette[i], borderColor: t.pageBg, borderWidth: 2 },\n        label: { show: false },\n        data: [{ coord: traj.points[0], name: \"start\" }],\n      },\n      // nullclines theta=0 / dtheta/dt=0 -- attached once, to the first trajectory\n      markLine:\n        i === 0\n          ? {\n              symbol: \"none\",\n              silent: true,\n              lineStyle: { type: \"dashed\", width: 1.5, color: t.inkSoft, opacity: 0.4 },\n              label: { show: false },\n              data: [{ xAxis: 0 }, { yAxis: 0 }],\n            }\n          : undefined,\n    })),\n    {\n      name: \"Equilibrium (fixed point)\",\n      type: \"scatter\",\n      data: [[0, 0]],\n      symbol: \"diamond\",\n      symbolSize: 18,\n      itemStyle: { color: t.ink, borderColor: t.pageBg, borderWidth: 2 },\n      label: { show: false },\n      z: 10,\n    },\n    // Leader line from the true fixed point to the callout label, placed in\n    // whichever corner sits farthest from the converging trajectories.\n    {\n      type: \"line\",\n      data: [\n        [0, 0],\n        [labelX, labelY],\n      ],\n      showSymbol: false,\n      silent: true,\n      lineStyle: { type: \"dashed\", width: 1.5, color: t.inkSoft, opacity: 0.6 },\n      z: 9,\n    },\n    {\n      type: \"scatter\",\n      data: [[labelX, labelY]],\n      symbol: \"circle\",\n      symbolSize: 6,\n      itemStyle: { color: t.ink },\n      label: {\n        show: true,\n        formatter: \"Equilibrium\\n(θ=0, dθ/dt=0)\",\n        position: [labelX >= 0 ? 10 : -10, labelY >= 0 ? -10 : 10],\n        align: labelX >= 0 ? \"left\" : \"right\",\n        verticalAlign: labelY >= 0 ? \"bottom\" : \"top\",\n        color: t.ink,\n        fontSize: 14,\n      },\n      z: 10,\n    },\n  ],\n});\n"}