Crossword Puzzle Grid — lets-plot

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 lets-plot

Python source (lets-plot)

""" anyplot.ai
crossword-basic: Crossword Puzzle Grid
Library: letsplot 4.9.0 | Python 3.13.13
Quality: 85/100 | Updated: 2026-05-20
"""

import os

import numpy as np
import pandas as pd
from lets_plot import *


LetsPlot.setup_html()

# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"

# Data — 15x15 crossword grid with 180-degree rotational symmetry
grid_size = 15
grid = np.zeros((grid_size, grid_size), dtype=int)

black_positions = [
    (0, 4),
    (0, 10),
    (1, 4),
    (1, 10),
    (2, 7),
    (3, 0),
    (3, 1),
    (3, 6),
    (3, 11),
    (3, 12),
    (4, 5),
    (4, 9),
    (5, 3),
    (5, 8),
    (5, 13),
    (5, 14),
    (6, 2),
    (6, 7),
    (6, 12),
    (7, 6),
    (7, 8),
]

for r, c in black_positions:
    grid[r, c] = 1
    grid[grid_size - 1 - r, grid_size - 1 - c] = 1

# Generate clue numbers
numbers = {}
clue_num = 1
for r in range(grid_size):
    for c in range(grid_size):
        if grid[r, c] == 1:
            continue
        starts_across = (c == 0 or grid[r, c - 1] == 1) and (c < grid_size - 1 and grid[r, c + 1] == 0)
        starts_down = (r == 0 or grid[r - 1, c] == 1) and (r < grid_size - 1 and grid[r + 1, c] == 0)
        if starts_across or starts_down:
            numbers[(r, c)] = clue_num
            clue_num += 1

# Prepare cell data
cells_data = []
for r in range(grid_size):
    for c in range(grid_size):
        cell_type = "Blocked" if grid[r, c] == 1 else "Entry"
        cells_data.append({"x": c, "y": grid_size - 1 - r, "blocked": grid[r, c], "cell_type": cell_type})

df_cells = pd.DataFrame(cells_data)

# Prepare number labels with tooltip info
number_labels = []
for (r, c), num in numbers.items():
    direction_parts = []
    starts_across = (c == 0 or grid[r, c - 1] == 1) and (c < grid_size - 1 and grid[r, c + 1] == 0)
    starts_down = (r == 0 or grid[r - 1, c] == 1) and (r < grid_size - 1 and grid[r + 1, c] == 0)
    if starts_across:
        direction_parts.append("Across")
    if starts_down:
        direction_parts.append("Down")
    number_labels.append(
        {
            "x": c + 0.05,
            "y": grid_size - 1 - r + 0.35,
            "label": str(num),
            "clue": f"Clue {num}: {' / '.join(direction_parts)}",
        }
    )

df_numbers = pd.DataFrame(number_labels)

# Plot
plot = (
    ggplot()
    + geom_tile(
        data=df_cells[df_cells["blocked"] == 0],
        mapping=aes(x="x", y="y"),
        fill="white",
        color="black",
        size=0.8,
        width=0.98,
        height=0.98,
        tooltips=layer_tooltips().line("@cell_type cell"),
    )
    + geom_tile(
        data=df_cells[df_cells["blocked"] == 1],
        mapping=aes(x="x", y="y"),
        fill="black",
        color="black",
        size=0.8,
        width=0.98,
        height=0.98,
        tooltips=layer_tooltips().line("@cell_type cell"),
    )
    + geom_text(
        data=df_numbers,
        mapping=aes(x="x", y="y", label="label"),
        size=8,
        color="#1A1A17",
        hjust=0,
        vjust=1,
        fontface="bold",
        tooltips=layer_tooltips().line("@clue"),
    )
    + coord_fixed(ratio=1)
    + labs(title="crossword-basic · python · letsplot · anyplot.ai")
    + theme_void()
    + theme(
        plot_title=element_text(size=16, hjust=0.5, face="bold", color=INK),
        plot_background=element_rect(fill=PAGE_BG),
        panel_background=element_rect(fill=PAGE_BG),
    )
    + ggsize(600, 600)
)

# Save
ggsave(plot, f"plot-{THEME}.png", path=".", scale=4)
ggsave(plot, f"plot-{THEME}.html", path=".")

Part of Crossword Puzzle Grid on anyplot.ai.

Other implementations