{"spec_id":"scatter-3d","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// scatter-3d: 3D Scatter Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 84/100 | Created: 2026-09-10\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Server-hall thermal sensors: x = aisle position (m), z = row depth (m),\n// y = rack height (m). Points fall into three rack rows (clusters); color\n// encodes the temperature deviation from the 22 °C cooling setpoint — the\n// fourth, continuous variable from the spec.\nfunction makeLcg(seed) {\n  let state = seed;\n  return function lcg() {\n    state = (state * 1103515245 + 12345) & 0x7fffffff;\n    return state / 0x7fffffff;\n  };\n}\nconst rand = makeLcg(42);\nfunction gaussian() {\n  const u1 = Math.max(rand(), 1e-6);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst X_MAX = 42; // aisle extent, m\nconst Z_MAX = 20; // row depth extent, m\nconst Y_MAX = 3; // rack height extent, m\nconst VERTICAL_EXAGGERATION = 9; // exaggerate height so the y-axis reads clearly\n\nconst CLUSTERS = [\n  { x: 8, z: 6, biasC: -0.6, n: 46 }, // Row A\n  { x: 24, z: 14, biasC: 2.6, n: 50 }, // Row B — hot aisle\n  { x: 34, z: 4, biasC: 0.4, n: 44 }, // Row C\n];\n\nconst sensors = [];\nCLUSTERS.forEach((cluster) => {\n  for (let i = 0; i < cluster.n; i += 1) {\n    const xPos = Math.min(Math.max(cluster.x + gaussian() * 3.2, 0), X_MAX);\n    const zPos = Math.min(Math.max(cluster.z + gaussian() * 3.2, 0), Z_MAX);\n    const height = Math.min(Math.max(0.3 + rand() * 2.4, 0), Y_MAX);\n    const tempDeviation = cluster.biasC + (height - 1.5) * 1.15 + gaussian() * 0.7;\n    sensors.push({ x: xPos, y: height, z: zPos, colorValue: tempDeviation });\n  }\n});\n\n// --- Isometric projection ----------------------------------------------------\n// Highcharts core has no 3D module (highcharts-3d is an add-on, out of scope —\n// see prompts/library/highcharts.md). A classic axonometric transform still\n// gives an honest 3D read from plain x/y scatter coordinates, no z-axis needed.\nconst ISO_ANGLE = Math.PI / 6;\nfunction project(x, y, z) {\n  return {\n    px: (x - z) * Math.cos(ISO_ANGLE),\n    py: (x + z) * Math.sin(ISO_ANGLE) - y * VERTICAL_EXAGGERATION,\n  };\n}\n\n// Plain `scatter` series isn't in Highcharts' colorAxis-composed series list\n// (only scatter3d/bubble/heatmap/… are), so per-point colorAxis coloring is\n// silently ignored for it — interpolate the imprint_div stops by hand instead.\nconst TEMP_RANGE = 4; // °C, symmetric around the setpoint\nfunction hexToRgb(hex) {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\nfunction lerpHex(hexA, hexB, frac) {\n  const a = hexToRgb(hexA);\n  const b = hexToRgb(hexB);\n  const rgb = a.map((v, i) => Math.round(v + (b[i] - v) * frac));\n  return `rgb(${rgb.join(\",\")})`;\n}\nfunction divergingColor(value) {\n  const frac = Math.min(Math.max((value + TEMP_RANGE) / (2 * TEMP_RANGE), 0), 1);\n  return frac < 0.5 ? lerpHex(t.div[0], t.div[1], frac / 0.5) : lerpHex(t.div[1], t.div[2], (frac - 0.5) / 0.5);\n}\n// Light alpha blending so overlapping sensors in the denser clusters stay legible.\nconst POINT_ALPHA = 0.82;\nfunction withAlpha(rgb, alpha) {\n  return rgb.replace(\"rgb(\", \"rgba(\").replace(\")\", `,${alpha})`);\n}\n\nconst sensorPoints = sensors.map((s) => {\n  const { px, py } = project(s.x, s.y, s.z);\n  const depthT = s.z / Z_MAX; // farther rows (larger z) sit smaller — depth cue\n  return {\n    x: px,\n    y: py,\n    colorValue: s.colorValue,\n    color: withAlpha(divergingColor(s.colorValue), POINT_ALPHA),\n    origX: s.x,\n    origY: s.y,\n    origZ: s.z,\n    marker: { radius: 10 - depthT * 5 },\n  };\n});\n\n// --- Reference wireframe (floor grid + height axis) ---------------------------\nfunction gridLine(a, b) {\n  return [project(a[0], a[1], a[2]), project(b[0], b[1], b[2])].map((p) => [p.px, p.py]);\n}\nconst floorLines = [];\nfor (let z = 0; z <= Z_MAX; z += Z_MAX / 4) floorLines.push(gridLine([0, 0, z], [X_MAX, 0, z]));\nfor (let x = 0; x <= X_MAX; x += X_MAX / 4) floorLines.push(gridLine([x, 0, 0], [x, 0, Z_MAX]));\n\nconst heightAxisLine = gridLine([0, 0, 0], [0, Y_MAX, 0]);\n\n// Numeric tick marks along the height axis so absolute values are readable,\n// not just the \"↑ Rack height (m)\" direction label.\nconst HEIGHT_TICKS = [0, 1.5, 3];\nconst heightTickLines = HEIGHT_TICKS.map((h) => gridLine([-0.9, h, 0], [0.9, h, 0]));\nconst heightTickLabels = HEIGHT_TICKS.map((h) => {\n  const p = project(-1.6, h, 0);\n  return { x: p.px, y: p.py, name: `${h}m` };\n});\n\nconst axisLabelPoints = [\n  { p: project(X_MAX, 0, 0), text: \"Aisle position (m) →\" },\n  { p: project(0, 0, Z_MAX), text: \"← Row depth (m)\" },\n  // Sits a bit past the topmost tick (Y_MAX) so it doesn't collide with it.\n  { p: project(0, Y_MAX + 0.5, 0), text: \"↑ Rack height (m)\" },\n].map((d) => ({ x: d.p.px, y: d.p.py, name: d.text }));\n\n// --- Chart --------------------------------------------------------------------\n// Highcharts' built-in colorAxis legend only composes onto series types that\n// ship in add-on modules (bubble/heatmap/…), none of which are loaded — so the\n// Δ-temp color scale is drawn by hand with the core renderer once the chart\n// has laid out (chart.events.load), reading the same divergingColor() stops\n// used to color the points above.\nfunction drawColorLegend(chart) {\n  const barWidth = 220;\n  const barHeight = 14;\n  const barX = chart.chartWidth - barWidth - 60;\n  const barY = 56;\n  chart.renderer\n    .text(\"Δ Temp vs. 22°C setpoint\", barX, barY - 8)\n    .css({ color: t.inkSoft, fontSize: \"14px\" })\n    .add();\n  chart.renderer\n    .rect(barX, barY, barWidth, barHeight)\n    .attr({\n      fill: {\n        linearGradient: { x1: 0, y1: 0, x2: 1, y2: 0 },\n        stops: [\n          [0, t.div[0]],\n          [0.5, t.div[1]],\n          [1, t.div[2]],\n        ],\n      },\n      stroke: t.inkSoft,\n      \"stroke-width\": 1,\n    })\n    .add();\n  chart.renderer\n    .text(`${-TEMP_RANGE}°C`, barX, barY + barHeight + 18)\n    .css({ color: t.inkSoft, fontSize: \"13px\" })\n    .add();\n  chart.renderer\n    .text(`+${TEMP_RANGE}°C`, barX + barWidth - 26, barY + barHeight + 18)\n    .css({ color: t.inkSoft, fontSize: \"13px\" })\n    .add();\n}\n\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    events: { load: function onLoad() { drawColorLegend(this); } },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"Data Center Thermal Sensors · scatter-3d · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"19px\", fontWeight: \"600\" },\n  },\n  xAxis: { visible: false },\n  yAxis: { visible: false, title: { text: null } },\n  legend: { enabled: false },\n  tooltip: {\n    pointFormat:\n      \"Aisle: {point.origX:.1f} m<br/>Row depth: {point.origZ:.1f} m<br/>\" +\n      \"Rack height: {point.origY:.1f} m<br/>ΔTemp: {point.colorValue:.1f}°C\",\n  },\n  plotOptions: {\n    series: { animation: false },\n  },\n  series: [\n    ...floorLines.map((line) => ({\n      type: \"line\",\n      data: line,\n      color: t.grid,\n      lineWidth: 1,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n    })),\n    {\n      type: \"line\",\n      data: heightAxisLine,\n      color: t.inkSoft,\n      lineWidth: 1.5,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    ...heightTickLines.map((line) => ({\n      type: \"line\",\n      data: line,\n      color: t.inkSoft,\n      lineWidth: 1.5,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n    })),\n    {\n      type: \"scatter\",\n      name: \"Height ticks\",\n      data: heightTickLabels,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n      dataLabels: {\n        enabled: true,\n        format: \"{point.name}\",\n        style: { color: t.inkSoft, fontSize: \"12px\", fontWeight: \"normal\", textOutline: \"none\" },\n      },\n    },\n    {\n      type: \"scatter\",\n      name: \"Axis labels\",\n      data: axisLabelPoints,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n      dataLabels: {\n        enabled: true,\n        format: \"{point.name}\",\n        style: { color: t.inkSoft, fontSize: \"14px\", fontWeight: \"normal\", textOutline: \"none\" },\n      },\n    },\n    {\n      type: \"scatter\",\n      name: \"Sensors\",\n      data: sensorPoints,\n      showInLegend: false,\n      // Fill is the diverging color, which equals PAGE_BG right at the\n      // midpoint — an inkSoft stroke (not PAGE_BG) keeps near-zero-deviation\n      // points from vanishing into the background. Explicit circle symbol\n      // avoids Highcharts' default per-series-index symbol cycling (which\n      // otherwise lands this series on squares).\n      marker: { symbol: \"circle\", lineColor: t.inkSoft, lineWidth: 1 },\n    },\n  ],\n});\n"}