Skip to content

Commit 5dd4fe4

Browse files
feat(highcharts): implement crossword-basic (#3875)
## Implementation: `crossword-basic` - highcharts Implements the **highcharts** version of `crossword-basic`. **File:** `plots/crossword-basic/implementations/highcharts.py` **Parent Issue:** #3805 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/21048827576)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 0d3d5b6 commit 5dd4fe4

2 files changed

Lines changed: 437 additions & 0 deletions

File tree

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
""" pyplots.ai
2+
crossword-basic: Crossword Puzzle Grid
3+
Library: highcharts unknown | Python 3.13.11
4+
Quality: 91/100 | Created: 2026-01-15
5+
"""
6+
7+
import tempfile
8+
import time
9+
import urllib.request
10+
from pathlib import Path
11+
12+
import numpy as np
13+
from highcharts_core.chart import Chart
14+
from highcharts_core.options import HighchartsOptions
15+
from highcharts_core.options.series.heatmap import HeatmapSeries
16+
from selenium import webdriver
17+
from selenium.webdriver.chrome.options import Options
18+
19+
20+
# Data - Create a 15x15 crossword grid with 180-degree rotational symmetry
21+
np.random.seed(42)
22+
grid_size = 15
23+
24+
# Generate symmetric black cell pattern (1 = black/blocked, 0 = white/entry)
25+
grid = np.zeros((grid_size, grid_size), dtype=int)
26+
27+
# Define black cells for one half (will be mirrored for symmetry)
28+
black_cells = [
29+
(0, 4),
30+
(0, 10),
31+
(1, 4),
32+
(1, 10),
33+
(2, 7),
34+
(3, 0),
35+
(3, 5),
36+
(3, 9),
37+
(4, 3),
38+
(4, 8),
39+
(4, 13),
40+
(5, 6),
41+
(5, 11),
42+
(6, 1),
43+
(6, 2),
44+
(6, 10),
45+
(6, 14),
46+
(7, 7),
47+
]
48+
49+
# Place black cells with 180-degree symmetry
50+
for r, c in black_cells:
51+
grid[r, c] = 1
52+
grid[grid_size - 1 - r, grid_size - 1 - c] = 1
53+
54+
# Calculate clue numbers - number cells that start words (across or down)
55+
numbers = {}
56+
clue_num = 1
57+
for r in range(grid_size):
58+
for c in range(grid_size):
59+
if grid[r, c] == 1:
60+
continue
61+
starts_across = (c == 0 or grid[r, c - 1] == 1) and (c < grid_size - 1 and grid[r, c + 1] == 0)
62+
starts_down = (r == 0 or grid[r - 1, c] == 1) and (r < grid_size - 1 and grid[r + 1, c] == 0)
63+
if starts_across or starts_down:
64+
numbers[(r, c)] = clue_num
65+
clue_num += 1
66+
67+
# Prepare heatmap data - black cells = 1, white cells = 0
68+
heatmap_data = []
69+
for r in range(grid_size):
70+
for c in range(grid_size):
71+
heatmap_data.append([c, grid_size - 1 - r, int(grid[r, c])])
72+
73+
# Create chart
74+
chart = Chart(container="container")
75+
chart.options = HighchartsOptions()
76+
77+
# Chart configuration
78+
chart.options.chart = {
79+
"type": "heatmap",
80+
"width": 3600,
81+
"height": 3600,
82+
"backgroundColor": "#ffffff",
83+
"marginTop": 120,
84+
"marginBottom": 100,
85+
"marginLeft": 100,
86+
"marginRight": 100,
87+
}
88+
89+
# Title
90+
chart.options.title = {
91+
"text": "crossword-basic · highcharts · pyplots.ai",
92+
"style": {"fontSize": "48px", "fontWeight": "bold"},
93+
}
94+
95+
# Remove colorAxis legend
96+
chart.options.color_axis = {"min": 0, "max": 1, "stops": [[0, "#ffffff"], [1, "#000000"]], "visible": False}
97+
98+
# X-axis (columns 1-15)
99+
chart.options.x_axis = {
100+
"categories": [str(i + 1) for i in range(grid_size)],
101+
"title": {"text": None},
102+
"labels": {"style": {"fontSize": "28px"}},
103+
"lineWidth": 2,
104+
"lineColor": "#000000",
105+
"tickWidth": 0,
106+
"tickLength": 0,
107+
"startOnTick": False,
108+
"endOnTick": False,
109+
}
110+
111+
# Y-axis (rows A-O)
112+
row_labels = [chr(65 + i) for i in range(grid_size)]
113+
chart.options.y_axis = {
114+
"categories": list(reversed(row_labels)),
115+
"title": {"text": None},
116+
"labels": {"style": {"fontSize": "28px"}},
117+
"lineWidth": 2,
118+
"lineColor": "#000000",
119+
"tickWidth": 0,
120+
"tickLength": 0,
121+
"startOnTick": False,
122+
"endOnTick": False,
123+
"reversed": False,
124+
}
125+
126+
# Legend disabled
127+
chart.options.legend = {"enabled": False}
128+
129+
# Create heatmap series
130+
series = HeatmapSeries()
131+
series.name = "Grid"
132+
series.data = heatmap_data
133+
series.border_width = 2
134+
series.border_color = "#000000"
135+
series.data_labels = {"enabled": False}
136+
137+
chart.add_series(series)
138+
139+
# Add clue numbers as annotations
140+
annotations = []
141+
for (r, c), num in numbers.items():
142+
annotations.append(
143+
{
144+
"point": {"x": c, "y": grid_size - 1 - r, "xAxis": 0, "yAxis": 0},
145+
"text": str(num),
146+
"align": "left",
147+
"verticalAlign": "top",
148+
"x": -85,
149+
"y": -75,
150+
"style": {"fontSize": "28px", "fontWeight": "bold", "color": "#000000"},
151+
"backgroundColor": "transparent",
152+
"borderWidth": 0,
153+
"shadow": False,
154+
}
155+
)
156+
157+
chart.options.annotations = [{"labels": annotations, "draggable": ""}]
158+
159+
# Plot options for heatmap - ensure all cells have visible borders
160+
chart.options.plot_options = {
161+
"heatmap": {
162+
"borderWidth": 3,
163+
"borderColor": "#333333",
164+
"dataLabels": {"enabled": False},
165+
"colsize": 1,
166+
"rowsize": 1,
167+
}
168+
}
169+
170+
# Download Highcharts JS and heatmap module
171+
highcharts_url = "https://code.highcharts.com/highcharts.js"
172+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
173+
highcharts_js = response.read().decode("utf-8")
174+
175+
heatmap_url = "https://code.highcharts.com/modules/heatmap.js"
176+
with urllib.request.urlopen(heatmap_url, timeout=30) as response:
177+
heatmap_js = response.read().decode("utf-8")
178+
179+
annotations_url = "https://code.highcharts.com/modules/annotations.js"
180+
with urllib.request.urlopen(annotations_url, timeout=30) as response:
181+
annotations_js = response.read().decode("utf-8")
182+
183+
# Generate HTML with inline scripts
184+
html_str = chart.to_js_literal()
185+
html_content = f"""<!DOCTYPE html>
186+
<html>
187+
<head>
188+
<meta charset="utf-8">
189+
<script>{highcharts_js}</script>
190+
<script>{heatmap_js}</script>
191+
<script>{annotations_js}</script>
192+
</head>
193+
<body style="margin:0;">
194+
<div id="container" style="width: 3600px; height: 3600px;"></div>
195+
<script>{html_str}</script>
196+
</body>
197+
</html>"""
198+
199+
# Write temp HTML and take screenshot
200+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
201+
f.write(html_content)
202+
temp_path = f.name
203+
204+
chrome_options = Options()
205+
chrome_options.add_argument("--headless")
206+
chrome_options.add_argument("--no-sandbox")
207+
chrome_options.add_argument("--disable-dev-shm-usage")
208+
chrome_options.add_argument("--disable-gpu")
209+
chrome_options.add_argument("--window-size=3600,3600")
210+
211+
driver = webdriver.Chrome(options=chrome_options)
212+
driver.get(f"file://{temp_path}")
213+
time.sleep(5)
214+
driver.save_screenshot("plot.png")
215+
driver.quit()
216+
217+
Path(temp_path).unlink()
218+
219+
# Also save HTML for interactive version
220+
html_output = f"""<!DOCTYPE html>
221+
<html>
222+
<head>
223+
<meta charset="utf-8">
224+
<script src="https://code.highcharts.com/highcharts.js"></script>
225+
<script src="https://code.highcharts.com/modules/heatmap.js"></script>
226+
<script src="https://code.highcharts.com/modules/annotations.js"></script>
227+
</head>
228+
<body style="margin:0;">
229+
<div id="container" style="width: 100%; height: 100vh;"></div>
230+
<script>{html_str}</script>
231+
</body>
232+
</html>"""
233+
with open("plot.html", "w", encoding="utf-8") as f:
234+
f.write(html_output)

0 commit comments

Comments
 (0)