{"spec_id":"chessboard-pieces","library":"muix","language":"javascript","code":"// anyplot.ai\n// chessboard-pieces: Chess Board with Pieces for Position Diagrams\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-09-01\n//# anyplot-orientation: square\n// anyplot.ai\n// chessboard-pieces: Chess Board with Pieces for Position Diagrams\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-01\n\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\nimport { ScatterChart } from \"@mui/x-charts/ScatterChart\";\n\nconst t = window.ANYPLOT_TOKENS;\n\nconst FILES = [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\"];\nconst RANKS = [\"8\", \"7\", \"6\", \"5\", \"4\", \"3\", \"2\", \"1\"]; // top -> bottom, white sits on ranks 1-2\n\n// Position after the \"Scholar's Mate\" tactic (1.e4 e5 2.Qh5 Nc6 3.Bc4 Nf6??\n// 4.Qxf7#) — the white queen has just captured on f7, delivering checkmate.\n// Uppercase = white, lowercase = black, per the spec's notation convention.\nconst PIECES = {\n  e1: \"K\", f7: \"Q\", a1: \"R\", h1: \"R\", c1: \"B\", c4: \"B\", b1: \"N\", g1: \"N\",\n  a2: \"P\", b2: \"P\", c2: \"P\", d2: \"P\", e4: \"P\", f2: \"P\", g2: \"P\", h2: \"P\",\n  e8: \"k\", d8: \"q\", a8: \"r\", h8: \"r\", c8: \"b\", f8: \"b\", c6: \"n\", f6: \"n\",\n  a7: \"p\", b7: \"p\", c7: \"p\", d7: \"p\", e5: \"p\", g7: \"p\", h7: \"p\",\n};\n\nconst GLYPHS = {\n  K: \"♔\", Q: \"♕\", R: \"♖\", B: \"♗\", N: \"♘\", P: \"♙\",\n  k: \"♚\", q: \"♛\", r: \"♜\", b: \"♝\", n: \"♞\", p: \"♟\",\n};\n\n// The mating queen (f7) and the checkmated king (e8) — highlighted on the\n// board itself so the \"Scholar's Mate\" title has a visual anchor.\nconst MATE_SQUARES = new Set([\"f7\", \"e8\"]);\n\n// Every square, so the checkerboard pattern renders as one scatter series.\nconst squarePoints = FILES.flatMap((file) =>\n  RANKS.map((rank) => ({ id: `sq-${file}${rank}`, x: file, y: rank })),\n);\n\n// Only occupied squares, as a second scatter series sharing the same axes.\nconst piecePoints = Object.entries(PIECES).map(([square, code]) => ({\n  id: `pc-${square}`,\n  x: square[0],\n  y: square[1],\n  symbol: GLYPHS[code],\n  isWhite: code === code.toUpperCase(),\n}));\n\n// Custom marker: the same slot renders either the checkerboard squares (plus\n// the board frame) or the piece glyphs, keyed off which series it's drawing.\n// White pieces get a light fill with an ink outline so they read clearly on\n// dark squares; black pieces are a solid ink fill.\nfunction BoardMark(props) {\n  const { series, xScale, yScale } = props;\n  const cellW = xScale.bandwidth();\n  const cellH = yScale.bandwidth();\n\n  if (series.id === \"squares\") {\n    return (\n      <g>\n        {series.data.map((pt) => {\n          const fileIdx = FILES.indexOf(pt.x);\n          const rankIdx = Number(pt.y) - 1;\n          const dark = (fileIdx + rankIdx) % 2 === 0;\n          const cx = xScale(pt.x) ?? 0;\n          const cy = yScale(pt.y) ?? 0;\n          const isMateSquare = MATE_SQUARES.has(`${pt.x}${pt.y}`);\n          return (\n            <g key={pt.id}>\n              <rect x={cx} y={cy} width={cellW} height={cellH} fill={t.elevatedBg} />\n              {dark && (\n                <rect x={cx} y={cy} width={cellW} height={cellH} fill={t.ink} fillOpacity={0.22} />\n              )}\n              {isMateSquare && (\n                <rect\n                  x={cx + 2}\n                  y={cy + 2}\n                  width={cellW - 4}\n                  height={cellH - 4}\n                  fill={t.amber}\n                  fillOpacity={0.22}\n                  stroke={t.amber}\n                  strokeWidth={2}\n                />\n              )}\n            </g>\n          );\n        })}\n        <rect\n          x={xScale(\"a\") ?? 0}\n          y={yScale(\"8\") ?? 0}\n          width={cellW * FILES.length}\n          height={cellH * RANKS.length}\n          fill=\"none\"\n          stroke={t.inkSoft}\n          strokeWidth={2}\n        />\n      </g>\n    );\n  }\n\n  return (\n    <g>\n      {series.data.map((pt) => {\n        const cx = (xScale(pt.x) ?? 0) + cellW / 2;\n        const cy = (yScale(pt.y) ?? 0) + cellH / 2;\n        return (\n          <text\n            key={pt.id}\n            x={cx}\n            y={cy}\n            textAnchor=\"middle\"\n            dominantBaseline=\"central\"\n            fontSize={cellW * 0.68}\n            fill={pt.isWhite ? t.elevatedBg : t.ink}\n            stroke={pt.isWhite ? t.ink : \"none\"}\n            strokeWidth={pt.isWhite ? 1.5 : 0}\n            paintOrder=\"stroke\"\n          >\n            {pt.symbol}\n          </text>\n        );\n      })}\n    </g>\n  );\n}\n\nconst TITLE = \"Scholar's Mate · chessboard-pieces · javascript · muix · anyplot.ai\";\nconst TITLE_HEIGHT = 64;\n\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const chartHeight = height - TITLE_HEIGHT;\n\n  // Keep every square perfectly square: pick vertical margins first (room for\n  // the file labels below the board), derive the board size from what's left,\n  // then split the width minus that board size into left/right margins (room\n  // for the rank labels on the left).\n  const TOP = 24;\n  const BOTTOM = 56;\n  const boardSize = chartHeight - TOP - BOTTOM;\n  const LEFT = 72;\n  const RIGHT = width - boardSize - LEFT;\n\n  return (\n    <Box sx={{ width, height, bgcolor: t.pageBg, display: \"flex\", flexDirection: \"column\" }}>\n      <Typography\n        sx={{\n          color: t.ink,\n          fontSize: 24,\n          fontWeight: 500,\n          textAlign: \"center\",\n          lineHeight: 1.2,\n          pt: \"16px\",\n          height: TITLE_HEIGHT,\n          fontFamily: \"inherit\",\n        }}\n      >\n        {TITLE}\n      </Typography>\n      <Box sx={{ flex: 1, display: \"flex\", alignItems: \"center\", justifyContent: \"center\" }}>\n        <ScatterChart\n          width={width}\n          height={chartHeight}\n          skipAnimation\n          disableVoronoi\n          series={[\n            { id: \"squares\", type: \"scatter\", data: squarePoints, xAxisId: \"file\", yAxisId: \"rank\" },\n            { id: \"pieces\", type: \"scatter\", data: piecePoints, xAxisId: \"file\", yAxisId: \"rank\" },\n          ]}\n          xAxis={[\n            {\n              id: \"file\",\n              scaleType: \"band\",\n              data: FILES,\n              categoryGapRatio: 0,\n              tickLabelStyle: { fontSize: 19, fill: t.inkSoft },\n              disableTicks: true,\n              disableLine: true,\n            },\n          ]}\n          yAxis={[\n            {\n              id: \"rank\",\n              scaleType: \"band\",\n              data: RANKS,\n              categoryGapRatio: 0,\n              tickLabelStyle: { fontSize: 19, fill: t.inkSoft },\n              disableTicks: true,\n              disableLine: true,\n            },\n          ]}\n          topAxis={null}\n          bottomAxis=\"file\"\n          leftAxis=\"rank\"\n          rightAxis={null}\n          margin={{ top: TOP, bottom: BOTTOM, left: LEFT, right: RIGHT }}\n          slots={{ scatter: BoardMark }}\n          slotProps={{ legend: { hidden: true } }}\n        />\n      </Box>\n    </Box>\n  );\n}\n"}