|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | crossword-basic: Crossword Puzzle Grid |
3 | | -Library: altair 6.0.0 | Python 3.13.11 |
4 | | -Quality: 92/100 | Created: 2026-01-15 |
| 3 | +Library: altair 6.1.0 | Python 3.13.13 |
| 4 | +Quality: 90/100 | Updated: 2026-05-20 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | +import sys |
| 9 | + |
| 10 | + |
| 11 | +# Prevent the script's own directory from shadowing the 'altair' package |
| 12 | +sys.path = [p for p in sys.path if not p.endswith("/python")] |
| 13 | + |
7 | 14 | import altair as alt |
8 | 15 | import numpy as np |
9 | 16 | import pandas as pd |
10 | 17 |
|
11 | 18 |
|
12 | | -# Data - 15x15 crossword grid with symmetric black cell pattern |
| 19 | +# Theme tokens |
| 20 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 21 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 22 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 23 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 24 | + |
| 25 | +# Crossword cell colors (theme-adaptive monochrome design) |
| 26 | +CELL_ENTRY = "#FFFFFF" if THEME == "light" else "#DEDAD3" |
| 27 | +CELL_BLOCK = "#1A1A17" if THEME == "light" else "#3E3E3A" |
| 28 | + |
| 29 | +# Data — 15x15 crossword grid with 180-degree rotational symmetry |
13 | 30 | np.random.seed(42) |
14 | 31 | grid_size = 15 |
15 | | - |
16 | | -# Create symmetric black cell pattern (180-degree rotational symmetry) |
17 | | -# Start with empty grid (0 = white/entry cell, 1 = black/blocked cell) |
18 | 32 | grid = np.zeros((grid_size, grid_size), dtype=int) |
19 | 33 |
|
20 | | -# Define black cell positions for upper-left quadrant + center row/col |
21 | | -# These will be mirrored for symmetry |
22 | 34 | black_positions = [ |
23 | 35 | (0, 4), |
24 | 36 | (0, 10), |
|
43 | 55 | (7, 12), |
44 | 56 | ] |
45 | 57 |
|
46 | | -# Apply black cells with 180-degree rotational symmetry |
47 | 58 | for r, c in black_positions: |
48 | 59 | grid[r, c] = 1 |
49 | 60 | grid[grid_size - 1 - r, grid_size - 1 - c] = 1 |
|
54 | 65 |
|
55 | 66 | for r in range(grid_size): |
56 | 67 | for c in range(grid_size): |
57 | | - if grid[r, c] == 1: # Skip black cells |
| 68 | + if grid[r, c] == 1: |
58 | 69 | continue |
59 | | - |
60 | | - # Check if this starts an across word (left edge or black cell to left, white to right) |
61 | 70 | starts_across = (c == 0 or grid[r, c - 1] == 1) and (c < grid_size - 1 and grid[r, c + 1] == 0) |
62 | | - |
63 | | - # Check if this starts a down word (top edge or black cell above, white below) |
64 | 71 | starts_down = (r == 0 or grid[r - 1, c] == 1) and (r < grid_size - 1 and grid[r + 1, c] == 0) |
65 | | - |
66 | 72 | if starts_across or starts_down: |
67 | 73 | numbers[(r, c)] = clue_num |
68 | 74 | clue_num += 1 |
69 | 75 |
|
70 | | -# Build dataframe for grid cells |
71 | | -cells_data = [] |
72 | | -for r in range(grid_size): |
73 | | - for c in range(grid_size): |
74 | | - cells_data.append( |
75 | | - {"row": r, "col": c, "is_black": grid[r, c] == 1, "color": "#1a1a1a" if grid[r, c] == 1 else "#ffffff"} |
76 | | - ) |
77 | | - |
| 76 | +# Build dataframes |
| 77 | +cells_data = [ |
| 78 | + {"row": r, "col": c, "color": CELL_BLOCK if grid[r, c] == 1 else CELL_ENTRY} |
| 79 | + for r in range(grid_size) |
| 80 | + for c in range(grid_size) |
| 81 | +] |
78 | 82 | cells_df = pd.DataFrame(cells_data) |
79 | 83 |
|
80 | | -# Build dataframe for clue numbers |
81 | | -numbers_data = [] |
82 | | -for (r, c), num in numbers.items(): |
83 | | - numbers_data.append({"row": r, "col": c, "number": str(num)}) |
84 | | - |
85 | | -numbers_df = pd.DataFrame(numbers_data) |
| 84 | +numbers_df = pd.DataFrame([{"row": r, "col": c, "number": str(num)} for (r, c), num in numbers.items()]) |
86 | 85 |
|
87 | | -# Create grid cells chart |
| 86 | +# Grid cells layer |
88 | 87 | cells = ( |
89 | 88 | alt.Chart(cells_df) |
90 | | - .mark_rect(stroke="#333333", strokeWidth=2) |
| 89 | + .mark_rect(stroke=INK_SOFT, strokeWidth=1.5) |
91 | 90 | .encode(x=alt.X("col:O", axis=None), y=alt.Y("row:O", axis=None), color=alt.Color("color:N", scale=None)) |
92 | | - .properties(width=900, height=900) |
93 | 91 | ) |
94 | 92 |
|
95 | | -# Create clue numbers overlay |
| 93 | +# Clue numbers overlay |
96 | 94 | clue_numbers = ( |
97 | 95 | alt.Chart(numbers_df) |
98 | | - .mark_text(align="left", baseline="top", dx=-12, dy=-12, fontSize=14, fontWeight="bold", color="#333333") |
| 96 | + .mark_text(align="left", baseline="top", dx=-12, dy=-12, fontSize=10, fontWeight="bold", color=INK_SOFT) |
99 | 97 | .encode(x=alt.X("col:O", axis=None), y=alt.Y("row:O", axis=None), text="number:N") |
100 | 98 | ) |
101 | 99 |
|
102 | | -# Combine layers and add title |
| 100 | +# Compose chart — square 600×600 canvas → 2400×2400 px at scale=4 |
103 | 101 | chart = ( |
104 | 102 | (cells + clue_numbers) |
105 | | - .properties(title=alt.Title("crossword-basic · altair · pyplots.ai", fontSize=32, anchor="middle", offset=20)) |
106 | | - .configure_view(strokeWidth=0) |
| 103 | + .properties( |
| 104 | + width=600, |
| 105 | + height=600, |
| 106 | + background=PAGE_BG, |
| 107 | + title=alt.Title( |
| 108 | + "crossword-basic · python · altair · anyplot.ai", fontSize=16, anchor="middle", offset=18, color=INK |
| 109 | + ), |
| 110 | + ) |
| 111 | + .configure_view(fill=PAGE_BG, strokeWidth=0) |
107 | 112 | ) |
108 | 113 |
|
109 | 114 | # Save outputs |
110 | | -chart.save("plot.png", scale_factor=4.0) |
111 | | -chart.save("plot.html") |
| 115 | +chart.save(f"plot-{THEME}.png", scale_factor=4.0) |
| 116 | +chart.save(f"plot-{THEME}.html") |
0 commit comments