{"spec_id":"line-3d-trajectory","library":"d3","language":"javascript","code":"// anyplot.ai\n// line-3d-trajectory: 3D Line Plot for Trajectory Visualization\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 86/100 | Created: 2026-09-10\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 140, right: 170, bottom: 60, left: 70 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\nconst cx0 = margin.left + iw / 2;\nconst cy0 = margin.top + ih / 2;\n\n// --- Data: Lorenz attractor, integrated with RK4 (in-memory, deterministic) -\nconst SIGMA = 10;\nconst RHO = 28;\nconst BETA = 8 / 3;\nconst SIM_DT = 0.005;\nconst SIM_STEPS = 8000; // t = 0..40 — long enough to switch between both wings\nconst DOWNSAMPLE = 5; // 1601 plotted points — within the spec's 100-2000 range\n\nfunction lorenzDeriv(p) {\n  return {\n    dx: SIGMA * (p.y - p.x),\n    dy: p.x * (RHO - p.z) - p.y,\n    dz: p.x * p.y - BETA * p.z,\n  };\n}\n\nfunction rk4Step(p, dt) {\n  const k1 = lorenzDeriv(p);\n  const p2 = { x: p.x + (k1.dx * dt) / 2, y: p.y + (k1.dy * dt) / 2, z: p.z + (k1.dz * dt) / 2 };\n  const k2 = lorenzDeriv(p2);\n  const p3 = { x: p.x + (k2.dx * dt) / 2, y: p.y + (k2.dy * dt) / 2, z: p.z + (k2.dz * dt) / 2 };\n  const k3 = lorenzDeriv(p3);\n  const p4 = { x: p.x + k3.dx * dt, y: p.y + k3.dy * dt, z: p.z + k3.dz * dt };\n  const k4 = lorenzDeriv(p4);\n  return {\n    x: p.x + (dt / 6) * (k1.dx + 2 * k2.dx + 2 * k3.dx + k4.dx),\n    y: p.y + (dt / 6) * (k1.dy + 2 * k2.dy + 2 * k3.dy + k4.dy),\n    z: p.z + (dt / 6) * (k1.dz + 2 * k2.dz + 2 * k3.dz + k4.dz),\n  };\n}\n\nconst simulated = [{ x: 1, y: 1, z: 1 }];\nfor (let i = 1; i <= SIM_STEPS; i++) simulated.push(rk4Step(simulated[i - 1], SIM_DT));\nconst points = simulated.filter((_, i) => i % DOWNSAMPLE === 0);\n\n// --- 3D -> 2D projection: normalize, rotate (interactively, via drag), then\n// perspective-divide. \"up\" on screen is data z (classic Lorenz convention),\n// \"right\" is data x, \"depth\" is data y.\nconst xExtent = d3.extent(points, (p) => p.x);\nconst yExtent = d3.extent(points, (p) => p.y);\nconst zExtent = d3.extent(points, (p) => p.z);\nconst xMid = (xExtent[0] + xExtent[1]) / 2;\nconst yMid = (yExtent[0] + yExtent[1]) / 2;\nconst zMid = (zExtent[0] + zExtent[1]) / 2;\nconst maxRange =\n  Math.max(xExtent[1] - xExtent[0], yExtent[1] - yExtent[0], zExtent[1] - zExtent[0]) / 2;\n\nconst toUVW = (p) => ({\n  u: (p.x - xMid) / maxRange,\n  v: (p.z - zMid) / maxRange,\n  w: (p.y - yMid) / maxRange,\n});\n\nlet YAW = -0.95; // rotation around the vertical (v) axis — mutable, drag-controlled\nlet PITCH = 0.42; // rotation around the horizontal (u) axis — mutable, drag-controlled\nconst CAM_DIST = 3.4; // perspective camera distance, in normalized units\n\nfunction toCamera(p, yaw, pitch) {\n  const cosY = Math.cos(yaw);\n  const sinY = Math.sin(yaw);\n  const u1 = p.u * cosY + p.w * sinY;\n  const w1 = -p.u * sinY + p.w * cosY;\n  const cosP = Math.cos(pitch);\n  const sinP = Math.sin(pitch);\n  const v2 = p.v * cosP - w1 * sinP;\n  const w2 = p.v * sinP + w1 * cosP;\n  return { cx: u1, cy: v2, cz: w2 };\n}\n\nfunction toRawScreen(c) {\n  const k = CAM_DIST / (CAM_DIST + c.cz);\n  return { sx: c.cx * k, sy: -c.cy * k, depth: c.cz };\n}\n\n// Reference-frame corner extents (padded slightly beyond the data)\nconst PAD = 1.0;\nconst uLo = ((xExtent[0] - xMid) / maxRange) * PAD;\nconst uHi = ((xExtent[1] - xMid) / maxRange) * PAD;\nconst vLo = ((zExtent[0] - zMid) / maxRange) * PAD;\nconst vHi = ((zExtent[1] - zMid) / maxRange) * PAD;\nconst wLo = ((yExtent[0] - yMid) / maxRange) * PAD;\nconst wHi = ((yExtent[1] - yMid) / maxRange) * PAD;\nconst origin = { u: uLo, v: vLo, w: wLo };\nconst FRAME_CORNERS = [\n  { u: uLo, v: vLo, w: wLo },\n  { u: uHi, v: vLo, w: wLo },\n  { u: uLo, v: vHi, w: wLo },\n  { u: uLo, v: vLo, w: wHi },\n];\nconst axisSpecs = [\n  { end: { u: uHi, v: vLo, w: wLo }, extent: xExtent, mid: xMid, axis: \"u\", label: \"X (state)\" },\n  { end: { u: uLo, v: vLo, w: wHi }, extent: yExtent, mid: yMid, axis: \"w\", label: \"Y (state)\" },\n  { end: { u: uLo, v: vHi, w: wLo }, extent: zExtent, mid: zMid, axis: \"v\", label: \"Z (state)\" },\n];\n\n// Fit the scale to the trajectory + the axis-frame endpoints together (for the\n// CURRENT yaw/pitch), so the reference frame never overflows the plot area\n// while the curve still fills most of it. Recomputed on every rotation so the\n// scene stays framed as the user drags.\nfunction computeFit(yaw, pitch) {\n  const cam = (p) => toRawScreen(toCamera(p, yaw, pitch));\n  const trajectoryRaw = points.map((p) => cam(toUVW(p)));\n  const frameRaw = FRAME_CORNERS.map(cam);\n  const allRaw = trajectoryRaw.concat(frameRaw);\n  const sxExtent = d3.extent(allRaw, (p) => p.sx);\n  const syExtent = d3.extent(allRaw, (p) => p.sy);\n  const scale = 0.9 * Math.min(iw / (sxExtent[1] - sxExtent[0]), ih / (syExtent[1] - syExtent[0]));\n  const sxMid = (sxExtent[0] + sxExtent[1]) / 2;\n  const syMid = (syExtent[0] + syExtent[1]) / 2;\n  const toPixel = (s) => ({ x: cx0 + (s.sx - sxMid) * scale, y: cy0 + (s.sy - syMid) * scale, depth: s.depth });\n  return {\n    projectUVW: (p) => toPixel(cam(p)),\n    trajectoryPixels: trajectoryRaw.map(toPixel),\n  };\n}\n\n// --- SVG mount ----------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst floor = svg.append(\"g\");\nconst axes = svg.append(\"g\");\nconst trajectory = svg.append(\"g\").attr(\"fill\", \"none\");\n\nconst seqColor = d3.scaleSequential(d3.interpolateRgbBasis(t.seq)).domain([0, points.length - 1]);\nconst line = d3\n  .line()\n  .x((p) => p.x)\n  .y((p) => p.y)\n  .curve(d3.curveCatmullRom.alpha(0.5));\nconst CHUNK = 8;\nconst GRID_LINES = 6;\n\nfunction redraw() {\n  const { projectUVW, trajectoryPixels } = computeFit(YAW, PITCH);\n\n  // Floor grid (reference plane at the base of the attractor)\n  const floorSegments = [];\n  for (let i = 0; i <= GRID_LINES; i++) {\n    const u = uLo + ((uHi - uLo) * i) / GRID_LINES;\n    floorSegments.push([projectUVW({ u, v: vLo, w: wLo }), projectUVW({ u, v: vLo, w: wHi })]);\n  }\n  for (let i = 0; i <= GRID_LINES; i++) {\n    const w = wLo + ((wHi - wLo) * i) / GRID_LINES;\n    floorSegments.push([projectUVW({ u: uLo, v: vLo, w }), projectUVW({ u: uHi, v: vLo, w })]);\n  }\n  floor\n    .selectAll(\"line\")\n    .data(floorSegments)\n    .join(\"line\")\n    .attr(\"x1\", (d) => d[0].x)\n    .attr(\"y1\", (d) => d[0].y)\n    .attr(\"x2\", (d) => d[1].x)\n    .attr(\"y2\", (d) => d[1].y)\n    .attr(\"stroke\", t.grid)\n    .attr(\"stroke-width\", 1);\n\n  // Axis frame (corner-anchored X / Y / Z reference lines + ticks)\n  axes.selectAll(\"*\").remove();\n  for (const spec of axisSpecs) {\n    const p0 = projectUVW(origin);\n    const p1 = projectUVW(spec.end);\n    axes\n      .append(\"line\")\n      .attr(\"x1\", p0.x)\n      .attr(\"y1\", p0.y)\n      .attr(\"x2\", p1.x)\n      .attr(\"y2\", p1.y)\n      .attr(\"stroke\", t.inkSoft)\n      .attr(\"stroke-width\", 2);\n\n    const ticks = d3.scaleLinear().domain(spec.extent).ticks(4);\n    for (const tickVal of ticks) {\n      const n = (tickVal - spec.mid) / maxRange;\n      const tickPoint = { ...origin, [spec.axis]: n };\n      const tp = projectUVW(tickPoint);\n      axes.append(\"circle\").attr(\"cx\", tp.x).attr(\"cy\", tp.y).attr(\"r\", 2.5).attr(\"fill\", t.inkSoft);\n      axes\n        .append(\"text\")\n        .attr(\"x\", tp.x)\n        .attr(\"y\", tp.y + 16)\n        .attr(\"text-anchor\", \"middle\")\n        .attr(\"fill\", t.inkSoft)\n        .style(\"font-size\", \"12px\")\n        .text(d3.format(\".0f\")(tickVal));\n    }\n\n    axes\n      .append(\"text\")\n      .attr(\"x\", p1.x)\n      .attr(\"y\", p1.y - 12)\n      .attr(\"text-anchor\", \"middle\")\n      .attr(\"fill\", t.ink)\n      .style(\"font-size\", \"17px\")\n      .style(\"font-weight\", \"600\")\n      .text(spec.label);\n  }\n\n  // Trajectory: chunked, time-colored, depth-shaded segments\n  const depthExtent = d3.extent(trajectoryPixels, (p) => p.depth);\n  const opacityScale = d3.scaleLinear().domain(depthExtent).range([1, 0.5]);\n  const widthScale = d3.scaleLinear().domain(depthExtent).range([3.4, 1.8]);\n  const chunks = [];\n  for (let start = 0; start < trajectoryPixels.length - 1; start += CHUNK) {\n    const end = Math.min(start + CHUNK, trajectoryPixels.length - 1);\n    const chunkPoints = trajectoryPixels.slice(start, end + 1);\n    const mid = Math.floor((start + end) / 2);\n    const avgDepth = d3.mean(chunkPoints, (p) => p.depth);\n    chunks.push({\n      d: line(chunkPoints),\n      color: seqColor(mid),\n      opacity: opacityScale(avgDepth),\n      width: widthScale(avgDepth),\n    });\n  }\n  trajectory\n    .selectAll(\"path\")\n    .data(chunks)\n    .join(\"path\")\n    .attr(\"d\", (c) => c.d)\n    .attr(\"stroke\", (c) => c.color)\n    .attr(\"stroke-opacity\", (c) => c.opacity)\n    .attr(\"stroke-width\", (c) => c.width)\n    .attr(\"stroke-linecap\", \"round\")\n    .attr(\"stroke-linejoin\", \"round\");\n\n  return { projectUVW, trajectoryPixels };\n}\n\n// --- Interactive rotation (genuine, drag-driven re-projection) ----------\nconst drag = d3\n  .drag()\n  .on(\"start\", () => svg.style(\"cursor\", \"grabbing\"))\n  .on(\"drag\", (event) => {\n    YAW += event.dx * 0.006;\n    PITCH = Math.max(-1.4, Math.min(1.4, PITCH - event.dy * 0.006));\n    redraw();\n  })\n  .on(\"end\", () => svg.style(\"cursor\", \"grab\"));\nsvg.style(\"cursor\", \"grab\").call(drag);\n\nconst initialFit = redraw();\n\n// --- Colorbar legend (time progression along the trajectory) ------------\n// Anchored to the actual right edge of the projected scene (not a fixed\n// offset), so it sits close to the plot instead of floating in empty space.\nconst sceneRightEdge = d3.max(\n  initialFit.trajectoryPixels.concat(FRAME_CORNERS.map(initialFit.projectUVW)),\n  (p) => p.x,\n);\nconst legendX = Math.min(sceneRightEdge + 50, width - 110);\nconst legend = svg.append(\"g\").attr(\"transform\", `translate(${legendX},${margin.top})`);\nconst barHeight = ih * 0.55;\nconst barWidth = 16;\nconst gradientId = \"time-gradient\";\nconst stops = d3.range(0, 1.001, 0.1);\nconst defs = svg.append(\"defs\");\ndefs\n  .append(\"linearGradient\")\n  .attr(\"id\", gradientId)\n  .attr(\"x1\", \"0\")\n  .attr(\"x2\", \"0\")\n  .attr(\"y1\", \"1\")\n  .attr(\"y2\", \"0\")\n  .selectAll(\"stop\")\n  .data(stops)\n  .join(\"stop\")\n  .attr(\"offset\", (d) => `${d * 100}%`)\n  .attr(\"stop-color\", (d) => d3.interpolateRgbBasis(t.seq)(d));\n\nlegend\n  .append(\"rect\")\n  .attr(\"width\", barWidth)\n  .attr(\"height\", barHeight)\n  .attr(\"fill\", `url(#${gradientId})`)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1);\n\nlegend\n  .append(\"text\")\n  .attr(\"x\", barWidth + 12)\n  .attr(\"y\", 4)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(\"t = end\");\n\nlegend\n  .append(\"text\")\n  .attr(\"x\", barWidth + 12)\n  .attr(\"y\", barHeight)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(\"t = 0\");\n\nlegend\n  .append(\"text\")\n  .attr(\"x\", 0)\n  .attr(\"y\", -20)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Time\");\n\n// --- Title ----------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"21px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Lorenz Attractor · line-3d-trajectory · javascript · d3 · anyplot.ai\");\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 78)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(\"Drag to rotate\");\n"}