Basic Waffle Chart — MUI X Charts

A waffle chart displays proportions using a grid of equal-sized squares where colored squares represent parts of a whole. Each square typically represents 1% of the total, making it easy to count and compare values visually. It provides an intuitive alternative to pie charts, offering more accurate perception of proportions.

Basic Waffle Chart rendered with MUI X Charts

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// waffle-basic: Basic Waffle Chart
// Library: muix 7.29.1 | JavaScript 22.23.2
// Quality: 89/100 | Created: 2026-09-09
//# anyplot-orientation: square

import { ChartContainer } from "@mui/x-charts/ChartContainer";
import { ScatterPlot } from "@mui/x-charts/ScatterChart";
import { ChartsLegend } from "@mui/x-charts/ChartsLegend";

const t = window.ANYPLOT_TOKENS;
const FONT = "system-ui, -apple-system, sans-serif";
const TITLE = "waffle-basic · javascript · muix · anyplot.ai";

// Imprint's theme-adaptive "muted" anchor (other/rest) isn't part of
// ANYPLOT_TOKENS, so it's derived here the same way the style guide defines it.
const MUTED = window.ANYPLOT_THEME === "dark" ? "#A8A79F" : "#6B6A63";

// --- Data (in-memory, deterministic) ----------------------------------------
// Annual engineering-org budget allocation by department. Values sum to 100 —
// one grid square per percentage point, the canonical waffle-chart mapping.
// "Other" gets the theme-adaptive muted anchor (other/rest semantic exception)
// instead of the next ordinal palette slot.
const CATEGORIES = [
  { label: "Engineering", pct: 34, color: t.palette[0] },
  { label: "Sales", pct: 21, color: t.palette[1] },
  { label: "Marketing", pct: 17, color: t.palette[2] },
  { label: "Operations", pct: 16, color: t.palette[3] },
  { label: "Other", pct: 12, color: MUTED },
];

const GRID_SIZE = 10; // 10x10 = 100 squares, one per percentage point
const COLS = Array.from({ length: GRID_SIZE }, (_, i) => i);
const ROWS = Array.from({ length: GRID_SIZE }, (_, i) => i);

// Fill the grid row-major, left-to-right then top-to-bottom, handing each
// category a contiguous block of squares in the order above.
let cellIndex = 0;
const series = CATEGORIES.map((category) => {
  const data = [];
  for (let i = 0; i < category.pct; i += 1) {
    data.push({
      id: `cell-${cellIndex}`,
      x: cellIndex % GRID_SIZE,
      y: Math.floor(cellIndex / GRID_SIZE),
    });
    cellIndex += 1;
  }
  return {
    type: "scatter",
    id: category.label.toLowerCase(),
    data,
    label: `${category.label} — ${category.pct}%`,
  };
});
const COLORS = CATEGORIES.map((category) => category.color);

// Custom scatter mark: opaque squares tiling the band grid, colored per series
// rather than by a continuous data value — this is what turns MUI X's
// ScatterPlot into a waffle/unit-chart grid instead of a scatter of dots.
function WaffleSquares({ series: seriesData, xScale, yScale, color }) {
  const cellW = xScale.bandwidth();
  const cellH = yScale.bandwidth();
  const radius = Math.min(cellW, cellH) * 0.15;
  return (
    <g>
      {seriesData.data.map((pt) => (
        <rect
          key={pt.id}
          x={xScale(pt.x) ?? 0}
          y={yScale(pt.y) ?? 0}
          width={cellW}
          height={cellH}
          rx={radius}
          fill={color}
        />
      ))}
    </g>
  );
}

export default function Chart() {
  const W = window.ANYPLOT_SIZE.width;
  const H = window.ANYPLOT_SIZE.height;

  // Reserve width for the legend (right), then size the waffle grid to the
  // largest square that fits — width is the binding constraint since the
  // legend column eats more width than the title eats height — so every one
  // of the 100 cells is a true square.
  const TITLE_SPACE = 80; // minimum clearance the grid must leave below the title
  const PAD = 28;
  const LEGEND_WIDTH = 320;

  const availW = W - PAD * 2 - LEGEND_WIDTH;
  const availH = H - PAD * 2;
  const grid = Math.min(availW, availH);

  // Center the grid+legend block vertically in the full canvas (instead of
  // top-aligning it against the title) so leftover height doesn't collect as
  // dead space below. Clamp so the grid never rises above the title's
  // reserved band.
  const vPad = Math.max(TITLE_SPACE, (H - grid) / 2);
  const margin = {
    left: PAD + (availW - grid) / 2,
    right: W - PAD - (availW - grid) / 2 - grid,
    top: vPad,
    bottom: H - vPad - grid,
  };
  const gridCenterY = margin.top + grid / 2;

  return (
    <ChartContainer
      width={W}
      height={H}
      margin={margin}
      skipAnimation
      colors={COLORS}
      series={series}
      xAxis={[{ scaleType: "band", data: COLS, categoryGapRatio: 0.18 }]}
      yAxis={[{ scaleType: "band", data: ROWS, categoryGapRatio: 0.18 }]}
    >
      {/* Chart title */}
      <text
        x={W / 2}
        y={44}
        textAnchor="middle"
        fontSize={22}
        fontWeight="500"
        fill={t.ink}
        fontFamily={FONT}
      >
        {TITLE}
      </text>

      {/* The waffle grid — 100 squares, one per percentage point */}
      <ScatterPlot slots={{ scatter: WaffleSquares }} />

      <ChartsLegend
        position={{ horizontal: "right", vertical: "middle" }}
        direction="column"
        itemMarkWidth={26}
        itemMarkHeight={26}
        markGap={14}
        itemGap={22}
        // "middle" centers within [padding.top, H - padding.bottom] — bias the
        // padding so that band's midpoint lands on the grid's true vertical
        // center (which sits below the title, not the full canvas center).
        padding={{ top: 0, right: 24, bottom: Math.max(0, H - 2 * gridCenterY), left: 0 }}
        labelStyle={{ fontSize: 20, fill: t.ink, fontFamily: FONT }}
      />
    </ChartContainer>
  );
}

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/waffle-basic/muix/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "waffle-basic",
  "language": "javascript",
  "library": "muix",
  "page": "https://anyplot.ai/waffle-basic/javascript/muix",
  "hub": "https://anyplot.ai/waffle-basic",
  "code_json": "https://api.anyplot.ai/specs/waffle-basic/muix/code",
  "spec_json": "https://api.anyplot.ai/specs/waffle-basic",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/waffle-basic/javascript/muix/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/waffle-basic/javascript/muix/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/waffle-basic/javascript/muix/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/waffle-basic/javascript/muix/plot-dark.html",
  "quality_score": 89.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Basic Waffle Chart on anyplot.ai.

Other implementations