{"spec_id":"phase-diagram","library":"d3","language":"javascript","code":"// anyplot.ai\n// phase-diagram: Phase Diagram (State Space Plot)\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 87/100 | Created: 2026-09-05\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 120, right: 100, bottom: 80, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Physics: damped harmonic oscillator ------------------------------------\n// dx/dt = v ; dv/dt = -2*zeta*omega*v - omega^2*x  (RK4, fixed-seed deterministic)\nconst OMEGA = 1.3;\nconst ZETA = 0.12;\nconst DT = 0.05;\nconst STEPS = 220;\n\nfunction derivative(state) {\n  const [x, v] = state;\n  return [v, -2 * ZETA * OMEGA * v - OMEGA * OMEGA * x];\n}\n\nfunction rk4Step(state, dt) {\n  const k1 = derivative(state);\n  const k2 = derivative([state[0] + (dt / 2) * k1[0], state[1] + (dt / 2) * k1[1]]);\n  const k3 = derivative([state[0] + (dt / 2) * k2[0], state[1] + (dt / 2) * k2[1]]);\n  const k4 = derivative([state[0] + dt * k3[0], state[1] + dt * k3[1]]);\n  return [\n    state[0] + (dt / 6) * (k1[0] + 2 * k2[0] + 2 * k3[0] + k4[0]),\n    state[1] + (dt / 6) * (k1[1] + 2 * k2[1] + 2 * k3[1] + k4[1]),\n  ];\n}\n\nfunction integrate(x0, v0, dt, steps) {\n  const points = [{ x: x0, v: v0 }];\n  let state = [x0, v0];\n  for (let i = 0; i < steps; i++) {\n    state = rk4Step(state, dt);\n    points.push({ x: state[0], v: state[1] });\n  }\n  return points;\n}\n\n// --- Data: three initial conditions spiraling into the stable fixed point --\nconst initialConditions = [\n  { x0: 2.2, v0: 0.0, label: \"x₀ = 2.2, v₀ = 0.0\" },\n  { x0: -1.8, v0: 1.4, label: \"x₀ = −1.8, v₀ = 1.4\" },\n  { x0: 0.6, v0: -2.0, label: \"x₀ = 0.6, v₀ = −2.0\" },\n];\nconst trajectories = initialConditions.map((ic, i) => ({\n  ...ic,\n  color: t.palette[i],\n  points: integrate(ic.x0, ic.v0, DT, STEPS),\n}));\n\nconst allValues = trajectories.flatMap((tr) => tr.points.flatMap((p) => [p.x, p.v]));\nconst maxAbs = d3.max(allValues, (v) => Math.abs(v)) * 1.15;\n\n// --- SVG mount + scales (equal domain span on both axes keeps spirals\n//     geometrically correct instead of visually stretched) ------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\nconst x = d3.scaleLinear().domain([-maxAbs, maxAbs]).range([0, iw]);\nconst v = d3.scaleLinear().domain([-maxAbs, maxAbs]).range([ih, 0]);\n\n// --- Axes crossing at the origin (the fixed point) --------------------------\nconst xAxisG = g.append(\"g\").attr(\"transform\", `translate(0,${v(0)})`).call(d3.axisBottom(x).ticks(8));\nconst yAxisG = g.append(\"g\").attr(\"transform\", `translate(${x(0)},0)`).call(d3.axisLeft(v).ticks(8));\nfor (const ax of [xAxisG, yAxisG]) {\n  // Origin-crossing ticks sit inside the densest trajectory overlap, so give\n  // the labels a page-colored halo (paint-order stroke) to stay legible\n  // instead of competing with the spiral strokes underneath.\n  ax.selectAll(\"text\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .style(\"paint-order\", \"stroke\")\n    .style(\"stroke\", t.pageBg)\n    .style(\"stroke-width\", \"5px\")\n    .style(\"stroke-linejoin\", \"round\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.grid);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Trajectories: opacity ramps with elapsed time, arrowheads show flow ---\n// Each RK4 step is drawn as its own butt-capped segment (no shared-endpoint\n// overlap between adjacent segments) so the per-step opacity ramp reads as a\n// smooth fade instead of banding at chunk boundaries.\nfor (const tr of trajectories) {\n  const n = tr.points.length;\n  for (let i = 0; i < n - 1; i++) {\n    const opacity = 0.45 + 0.55 * (i / (n - 2));\n    g.append(\"line\")\n      .attr(\"x1\", x(tr.points[i].x))\n      .attr(\"y1\", v(tr.points[i].v))\n      .attr(\"x2\", x(tr.points[i + 1].x))\n      .attr(\"y2\", v(tr.points[i + 1].v))\n      .attr(\"stroke\", tr.color)\n      .attr(\"stroke-width\", 4)\n      .attr(\"stroke-linecap\", \"butt\")\n      .attr(\"opacity\", opacity);\n  }\n\n  const arrowTriangle = d3.symbol().type(d3.symbolTriangle).size(240)();\n  for (const frac of [0.28, 0.6, 0.9]) {\n    const i = Math.min(n - 2, Math.max(1, Math.round(frac * n)));\n    const before = tr.points[i - 1];\n    const after = tr.points[i + 1];\n    const dx = x(after.x) - x(before.x);\n    const dy = v(after.v) - v(before.v);\n    const angle = (Math.atan2(dy, dx) * 180) / Math.PI + 90;\n    g.append(\"path\")\n      .attr(\"d\", arrowTriangle)\n      .attr(\"transform\", `translate(${x(tr.points[i].x)},${v(tr.points[i].v)}) rotate(${angle})`)\n      .attr(\"fill\", tr.color);\n  }\n}\n\n// --- Fixed point marker (stable equilibrium at the origin) ------------------\ng.append(\"circle\")\n  .attr(\"cx\", x(0))\n  .attr(\"cy\", v(0))\n  .attr(\"r\", 16)\n  .attr(\"fill\", t.pageBg)\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 3);\n\n// --- Legend (initial conditions) --------------------------------------------\nconst legendX = iw - 260;\nconst legendY = 10;\ntrajectories.forEach((tr, i) => {\n  const ly = legendY + i * 34;\n  g.append(\"line\")\n    .attr(\"x1\", legendX)\n    .attr(\"x2\", legendX + 36)\n    .attr(\"y1\", ly)\n    .attr(\"y2\", ly)\n    .attr(\"stroke\", tr.color)\n    .attr(\"stroke-width\", 5)\n    .attr(\"stroke-linecap\", \"round\");\n  g.append(\"text\")\n    .attr(\"x\", legendX + 46)\n    .attr(\"y\", ly + 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .text(tr.label);\n});\n\n// --- Axis labels --------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", height - 24)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Position, x\");\n\nsvg\n  .append(\"text\")\n  .attr(\"transform\", `translate(28,${margin.top + ih / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Velocity, dx/dt\");\n\n// --- Title -------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"phase-diagram · javascript · d3 · anyplot.ai\");\n"}