{"spec_id":"chessboard-pieces","library":"d3","language":"javascript","code":"// anyplot.ai\n// chessboard-pieces: Chess Board with Pieces for Position Diagrams\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-01\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data: Scholar's Mate, final position after 4. Qxf7# --------------------\n// Uppercase = white (K/Q/R/B/N/P), lowercase = black (k/q/r/b/n/p).\nconst pieces = {\n  e1: \"K\",\n  a1: \"R\",\n  h1: \"R\",\n  c1: \"B\",\n  c4: \"B\",\n  b1: \"N\",\n  g1: \"N\",\n  f7: \"Q\",\n  a2: \"P\",\n  b2: \"P\",\n  c2: \"P\",\n  d2: \"P\",\n  e4: \"P\",\n  f2: \"P\",\n  g2: \"P\",\n  h2: \"P\",\n  e8: \"k\",\n  d8: \"q\",\n  a8: \"r\",\n  h8: \"r\",\n  c8: \"b\",\n  f8: \"b\",\n  c6: \"n\",\n  f6: \"n\",\n  a7: \"p\",\n  b7: \"p\",\n  c7: \"p\",\n  d7: \"p\",\n  e5: \"p\",\n  g7: \"p\",\n  h7: \"p\",\n};\n\nconst GLYPHS = {\n  K: \"♔\",\n  Q: \"♕\",\n  R: \"♖\",\n  B: \"♗\",\n  N: \"♘\",\n  P: \"♙\",\n  k: \"♚\",\n  q: \"♛\",\n  r: \"♜\",\n  b: \"♝\",\n  n: \"♞\",\n  p: \"♟\",\n};\n\n// Piece identity (white/black) is fixed across themes, like the game itself —\n// only the board and chrome follow ANYPLOT_THEME.\nconst PIECE_LIGHT = \"#F2ECDA\";\nconst PIECE_DARK = \"#2A2822\";\n\n// --- Layout -------------------------------------------------------------\nconst margin = { top: 130, right: 60, bottom: 90, left: 90 };\nconst availW = width - margin.left - margin.right;\nconst availH = height - margin.top - margin.bottom;\nconst boardSize = Math.min(availW, availH);\nconst cell = boardSize / 8;\nconst boardX = margin.left + (availW - boardSize) / 2;\nconst boardY = margin.top + (availH - boardSize) / 2;\n\nconst files = [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\"];\n\n// --- SVG mount ------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\nconst board = svg.append(\"g\").attr(\"transform\", `translate(${boardX},${boardY})`);\n\n// Column/row -> pixel placement via d3-scale bands (rank 8 at the top, rank 1\n// at the bottom — white-at-bottom board orientation).\nconst xScale = d3.scaleBand().domain(d3.range(8)).range([0, boardSize]);\nconst yScale = d3.scaleBand().domain(d3.range(8)).range([boardSize, 0]);\n\n// --- Squares (light square on h1, per standard chess convention) ----------\n// Explicit per-theme board tones (not a pageBg/ink blend) so the light square\n// is always the higher-luminance one in both themes. The previous\n// `d3.interpolateRgb(t.pageBg, t.ink)` approach inverted in dark mode because\n// `t.ink` flips from dark to light between themes, flipping the blend\n// direction along with it.\nconst boardTones =\n  t.theme === \"dark\" ? { light: \"#403F38\", dark: t.elevatedBg } : { light: t.elevatedBg, dark: \"#C9C7C1\" };\nconst squares = [];\nfor (let row = 0; row < 8; row++) {\n  for (let col = 0; col < 8; col++) {\n    squares.push({ col, row, light: (col + row) % 2 === 1 });\n  }\n}\n\nboard\n  .selectAll(\"rect.square\")\n  .data(squares)\n  .join(\"rect\")\n  .attr(\"class\", \"square\")\n  .attr(\"x\", (d) => xScale(d.col))\n  .attr(\"y\", (d) => yScale(d.row))\n  .attr(\"width\", xScale.bandwidth())\n  .attr(\"height\", yScale.bandwidth())\n  .attr(\"fill\", (d) => (d.light ? boardTones.light : boardTones.dark));\n\nboard\n  .append(\"rect\")\n  .attr(\"x\", 0)\n  .attr(\"y\", 0)\n  .attr(\"width\", boardSize)\n  .attr(\"height\", boardSize)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 2);\n\n// --- Pieces (white glyphs get a light fill + dark stroke, black glyphs the\n// reverse, so identity reads clearly on either square color or theme) ------\nconst pieceData = Object.entries(pieces).map(([square, code]) => {\n  const col = files.indexOf(square[0]);\n  const row = Number(square[1]) - 1;\n  return { square, code, col, row, isWhite: code === code.toUpperCase() };\n});\n\nboard\n  .selectAll(\"text.piece\")\n  .data(pieceData)\n  .join(\"text\")\n  .attr(\"class\", \"piece\")\n  .attr(\"x\", (d) => xScale(d.col) + xScale.bandwidth() / 2)\n  .attr(\"y\", (d) => yScale(d.row) + yScale.bandwidth() / 2)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"central\")\n  .style(\"font-family\", \"'DejaVu Sans', sans-serif\")\n  .style(\"font-size\", `${cell * 0.72}px`)\n  .style(\"paint-order\", \"stroke fill\")\n  .attr(\"fill\", (d) => (d.isWhite ? PIECE_LIGHT : PIECE_DARK))\n  .attr(\"stroke\", (d) => (d.isWhite ? PIECE_DARK : PIECE_LIGHT))\n  .attr(\"stroke-width\", (d) => (d.isWhite ? cell * 0.02 : cell * 0.015))\n  .text((d) => GLYPHS[d.code]);\n\n// --- Coordinate labels (files below, ranks along the left) ----------------\nboard\n  .selectAll(\"text.file-label\")\n  .data(files)\n  .join(\"text\")\n  .attr(\"class\", \"file-label\")\n  .attr(\"x\", (_, i) => xScale(i) + xScale.bandwidth() / 2)\n  .attr(\"y\", boardSize + 34)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"20px\")\n  .text((f) => f);\n\nboard\n  .selectAll(\"text.rank-label\")\n  .data(d3.range(1, 9))\n  .join(\"text\")\n  .attr(\"class\", \"rank-label\")\n  .attr(\"x\", -26)\n  .attr(\"y\", (r) => yScale(r - 1) + yScale.bandwidth() / 2)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"central\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"20px\")\n  .text((r) => r);\n\n// --- Title ------------------------------------------------------------------\nconst title = \"Scholar's Mate, Move 4 · chessboard-pieces · javascript · d3 · anyplot.ai\";\nconst titleRatio = title.length > 67 ? 67 / title.length : 1.0;\nconst titleSize = Math.max(16, Math.round(22 * titleRatio));\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${titleSize}px`)\n  .style(\"font-weight\", \"600\")\n  .text(title);\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 88)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"18px\")\n  .text(\"White delivers checkmate: 1.e4 e5 2.Bc4 Nc6 3.Qh5 Nf6?? 4.Qxf7#\");\n"}