{"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 9.16.0 | JavaScript 22.23.3\n// Quality: 83/100 | Updated: 2026-10-24\n\nimport { ChartsContainer } from \"@mui/x-charts/ChartsContainer\";\nimport { useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Constants\nconst FONT = '\"Plus Jakarta Sans\", \"Inter\", -apple-system, BlinkMacSystemFont, sans-serif';\nconst MONO = '\"JetBrains Mono\", \"SFMono-Regular\", Menlo, monospace';\nconst ELEV_DEG = 24;\nconst AZIM_DEG = -40;\nconst ELEV_RAD = (ELEV_DEG * Math.PI) / 180;\nconst AZIM_RAD = (AZIM_DEG * Math.PI) / 180;\nconst COS_AZ = Math.cos(AZIM_RAD);\nconst SIN_AZ = Math.sin(AZIM_RAD);\nconst COS_EL = Math.cos(ELEV_RAD);\nconst SIN_EL = Math.sin(ELEV_RAD);\n// Half-extents of the drawn box per axis (x, y, z): a flatter box fits the landscape canvas\nconst BOX = [1, 1, 0.52];\nconst PAD_SHARE = 0.05;\n\n// Columns: set these to your data's column names\nconst X = \"Convection rate (X)\"; // role: x\nconst Y = \"Horizontal temp var (Y)\"; // role: y\nconst Z = \"Vertical temp var (Z)\"; // role: z\nconst TRAJECTORY = \"Trajectory\"; // role: color\n\n// Data: replace this block with your own records\nconst SIGMA = 10;\nconst RHO = 28;\nconst BETA = 8 / 3;\nconst DT = 0.01;\nconst STEPS = 2200;\nconst TRANSIENT = 200;\nconst SUBSAMPLE = 1;\n\nconst INITIAL_STATES: Array<{ label: string; init: [number, number, number] }> = [\n  { label: \"Trajectory A (x₀ = 5.0000)\", init: [5, 5, 20] },\n  { label: \"Trajectory B (x₀ = 5.0001)\", init: [5.0001, 5, 20] },\n];\n\nconst data: Array<{\n  [TRAJECTORY]: string;\n  [X]: number;\n  [Y]: number;\n  [Z]: number;\n}> = [];\n\nfor (let s = 0; s < INITIAL_STATES.length; s += 1) {\n  const { label, init } = INITIAL_STATES[s];\n  let x = init[0];\n  let y = init[1];\n  let z = init[2];\n\n  for (let i = 0; i < STEPS; i += 1) {\n    const k1x = SIGMA * (y - x);\n    const k1y = x * (RHO - z) - y;\n    const k1z = x * y - BETA * z;\n\n    const x2 = x + 0.5 * DT * k1x;\n    const y2 = y + 0.5 * DT * k1y;\n    const z2 = z + 0.5 * DT * k1z;\n    const k2x = SIGMA * (y2 - x2);\n    const k2y = x2 * (RHO - z2) - y2;\n    const k2z = x2 * y2 - BETA * z2;\n\n    const x3 = x + 0.5 * DT * k2x;\n    const y3 = y + 0.5 * DT * k2y;\n    const z3 = z + 0.5 * DT * k2z;\n    const k3x = SIGMA * (y3 - x3);\n    const k3y = x3 * (RHO - z3) - y3;\n    const k3z = x3 * y3 - BETA * z3;\n\n    const x4 = x + DT * k3x;\n    const y4 = y + DT * k3y;\n    const z4 = z + DT * k3z;\n    const k4x = SIGMA * (y4 - x4);\n    const k4y = x4 * (RHO - z4) - y4;\n    const k4z = x4 * y4 - BETA * z4;\n\n    x += (DT / 6) * (k1x + 2 * k2x + 2 * k3x + k4x);\n    y += (DT / 6) * (k1y + 2 * k2y + 2 * k3y + k4y);\n    z += (DT / 6) * (k1z + 2 * k2z + 2 * k3z + k4z);\n\n    if (i >= TRANSIENT && (i - TRANSIENT) % SUBSAMPLE === 0) {\n      data.push({\n        [TRAJECTORY]: label,\n        [X]: x,\n        [Y]: y,\n        [Z]: z,\n      });\n    }\n  }\n}\n\n// Plot: reads only data and the constants above\nconst trajectoryNames = Array.from(new Set(data.map((d) => d[TRAJECTORY])));\n\nconst COLS = [X, Y, Z] as const;\nconst ranges = COLS.map((c) => {\n  const vals = data.map((d) => d[c]);\n  return { min: Math.min(...vals), max: Math.max(...vals) };\n});\n\ninterface ScreenPt {\n  sx: number;\n  sy: number;\n  depth: number;\n}\n\n// Orthographic oblique projection of box coordinates (each in [-1, 1])\nfunction project(n: [number, number, number]): ScreenPt {\n  const bx = n[0] * BOX[0];\n  const by = n[1] * BOX[1];\n  const bz = n[2] * BOX[2];\n  const xRot = bx * COS_AZ - by * SIN_AZ;\n  const depth = bx * SIN_AZ + by * COS_AZ;\n  return { sx: xRot, sy: depth * SIN_EL + bz * COS_EL, depth };\n}\n\nconst projectedTrajectories = trajectoryNames.map((name, idx) => ({\n  name,\n  color: t.palette[idx],\n  points: data\n    .filter((d) => d[TRAJECTORY] === name)\n    .map((d) =>\n      project(\n        COLS.map(\n          (c, k) => (2 * (d[c] - ranges[k].min)) / (ranges[k].max - ranges[k].min) - 1,\n        ) as [number, number, number],\n      ),\n    ),\n}));\n\n// Floor corners sorted into near / left / right / far for the current view\nconst floor = (\n  [\n    [-1, -1],\n    [1, -1],\n    [1, 1],\n    [-1, 1],\n  ] as Array<[number, number]>\n).map(([a, b]) => ({ a, b, bottom: project([a, b, -1]), top: project([a, b, 1]) }));\nconst byDepth = [...floor].sort((p, q) => p.bottom.depth - q.bottom.depth);\nconst near = byDepth[0];\nconst far = byDepth[3];\nconst [leftC, rightC] = [byDepth[1], byDepth[2]].sort((p, q) => p.bottom.sx - q.bottom.sx);\n\nconst allScreenPts = [\n  ...projectedTrajectories.flatMap((tr) => tr.points),\n  ...floor.flatMap((c) => [c.bottom, c.top]),\n];\nconst SX_MIN = Math.min(...allScreenPts.map((p) => p.sx));\nconst SX_MAX = Math.max(...allScreenPts.map((p) => p.sx));\nconst SY_MIN = Math.min(...allScreenPts.map((p) => p.sy));\nconst SY_MAX = Math.max(...allScreenPts.map((p) => p.sy));\nconst PAD = Math.max(SX_MAX - SX_MIN, SY_MAX - SY_MIN) * PAD_SHARE;\nconst DOM_X0 = SX_MIN - PAD;\nconst DOM_X1 = SX_MAX + PAD;\nconst DOM_Y0 = SY_MIN - PAD;\nconst DOM_Y1 = SY_MAX + PAD;\n\nconst axisLabel = (k: number) => {\n  const { min, max } = ranges[k];\n  const fmt = (v: number) => (v > 0 ? `+${v.toFixed(0)}` : v.toFixed(0));\n  return `${COLS[k]} [${fmt(min)} to ${fmt(max)}]`;\n};\n\nconst gizmoAxes = [\"X\", \"Y\", \"Z\"].map((label, k) => {\n  const unit: [number, number, number] = [0, 0, 0];\n  unit[k] = 1;\n  const p = project(unit);\n  return { label, sx: p.sx / BOX[k], sy: p.sy / BOX[k] };\n});\n\nfunction TrajectoriesLayer() {\n  const { left, top, width, height: areaHeight } = useDrawingArea();\n\n  const xDomSpan = DOM_X1 - DOM_X0;\n  const yDomSpan = DOM_Y1 - DOM_Y0;\n  const scale = Math.min(width / xDomSpan, areaHeight / yDomSpan);\n  const offsetX = left + (width - xDomSpan * scale) / 2;\n  const offsetY = top + (areaHeight - yDomSpan * scale) / 2;\n  const px = (p: ScreenPt) => ({\n    x: offsetX + (p.sx - DOM_X0) * scale,\n    y: offsetY + (DOM_Y1 - p.sy) * scale,\n  });\n  const pts = (...ps: ScreenPt[]) =>\n    ps\n      .map(px)\n      .map((q) => `${q.x.toFixed(1)},${q.y.toFixed(1)}`)\n      .join(\" \");\n  // Uniform Catmull-Rom spline through every point: same vertices, smooth turns\n  const smoothPath = (ps: ScreenPt[]) => {\n    const q = ps.map(px);\n    const f = (v: number) => v.toFixed(1);\n    let d = `M${f(q[0].x)},${f(q[0].y)}`;\n    for (let i = 0; i < q.length - 1; i += 1) {\n      const p0 = q[Math.max(i - 1, 0)];\n      const p1 = q[i];\n      const p2 = q[i + 1];\n      const p3 = q[Math.min(i + 2, q.length - 1)];\n      d += `C${f(p1.x + (p2.x - p0.x) / 6)},${f(p1.y + (p2.y - p0.y) / 6)} ${f(p2.x - (p3.x - p1.x) / 6)},${f(p2.y - (p3.y - p1.y) / 6)} ${f(p2.x)},${f(p2.y)}`;\n    }\n    return d;\n  };\n\n  const boxCenter = px(project([0, 0, -1]));\n\n  // Front floor edges carry the X / Y titles, parallel to the edge and pushed outward\n  const edgeTitle = (c: typeof near, k: number) => {\n    const p0 = px(near.bottom);\n    const p1 = px(c.bottom);\n    const mid = { x: (p0.x + p1.x) / 2, y: (p0.y + p1.y) / 2 };\n    const len = Math.hypot(p1.x - p0.x, p1.y - p0.y);\n    let nx = (p1.y - p0.y) / len;\n    let ny = -(p1.x - p0.x) / len;\n    if (nx * (mid.x - boxCenter.x) + ny * (mid.y - boxCenter.y) < 0) {\n      nx = -nx;\n      ny = -ny;\n    }\n    let angle = (Math.atan2(p1.y - p0.y, p1.x - p0.x) * 180) / Math.PI;\n    if (angle > 90) angle -= 180;\n    if (angle < -90) angle += 180;\n    return { x: mid.x + nx * 30, y: mid.y + ny * 30, angle, text: axisLabel(k) };\n  };\n  const frontTitles = [leftC, rightC].map((c) => edgeTitle(c, c.a !== near.a ? 0 : 1));\n\n  const zBottom = px(leftC.bottom);\n  const zTop = px(leftC.top);\n  const zTitle = { x: zBottom.x - 26, y: (zBottom.y + zTop.y) / 2 };\n\n  const starts = projectedTrajectories.map((tr) => px(tr.points[0]));\n  const startsClose = starts.length > 1 && starts.every((s) => Math.hypot(s.x - starts[0].x, s.y - starts[0].y) < 6);\n  const gizmoOrigin = { x: left + 96, y: top + areaHeight - 104 };\n  const ARM = 52;\n\n  return (\n    <g>\n      {/* Floor and the two back walls as a light reference frame */}\n      <polygon\n        points={pts(near.bottom, rightC.bottom, far.bottom, leftC.bottom)}\n        fill={t.elevatedBg}\n        fillOpacity={0.35}\n        stroke={t.grid}\n        strokeWidth={1.2}\n      />\n      {[leftC, rightC].map((c) => (\n        <polygon\n          key={`wall-${c.a}-${c.b}`}\n          points={pts(c.bottom, far.bottom, far.top, c.top)}\n          fill={t.elevatedBg}\n          fillOpacity={0.18}\n          stroke={t.grid}\n          strokeWidth={1.2}\n          strokeDasharray=\"4 4\"\n        />\n      ))}\n\n      {/* Labeled outer axis edges */}\n      {[leftC, rightC].map((c) => {\n        const a = px(near.bottom);\n        const b = px(c.bottom);\n        return (\n          <line key={`edge-${c.a}-${c.b}`} x1={a.x} y1={a.y} x2={b.x} y2={b.y} stroke={t.inkSoft} strokeWidth={2} />\n        );\n      })}\n      <line x1={zBottom.x} y1={zBottom.y} x2={zTop.x} y2={zTop.y} stroke={t.inkSoft} strokeWidth={2} />\n      {frontTitles.map((ft) => (\n        <text\n          key={ft.text}\n          x={ft.x}\n          y={ft.y}\n          fill={t.ink}\n          fontSize={14}\n          fontWeight={600}\n          textAnchor=\"middle\"\n          dominantBaseline=\"middle\"\n          transform={`rotate(${ft.angle.toFixed(1)} ${ft.x.toFixed(1)} ${ft.y.toFixed(1)})`}\n        >\n          {ft.text}\n        </text>\n      ))}\n      <text\n        x={zTitle.x}\n        y={zTitle.y}\n        fill={t.ink}\n        fontSize={14}\n        fontWeight={600}\n        textAnchor=\"middle\"\n        dominantBaseline=\"middle\"\n        transform={`rotate(-90 ${zTitle.x.toFixed(1)} ${zTitle.y.toFixed(1)})`}\n      >\n        {axisLabel(2)}\n      </text>\n\n      {/* Trajectories: thin translucent strands so overlapping paths stay traceable */}\n      {projectedTrajectories.map((traj) => (\n        <path\n          key={traj.name}\n          d={smoothPath(traj.points)}\n          fill=\"none\"\n          stroke={traj.color}\n          strokeWidth={1.2}\n          strokeOpacity={0.5}\n          strokeLinejoin=\"round\"\n        />\n      ))}\n\n      {/* End markers drawn above both strands */}\n      {projectedTrajectories.map((traj) => {\n        const end = px(traj.points[traj.points.length - 1]);\n        return (\n          <circle\n            key={`end-${traj.name}`}\n            cx={end.x}\n            cy={end.y}\n            r={6}\n            fill={traj.color}\n            stroke={t.pageBg}\n            strokeWidth={1.8}\n          />\n        );\n      })}\n\n      {/* Start marker and conditional shared-start callout */}\n      {startsClose ? (\n        <g>\n          <circle cx={starts[0].x} cy={starts[0].y} r={6.5} fill={t.ink} stroke={t.pageBg} strokeWidth={1.5} />\n          <text\n            x={starts[0].x + 12}\n            y={starts[0].y - 12}\n            fill={t.ink}\n            fontSize={14}\n            fontWeight={600}\n            fontFamily={MONO}\n            textAnchor=\"start\"\n          >\n            Shared start\n          </text>\n        </g>\n      ) : (\n        projectedTrajectories.map((traj, i) => (\n          <circle\n            key={`start-${traj.name}`}\n            cx={starts[i].x}\n            cy={starts[i].y}\n            r={5}\n            fill={t.ink}\n            stroke={traj.color}\n            strokeWidth={2}\n          />\n        ))\n      )}\n\n      {/* 3D orientation triad */}\n      {gizmoAxes.map((a) => (\n        <g key={a.label}>\n          <line\n            x1={gizmoOrigin.x}\n            y1={gizmoOrigin.y}\n            x2={gizmoOrigin.x + a.sx * ARM}\n            y2={gizmoOrigin.y - a.sy * ARM}\n            stroke={t.inkSoft}\n            strokeWidth={2}\n          />\n          <text\n            x={gizmoOrigin.x + a.sx * (ARM + 16)}\n            y={gizmoOrigin.y - a.sy * (ARM + 16)}\n            fill={t.ink}\n            textAnchor=\"middle\"\n            dominantBaseline=\"middle\"\n            fontSize={15}\n            fontWeight={700}\n          >\n            {a.label}\n          </text>\n        </g>\n      ))}\n      <text\n        x={gizmoOrigin.x}\n        y={gizmoOrigin.y + 72}\n        fill={t.inkSoft}\n        textAnchor=\"middle\"\n        fontSize={14}\n        fontFamily={MONO}\n      >\n        elev {ELEV_DEG}°, azim {AZIM_DEG}°\n      </text>\n    </g>\n  );\n}\n\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n  const HEADER_H = 84;\n  const chartH = H - HEADER_H;\n\n  return (\n    <div\n      style={{\n        width: W,\n        height: H,\n        background: t.pageBg,\n        color: t.ink,\n        fontFamily: FONT,\n        display: \"flex\",\n        flexDirection: \"column\",\n        boxSizing: \"border-box\",\n      }}\n    >\n      <div\n        style={{\n          height: HEADER_H,\n          padding: \"14px 44px 4px 44px\",\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"space-between\",\n          boxSizing: \"border-box\",\n        }}\n      >\n        <div>\n          <div\n            style={{\n              fontSize: 22,\n              fontWeight: 700,\n              color: t.ink,\n              lineHeight: 1.2,\n            }}\n          >\n            line-3d-trajectory · javascript · muix · anyplot.ai\n          </div>\n          <div\n            style={{\n              fontSize: 14,\n              color: t.inkSoft,\n              marginTop: 4,\n              fontFamily: MONO,\n            }}\n          >\n            {projectedTrajectories.length} trajectories ·{\" \"}\n            {projectedTrajectories[0].points.length.toLocaleString()} points per\n            trajectory ({X}, {Y}, {Z})\n          </div>\n        </div>\n\n        <div\n          style={{\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: 24,\n            fontSize: 15,\n            color: t.ink,\n          }}\n        >\n          {projectedTrajectories.map((traj) => (\n            <div\n              key={traj.name}\n              style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}\n            >\n              <span\n                style={{\n                  width: 22,\n                  height: 3,\n                  borderRadius: 2,\n                  background: traj.color,\n                  display: \"inline-block\",\n                }}\n              />\n              <span>{traj.name}</span>\n            </div>\n          ))}\n        </div>\n      </div>\n\n      <ChartsContainer\n        width={W}\n        height={chartH}\n        skipAnimation\n        series={[]}\n        xAxis={[{ min: 0, max: 1, position: \"none\" }]}\n        yAxis={[{ min: 0, max: 1, position: \"none\" }]}\n        margin={{ top: 12, bottom: 24, left: 32, right: 32 }}\n      >\n        <TrajectoriesLayer />\n      </ChartsContainer>\n    </div>\n  );\n}\n"}