Crossword Puzzle Grid — Apache ECharts

A crossword puzzle grid visualization with white entry cells for letters, black blocking cells, and numbered starting positions for across and down words. The symmetric black cell pattern follows traditional newspaper-style crossword conventions, creating a clean and recognizable puzzle layout suitable for printing or digital display.

Crossword Puzzle Grid rendered with Apache ECharts

Renders

JavaScript source (Apache ECharts)

// anyplot.ai
// crossword-basic: Crossword Puzzle Grid
// Library: echarts 6.1.0 | JavaScript 22.23.2
// Quality: 84/100 | Created: 2026-09-02

//# anyplot-orientation: square

const t = window.ANYPLOT_TOKENS;
const mount = window.ANYPLOT_SIZE || { width: 1200, height: 1200 };

// --- Data: 15x15 grid with 180-degree rotational symmetry -------------------
// 0 = white entry cell, 1 = black blocking cell.
// Only the top 7 rows + the palindromic middle row are authored by hand; the
// bottom 7 rows are derived by rotating the top half 180 degrees so the
// traditional newspaper-style symmetry is guaranteed by construction.
const gridSize = 15;
const topHalf = [
  [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
  [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
  [1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1],
  [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
  [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
  [1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1],
  [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
];
const middleRow = [0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0];

const grid = [];
for (let row = 0; row < gridSize; row += 1) {
  if (row < topHalf.length) {
    grid.push(topHalf[row].slice());
  } else if (row === Math.floor(gridSize / 2)) {
    grid.push(middleRow.slice());
  } else {
    grid.push(topHalf[gridSize - 1 - row].slice().reverse());
  }
}

// --- Numbering: standard across/down word-start scan -------------------------
const cells = [];
let nextNumber = 1;
for (let row = 0; row < gridSize; row += 1) {
  for (let col = 0; col < gridSize; col += 1) {
    const blocked = grid[row][col] === 1;
    let number = null;
    if (!blocked) {
      const startsAcross =
        (col === 0 || grid[row][col - 1] === 1) &&
        col < gridSize - 1 &&
        grid[row][col + 1] === 0;
      const startsDown =
        (row === 0 || grid[row - 1][col] === 1) &&
        row < gridSize - 1 &&
        grid[row + 1][col] === 0;
      if (startsAcross || startsDown) {
        number = nextNumber;
        nextNumber += 1;
      }
    }
    cells.push({ col, row, blocked, number });
  }
}

// --- Square plotting area, regardless of exact mount pixel size -------------
const titleSpace = 130;
const margin = 60;
const available = Math.min(
  mount.width - margin * 2,
  mount.height - titleSpace - margin
);
const gridLeft = (mount.width - available) / 2;
const gridTop = titleSpace + (mount.height - titleSpace - margin - available) / 2;

// The grid itself is content (like a heatmap's cell colors), not UI chrome —
// it reads as a printed puzzle card, so its paper/ink tones stay fixed
// instead of flipping with ANYPLOT_THEME (which would invert the black/white
// cell metaphor and crush entry-cell contrast against a dark page).
const entryFill = "#FFFDF6";
const blockedFill = "#1A1A17";
const cellStroke = "#4A4A44";
const outerFrameStroke = "#1A1A17";
const numberFill = "#1A1A17";

// --- Init ---------------------------------------------------------------
const chart = echarts.init(document.getElementById("container"));

// --- Option ---------------------------------------------------------------
chart.setOption({
  animation: false,
  backgroundColor: "transparent",
  title: {
    text: "crossword-basic · javascript · echarts · anyplot.ai",
    left: "center",
    top: 40,
    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },
  },
  grid: { left: gridLeft, top: gridTop, width: available, height: available },
  xAxis: {
    type: "value",
    min: -0.5,
    max: gridSize - 0.5,
    show: false,
  },
  yAxis: {
    type: "value",
    min: -0.5,
    max: gridSize - 0.5,
    inverse: true,
    show: false,
  },
  series: [
    {
      type: "custom",
      coordinateSystem: "cartesian2d",
      data: cells.map((cell) => [cell.col, cell.row]),
      encode: { x: 0, y: 1 },
      renderItem: (params, api) => {
        const cell = cells[params.dataIndex];
        const topLeft = api.coord([cell.col - 0.5, cell.row - 0.5]);
        const size = api.size([1, 1]);
        const children = [
          {
            type: "rect",
            shape: { x: topLeft[0], y: topLeft[1], width: size[0], height: size[1] },
            style: {
              fill: cell.blocked ? blockedFill : entryFill,
              stroke: cellStroke,
              lineWidth: 1.5,
            },
          },
        ];
        if (cell.number !== null) {
          children.push({
            type: "text",
            style: {
              text: String(cell.number),
              x: topLeft[0] + 6,
              y: topLeft[1] + 4,
              fill: numberFill,
              fontSize: 15,
              fontWeight: 600,
              textVerticalAlign: "top",
              textAlign: "left",
            },
          });
        }
        return { type: "group", children };
      },
    },
  ],
  // Bolder outer frame vs. the thinner inner dividers gives the printed card a
  // typographic hierarchy instead of one uniform line weight throughout.
  graphic: [
    {
      type: "rect",
      shape: { x: gridLeft, y: gridTop, width: available, height: available },
      style: { fill: "none", stroke: outerFrameStroke, lineWidth: 5 },
      z: 100,
      silent: true,
    },
  ],
});

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/crossword-basic/echarts/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": "crossword-basic",
  "language": "javascript",
  "library": "echarts",
  "page": "https://anyplot.ai/crossword-basic/javascript/echarts",
  "hub": "https://anyplot.ai/crossword-basic",
  "code_json": "https://api.anyplot.ai/specs/crossword-basic/echarts/code",
  "spec_json": "https://api.anyplot.ai/specs/crossword-basic",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/crossword-basic/javascript/echarts/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/crossword-basic/javascript/echarts/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/crossword-basic/javascript/echarts/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/crossword-basic/javascript/echarts/plot-dark.html",
  "quality_score": 84.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Crossword Puzzle Grid on anyplot.ai.

Other implementations