{"spec_id":"phase-diagram","library":"muix","language":"javascript","code":"// anyplot.ai\n// phase-diagram: Phase Diagram (State Space Plot)\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-05\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { LinePlot } from \"@mui/x-charts/LineChart\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport { ChartsLegend } from \"@mui/x-charts/ChartsLegend\";\nimport { ChartsText } from \"@mui/x-charts/ChartsText\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Damped harmonic oscillator: x'' + 2*ZETA*OMEGA*x' + OMEGA^2*x = 0 ------\n// A linear system has a closed-form underdamped solution, so every\n// trajectory is generated analytically instead of via numeric integration.\nconst OMEGA = 1; // natural frequency, rad/s\nconst ZETA = 0.15; // damping ratio (< 1 -> underdamped, spirals to the origin)\nconst OMEGA_D = OMEGA * Math.sqrt(1 - ZETA * ZETA);\nconst STEPS_PER_TRAJECTORY = 360;\nconst T_MAX = (4.5 * 2 * Math.PI) / OMEGA_D; // ~4.5 decaying oscillation periods\n\nfunction trajectory(x0, v0) {\n  const A = x0;\n  const B = (v0 + ZETA * OMEGA * A) / OMEGA_D;\n  const x = [];\n  const v = [];\n  for (let i = 0; i <= STEPS_PER_TRAJECTORY; i++) {\n    const time = (i / STEPS_PER_TRAJECTORY) * T_MAX;\n    const decay = Math.exp(-ZETA * OMEGA * time);\n    const cosTerm = Math.cos(OMEGA_D * time);\n    const sinTerm = Math.sin(OMEGA_D * time);\n    x.push(decay * (A * cosTerm + B * sinTerm));\n    v.push(\n      decay *\n        (-ZETA * OMEGA * (A * cosTerm + B * sinTerm) +\n          OMEGA_D * (-A * sinTerm + B * cosTerm)),\n    );\n  }\n  return { x, v, x0, v0 };\n}\n\n// --- Data: four initial conditions spiralling into the same equilibrium —\n// together they trace the shared basin of attraction. Each trajectory gets\n// its own xAxisId because x is non-monotonic (a spiral revisits x values),\n// which rules out a single shared/sorted xAxis. -----------------------------\nconst trajectories = [\n  trajectory(2.2, 0),\n  trajectory(-2.0, 0.6),\n  trajectory(0, 2.6),\n  trajectory(1.3, -2.0),\n].map((traj, i) => ({\n  ...traj,\n  axisId: `x-axis-${i}`,\n  color: t.palette[i],\n  label: `x₀=${traj.x0}, v₀=${traj.v0}`,\n}));\n\nconst allX = trajectories.flatMap((traj) => traj.x);\nconst allV = trajectories.flatMap((traj) => traj.v);\nconst xSpan = Math.max(...allX) - Math.min(...allX);\nconst vSpan = Math.max(...allV) - Math.min(...allV);\nconst X_MIN = Math.min(...allX) - xSpan * 0.12;\nconst X_MAX = Math.max(...allX) + xSpan * 0.12;\nconst V_MIN = Math.min(...allV) - vSpan * 0.12;\nconst V_MAX = Math.max(...allV) + vSpan * 0.12;\n\nconst TITLE =\n  \"Damped Oscillator · phase-diagram · javascript · muix · anyplot.ai\";\nconst TITLE_HEIGHT = 70;\nconst TITLE_FONT_SIZE = Math.max(\n  15,\n  Math.round(22 * Math.min(1, 67 / TITLE.length)),\n);\n\nconst MARGIN = { top: 30, bottom: 150, left: 130, right: 60 };\n\n// --- Time-direction arrowheads: small triangles dropped at fixed points\n// along each trajectory's arc (as fractions of its 0..STEPS_PER_TRAJECTORY\n// index range), oriented along the local tangent. The spec explicitly\n// suggests a direction cue since motion along a spiral is otherwise\n// ambiguous in a static render. -------------------------------------------\nconst ARROW_FRACTIONS = [0.08, 0.24, 0.44, 0.68];\nconst ARROW_TANGENT_STEP = 2;\nconst ARROW_SIZE = 9;\nconst ARROW_SPREAD = 0.5; // radians between each back corner and the tip\n\nfunction arrowPoints(cx, cy, angle, size) {\n  const backAngle = angle + Math.PI;\n  const tip = [cx + Math.cos(angle) * size, cy + Math.sin(angle) * size];\n  const left = [\n    cx + Math.cos(backAngle - ARROW_SPREAD) * size,\n    cy + Math.sin(backAngle - ARROW_SPREAD) * size,\n  ];\n  const right = [\n    cx + Math.cos(backAngle + ARROW_SPREAD) * size,\n    cy + Math.sin(backAngle + ARROW_SPREAD) * size,\n  ];\n  return `${tip.join(\",\")} ${left.join(\",\")} ${right.join(\",\")}`;\n}\n\n// --- Custom overlay: the shared equilibrium, each trajectory's starting\n// point, and direction-of-travel arrowheads. Community `@mui/x-charts/hooks`\n// (useXScale/useYScale) map data coordinates to pixels so the markers stay\n// aligned with the lines at any size. Hooks are called unconditionally, once\n// per (fixed) trajectory. ---------------------------------------------------\nfunction PhaseMarkers() {\n  const yScale = useYScale();\n  const xScale0 = useXScale(trajectories[0].axisId);\n  const xScale1 = useXScale(trajectories[1].axisId);\n  const xScale2 = useXScale(trajectories[2].axisId);\n  const xScale3 = useXScale(trajectories[3].axisId);\n  const xScales = [xScale0, xScale1, xScale2, xScale3];\n\n  const origin = { x: xScale0(0), y: yScale(0) };\n\n  return (\n    <g>\n      {trajectories.map((traj, i) => {\n        const xScale = xScales[i];\n        const start = { x: xScale(traj.x[0]), y: yScale(traj.v[0]) };\n        return (\n          <g key={traj.axisId}>\n            {ARROW_FRACTIONS.map((frac) => {\n              const idx = Math.round(frac * STEPS_PER_TRAJECTORY);\n              const p0 = { x: xScale(traj.x[idx]), y: yScale(traj.v[idx]) };\n              const p1 = {\n                x: xScale(traj.x[idx + ARROW_TANGENT_STEP]),\n                y: yScale(traj.v[idx + ARROW_TANGENT_STEP]),\n              };\n              const angle = Math.atan2(p1.y - p0.y, p1.x - p0.x);\n              return (\n                <polygon\n                  key={`${traj.axisId}-arrow-${frac}`}\n                  points={arrowPoints(p0.x, p0.y, angle, ARROW_SIZE)}\n                  fill={traj.color}\n                  stroke={t.pageBg}\n                  strokeWidth={1}\n                />\n              );\n            })}\n            <circle\n              cx={start.x}\n              cy={start.y}\n              r={10}\n              fill={traj.color}\n              stroke={t.pageBg}\n              strokeWidth={2.5}\n            />\n          </g>\n        );\n      })}\n      <circle\n        cx={origin.x}\n        cy={origin.y}\n        r={13}\n        fill={t.ink}\n        stroke={t.pageBg}\n        strokeWidth={3}\n      />\n      {/* Backing plate keeps the label legible over the densely wound spirals */}\n      <rect\n        x={origin.x + 18}\n        y={origin.y - 38}\n        width={190}\n        height={28}\n        rx={5}\n        fill={t.elevatedBg}\n        opacity={0.9}\n      />\n      <ChartsText\n        x={origin.x + 26}\n        y={origin.y - 24}\n        text=\"Equilibrium (0, 0)\"\n        style={{ fontSize: 16, fill: t.inkSoft, dominantBaseline: \"central\" }}\n      />\n    </g>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  return (\n    <div\n      style={{\n        width: window.ANYPLOT_SIZE.width,\n        height: window.ANYPLOT_SIZE.height,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <div\n        style={{\n          height: TITLE_HEIGHT,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          fontSize: TITLE_FONT_SIZE,\n          fontWeight: 500,\n          color: t.ink,\n        }}\n      >\n        {TITLE}\n      </div>\n      <ChartContainer\n        width={window.ANYPLOT_SIZE.width}\n        height={window.ANYPLOT_SIZE.height - TITLE_HEIGHT}\n        margin={MARGIN}\n        skipAnimation\n        xAxis={trajectories.map((traj) => ({\n          id: traj.axisId,\n          scaleType: \"linear\",\n          data: traj.x,\n          min: X_MIN,\n          max: X_MAX,\n        }))}\n        yAxis={[{ scaleType: \"linear\", min: V_MIN, max: V_MAX }]}\n        series={trajectories.map((traj) => ({\n          type: \"line\",\n          data: traj.v,\n          xAxisId: traj.axisId,\n          color: traj.color,\n          label: traj.label,\n          curve: \"linear\",\n          showMark: false,\n        }))}\n      >\n        <ChartsGrid horizontal vertical />\n        <ChartsReferenceLine\n          x={0}\n          lineStyle={{ stroke: t.grid, strokeWidth: 2 }}\n        />\n        <ChartsReferenceLine\n          y={0}\n          lineStyle={{ stroke: t.grid, strokeWidth: 2 }}\n        />\n        <LinePlot />\n        <PhaseMarkers />\n        <ChartsXAxis\n          axisId={trajectories[0].axisId}\n          label=\"Position x\"\n          labelStyle={{ fontSize: 16, fill: t.ink, fontWeight: 500 }}\n          tickLabelStyle={{ fontSize: 14, fill: t.inkSoft }}\n          stroke={t.inkSoft}\n        />\n        <ChartsYAxis\n          label=\"Velocity dx/dt\"\n          labelStyle={{ fontSize: 16, fill: t.ink, fontWeight: 500 }}\n          tickLabelStyle={{ fontSize: 14, fill: t.inkSoft }}\n          stroke={t.inkSoft}\n        />\n        <ChartsLegend\n          direction=\"row\"\n          position={{ vertical: \"bottom\", horizontal: \"middle\" }}\n          slotProps={{\n            legend: { labelStyle: { fontSize: 14, fill: t.inkSoft } },\n          }}\n        />\n      </ChartContainer>\n    </div>\n  );\n}\n"}