{"spec_id":"line-3d-trajectory","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// line-3d-trajectory: 3D Line Plot for Trajectory Visualization\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-10\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: Lorenz attractor, integrated with a fixed-step RK4 --------------\nconst SIGMA = 10;\nconst RHO = 28;\nconst BETA = 8 / 3;\nconst DT = 0.006;\nconst STEPS = 4200;\n\nconst derivative = (x, y, z) => [\n  SIGMA * (y - x),\n  x * (RHO - z) - y,\n  x * y - BETA * z,\n];\n\nlet [x, y, z] = [0.6, 0.6, 0.6];\nconst path = [[x, y, z]];\nfor (let i = 0; i < STEPS; i += 1) {\n  const [k1x, k1y, k1z] = derivative(x, y, z);\n  const [k2x, k2y, k2z] = derivative(x + (k1x * DT) / 2, y + (k1y * DT) / 2, z + (k1z * DT) / 2);\n  const [k3x, k3y, k3z] = derivative(x + (k2x * DT) / 2, y + (k2y * DT) / 2, z + (k2z * DT) / 2);\n  const [k4x, k4y, k4z] = derivative(x + k3x * DT, y + k3y * DT, z + k3z * DT);\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  path.push([x, y, z]);\n}\n\nconst xs = path.map((p) => p[0]);\nconst ys = path.map((p) => p[1]);\nconst zs = path.map((p) => p[2]);\nconst [xMin, xMax] = [Math.min(...xs), Math.max(...xs)];\nconst [yMin, yMax] = [Math.min(...ys), Math.max(...ys)];\nconst [zMin, zMax] = [Math.min(...zs), Math.max(...zs)];\n\n// --- 3D -> 2D orthographic projection (elevation 22°, azimuth 55°) ---------\n// Highcharts core has no chart3d/highcharts-3d module (see prompts/library/\n// highcharts.md \"Forbidden patterns\"), so the trajectory is projected by hand\n// into plain (x, y) screen coordinates and drawn as ordinary `line` series —\n// the same math a native 3D engine applies before rasterizing, computed here\n// instead of in an unavailable add-on.\nconst ELEV = (22 * Math.PI) / 180;\nconst AZIM = (55 * Math.PI) / 180;\nconst cosAz = Math.cos(AZIM);\nconst sinAz = Math.sin(AZIM);\nconst cosEl = Math.cos(ELEV);\nconst sinEl = Math.sin(ELEV);\n\nconst project = (px, py, pz) => {\n  const xr = px * cosAz + py * sinAz;\n  const yr = -px * sinAz + py * cosAz;\n  const zScreen = yr * sinEl + pz * cosEl;\n  return [xr, zScreen];\n};\n\n// Each projected point carries its pre-projection coordinates and time\n// fraction under `custom` so the tooltip can surface them on hover.\nconst projectedPoints = path.map(([px, py, pz], i) => {\n  const [sx, sy] = project(px, py, pz);\n  return { x: sx, y: sy, custom: { ox: px, oy: py, oz: pz, frac: i / (path.length - 1) } };\n});\n\n// --- Color: time progression along the imprint_seq colormap ----------------\nconst hexToRgb = (hex) => {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n};\nconst seqLo = hexToRgb(t.seq[0]);\nconst seqHi = hexToRgb(t.seq[1]);\nconst timeColor = (frac) => {\n  const r = Math.round(seqLo[0] + (seqHi[0] - seqLo[0]) * frac);\n  const g = Math.round(seqLo[1] + (seqHi[1] - seqLo[1]) * frac);\n  const b = Math.round(seqLo[2] + (seqHi[2] - seqLo[2]) * frac);\n  return `rgb(${r}, ${g}, ${b})`;\n};\n\n// The path is drawn as many short overlapping segments, each its own color —\n// Highcharts core has no per-point line-color gradient, so a fine segment\n// chain is how a single line reads as a smooth time gradient.\nconst SEGMENTS = 140;\nconst pointsPerSegment = Math.ceil(projectedPoints.length / SEGMENTS);\nconst trajectorySeries = [];\nfor (let s = 0; s < SEGMENTS; s += 1) {\n  const start = Math.max(0, s * pointsPerSegment - 1);\n  const end = Math.min(projectedPoints.length, (s + 1) * pointsPerSegment);\n  if (end - start < 2) continue;\n  trajectorySeries.push({\n    type: \"line\",\n    data: projectedPoints.slice(start, end),\n    color: timeColor(s / (SEGMENTS - 1)),\n    lineWidth: 2.2,\n    marker: { enabled: false, states: { hover: { enabled: true, radius: 4, lineWidth: 1 } } },\n    enableMouseTracking: true,\n    stickyTracking: false,\n    showInLegend: false,\n  });\n}\n\n// --- Axis frame: an L-shaped X/Y/Z reference below the attractor -----------\nconst padFrac = 0.12;\nconst xPad = (xMax - xMin) * padFrac;\nconst yPad = (yMax - yMin) * padFrac;\nconst zPad = (zMax - zMin) * padFrac;\nconst floorZ = zMin - zPad;\nconst topZ = zMax + zPad * 1.6;\nconst corner = project(xMin - xPad, yMin - yPad, floorZ);\nconst xEnd = project(xMax + xPad, yMin - yPad, floorZ);\nconst yEnd = project(xMin - xPad, yMax + yPad, floorZ);\nconst zEnd = project(xMin - xPad, yMin - yPad, topZ);\n\nconst axisFrameSeries = [\n  { type: \"line\", data: [corner, xEnd], color: t.inkSoft, lineWidth: 2, marker: { enabled: false }, enableMouseTracking: false, showInLegend: false },\n  { type: \"line\", data: [corner, yEnd], color: t.inkSoft, lineWidth: 2, marker: { enabled: false }, enableMouseTracking: false, showInLegend: false },\n  { type: \"line\", data: [corner, zEnd], color: t.inkSoft, lineWidth: 2, marker: { enabled: false }, enableMouseTracking: false, showInLegend: false },\n];\n\nconst [xTitleX, xTitleY] = project(xMax + xPad * 2.2, yMin - yPad, floorZ);\nconst [yTitleX, yTitleY] = project(xMin - xPad, yMax + yPad * 2.2, floorZ);\nconst [zTitleX, zTitleY] = project(xMin - xPad, yMin - yPad, topZ * 1.08);\nconst startP = projectedPoints[0];\nconst endP = projectedPoints[projectedPoints.length - 1];\n\nconst labelSeries = [\n  {\n    type: \"scatter\",\n    data: [\n      { x: xTitleX, y: xTitleY, name: \"X\" },\n      { x: yTitleX, y: yTitleY, name: \"Y\" },\n      { x: zTitleX, y: zTitleY, name: \"Z\" },\n    ],\n    marker: { enabled: false },\n    enableMouseTracking: false,\n    showInLegend: false,\n    dataLabels: {\n      enabled: true,\n      format: \"{point.name}\",\n      allowOverlap: true,\n      style: { color: t.ink, fontSize: \"16px\", fontWeight: \"600\", textOutline: \"none\" },\n    },\n  },\n  {\n    type: \"scatter\",\n    data: [\n      { x: startP.x, y: startP.y, name: \"start\", marker: { enabled: true, radius: 6, fillColor: timeColor(0), lineColor: t.pageBg, lineWidth: 1.5 } },\n      { x: endP.x, y: endP.y, name: \"end\", marker: { enabled: true, radius: 6, fillColor: timeColor(1), lineColor: t.pageBg, lineWidth: 1.5 } },\n    ],\n    enableMouseTracking: false,\n    showInLegend: false,\n    dataLabels: {\n      enabled: true,\n      format: \"{point.name}\",\n      y: -14,\n      allowOverlap: true,\n      style: { color: t.inkSoft, fontSize: \"13px\", fontWeight: \"500\", textOutline: \"none\" },\n    },\n  },\n];\n\n// --- Axis bounds: fit every projected coordinate with padding --------------\nconst allPoints = [\n  ...projectedPoints.map((p) => [p.x, p.y]),\n  corner,\n  xEnd,\n  yEnd,\n  zEnd,\n  [xTitleX, xTitleY],\n  [yTitleX, yTitleY],\n  [zTitleX, zTitleY],\n];\nconst allX = allPoints.map((p) => p[0]);\nconst allY = allPoints.map((p) => p[1]);\nconst boundsPadX = (Math.max(...allX) - Math.min(...allX)) * 0.06;\nconst boundsPadY = (Math.max(...allY) - Math.min(...allY)) * 0.06;\n\n// --- Chart -------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"line\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  title: {\n    // Title is 76 chars (> 67-char baseline), so fontsize scales down from the\n    // ~22px CSS default: round(22 * 67 / 76) = 19px — see prompts/plot-generator.md\n    // \"Title fontsize must scale with title length\".\n    text: \"Lorenz Attractor · line-3d-trajectory · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"19px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"σ=10, ρ=28, β=8/3 — 4,200 RK4 steps, color = time progression\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    visible: false,\n    min: Math.min(...allX) - boundsPadX,\n    max: Math.max(...allX) + boundsPadX,\n    startOnTick: false,\n    endOnTick: false,\n  },\n  yAxis: {\n    visible: false,\n    min: Math.min(...allY) - boundsPadY,\n    max: Math.max(...allY) + boundsPadY,\n    startOnTick: false,\n    endOnTick: false,\n    title: { text: null },\n  },\n  legend: { enabled: false },\n  tooltip: {\n    enabled: true,\n    backgroundColor: t.elevatedBg,\n    borderColor: t.inkSoft,\n    borderRadius: 4,\n    style: { color: t.ink, fontSize: \"12px\" },\n    formatter() {\n      const c = this.point.custom;\n      return `<b>t = ${(c.frac * 100).toFixed(0)}%</b><br/>x: ${c.ox.toFixed(2)}<br/>y: ${c.oy.toFixed(2)}<br/>z: ${c.oz.toFixed(2)}`;\n    },\n  },\n  plotOptions: { series: { animation: false } },\n  series: [...trajectorySeries, ...axisFrameSeries, ...labelSeries],\n});\n"}