{"spec_id":"crossword-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\ncrossword-basic: Crossword Puzzle Grid\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-05-20\n\"\"\"\n\nimport os as _os\nimport sys as _sys\n\n\n# This file is named pygal.py — remove its directory from sys.path first so\n# Python resolves `pygal` to the installed package, not this file itself.\n_here = _os.path.abspath(_os.path.dirname(__file__))\n_sys.path = [p for p in _sys.path if _os.path.abspath(p or \".\") != _here]\n\nimport os\n\nimport cairosvg\nimport numpy as np\nfrom pygal.graph.graph import Graph\nfrom pygal.style import Style\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\nCANVAS = 2400\nCELL = 140\nGRID_SIZE = 15\nGRID_W = CELL * GRID_SIZE  # 2100 px — fills the plot area exactly\nNUM_SZ = 28\n\nTITLE = \"crossword-basic · python · pygal · anyplot.ai\"\n\n# Pygal Style — applied to the chart for theme-consistent background, title, and chrome\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_SOFT,\n    colors=(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\"),\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    stroke_width=2.5,\n)\n\n# Crossword grid data — 15×15 with 180° rotational symmetry\nnp.random.seed(42)\ngrid = np.zeros((GRID_SIZE, GRID_SIZE), dtype=int)\n\nblack_positions_top = [\n    (0, 4),\n    (0, 10),\n    (1, 4),\n    (1, 10),\n    (2, 7),\n    (3, 0),\n    (3, 1),\n    (3, 6),\n    (4, 5),\n    (4, 9),\n    (5, 3),\n    (5, 11),\n    (6, 0),\n    (6, 8),\n    (6, 14),\n]\nblack_positions_center = [(7, 2), (7, 7), (7, 12)]\n\nfor r, c in black_positions_top:\n    grid[r, c] = 1\n    grid[GRID_SIZE - 1 - r, GRID_SIZE - 1 - c] = 1\nfor r, c in black_positions_center:\n    grid[r, c] = 1\n\n# Clue numbering — cells that start an across or down word\nnumbers = {}\nclue_num = 1\nfor row in range(GRID_SIZE):\n    for col in range(GRID_SIZE):\n        if grid[row, col] == 0:\n            starts_across = (col == 0 or grid[row, col - 1] == 1) and (col < GRID_SIZE - 1 and grid[row, col + 1] == 0)\n            starts_down = (row == 0 or grid[row - 1, col] == 1) and (row < GRID_SIZE - 1 and grid[row + 1, col] == 0)\n            if starts_across or starts_down:\n                numbers[(row, col)] = clue_num\n                clue_num += 1\n\n\nclass CrosswordGrid(Graph):\n    \"\"\"Custom pygal chart type that renders a crossword puzzle grid.\n\n    Subclasses Graph to use pygal's SVG infrastructure (style, background,\n    title, render pipeline) while drawing grid cells via pygal's SVG node API.\n    \"\"\"\n\n    def _compute(self):\n        pass\n\n    def _compute_x_labels(self):\n        pass\n\n    def _compute_y_labels(self):\n        pass\n\n    def _draw(self):\n        \"\"\"Draw chart elements; always calls _plot since no series data is needed.\"\"\"\n        self._compute()\n        self._compute_x_labels()\n        self._compute_x_labels_major()\n        self._compute_y_labels()\n        self._compute_y_labels_major()\n        self._compute_secondary()\n        self._post_compute()\n        self._compute_margin()\n        self._decorate()\n        self._plot()\n\n    def _plot(self):\n        \"\"\"Render crossword cells using pygal's SVG node API.\n\n        Coordinates are relative to the plot group (margin already applied),\n        so (0, 0) maps to the top-left corner of the grid area on the canvas.\n        \"\"\"\n        plot = self.nodes[\"plot\"]\n        for row in range(GRID_SIZE):\n            for col in range(GRID_SIZE):\n                x = col * CELL\n                y = row * CELL\n                fill = INK if grid[row, col] == 1 else ELEVATED_BG\n\n                rect = self.svg.node(plot, \"rect\", x=x, y=y, width=CELL, height=CELL, fill=fill)\n                rect.set(\"stroke\", INK_SOFT)\n                rect.set(\"stroke-width\", \"2\")\n\n                if (row, col) in numbers:\n                    txt = self.svg.node(plot, \"text\", x=x + 6, y=y + NUM_SZ + 3, fill=INK)\n                    txt.set(\"font-family\", \"Arial, sans-serif\")\n                    txt.set(\"font-size\", str(NUM_SZ))\n                    txt.text = str(numbers[(row, col)])\n\n\n# margin=150 → equal 150 px margins on all sides; plot area = 2100×2100 = grid size\nchart = CrosswordGrid(style=custom_style, width=CANVAS, height=CANVAS, title=TITLE, show_legend=False, margin=150)\n\n# Render via pygal's pipeline: render() produces the SVG, cairosvg converts to PNG\nsvg_bytes = chart.render()\ncairosvg.svg2png(bytestring=svg_bytes, write_to=f\"plot-{THEME}.png\", output_width=CANVAS, output_height=CANVAS)\n\n# HTML: pygal's interactive SVG output (embedded in a minimal page)\nwith open(f\"plot-{THEME}.html\", \"w\", encoding=\"utf-8\") as f:\n    f.write(chart.render(is_unicode=True))\n"}