{"spec_id":"line-3d-trajectory","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// line-3d-trajectory: 3D Line Plot for Trajectory Visualization\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 87/100 | Created: 2026-09-10\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: a decaying satellite orbit spiraling toward re-entry -----------\n// Chart.js has no native 3D chart type, so the (x, y, z) trajectory is\n// projected onto the 2D canvas with a fixed isometric transform before it is\n// handed to a standard line dataset — the same approach a static 3D plot\n// uses internally, just computed by hand instead of by a 3D engine.\nconst POINT_COUNT = 260;\nconst REVOLUTIONS = 3.5;\nconst RADIUS_START = 6.4;\nconst RADIUS_END = 1.2;\nconst ALTITUDE_START = 5.5;\nconst COS30 = Math.cos(Math.PI / 6);\nconst SIN30 = Math.sin(Math.PI / 6);\n\nfunction project(x, y, z) {\n  return { x: (x - y) * COS30, y: z - (x + y) * SIN30 };\n}\n\nconst trajectory = [];\nfor (let i = 0; i < POINT_COUNT; i++) {\n  const progress = i / (POINT_COUNT - 1);\n  const angle = progress * REVOLUTIONS * 2 * Math.PI;\n  const radius = RADIUS_START - (RADIUS_START - RADIUS_END) * progress;\n  const origX = radius * Math.cos(angle);\n  const origY = radius * Math.sin(angle);\n  const origZ = ALTITUDE_START * Math.pow(1 - progress, 1.3);\n  trajectory.push({\n    ...project(origX, origY, origZ),\n    origX,\n    origY,\n    origZ,\n    progress,\n  });\n}\n\nconst origin = { ...project(0, 0, 0), origX: 0, origY: 0, origZ: 0 };\nconst axisLength = RADIUS_START + 1.2;\nconst xAxisEnd = {\n  ...project(axisLength, 0, 0),\n  origX: axisLength,\n  origY: 0,\n  origZ: 0,\n};\nconst yAxisEnd = {\n  ...project(0, axisLength, 0),\n  origX: 0,\n  origY: axisLength,\n  origZ: 0,\n};\nconst zAxisEnd = {\n  ...project(0, 0, axisLength),\n  origX: 0,\n  origY: 0,\n  origZ: axisLength,\n};\n\n// --- Color: elapsed time mapped through the Imprint sequential scale ------\nfunction lerpChannel(a, b, ratio) {\n  return Math.round(a + (b - a) * ratio);\n}\nfunction lerpColor(hexA, hexB, ratio) {\n  const a = parseInt(hexA.slice(1), 16);\n  const b = parseInt(hexB.slice(1), 16);\n  const r = lerpChannel((a >> 16) & 255, (b >> 16) & 255, ratio);\n  const g = lerpChannel((a >> 8) & 255, (b >> 8) & 255, ratio);\n  const bl = lerpChannel(a & 255, b & 255, ratio);\n  return `rgb(${r}, ${g}, ${bl})`;\n}\n\n// --- Mount ------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart -------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"line\",\n  data: {\n    datasets: [\n      {\n        label: \"X axis\",\n        data: [origin, xAxisEnd],\n        borderColor: t.inkSoft,\n        borderWidth: 1.5,\n        borderDash: [6, 5],\n        pointRadius: 0,\n        order: 3,\n      },\n      {\n        label: \"Y axis\",\n        data: [origin, yAxisEnd],\n        borderColor: t.inkSoft,\n        borderWidth: 1.5,\n        borderDash: [6, 5],\n        pointRadius: 0,\n        order: 3,\n      },\n      {\n        label: \"Z axis (altitude)\",\n        data: [origin, zAxisEnd],\n        borderColor: t.inkSoft,\n        borderWidth: 1.5,\n        borderDash: [2, 4],\n        pointRadius: 0,\n        order: 3,\n      },\n      {\n        label: \"Orbit (color = elapsed time)\",\n        data: trajectory,\n        borderColor: t.palette[0],\n        borderWidth: 3,\n        pointRadius: 0,\n        tension: 0,\n        segment: {\n          borderColor: (ctx) => {\n            const p0 = trajectory[ctx.p0DataIndex];\n            const p1 = trajectory[ctx.p1DataIndex];\n            const avgProgress = (p0.progress + p1.progress) / 2;\n            return lerpColor(t.seq[0], t.seq[1], avgProgress);\n          },\n        },\n        order: 2,\n      },\n      {\n        label: \"Orbit start\",\n        data: [trajectory[0]],\n        showLine: false,\n        pointRadius: 11,\n        backgroundColor: t.palette[0],\n        pointBackgroundColor: t.palette[0],\n        pointBorderColor: t.pageBg,\n        pointBorderWidth: 2,\n        order: 1,\n      },\n      {\n        label: \"Re-entry (impact)\",\n        data: [trajectory[trajectory.length - 1]],\n        showLine: false,\n        pointRadius: 11,\n        pointStyle: \"triangle\",\n        backgroundColor: t.palette[4],\n        pointBackgroundColor: t.palette[4],\n        pointBorderColor: t.pageBg,\n        pointBorderWidth: 2,\n        order: 1,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    parsing: { xAxisKey: \"x\", yAxisKey: \"y\" },\n    plugins: {\n      title: {\n        display: true,\n        text: \"line-3d-trajectory · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: {\n        labels: { color: t.ink, font: { size: 15 } },\n      },\n      tooltip: {\n        callbacks: {\n          label: (ctx) => {\n            const p = ctx.raw;\n            return `x=${p.origX.toFixed(2)}, y=${p.origY.toFixed(2)}, z=${p.origZ.toFixed(2)}`;\n          },\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        grid: { display: false },\n        border: { color: t.grid },\n        ticks: {\n          display: true,\n          color: t.inkSoft,\n          font: { size: 11 },\n          maxTicksLimit: 5,\n        },\n        title: {\n          display: true,\n          text: \"Projected X–Y plane\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n      },\n      y: {\n        type: \"linear\",\n        grid: { display: false },\n        border: { color: t.grid },\n        ticks: {\n          display: true,\n          color: t.inkSoft,\n          font: { size: 11 },\n          maxTicksLimit: 5,\n        },\n        title: {\n          display: true,\n          text: \"Altitude (Z)\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n      },\n    },\n  },\n});\n"}