{"spec_id":"crossword-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// crossword-basic: Crossword Puzzle Grid\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 84/100 | Created: 2026-09-02\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\n\n// Crossword cell colors — fixed regardless of theme (contrast IS the data):\n// blocked squares stay near-black and entry squares stay near-white in both\n// light and dark renders, matching the traditional newspaper-crossword\n// convention instead of flipping with the page chrome.\nconst CELL_BLOCK = \"#1A1A17\";\nconst CELL_ENTRY = \"#FAF8F1\";\n\n// --- Data: symmetric 15x15 crossword grid (180-degree rotational symmetry) -\nconst N = 15;\n\n// Only the \"first half\" of blocked cells is listed; the mirrored partner\n// (N-1-row, N-1-col) is added below, which is how newspaper crosswords keep\n// their traditional 180-degree rotational symmetry.\nconst blockedHalf = [\n  [0, 3], [0, 10],\n  [1, 3], [1, 10],\n  [2, 6], [2, 8],\n  [3, 0], [3, 6], [3, 11],\n  [4, 4], [4, 9],\n  [5, 2], [5, 6], [5, 8], [5, 12],\n  [6, 0], [6, 5], [6, 9], [6, 14],\n  [7, 7],\n];\nconst blocked = new Set();\nblockedHalf.forEach(([row, col]) => {\n  blocked.add(`${row},${col}`);\n  blocked.add(`${N - 1 - row},${N - 1 - col}`);\n});\nconst isBlocked = (row, col) => blocked.has(`${row},${col}`);\n\n// Standard crossword numbering: a cell is numbered when it starts an across\n// and/or a down entry (no open cell to its left/above, and an open cell\n// follows to its right/below).\nlet nextNumber = 1;\nconst numberedCells = [];\nfor (let row = 0; row < N; row += 1) {\n  for (let col = 0; col < N; col += 1) {\n    if (isBlocked(row, col)) continue;\n    const startsAcross = (col === 0 || isBlocked(row, col - 1)) &&\n      col + 1 < N && !isBlocked(row, col + 1);\n    const startsDown = (row === 0 || isBlocked(row - 1, col)) &&\n      row + 1 < N && !isBlocked(row + 1, col);\n    if (startsAcross || startsDown) {\n      numberedCells.push({ row, col, number: nextNumber });\n      nextNumber += 1;\n    }\n  }\n}\n\n// --- Layout: fixed margins keep the plot area an exact square so grid cells\n// stay 1:1, independent of Highcharts' auto layout for title/axes -----------\nconst MARGIN_TOP = 100;\nconst MARGIN_BOTTOM = 40;\nconst MARGIN_SIDE = 70;\nconst cellPx = (size.width - MARGIN_SIDE * 2) / N;\n\nconst blockedCells = [...blocked].map((key) => {\n  const [row, col] = key.split(\",\").map(Number);\n  return { x: col + 0.5, y: row + 0.5 };\n});\n\nconst entryCells = [];\nfor (let row = 0; row < N; row += 1) {\n  for (let col = 0; col < N; col += 1) {\n    if (!isBlocked(row, col)) entryCells.push({ x: col + 0.5, y: row + 0.5 });\n  }\n}\n\nconst numberPoints = numberedCells.map(({ row, col, number }) => ({\n  x: col,\n  y: row,\n  name: String(number),\n}));\n\n// --- Chart -------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    spacing: [0, 0, 0, 0],\n    marginTop: MARGIN_TOP,\n    marginBottom: MARGIN_BOTTOM,\n    marginLeft: MARGIN_SIDE,\n    marginRight: MARGIN_SIDE,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"crossword-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  tooltip: { enabled: false },\n  legend: { enabled: false },\n  xAxis: {\n    min: 0,\n    max: N,\n    tickInterval: 1,\n    gridLineWidth: 1,\n    gridLineColor: t.grid,\n    lineWidth: 0,\n    tickLength: 0,\n    labels: { enabled: false },\n    title: { text: null },\n  },\n  yAxis: {\n    min: 0,\n    max: N,\n    reversed: true,\n    tickInterval: 1,\n    gridLineWidth: 1,\n    gridLineColor: t.grid,\n    lineWidth: 0,\n    tickLength: 0,\n    labels: { enabled: false },\n    title: { text: null },\n  },\n  plotOptions: {\n    series: { animation: false, enableMouseTracking: false },\n  },\n  series: [\n    {\n      name: \"Entry cells\",\n      type: \"scatter\",\n      data: entryCells,\n      marker: {\n        symbol: \"square\",\n        radius: cellPx / 2 + 0.5,\n        fillColor: CELL_ENTRY,\n        lineWidth: 0,\n      },\n      dataLabels: { enabled: false },\n    },\n    {\n      name: \"Blocked cells\",\n      type: \"scatter\",\n      data: blockedCells,\n      marker: {\n        symbol: \"square\",\n        radius: cellPx / 2 + 0.5,\n        fillColor: CELL_BLOCK,\n        lineWidth: 0,\n      },\n      dataLabels: { enabled: false },\n    },\n    {\n      name: \"Clue numbers\",\n      type: \"scatter\",\n      data: numberPoints,\n      marker: { enabled: false },\n      dataLabels: {\n        enabled: true,\n        format: \"{point.name}\",\n        align: \"left\",\n        verticalAlign: \"top\",\n        x: 3,\n        y: 2,\n        padding: 0,\n        crop: false,\n        overflow: \"allow\",\n        allowOverlap: true,\n        style: {\n          color: CELL_BLOCK,\n          fontSize: \"14px\",\n          fontWeight: \"600\",\n          textOutline: \"none\",\n        },\n      },\n    },\n  ],\n});\n"}