{"spec_id":"chessboard-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// chessboard-basic: Chess Board Grid Visualization\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-09-01\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Standard 8x8 board: files a-h left-to-right, ranks 1-8 bottom-to-top.\n// Light squares fall on h1 and a8 (file-index + rank-index is odd).\nconst FILES = [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\"];\nconst RANKS = [\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\"];\n\nconst squares = [];\nfor (let r = 0; r < 8; r++) {\n  for (let c = 0; c < 8; c++) {\n    squares.push({ x: c, y: r, file: FILES[c], rank: RANKS[r], isLight: (c + r) % 2 === 1 });\n  }\n}\n\n// Semantic exception (default-style-guide.md): a chess board is a real-world\n// object with a strong cream/wood color expectation, so dark squares use the\n// Imprint ochre hue (earth/wood) instead of the canonical position-2 lavender,\n// and light squares use the elevated-surface token rather than a custom hex.\nconst LIGHT_SQUARE = t.elevatedBg;\nconst DARK_SQUARE = t.palette[3]; // #BD8233 ochre — wood association\n\n// --- Square-aspect layout plugin --------------------------------------------\n// The title block eats vertical space the axis labels don't eat horizontally,\n// so the raw chart area isn't square. Shrink it to the largest centered\n// square before the scatter points (and their pixel-derived radius) are laid\n// out, then draw a crisp outer frame so the board reads as one solid block\n// even where a corner square's fill is close in value to the page background.\nconst squareBoard = {\n  id: \"squareBoard\",\n  afterLayout(chart) {\n    const { x, y } = chart.scales;\n    if (!x || !y) return;\n    const width = x.right - x.left;\n    const height = y.bottom - y.top;\n    const side = Math.min(width, height);\n    x.left += (width - side) / 2;\n    x.right = x.left + side;\n    y.top += (height - side) / 2;\n    y.bottom = y.top + side;\n    chart.chartArea.left = x.left;\n    chart.chartArea.right = x.right;\n    chart.chartArea.top = y.top;\n    chart.chartArea.bottom = y.bottom;\n    // LinearScale caches _startPixel/_length in configure() during layout —\n    // it must re-run for the shrunk box to affect pixel conversion.\n    x.configure();\n    y.configure();\n  },\n  afterDatasetsDraw(chart) {\n    const { ctx, chartArea: area } = chart;\n    ctx.save();\n    ctx.strokeStyle = t.inkSoft;\n    ctx.lineWidth = 2.5;\n    ctx.strokeRect(area.left, area.top, area.right - area.left, area.bottom - area.top);\n    ctx.restore();\n  },\n};\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart -------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  plugins: [squareBoard],\n  data: {\n    datasets: [\n      {\n        data: squares,\n        showLine: false,\n        pointStyle: \"rect\",\n        pointBackgroundColor: (ctx) => (ctx.raw.isLight ? LIGHT_SQUARE : DARK_SQUARE),\n        pointBorderColor: t.grid,\n        pointBorderWidth: 1.5,\n        pointRadius: (ctx) => {\n          const { x, y } = ctx.chart.scales;\n          if (!x || !y) return 8;\n          const pxPerCol = Math.abs(x.getPixelForValue(1) - x.getPixelForValue(0));\n          const pxPerRow = Math.abs(y.getPixelForValue(1) - y.getPixelForValue(0));\n          return Math.min(pxPerCol, pxPerRow) / 2 + 0.5; // slight overlap hides seams\n        },\n        pointHoverBackgroundColor: (ctx) => (ctx.raw.isLight ? LIGHT_SQUARE : DARK_SQUARE),\n        pointHoverBorderColor: t.ink,\n        pointHoverBorderWidth: 2,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 8, right: 24, bottom: 8, left: 8 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"chessboard-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 26, weight: \"500\" },\n        padding: { bottom: 20 },\n      },\n      legend: { display: false },\n      tooltip: {\n        callbacks: {\n          title: (items) => `${items[0].raw.file}${items[0].raw.rank}`,\n          label: (item) => (item.raw.isLight ? \"Light square\" : \"Dark square\"),\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: -0.5,\n        max: 7.5,\n        afterBuildTicks: (axis) => {\n          axis.ticks = FILES.map((_, i) => ({ value: i }));\n        },\n        ticks: { callback: (v) => FILES[v] ?? \"\", color: t.inkSoft, font: { size: 16 } },\n        grid: { display: false },\n        border: { display: false },\n      },\n      y: {\n        type: \"linear\",\n        min: -0.5,\n        max: 7.5,\n        afterBuildTicks: (axis) => {\n          axis.ticks = RANKS.map((_, i) => ({ value: i }));\n        },\n        ticks: { callback: (v) => RANKS[v] ?? \"\", color: t.inkSoft, font: { size: 16 } },\n        grid: { display: false },\n        border: { display: false },\n      },\n    },\n  },\n});\n"}