{"spec_id":"waffle-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// waffle-basic: Basic Waffle Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 85/100 | Created: 2026-09-09\n\n//# anyplot-orientation: square\n\n// Chart.js has no native waffle/matrix chart type (chartjs-chart-matrix is an\n// unpinned community plugin and out of scope). We draw the 10x10 grid and its\n// legend ourselves in a plugin's `afterDraw` hook, anchored to `chart.chartArea`\n// so the built-in title plugin still reserves its own space above the grid.\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) — quarterly marketing budget, % of total\nconst categories = [\n  { label: \"Digital ads\", value: 32 },\n  { label: \"Content & SEO\", value: 24 },\n  { label: \"Events\", value: 18 },\n  { label: \"Sponsorships\", value: 14 },\n  { label: \"Email & CRM\", value: 8 },\n  { label: \"Other\", value: 4 },\n];\n// Values sum to 100 -> one square per percentage point, no rounding drift.\ncategories.forEach((c, i) => {\n  c.color = t.palette[i % t.palette.length];\n});\n\nconst GRID_SIDE = 10; // 10x10 = 100 squares, each worth 1%\nconst cells = categories.flatMap((c, i) => Array(c.value).fill(i));\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Waffle grid + legend plugin ---------------------------------------------\nconst wafflePlugin = {\n  id: \"wafflePlugin\",\n  afterDraw(chart) {\n    const { ctx, chartArea } = chart;\n    const areaW = chartArea.right - chartArea.left;\n    const areaH = chartArea.bottom - chartArea.top;\n\n    const legendW = Math.min(areaW * 0.34, 460);\n    const gutter = areaW * 0.05;\n    const gridBoxW = areaW - legendW - gutter;\n    const gridSize = Math.min(gridBoxW, areaH);\n    const cell = gridSize / GRID_SIDE;\n    const gap = cell * 0.08;\n\n    const gridLeft = chartArea.left + (gridBoxW - gridSize) / 2;\n    const gridTop = chartArea.top + (areaH - gridSize) / 2;\n\n    ctx.save();\n    cells.forEach((catIdx, i) => {\n      const row = Math.floor(i / GRID_SIDE);\n      const col = i % GRID_SIDE;\n      const x = gridLeft + col * cell;\n      const y = gridTop + row * cell;\n      ctx.fillStyle = categories[catIdx].color;\n      ctx.fillRect(x + gap / 2, y + gap / 2, cell - gap, cell - gap);\n    });\n\n    // --- Legend: swatch + label + percentage, vertically centered ------------\n    const legendX = gridLeft + gridSize + gutter;\n    const rowH = Math.min(areaH / categories.length, cell * 1.3);\n    const legendH = rowH * categories.length;\n    const legendTop = chartArea.top + (areaH - legendH) / 2;\n    const swatch = rowH * 0.42;\n\n    ctx.textBaseline = \"middle\";\n    categories.forEach((c, idx) => {\n      const y = legendTop + idx * rowH + rowH / 2;\n      ctx.fillStyle = c.color;\n      ctx.fillRect(legendX, y - swatch / 2, swatch, swatch);\n      // Largest category (first, since data is sorted descending) gets a\n      // heavier weight + slightly larger size to anchor the reading order.\n      const isFocal = idx === 0;\n      ctx.font = `${isFocal ? 700 : 500} ${Math.round(swatch * (isFocal ? 0.68 : 0.6))}px sans-serif`;\n      ctx.fillStyle = t.ink;\n      ctx.fillText(`${c.label} — ${c.value}%`, legendX + swatch * 1.4, y);\n    });\n    ctx.restore();\n  },\n};\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"bar\",\n  data: { labels: [], datasets: [] },\n  plugins: [wafflePlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: 16 },\n    plugins: {\n      title: {\n        display: true,\n        text: \"waffle-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { bottom: 24 },\n      },\n      legend: { display: false },\n      tooltip: { enabled: false },\n    },\n    // The dummy bar-chart host would otherwise auto-generate default x/y\n    // cartesian scales (even with empty labels/datasets), whose reserved\n    // tick space shrinks chart.chartArea and confines the plugin's drawing\n    // to the upper portion of the canvas. Hiding both scales lets\n    // chartArea span the full area below the title.\n    scales: {\n      x: { display: false },\n      y: { display: false },\n    },\n  },\n});\n"}