diff --git a/plots/crossword-basic/implementations/highcharts.py b/plots/crossword-basic/implementations/highcharts.py new file mode 100644 index 0000000000..78e7d124bc --- /dev/null +++ b/plots/crossword-basic/implementations/highcharts.py @@ -0,0 +1,234 @@ +""" pyplots.ai +crossword-basic: Crossword Puzzle Grid +Library: highcharts unknown | Python 3.13.11 +Quality: 91/100 | Created: 2026-01-15 +""" + +import tempfile +import time +import urllib.request +from pathlib import Path + +import numpy as np +from highcharts_core.chart import Chart +from highcharts_core.options import HighchartsOptions +from highcharts_core.options.series.heatmap import HeatmapSeries +from selenium import webdriver +from selenium.webdriver.chrome.options import Options + + +# Data - Create a 15x15 crossword grid with 180-degree rotational symmetry +np.random.seed(42) +grid_size = 15 + +# Generate symmetric black cell pattern (1 = black/blocked, 0 = white/entry) +grid = np.zeros((grid_size, grid_size), dtype=int) + +# Define black cells for one half (will be mirrored for symmetry) +black_cells = [ + (0, 4), + (0, 10), + (1, 4), + (1, 10), + (2, 7), + (3, 0), + (3, 5), + (3, 9), + (4, 3), + (4, 8), + (4, 13), + (5, 6), + (5, 11), + (6, 1), + (6, 2), + (6, 10), + (6, 14), + (7, 7), +] + +# Place black cells with 180-degree symmetry +for r, c in black_cells: + grid[r, c] = 1 + grid[grid_size - 1 - r, grid_size - 1 - c] = 1 + +# Calculate clue numbers - number cells that start words (across or down) +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 heatmap data - black cells = 1, white cells = 0 +heatmap_data = [] +for r in range(grid_size): + for c in range(grid_size): + heatmap_data.append([c, grid_size - 1 - r, int(grid[r, c])]) + +# Create chart +chart = Chart(container="container") +chart.options = HighchartsOptions() + +# Chart configuration +chart.options.chart = { + "type": "heatmap", + "width": 3600, + "height": 3600, + "backgroundColor": "#ffffff", + "marginTop": 120, + "marginBottom": 100, + "marginLeft": 100, + "marginRight": 100, +} + +# Title +chart.options.title = { + "text": "crossword-basic · highcharts · pyplots.ai", + "style": {"fontSize": "48px", "fontWeight": "bold"}, +} + +# Remove colorAxis legend +chart.options.color_axis = {"min": 0, "max": 1, "stops": [[0, "#ffffff"], [1, "#000000"]], "visible": False} + +# X-axis (columns 1-15) +chart.options.x_axis = { + "categories": [str(i + 1) for i in range(grid_size)], + "title": {"text": None}, + "labels": {"style": {"fontSize": "28px"}}, + "lineWidth": 2, + "lineColor": "#000000", + "tickWidth": 0, + "tickLength": 0, + "startOnTick": False, + "endOnTick": False, +} + +# Y-axis (rows A-O) +row_labels = [chr(65 + i) for i in range(grid_size)] +chart.options.y_axis = { + "categories": list(reversed(row_labels)), + "title": {"text": None}, + "labels": {"style": {"fontSize": "28px"}}, + "lineWidth": 2, + "lineColor": "#000000", + "tickWidth": 0, + "tickLength": 0, + "startOnTick": False, + "endOnTick": False, + "reversed": False, +} + +# Legend disabled +chart.options.legend = {"enabled": False} + +# Create heatmap series +series = HeatmapSeries() +series.name = "Grid" +series.data = heatmap_data +series.border_width = 2 +series.border_color = "#000000" +series.data_labels = {"enabled": False} + +chart.add_series(series) + +# Add clue numbers as annotations +annotations = [] +for (r, c), num in numbers.items(): + annotations.append( + { + "point": {"x": c, "y": grid_size - 1 - r, "xAxis": 0, "yAxis": 0}, + "text": str(num), + "align": "left", + "verticalAlign": "top", + "x": -85, + "y": -75, + "style": {"fontSize": "28px", "fontWeight": "bold", "color": "#000000"}, + "backgroundColor": "transparent", + "borderWidth": 0, + "shadow": False, + } + ) + +chart.options.annotations = [{"labels": annotations, "draggable": ""}] + +# Plot options for heatmap - ensure all cells have visible borders +chart.options.plot_options = { + "heatmap": { + "borderWidth": 3, + "borderColor": "#333333", + "dataLabels": {"enabled": False}, + "colsize": 1, + "rowsize": 1, + } +} + +# Download Highcharts JS and heatmap module +highcharts_url = "https://code.highcharts.com/highcharts.js" +with urllib.request.urlopen(highcharts_url, timeout=30) as response: + highcharts_js = response.read().decode("utf-8") + +heatmap_url = "https://code.highcharts.com/modules/heatmap.js" +with urllib.request.urlopen(heatmap_url, timeout=30) as response: + heatmap_js = response.read().decode("utf-8") + +annotations_url = "https://code.highcharts.com/modules/annotations.js" +with urllib.request.urlopen(annotations_url, timeout=30) as response: + annotations_js = response.read().decode("utf-8") + +# Generate HTML with inline scripts +html_str = chart.to_js_literal() +html_content = f""" + + + + + + + + +
+ + +""" + +# Write temp HTML and take screenshot +with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f: + f.write(html_content) + temp_path = f.name + +chrome_options = Options() +chrome_options.add_argument("--headless") +chrome_options.add_argument("--no-sandbox") +chrome_options.add_argument("--disable-dev-shm-usage") +chrome_options.add_argument("--disable-gpu") +chrome_options.add_argument("--window-size=3600,3600") + +driver = webdriver.Chrome(options=chrome_options) +driver.get(f"file://{temp_path}") +time.sleep(5) +driver.save_screenshot("plot.png") +driver.quit() + +Path(temp_path).unlink() + +# Also save HTML for interactive version +html_output = f""" + + + + + + + + +
+ + +""" +with open("plot.html", "w", encoding="utf-8") as f: + f.write(html_output) diff --git a/plots/crossword-basic/metadata/highcharts.yaml b/plots/crossword-basic/metadata/highcharts.yaml new file mode 100644 index 0000000000..6a48335f13 --- /dev/null +++ b/plots/crossword-basic/metadata/highcharts.yaml @@ -0,0 +1,203 @@ +library: highcharts +specification_id: crossword-basic +created: '2026-01-15T22:43:04Z' +updated: '2026-01-15T22:45:44Z' +generated_by: claude-opus-4-5-20251101 +workflow_run: 21048827576 +issue: 3805 +python_version: 3.13.11 +library_version: unknown +preview_url: https://storage.googleapis.com/pyplots-images/plots/crossword-basic/highcharts/plot.png +preview_thumb: https://storage.googleapis.com/pyplots-images/plots/crossword-basic/highcharts/plot_thumb.png +preview_html: https://storage.googleapis.com/pyplots-images/plots/crossword-basic/highcharts/plot.html +quality_score: 91 +review: + strengths: + - Excellent implementation of crossword grid using Highcharts heatmap + - Perfect 180-degree rotational symmetry in black cell placement + - Clue numbering algorithm correctly identifies word start positions + - Clean monochrome design suitable for printing + - Square 1:1 aspect ratio as specified + - Both PNG and HTML outputs generated + weaknesses: + - Clue numbers could be slightly larger for better visibility at smaller display + sizes + - Axis labels are functional but lack descriptive text + image_description: The plot displays a 15x15 crossword puzzle grid with white entry + cells and black blocking cells. The title "crossword-basic · highcharts · pyplots.ai" + appears at the top in bold black text. Row labels (A-O) are displayed on the left + Y-axis, and column numbers (1-15) appear along the bottom X-axis. Black cells + form a symmetric pattern typical of newspaper crosswords, with 180-degree rotational + symmetry visible. Clue numbers (1-73) are positioned in the top-left corners of + cells that start across or down words. The grid has clean black borders separating + all cells, and the overall monochrome design follows traditional crossword conventions. + criteria_checklist: + visual_quality: + score: 36 + max: 40 + items: + - id: VQ-01 + name: Text Legibility + score: 9 + max: 10 + passed: true + comment: Title, axis labels, and clue numbers are readable, though clue numbers + could be slightly larger + - id: VQ-02 + name: No Overlap + score: 8 + max: 8 + passed: true + comment: No overlapping text elements + - id: VQ-03 + name: Element Visibility + score: 8 + max: 8 + passed: true + comment: Grid cells clearly visible with appropriate sizing + - id: VQ-04 + name: Color Accessibility + score: 5 + max: 5 + passed: true + comment: Pure black and white, perfectly accessible + - id: VQ-05 + name: Layout Balance + score: 4 + max: 5 + passed: true + comment: Good use of canvas, slight imbalance in margins + - id: VQ-06 + name: Axis Labels + score: 1 + max: 2 + passed: true + comment: Row (A-O) and column (1-15) labels present but no descriptive labels + - id: VQ-07 + name: Grid & Legend + score: 1 + max: 2 + passed: true + comment: Clean grid lines, no legend needed + spec_compliance: + score: 25 + max: 25 + items: + - id: SC-01 + name: Plot Type + score: 8 + max: 8 + passed: true + comment: Correct crossword grid visualization using heatmap + - id: SC-02 + name: Data Mapping + score: 5 + max: 5 + passed: true + comment: Grid cells correctly mapped with black/white values + - id: SC-03 + name: Required Features + score: 5 + max: 5 + passed: true + comment: White entry cells, black blocking cells, numbered positions all present + - id: SC-04 + name: Data Range + score: 3 + max: 3 + passed: true + comment: Full 15x15 grid displayed + - id: SC-05 + name: Legend Accuracy + score: 2 + max: 2 + passed: true + comment: No legend needed for this plot type + - id: SC-06 + name: Title Format + score: 2 + max: 2 + passed: true + comment: 'Correct format: crossword-basic · highcharts · pyplots.ai' + data_quality: + score: 18 + max: 20 + items: + - id: DQ-01 + name: Feature Coverage + score: 7 + max: 8 + passed: true + comment: Shows symmetric black cell pattern and numbered clues, demonstrates + crossword structure well + - id: DQ-02 + name: Realistic Context + score: 7 + max: 7 + passed: true + comment: Authentic newspaper-style crossword layout + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 5 + passed: true + comment: 15x15 is standard size, 73 clues is realistic + code_quality: + score: 9 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: 'Linear flow: imports → data → plot → save' + - id: CQ-02 + name: Reproducibility + score: 3 + max: 3 + passed: true + comment: Uses np.random.seed(42) + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: All imports are used + - id: CQ-04 + name: No Deprecated API + score: 0 + max: 1 + passed: false + comment: Using older annotation syntax + - id: CQ-05 + name: Output Correct + score: 1 + max: 1 + passed: true + comment: Saves as plot.png and plot.html + library_features: + score: 3 + max: 5 + items: + - id: LF-01 + name: Distinctive Features + score: 3 + max: 5 + passed: true + comment: Uses Highcharts heatmap with annotations module, but could leverage + more interactive features + verdict: APPROVED +impl_tags: + dependencies: + - selenium + techniques: + - annotations + - html-export + patterns: + - data-generation + - matrix-construction + - iteration-over-groups + dataprep: [] + styling: + - custom-colormap