{"spec_id":"bar-3d-categorical","library":"muix","language":"javascript","code":"// anyplot.ai\n// bar-3d-categorical: 3D Bar Chart for Categorical Comparison\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 95/100 | Created: 2026-09-04\n//# anyplot-orientation: landscape\n// anyplot.ai\n// bar-3d-categorical: 3D Bar Chart for Categorical Comparison\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: 88/100 | Created: 2026-09-04\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ChartsLegend } from \"@mui/x-charts/ChartsLegend\";\nimport { ChartsText } from \"@mui/x-charts/ChartsText\";\nimport { useXScale, useYScale, useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Factorial-design experiment: tensile strength across alloy × heat-treatment.\nconst alloys = [\"Al 6061\", \"Ti-6Al-4V\", \"Steel 4140\", \"Inconel 718\", \"Mg AZ31\"];\n// Depth order runs weakest → strongest per alloy, so the row offset (which\n// pushes further rows up-and-right) always reinforces the value-driven\n// height difference between neighbors instead of fighting it — that keeps\n// the value labels of adjacent rows from colliding even where two treatments\n// land close together (see Al 6061 / Mg AZ31 below).\nconst treatments = [\"Annealed\", \"Tempered\", \"Quenched\", \"Aged\"];\n// tensileStrengthMPa[treatmentIndex][alloyIndex], in MPa — strictly\n// increasing down each column (alloy) from Annealed to Aged.\nconst tensileStrengthMPa = [\n  [124, 830, 655, 965, 145], // Annealed\n  [200, 950, 780, 1100, 200], // Tempered\n  [275, 1100, 900, 1250, 240], // Quenched\n  [310, 1170, 1080, 1400, 290], // Aged\n];\n\nconst MAX_STRENGTH = 1500;\nconst ROW_COLORS = [t.palette[0], t.palette[1], t.palette[2], t.palette[3]];\n\nconst series = treatments.map((label, i) => ({\n  type: \"bar\",\n  id: label,\n  label,\n  data: alloys.map((_, j) => tensileStrengthMPa[i][j]),\n  color: ROW_COLORS[i],\n}));\n\n// Isometric depth vector, in CSS px within the mount's coordinate space —\n// this is each cuboid's own front-to-back thickness.\nconst ISO_DX = 20;\nconst ISO_DY = -13;\n\n// Per-treatment row stagger: slightly larger than the cuboid depth vector so\n// consecutive rows don't sit edge-to-edge — the extra step opens a thin gap\n// between the back face of one row and the front face of the next, per the\n// spec's \"slight spacing between them for visual clarity and depth\n// perception\".\nconst STAGGER_DX = ISO_DX * 1.3;\nconst STAGGER_DY = ISO_DY * 1.3;\n\nconst TITLE = \"bar-3d-categorical · javascript · muix · anyplot.ai\";\nconst TITLE_HEIGHT = 64;\nconst MARGIN = { top: 140, right: 230, bottom: 90, left: 120 };\n\nfunction shade(hex, amount) {\n  const num = parseInt(hex.slice(1), 16);\n  const r = (num >> 16) & 0xff;\n  const g = (num >> 8) & 0xff;\n  const b = num & 0xff;\n  const target = amount > 0 ? 255 : 0;\n  const k = Math.abs(amount);\n  const mix = (c) => Math.round(c + (target - c) * k);\n  return `rgb(${mix(r)}, ${mix(g)}, ${mix(b)})`;\n}\n\n// --- Custom Y-axis title -----------------------------------------------------\n// ChartsYAxis's own `label` places the rotated title at a fixed\n// `tickFontSize + tickSize + 10` offset from the axis line (ChartsYAxis.js),\n// which assumes short tick text; it collides with wide numeric tick labels\n// like \"1,500\". Rendering the title separately with a hand-picked offset\n// sidesteps that.\nfunction YAxisTitle() {\n  const { top, height } = useDrawingArea();\n  return (\n    <ChartsText\n      x={38}\n      y={top + height / 2}\n      text=\"Tensile Strength (MPa)\"\n      style={{ fontSize: 16, fill: t.ink, fontWeight: 500, textAnchor: \"middle\", dominantBaseline: \"auto\", angle: -90 }}\n    />\n  );\n}\n\n// --- Custom overlay: a 2D categorical grid (alloy × treatment) rendered as\n// isometric cuboids, height-encoded by value. Community `@mui/x-charts/hooks`\n// (useXScale/useYScale) map data coordinates to pixels so the grid stays\n// aligned with the axes at any size — the same composition pattern used for\n// mohr-circle's custom geometry, applied here to a chart type (3D bars) the\n// declarative BarChart can't express on its own. -----------------------------\nfunction Bars3D() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const { left, width } = useDrawingArea();\n  const bandwidth = xScale.bandwidth();\n  const barWidth = bandwidth * 0.55;\n  const baseline = yScale(0);\n\n  // Paint back row first so nearer (lower-index) rows correctly occlude it.\n  const paintOrder = [...treatments.keys()].sort((a, b) => b - a);\n\n  // Base-plane grid: faint iso-projected floor lines tying each bar back to\n  // its (alloy, treatment) cell — one receding line per treatment depth, and\n  // one line per alloy running front-to-back through all four depths.\n  const floorRowLines = treatments.map((_, row) => {\n    const dx = row * STAGGER_DX;\n    const dy = row * STAGGER_DY;\n    return (\n      <line\n        key={`row-${row}`}\n        x1={left + dx}\n        y1={baseline + dy}\n        x2={left + width + dx}\n        y2={baseline + dy}\n        stroke={t.inkSoft}\n        strokeWidth={1.5}\n        opacity={0.35}\n      />\n    );\n  });\n  const floorColumnLines = alloys.map((alloy) => {\n    const cx = xScale(alloy) + bandwidth / 2;\n    const points = treatments\n      .map((_, row) => `${cx + row * STAGGER_DX},${baseline + row * STAGGER_DY}`)\n      .join(\" \");\n    return (\n      <polyline\n        key={`col-${alloy}`}\n        points={points}\n        fill=\"none\"\n        stroke={t.inkSoft}\n        strokeWidth={1.5}\n        opacity={0.35}\n      />\n    );\n  });\n\n  return (\n    <>\n      <g>\n        {floorRowLines}\n        {floorColumnLines}\n      </g>\n      {paintOrder.map((row) => {\n        const dx = row * STAGGER_DX;\n        const dy = row * STAGGER_DY;\n        const color = ROW_COLORS[row];\n        const topColor = shade(color, 0.34);\n        const sideColor = shade(color, -0.3);\n        return (\n          <g key={treatments[row]} transform={`translate(${dx}, ${dy})`}>\n            {alloys.map((alloy, col) => {\n              const value = tensileStrengthMPa[row][col];\n              const x = xScale(alloy) + (bandwidth - barWidth) / 2;\n              const y = yScale(value);\n              const barHeight = baseline - y;\n              const topFace = [\n                `${x},${y}`,\n                `${x + barWidth},${y}`,\n                `${x + barWidth + ISO_DX},${y + ISO_DY}`,\n                `${x + ISO_DX},${y + ISO_DY}`,\n              ].join(\" \");\n              const sideFace = [\n                `${x + barWidth},${y}`,\n                `${x + barWidth},${y + barHeight}`,\n                `${x + barWidth + ISO_DX},${y + barHeight + ISO_DY}`,\n                `${x + barWidth + ISO_DX},${y + ISO_DY}`,\n              ].join(\" \");\n              return (\n                <g key={alloy}>\n                  <rect x={x} y={y} width={barWidth} height={barHeight} fill={color} />\n                  <polygon points={sideFace} fill={sideColor} />\n                  <polygon points={topFace} fill={topColor} />\n                  <ChartsText\n                    x={x + barWidth / 2 + ISO_DX / 2}\n                    y={y + ISO_DY - 8}\n                    text={String(value)}\n                    style={{ fontSize: 12, fill: t.ink, textAnchor: \"middle\", dominantBaseline: \"auto\" }}\n                  />\n                </g>\n              );\n            })}\n          </g>\n        );\n      })}\n    </>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const chartHeight = height - TITLE_HEIGHT;\n\n  return (\n    <div style={{ width, height, display: \"flex\", flexDirection: \"column\" }}>\n      <div\n        style={{\n          height: TITLE_HEIGHT,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          fontSize: 26,\n          fontWeight: 500,\n          color: t.ink,\n          fontFamily: \"'Roboto', 'Helvetica', 'Arial', sans-serif\",\n        }}\n      >\n        {TITLE}\n      </div>\n      <ChartContainer\n        width={width}\n        height={chartHeight}\n        margin={MARGIN}\n        skipAnimation\n        series={series}\n        xAxis={[{ scaleType: \"band\", data: alloys, label: \"Alloy\" }]}\n        yAxis={[{ scaleType: \"linear\", min: 0, max: MAX_STRENGTH }]}\n      >\n        <ChartsGrid horizontal />\n        <Bars3D />\n        <ChartsXAxis\n          labelStyle={{ fontSize: 16, fill: t.ink, fontWeight: 500 }}\n          tickLabelStyle={{ fontSize: 14, fill: t.inkSoft }}\n          stroke={t.inkSoft}\n        />\n        <ChartsYAxis tickLabelStyle={{ fontSize: 14, fill: t.inkSoft }} stroke={t.inkSoft} />\n        <YAxisTitle />\n        <ChartsLegend\n          direction=\"column\"\n          position={{ horizontal: \"right\", vertical: \"middle\" }}\n          labelStyle={{ fontSize: 15, fill: t.ink }}\n        />\n      </ChartContainer>\n    </div>\n  );\n}\n"}