{"spec_id":"scatter-3d","library":"muix","language":"javascript","code":"// anyplot.ai\n// scatter-3d: 3D Scatter Plot\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-10\n//# anyplot-orientation: landscape\n// anyplot.ai\n// scatter-3d: 3D Scatter Plot\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-09-10\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsTooltip } from \"@mui/x-charts/ChartsTooltip\";\nimport { ContinuousColorLegend } from \"@mui/x-charts/ChartsLegend\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\n\n// --- Deterministic PRNG (LCG + Box-Muller, no seeded RNG in the browser) ----\nfunction createLcg(seed) {\n  let state = seed;\n  return function nextUniform() {\n    state = (state * 16807) % 2147483647;\n    return (state - 1) / 2147483646;\n  };\n}\nconst nextUniform = createLcg(7);\nfunction nextGaussian() {\n  const u1 = Math.max(nextUniform(), 1e-9);\n  const u2 = nextUniform();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// --- Data: simulated nanocluster fragments from a molecular-dynamics run ----\n// Three atom groups (fragments) placed in a 100x100x100 nm cell; each atom's\n// local potential energy (eV) rises with distance from its fragment's core —\n// the classic \"stable core, strained periphery\" pattern used to demo a\n// genuine 4th-variable color encoding on top of real x/y/z spatial position.\nconst AXIS_MIN = 0;\nconst AXIS_MAX = 100;\nconst FRAGMENTS = [\n  { center: [22, 74, 30], sigma: 7, count: 55 },\n  { center: [78, 32, 68], sigma: 8, count: 55 },\n  { center: [42, 20, 84], sigma: 6, count: 45 },\n];\n\nconst atoms = [];\nFRAGMENTS.forEach((fragment, fragmentIndex) => {\n  for (let i = 0; i < fragment.count; i += 1) {\n    const x = fragment.center[0] + nextGaussian() * fragment.sigma;\n    const y = fragment.center[1] + nextGaussian() * fragment.sigma;\n    const z = fragment.center[2] + nextGaussian() * fragment.sigma;\n    const distanceFromCore = Math.sqrt(\n      (x - fragment.center[0]) ** 2 +\n        (y - fragment.center[1]) ** 2 +\n        (z - fragment.center[2]) ** 2\n    );\n    const energy = -6.4 + 0.062 * distanceFromCore + nextGaussian() * 0.22; // eV\n    atoms.push({\n      id: `atom-${fragmentIndex}-${i}`,\n      x: Math.min(Math.max(x, AXIS_MIN + 1), AXIS_MAX - 1),\n      y: Math.min(Math.max(y, AXIS_MIN + 1), AXIS_MAX - 1),\n      z: Math.min(Math.max(z, AXIS_MIN + 1), AXIS_MAX - 1),\n      energy,\n    });\n  }\n});\n\n// --- Isometric projection: (x, y, z) -> 2D (px, py) data-space coordinates -\n// x/z form the ground plane (down-right / down-left), y is height (straight\n// up). Fed as ordinary numeric x/y into a linear-scale scatter, so the chart\n// does the pixel conversion; the sign choices below make the axes read as a\n// conventional isometric cube once the y-axis' usual \"up = larger\" inversion\n// is applied.\nconst ISO_ANGLE = Math.PI / 6; // 30 degrees\nfunction project(x, y, z) {\n  return {\n    px: (x - z) * Math.cos(ISO_ANGLE),\n    py: y - (x + z) * Math.sin(ISO_ANGLE),\n  };\n}\n\natoms.forEach((atom) => {\n  const projected = project(atom.x, atom.y, atom.z);\n  atom.px = projected.px;\n  atom.py = projected.py;\n});\n\n// --- Depth cue: points nearer the (x=0, z=0) front edge render larger -------\nconst groundDepths = atoms.map((atom) => atom.x + atom.z).sort((a, b) => a - b);\nconst tierBoundary1 = groundDepths[Math.floor(groundDepths.length / 3)];\nconst tierBoundary2 = groundDepths[Math.floor((2 * groundDepths.length) / 3)];\nfunction depthTier(atom) {\n  const depth = atom.x + atom.z;\n  if (depth <= tierBoundary1) return \"near\";\n  if (depth <= tierBoundary2) return \"mid\";\n  return \"far\";\n}\nconst MARKER_SIZE_BY_TIER = { near: 12, mid: 8.5, far: 5.5 };\n\n// --- Domain bounds: project the cell's 8 corners so every axis line and all\n// data stay comfortably inside the computed x/y scale, then pad for labels --\nconst cellCorners = [];\n[AXIS_MIN, AXIS_MAX].forEach((cx) =>\n  [AXIS_MIN, AXIS_MAX].forEach((cy) =>\n    [AXIS_MIN, AXIS_MAX].forEach((cz) => cellCorners.push(project(cx, cy, cz)))\n  )\n);\nconst PAD = 16;\nconst pxMin = Math.min(...cellCorners.map((c) => c.px)) - PAD;\nconst pxMax = Math.max(...cellCorners.map((c) => c.px)) + PAD;\nconst pyMin = Math.min(...cellCorners.map((c) => c.py)) - PAD;\nconst pyMax = Math.max(...cellCorners.map((c) => c.py)) + PAD * 1.6;\n\n// --- Color axis: continuous Imprint sequential scale over the energy field -\n// The scatter series has no direct opacity prop, so a slight fill-opacity is\n// baked into the colorMap stops themselves to reduce occlusion where the\n// dense upper fragment cluster overlaps.\nconst withAlpha = (hex, alpha) => {\n  const n = parseInt(hex.slice(1), 16);\n  const r = (n >> 16) & 255;\n  const g = (n >> 8) & 255;\n  const b = n & 255;\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n};\nconst MARKER_ALPHA = 0.85;\n\nconst energyValues = atoms.map((atom) => atom.energy);\nconst energyMin = Math.min(...energyValues);\nconst energyMax = Math.max(...energyValues);\n\nconst zAxis = [\n  {\n    id: \"energyColor\",\n    colorMap: {\n      type: \"continuous\",\n      min: energyMin,\n      max: energyMax,\n      color: [withAlpha(t.seq[0], MARKER_ALPHA), withAlpha(t.seq[1], MARKER_ALPHA)],\n    },\n  },\n];\n\n// --- One scatter series per depth tier (all sharing the same color axis) so\n// per-point markerSize can vary while color stays driven by energy alone ----\nconst series = [\"near\", \"mid\", \"far\"].map((tier) => ({\n  type: \"scatter\",\n  id: `atoms-${tier}`,\n  zAxisId: \"energyColor\",\n  markerSize: MARKER_SIZE_BY_TIER[tier],\n  data: atoms\n    .filter((atom) => depthTier(atom) === tier)\n    .map((atom) => ({\n      x: atom.px,\n      y: atom.py,\n      z: atom.energy,\n      id: atom.id,\n      realX: atom.x,\n      realY: atom.y,\n      realZ: atom.z,\n      energy: atom.energy,\n    })),\n  valueFormatter: (value) =>\n    `x=${value.realX.toFixed(1)} nm, y=${value.realY.toFixed(1)} nm, z=${value.realZ.toFixed(1)} nm · E=${value.energy.toFixed(2)} eV`,\n}));\n\n// --- Wireframe cell + axis labels + depth mini-legend, drawn in data space\n// via the chart's own scale hooks (same pattern as biplot-pca's LoadingArrows)\nconst originPoint = project(AXIS_MIN, AXIS_MIN, AXIS_MIN);\nconst xEndPoint = project(AXIS_MAX, AXIS_MIN, AXIS_MIN);\nconst yEndPoint = project(AXIS_MIN, AXIS_MAX, AXIS_MIN);\nconst zEndPoint = project(AXIS_MIN, AXIS_MIN, AXIS_MAX);\nconst farFloorCorner = project(AXIS_MAX, AXIS_MIN, AXIS_MAX);\n\nfunction Iso3DFrame() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const toPixels = (p) => ({ x: xScale(p.px), y: yScale(p.py) });\n\n  const origin = toPixels(originPoint);\n  const xEnd = toPixels(xEndPoint);\n  const yEnd = toPixels(yEndPoint);\n  const zEnd = toPixels(zEndPoint);\n  const farFloor = toPixels(farFloorCorner);\n\n  const axisLine = (from, to) => (\n    <line\n      x1={from.x}\n      y1={from.y}\n      x2={to.x}\n      y2={to.y}\n      stroke={t.inkSoft}\n      strokeWidth={2}\n    />\n  );\n  const floorEdge = (from, to) => (\n    <line\n      x1={from.x}\n      y1={from.y}\n      x2={to.x}\n      y2={to.y}\n      stroke={t.grid}\n      strokeWidth={1.5}\n      strokeDasharray=\"6 5\"\n    />\n  );\n  const axisLabel = (point, text, dx, dy) => (\n    <text\n      x={point.x + dx}\n      y={point.y + dy}\n      fill={t.ink}\n      fontSize={15}\n      fontWeight={600}\n      fontFamily=\"system-ui, sans-serif\"\n      textAnchor=\"middle\"\n    >\n      {text}\n    </text>\n  );\n\n  const depthLegendX = 70;\n  const depthLegendY = 96;\n  const depthLegendItems = [\n    { tier: \"Near\", r: MARKER_SIZE_BY_TIER.near },\n    { tier: \"Mid\", r: MARKER_SIZE_BY_TIER.mid },\n    { tier: \"Far\", r: MARKER_SIZE_BY_TIER.far },\n  ];\n\n  return (\n    <g>\n      {floorEdge(xEnd, farFloor)}\n      {floorEdge(zEnd, farFloor)}\n      {axisLine(origin, xEnd)}\n      {axisLine(origin, yEnd)}\n      {axisLine(origin, zEnd)}\n      {axisLabel(origin, \"0\", -14, 18)}\n      {axisLabel(xEnd, `X · ${AXIS_MAX} nm`, 24, 8)}\n      {axisLabel(yEnd, `Height (Y) · ${AXIS_MAX} nm`, 0, -14)}\n      {axisLabel(zEnd, `Z · ${AXIS_MAX} nm`, -30, 8)}\n\n      <text\n        x={depthLegendX}\n        y={depthLegendY - 22}\n        fill={t.inkSoft}\n        fontSize={16}\n        fontFamily=\"system-ui, sans-serif\"\n      >\n        Marker size = depth\n      </text>\n      {depthLegendItems.map((item, i) => (\n        <g key={item.tier} transform={`translate(${depthLegendX + i * 90}, ${depthLegendY})`}>\n          <circle cx={0} cy={0} r={item.r} fill={t.inkSoft} opacity={0.55} />\n          <text\n            x={18}\n            y={5}\n            fill={t.inkSoft}\n            fontSize={15}\n            fontFamily=\"system-ui, sans-serif\"\n          >\n            {item.tier}\n          </text>\n        </g>\n      ))}\n    </g>\n  );\n}\n\nconst chartTitle = \"scatter-3d · javascript · muix · anyplot.ai\";\n\n// --- Chart (default-exported component — the harness mounts it) ------------\nexport default function Chart() {\n  return (\n    <ChartContainer\n      width={size.width}\n      height={size.height}\n      series={series}\n      zAxis={zAxis}\n      xAxis={[{ id: \"iso-x\", scaleType: \"linear\", min: pxMin, max: pxMax }]}\n      yAxis={[{ id: \"iso-y\", scaleType: \"linear\", min: pyMin, max: pyMax }]}\n      margin={{ top: 70, right: 60, bottom: 90, left: 60 }}\n      disableVoronoi\n      skipAnimation\n    >\n      <text\n        x={size.width / 2}\n        y={38}\n        textAnchor=\"middle\"\n        fontSize={26}\n        fontWeight={600}\n        fill={t.ink}\n        fontFamily=\"system-ui, sans-serif\"\n      >\n        {chartTitle}\n      </text>\n      <Iso3DFrame />\n      <ScatterPlot />\n      <ContinuousColorLegend\n        axisDirection=\"z\"\n        axisId=\"energyColor\"\n        position={{ horizontal: \"middle\", vertical: \"bottom\" }}\n        length=\"26%\"\n        thickness={10}\n        minLabel={() => `${energyMin.toFixed(1)} eV · core`}\n        maxLabel={() => `${energyMax.toFixed(1)} eV · edge`}\n        labelStyle={{ fontSize: 15, fill: t.inkSoft }}\n      />\n      <ChartsTooltip trigger=\"item\" />\n    </ChartContainer>\n  );\n}\n"}