|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | crossword-basic: Crossword Puzzle Grid |
3 | | -Library: matplotlib 3.10.8 | Python 3.13.11 |
4 | | -Quality: 93/100 | Created: 2026-01-15 |
| 3 | +Library: matplotlib 3.10.9 | Python 3.13.13 |
| 4 | +Quality: 88/100 | Updated: 2026-05-20 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | + |
7 | 9 | import matplotlib.pyplot as plt |
8 | 10 | import numpy as np |
9 | 11 | from matplotlib.patches import Rectangle |
10 | 12 |
|
11 | 13 |
|
| 14 | +# Theme tokens |
| 15 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 16 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 17 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 18 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 19 | +CELL_WHITE = "#FFFFFF" if THEME == "light" else "#D0CEC8" |
| 20 | +CELL_BLACK = "#1A1A17" if THEME == "light" else "#050503" |
| 21 | +NUM_COLOR = "#009E73" # Okabe-Ito position 1 (brand green) — sole colored element |
| 22 | + |
12 | 23 | # Data: 15x15 crossword grid with 180-degree rotational symmetry |
13 | 24 | # 0 = white (entry cell), 1 = black (blocked cell) |
14 | | -np.random.seed(42) |
15 | | - |
16 | 25 | grid_size = 15 |
17 | | - |
18 | | -# Create symmetric black cell pattern (traditional newspaper style) |
19 | | -# Start with empty grid |
20 | 26 | grid = np.zeros((grid_size, grid_size), dtype=int) |
21 | 27 |
|
22 | | -# Define black cell positions for one half (will mirror for symmetry) |
23 | 28 | black_cells = [ |
24 | 29 | (0, 4), |
25 | 30 | (0, 10), |
|
46 | 51 | (7, 12), |
47 | 52 | ] |
48 | 53 |
|
49 | | -# Set black cells and their symmetric counterparts |
50 | 54 | for r, c in black_cells: |
51 | 55 | grid[r, c] = 1 |
52 | | - # 180-degree rotational symmetry |
53 | 56 | grid[grid_size - 1 - r, grid_size - 1 - c] = 1 |
54 | 57 |
|
55 | | -# Calculate clue numbers (cells that start words across or down) |
| 58 | +# Calculate clue numbers (cells that start an across or down word) |
56 | 59 | numbers = {} |
57 | 60 | clue_num = 1 |
58 | 61 |
|
59 | 62 | for row in range(grid_size): |
60 | 63 | for col in range(grid_size): |
61 | 64 | if grid[row, col] == 1: |
62 | | - continue # Skip black cells |
63 | | - |
| 65 | + continue |
64 | 66 | starts_across = (col == 0 or grid[row, col - 1] == 1) and (col < grid_size - 1 and grid[row, col + 1] == 0) |
65 | 67 | starts_down = (row == 0 or grid[row - 1, col] == 1) and (row < grid_size - 1 and grid[row + 1, col] == 0) |
66 | | - |
67 | 68 | if starts_across or starts_down: |
68 | 69 | numbers[(row, col)] = clue_num |
69 | 70 | clue_num += 1 |
70 | 71 |
|
71 | | -# Create plot (square format for crossword) |
72 | | -fig, ax = plt.subplots(figsize=(12, 12)) |
| 72 | +# Plot — square format (2400×2400 px) suits the 1:1 crossword grid |
| 73 | +fig, ax = plt.subplots(figsize=(6, 6), dpi=400, facecolor=PAGE_BG) |
| 74 | +ax.set_facecolor(PAGE_BG) |
73 | 75 |
|
74 | | -# Draw the grid |
75 | 76 | cell_size = 1.0 |
76 | 77 |
|
77 | 78 | for row in range(grid_size): |
78 | 79 | for col in range(grid_size): |
79 | 80 | x = col * cell_size |
80 | | - y = (grid_size - 1 - row) * cell_size # Flip y-axis for proper orientation |
| 81 | + y = (grid_size - 1 - row) * cell_size # flip y so row 0 is at top |
81 | 82 |
|
82 | | - if grid[row, col] == 1: |
83 | | - # Black cell |
84 | | - rect = Rectangle((x, y), cell_size, cell_size, facecolor="#1a1a1a", edgecolor="#333333", linewidth=1.5) |
85 | | - else: |
86 | | - # White entry cell |
87 | | - rect = Rectangle((x, y), cell_size, cell_size, facecolor="white", edgecolor="#333333", linewidth=1.5) |
| 83 | + face = CELL_BLACK if grid[row, col] == 1 else CELL_WHITE |
| 84 | + rect = Rectangle((x, y), cell_size, cell_size, facecolor=face, edgecolor=INK_SOFT, linewidth=0.8) |
88 | 85 | ax.add_patch(rect) |
89 | 86 |
|
90 | | - # Add clue number if this cell starts a word |
91 | 87 | if (row, col) in numbers: |
92 | 88 | ax.text( |
93 | | - x + 0.08, |
94 | | - y + cell_size - 0.08, |
| 89 | + x + 0.07, |
| 90 | + y + cell_size - 0.07, |
95 | 91 | str(numbers[(row, col)]), |
96 | | - fontsize=11, |
| 92 | + fontsize=9, |
97 | 93 | fontweight="bold", |
98 | | - color="#306998", |
| 94 | + color=NUM_COLOR, |
99 | 95 | ha="left", |
100 | 96 | va="top", |
101 | 97 | ) |
102 | 98 |
|
103 | | -# Set axis properties |
| 99 | +# Outer border frame around the full grid |
| 100 | +border = Rectangle( |
| 101 | + (0, 0), grid_size * cell_size, grid_size * cell_size, fill=False, edgecolor=INK, linewidth=2.5, zorder=10 |
| 102 | +) |
| 103 | +ax.add_patch(border) |
| 104 | + |
| 105 | +# Style |
104 | 106 | ax.set_xlim(0, grid_size * cell_size) |
105 | 107 | ax.set_ylim(0, grid_size * cell_size) |
106 | 108 | ax.set_aspect("equal") |
107 | 109 | ax.axis("off") |
108 | | - |
109 | | -# Title |
110 | | -ax.set_title("crossword-basic · matplotlib · pyplots.ai", fontsize=24, fontweight="bold", pad=20, color="#306998") |
| 110 | +ax.set_title("crossword-basic · python · matplotlib · anyplot.ai", fontsize=12, fontweight="medium", color=INK, pad=20) |
111 | 111 |
|
112 | 112 | plt.tight_layout() |
113 | | -plt.savefig("plot.png", dpi=300, bbox_inches="tight", facecolor="white") |
| 113 | +plt.savefig(f"plot-{THEME}.png", dpi=400, bbox_inches="tight", facecolor=PAGE_BG) |
0 commit comments