{"spec_id":"line-3d-trajectory","library":"muix","language":"javascript","code":"// anyplot.ai\n// line-3d-trajectory: 3D Line Plot for Trajectory Visualization\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-10\n//# anyplot-orientation: landscape\n// anyplot.ai\n// line-3d-trajectory: 3D Line Plot for Trajectory Visualization\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-09-10\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: Lorenz attractor, two trajectories from nearly identical initial\n// conditions — the classic demonstration of chaotic sensitivity to initial\n// conditions (\"butterfly effect\"), integrated with RK4. ---------------------\nconst SIGMA = 10;\nconst RHO = 28;\nconst BETA = 8 / 3;\nconst DT = 0.01;\nconst STEPS = 4000; // 40 time units — long enough for the two trajectories to\nconst TRANSIENT = 400; // switch attractor wings at different moments\nconst SUBSAMPLE = 2; // effective 0.02 step keeps the tight loops smooth\n\nfunction lorenzDerivative(state: number[]) {\n  const [x, y, z] = state;\n  return [SIGMA * (y - x), x * (RHO - z) - y, x * y - BETA * z];\n}\n\nfunction rk4Step(state: number[], dt: number) {\n  const k1 = lorenzDerivative(state);\n  const k2 = lorenzDerivative(state.map((v, i) => v + (dt / 2) * k1[i]));\n  const k3 = lorenzDerivative(state.map((v, i) => v + (dt / 2) * k2[i]));\n  const k4 = lorenzDerivative(state.map((v, i) => v + dt * k3[i]));\n  return state.map(\n    (v, i) => v + (dt / 6) * (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i]),\n  );\n}\n\nfunction simulate(initial: number[]) {\n  let state = initial;\n  const points: number[][] = [];\n  for (let i = 0; i < STEPS; i++) {\n    state = rk4Step(state, DT);\n    if (i >= TRANSIENT && (i - TRANSIENT) % SUBSAMPLE === 0)\n      points.push(state.slice());\n  }\n  return points;\n}\n\nconst trajectoryA = simulate([0.1, 0, 0]);\nconst trajectoryB = simulate([0.1001, 0, 0]); // perturbed by 0.0001 in x\n\n// --- Normalize to a unit cube so the isometric projection treats every axis\n// equivalently, then project 3D -> 2D with a fixed camera angle (elevation\n// 20°, azimuth 32°) — the same technique a static mplot3d render uses. ------\nconst allPoints = [...trajectoryA, ...trajectoryB];\nconst X_MIN = Math.min(...allPoints.map((p) => p[0]));\nconst X_MAX = Math.max(...allPoints.map((p) => p[0]));\nconst Y_MIN = Math.min(...allPoints.map((p) => p[1]));\nconst Y_MAX = Math.max(...allPoints.map((p) => p[1]));\nconst Z_MIN = Math.min(...allPoints.map((p) => p[2]));\nconst Z_MAX = Math.max(...allPoints.map((p) => p[2]));\n\nfunction normalize(p: number[]) {\n  return [\n    (2 * (p[0] - X_MIN)) / (X_MAX - X_MIN) - 1,\n    (2 * (p[1] - Y_MIN)) / (Y_MAX - Y_MIN) - 1,\n    (2 * (p[2] - Z_MIN)) / (Z_MAX - Z_MIN) - 1,\n  ];\n}\n\nconst ELEV = (20 * Math.PI) / 180;\nconst AZIM = (32 * Math.PI) / 180;\n\nfunction projectUnit(x: number, y: number, z: number) {\n  const xRot = x * Math.cos(AZIM) - y * Math.sin(AZIM);\n  const yRot = x * Math.sin(AZIM) + y * Math.cos(AZIM);\n  return { sx: xRot, sy: yRot * Math.sin(ELEV) + z * Math.cos(ELEV) };\n}\n\nfunction project(p: number[]) {\n  const [x, y, z] = normalize(p);\n  return projectUnit(x, y, z);\n}\n\nconst projectedA = trajectoryA.map(project);\nconst projectedB = trajectoryB.map(project);\nconst allProjected = [...projectedA, ...projectedB];\n\nconst SX_MIN = Math.min(...allProjected.map((p) => p.sx));\nconst SX_MAX = Math.max(...allProjected.map((p) => p.sx));\nconst SY_MIN = Math.min(...allProjected.map((p) => p.sy));\nconst SY_MAX = Math.max(...allProjected.map((p) => p.sy));\nconst PAD_X = (SX_MAX - SX_MIN) * 0.08;\nconst PAD_Y = (SY_MAX - SY_MIN) * 0.12;\n\nconst TITLE_H = 60;\nconst FONT = \"Inter, system-ui, sans-serif\";\n\n// --- Custom overlay: MUI X's built-in series don't cover 3D paths, so we\n// project the trajectories ourselves and paint them as plain SVG inside the\n// ChartContainer's drawing area — the composition pattern MUI X documents\n// for chart types outside the built-in series set. --------------------------\nfunction Trajectories() {\n  const { left, top, width, height: areaHeight } = useDrawingArea();\n  const xOf = (sx: number) =>\n    left +\n    ((sx - (SX_MIN - PAD_X)) / (SX_MAX + PAD_X - (SX_MIN - PAD_X))) * width;\n  const yOf = (sy: number) =>\n    top +\n    areaHeight -\n    ((sy - (SY_MIN - PAD_Y)) / (SY_MAX + PAD_Y - (SY_MIN - PAD_Y))) *\n      areaHeight;\n\n  const toPolyline = (pts: { sx: number; sy: number }[]) =>\n    pts.map((p) => `${xOf(p.sx)},${yOf(p.sy)}`).join(\" \");\n\n  const startPx = { x: xOf(projectedA[0].sx), y: yOf(projectedA[0].sy) };\n  // The padding (PAD_X/PAD_Y) reserves a data-free margin around the projected\n  // bounding box, so the top-left corner of the drawing area is guaranteed\n  // clear of trajectory lines — a safe spot for the \"shared start\" label,\n  // reached via a leader line from the actual start marker.\n  const labelAnchor = { x: left + 14, y: top + 22 };\n\n  // Small axis-orientation gizmo fixed near the corner — a bounding-box axis\n  // frame would fight the attractor's irregular, self-crossing shape.\n  const gizmoOrigin = { x: left + 66, y: top + areaHeight - 60 };\n  const ARM = 60;\n  const gizmoAxes = [\n    { dir: projectUnit(1, 0, 0), label: \"X\" },\n    { dir: projectUnit(0, 1, 0), label: \"Y\" },\n    { dir: projectUnit(0, 0, 1), label: \"Z\" },\n  ];\n\n  return (\n    <g>\n      <polyline\n        points={toPolyline(projectedA)}\n        fill=\"none\"\n        stroke={t.palette[0]}\n        strokeWidth={1.5}\n        opacity={0.55}\n      />\n      <polyline\n        points={toPolyline(projectedB)}\n        fill=\"none\"\n        stroke={t.palette[1]}\n        strokeWidth={1.5}\n        opacity={0.55}\n      />\n      <circle cx={startPx.x} cy={startPx.y} r={6} fill={t.ink} />\n      <line\n        x1={startPx.x}\n        y1={startPx.y}\n        x2={labelAnchor.x}\n        y2={labelAnchor.y}\n        stroke={t.inkSoft}\n        strokeWidth={1}\n        strokeDasharray=\"3,3\"\n        opacity={0.7}\n      />\n      <text\n        x={labelAnchor.x}\n        y={labelAnchor.y}\n        fill={t.inkSoft}\n        style={{ fontSize: 14, fontFamily: FONT }}\n      >\n        shared start\n      </text>\n\n      {gizmoAxes.map((a) => {\n        const tip = {\n          x: gizmoOrigin.x + a.dir.sx * ARM,\n          y: gizmoOrigin.y - a.dir.sy * ARM,\n        };\n        return (\n          <g key={a.label}>\n            <line\n              x1={gizmoOrigin.x}\n              y1={gizmoOrigin.y}\n              x2={tip.x}\n              y2={tip.y}\n              stroke={t.inkSoft}\n              strokeWidth={2}\n            />\n            <text\n              x={gizmoOrigin.x + a.dir.sx * (ARM + 20)}\n              y={gizmoOrigin.y - a.dir.sy * (ARM + 20)}\n              fill={t.inkSoft}\n              textAnchor=\"middle\"\n              style={{ fontSize: 15, fontWeight: 600, fontFamily: FONT }}\n            >\n              {a.label}\n            </text>\n          </g>\n        );\n      })}\n      <text\n        x={gizmoOrigin.x}\n        y={gizmoOrigin.y + 20}\n        fill={t.inkSoft}\n        textAnchor=\"middle\"\n        style={{\n          fontSize: 10,\n          fontStyle: \"italic\",\n          fontFamily: FONT,\n          opacity: 0.85,\n        }}\n      >\n        normalized units\n      </text>\n\n      <g transform={`translate(${left + width - 300}, ${top + 6})`}>\n        <rect x={0} y={0} width={14} height={14} fill={t.palette[0]} />\n        <text\n          x={20}\n          y={12}\n          fill={t.inkSoft}\n          style={{ fontSize: 14, fontFamily: FONT }}\n        >\n          Trajectory A · x₀ = 0.1000\n        </text>\n        <rect x={0} y={24} width={14} height={14} fill={t.palette[1]} />\n        <text\n          x={20}\n          y={36}\n          fill={t.inkSoft}\n          style={{ fontSize: 14, fontFamily: FONT }}\n        >\n          Trajectory B · x₀ = 0.1001\n        </text>\n      </g>\n    </g>\n  );\n}\n\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n\n  return (\n    <div\n      style={{\n        width: W,\n        height: H,\n        background: t.pageBg,\n        fontFamily: FONT,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <div\n        style={{\n          height: TITLE_H,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n        }}\n      >\n        <span style={{ fontSize: 22, fontWeight: 600, color: t.ink }}>\n          line-3d-trajectory · javascript · muix · anyplot.ai\n        </span>\n      </div>\n      <ChartContainer\n        width={W}\n        height={H - TITLE_H}\n        skipAnimation\n        series={[]}\n        xAxis={[{ min: 0, max: 1 }]}\n        yAxis={[{ min: 0, max: 1 }]}\n        margin={{ top: 20, bottom: 30, left: 30, right: 30 }}\n      >\n        <Trajectories />\n      </ChartContainer>\n    </div>\n  );\n}\n"}