{"spec_id":"waffle-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// waffle-basic: Basic Waffle Chart\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-09\n//# anyplot-orientation: square\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsLegend } from \"@mui/x-charts/ChartsLegend\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst FONT = \"system-ui, -apple-system, sans-serif\";\nconst TITLE = \"waffle-basic · javascript · muix · anyplot.ai\";\n\n// Imprint's theme-adaptive \"muted\" anchor (other/rest) isn't part of\n// ANYPLOT_TOKENS, so it's derived here the same way the style guide defines it.\nconst MUTED = window.ANYPLOT_THEME === \"dark\" ? \"#A8A79F\" : \"#6B6A63\";\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Annual engineering-org budget allocation by department. Values sum to 100 —\n// one grid square per percentage point, the canonical waffle-chart mapping.\n// \"Other\" gets the theme-adaptive muted anchor (other/rest semantic exception)\n// instead of the next ordinal palette slot.\nconst CATEGORIES = [\n  { label: \"Engineering\", pct: 34, color: t.palette[0] },\n  { label: \"Sales\", pct: 21, color: t.palette[1] },\n  { label: \"Marketing\", pct: 17, color: t.palette[2] },\n  { label: \"Operations\", pct: 16, color: t.palette[3] },\n  { label: \"Other\", pct: 12, color: MUTED },\n];\n\nconst GRID_SIZE = 10; // 10x10 = 100 squares, one per percentage point\nconst COLS = Array.from({ length: GRID_SIZE }, (_, i) => i);\nconst ROWS = Array.from({ length: GRID_SIZE }, (_, i) => i);\n\n// Fill the grid row-major, left-to-right then top-to-bottom, handing each\n// category a contiguous block of squares in the order above.\nlet cellIndex = 0;\nconst series = CATEGORIES.map((category) => {\n  const data = [];\n  for (let i = 0; i < category.pct; i += 1) {\n    data.push({\n      id: `cell-${cellIndex}`,\n      x: cellIndex % GRID_SIZE,\n      y: Math.floor(cellIndex / GRID_SIZE),\n    });\n    cellIndex += 1;\n  }\n  return {\n    type: \"scatter\",\n    id: category.label.toLowerCase(),\n    data,\n    label: `${category.label} — ${category.pct}%`,\n  };\n});\nconst COLORS = CATEGORIES.map((category) => category.color);\n\n// Custom scatter mark: opaque squares tiling the band grid, colored per series\n// rather than by a continuous data value — this is what turns MUI X's\n// ScatterPlot into a waffle/unit-chart grid instead of a scatter of dots.\nfunction WaffleSquares({ series: seriesData, xScale, yScale, color }) {\n  const cellW = xScale.bandwidth();\n  const cellH = yScale.bandwidth();\n  const radius = Math.min(cellW, cellH) * 0.15;\n  return (\n    <g>\n      {seriesData.data.map((pt) => (\n        <rect\n          key={pt.id}\n          x={xScale(pt.x) ?? 0}\n          y={yScale(pt.y) ?? 0}\n          width={cellW}\n          height={cellH}\n          rx={radius}\n          fill={color}\n        />\n      ))}\n    </g>\n  );\n}\n\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n\n  // Reserve width for the legend (right), then size the waffle grid to the\n  // largest square that fits — width is the binding constraint since the\n  // legend column eats more width than the title eats height — so every one\n  // of the 100 cells is a true square.\n  const TITLE_SPACE = 80; // minimum clearance the grid must leave below the title\n  const PAD = 28;\n  const LEGEND_WIDTH = 320;\n\n  const availW = W - PAD * 2 - LEGEND_WIDTH;\n  const availH = H - PAD * 2;\n  const grid = Math.min(availW, availH);\n\n  // Center the grid+legend block vertically in the full canvas (instead of\n  // top-aligning it against the title) so leftover height doesn't collect as\n  // dead space below. Clamp so the grid never rises above the title's\n  // reserved band.\n  const vPad = Math.max(TITLE_SPACE, (H - grid) / 2);\n  const margin = {\n    left: PAD + (availW - grid) / 2,\n    right: W - PAD - (availW - grid) / 2 - grid,\n    top: vPad,\n    bottom: H - vPad - grid,\n  };\n  const gridCenterY = margin.top + grid / 2;\n\n  return (\n    <ChartContainer\n      width={W}\n      height={H}\n      margin={margin}\n      skipAnimation\n      colors={COLORS}\n      series={series}\n      xAxis={[{ scaleType: \"band\", data: COLS, categoryGapRatio: 0.18 }]}\n      yAxis={[{ scaleType: \"band\", data: ROWS, categoryGapRatio: 0.18 }]}\n    >\n      {/* Chart title */}\n      <text\n        x={W / 2}\n        y={44}\n        textAnchor=\"middle\"\n        fontSize={22}\n        fontWeight=\"500\"\n        fill={t.ink}\n        fontFamily={FONT}\n      >\n        {TITLE}\n      </text>\n\n      {/* The waffle grid — 100 squares, one per percentage point */}\n      <ScatterPlot slots={{ scatter: WaffleSquares }} />\n\n      <ChartsLegend\n        position={{ horizontal: \"right\", vertical: \"middle\" }}\n        direction=\"column\"\n        itemMarkWidth={26}\n        itemMarkHeight={26}\n        markGap={14}\n        itemGap={22}\n        // \"middle\" centers within [padding.top, H - padding.bottom] — bias the\n        // padding so that band's midpoint lands on the grid's true vertical\n        // center (which sits below the title, not the full canvas center).\n        padding={{ top: 0, right: 24, bottom: Math.max(0, H - 2 * gridCenterY), left: 0 }}\n        labelStyle={{ fontSize: 20, fill: t.ink, fontFamily: FONT }}\n      />\n    </ChartContainer>\n  );\n}\n"}