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.

""" anyplot.ai
crossword-basic: Crossword Puzzle Grid
Library: plotnine 0.15.4 | Python 3.13.13
Quality: 88/100 | Updated: 2026-05-20
"""
import os
import numpy as np
import pandas as pd
from plotnine import (
aes,
coord_fixed,
element_blank,
element_rect,
element_text,
geom_text,
geom_tile,
ggplot,
labs,
scale_fill_manual,
scale_x_continuous,
scale_y_continuous,
theme,
)
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
# Create 15x15 crossword grid with 180-degree rotational symmetry
size = 15
grid = np.zeros((size, size), dtype=int)
black_positions = [
(0, 4),
(0, 10),
(1, 4),
(1, 10),
(2, 4),
(2, 7),
(2, 10),
(3, 0),
(3, 7),
(3, 11),
(3, 12),
(3, 13),
(3, 14),
(4, 0),
(4, 1),
(4, 5),
(4, 6),
(4, 9),
(5, 5),
(5, 11),
(6, 2),
(6, 3),
(6, 8),
(6, 13),
(6, 14),
(7, 7),
]
for r, c in black_positions:
grid[r, c] = 1
grid[size - 1 - r, size - 1 - c] = 1
# Build tile DataFrame
data = []
for r in range(size):
for c in range(size):
data.append({"col": c + 1, "row": size - r, "cell_type": "blocked" if grid[r, c] == 1 else "entry"})
df = pd.DataFrame(data)
# Determine word start positions (cells beginning an across or down word)
word_starts = {}
num = 1
for r in range(size):
for c in range(size):
if grid[r, c] == 1:
continue
starts_across = (c == 0 or grid[r, c - 1] == 1) and c < size - 1 and grid[r, c + 1] == 0
starts_down = (r == 0 or grid[r - 1, c] == 1) and r < size - 1 and grid[r + 1, c] == 0
if starts_across or starts_down:
word_starts[(r, c)] = num
num += 1
number_data = []
for (r, c), clue_num in word_starts.items():
number_data.append({"col": c + 1, "row": size - r, "label": str(clue_num)})
numbers_df = pd.DataFrame(number_data)
# Cell fills: crossword convention — entry cells use a warm tint in light mode for visual depth
ENTRY_FILL = "#F5F3EC" if THEME == "light" else "#FFFFFF"
BLOCKED_FILL = "#1A1A1A"
plot = (
ggplot(df, aes(x="col", y="row", fill="cell_type"))
+ geom_tile(color=INK_SOFT, size=0.4)
+ scale_fill_manual(values={"entry": ENTRY_FILL, "blocked": BLOCKED_FILL})
+ geom_text(
data=numbers_df,
mapping=aes(x="col", y="row", label="label"),
size=6,
ha="left",
va="top",
nudge_x=-0.35,
nudge_y=0.35,
color="#333333",
inherit_aes=False,
)
+ scale_x_continuous(breaks=[], expand=(0, 0))
+ scale_y_continuous(breaks=[], expand=(0, 0))
+ coord_fixed(ratio=1)
+ labs(
title="crossword-basic · python · plotnine · anyplot.ai",
subtitle="180° rotational symmetry · 79 numbered positions",
)
+ theme(
figure_size=(6, 6),
plot_title=element_text(size=13, ha="center", color=INK, weight="bold"),
plot_subtitle=element_text(size=8, ha="center", color=INK_SOFT),
axis_text=element_blank(),
axis_title=element_blank(),
axis_ticks=element_blank(),
panel_background=element_rect(fill=PAGE_BG),
panel_border=element_rect(color=INK_SOFT, fill=None, size=0.8),
panel_grid_major=element_blank(),
panel_grid_minor=element_blank(),
legend_position="none",
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
plot_margin=0.02,
)
)
plot.save(f"plot-{THEME}.png", dpi=400, width=6, height=6, units="in")
Part of Crossword Puzzle Grid on anyplot.ai.