{"spec_id":"crossword-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// crossword-basic: Crossword Puzzle Grid\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 84/100 | Created: 2026-09-02\n\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst mount = window.ANYPLOT_SIZE || { width: 1200, height: 1200 };\n\n// --- Data: 15x15 grid with 180-degree rotational symmetry -------------------\n// 0 = white entry cell, 1 = black blocking cell.\n// Only the top 7 rows + the palindromic middle row are authored by hand; the\n// bottom 7 rows are derived by rotating the top half 180 degrees so the\n// traditional newspaper-style symmetry is guaranteed by construction.\nconst gridSize = 15;\nconst topHalf = [\n  [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],\n  [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],\n  [1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1],\n  [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0],\n  [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],\n  [1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1],\n  [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],\n];\nconst middleRow = [0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0];\n\nconst grid = [];\nfor (let row = 0; row < gridSize; row += 1) {\n  if (row < topHalf.length) {\n    grid.push(topHalf[row].slice());\n  } else if (row === Math.floor(gridSize / 2)) {\n    grid.push(middleRow.slice());\n  } else {\n    grid.push(topHalf[gridSize - 1 - row].slice().reverse());\n  }\n}\n\n// --- Numbering: standard across/down word-start scan -------------------------\nconst cells = [];\nlet nextNumber = 1;\nfor (let row = 0; row < gridSize; row += 1) {\n  for (let col = 0; col < gridSize; col += 1) {\n    const blocked = grid[row][col] === 1;\n    let number = null;\n    if (!blocked) {\n      const startsAcross =\n        (col === 0 || grid[row][col - 1] === 1) &&\n        col < gridSize - 1 &&\n        grid[row][col + 1] === 0;\n      const startsDown =\n        (row === 0 || grid[row - 1][col] === 1) &&\n        row < gridSize - 1 &&\n        grid[row + 1][col] === 0;\n      if (startsAcross || startsDown) {\n        number = nextNumber;\n        nextNumber += 1;\n      }\n    }\n    cells.push({ col, row, blocked, number });\n  }\n}\n\n// --- Square plotting area, regardless of exact mount pixel size -------------\nconst titleSpace = 130;\nconst margin = 60;\nconst available = Math.min(\n  mount.width - margin * 2,\n  mount.height - titleSpace - margin\n);\nconst gridLeft = (mount.width - available) / 2;\nconst gridTop = titleSpace + (mount.height - titleSpace - margin - available) / 2;\n\n// The grid itself is content (like a heatmap's cell colors), not UI chrome —\n// it reads as a printed puzzle card, so its paper/ink tones stay fixed\n// instead of flipping with ANYPLOT_THEME (which would invert the black/white\n// cell metaphor and crush entry-cell contrast against a dark page).\nconst entryFill = \"#FFFDF6\";\nconst blockedFill = \"#1A1A17\";\nconst cellStroke = \"#4A4A44\";\nconst outerFrameStroke = \"#1A1A17\";\nconst numberFill = \"#1A1A17\";\n\n// --- Init ---------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option ---------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"crossword-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 40,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  grid: { left: gridLeft, top: gridTop, width: available, height: available },\n  xAxis: {\n    type: \"value\",\n    min: -0.5,\n    max: gridSize - 0.5,\n    show: false,\n  },\n  yAxis: {\n    type: \"value\",\n    min: -0.5,\n    max: gridSize - 0.5,\n    inverse: true,\n    show: false,\n  },\n  series: [\n    {\n      type: \"custom\",\n      coordinateSystem: \"cartesian2d\",\n      data: cells.map((cell) => [cell.col, cell.row]),\n      encode: { x: 0, y: 1 },\n      renderItem: (params, api) => {\n        const cell = cells[params.dataIndex];\n        const topLeft = api.coord([cell.col - 0.5, cell.row - 0.5]);\n        const size = api.size([1, 1]);\n        const children = [\n          {\n            type: \"rect\",\n            shape: { x: topLeft[0], y: topLeft[1], width: size[0], height: size[1] },\n            style: {\n              fill: cell.blocked ? blockedFill : entryFill,\n              stroke: cellStroke,\n              lineWidth: 1.5,\n            },\n          },\n        ];\n        if (cell.number !== null) {\n          children.push({\n            type: \"text\",\n            style: {\n              text: String(cell.number),\n              x: topLeft[0] + 6,\n              y: topLeft[1] + 4,\n              fill: numberFill,\n              fontSize: 15,\n              fontWeight: 600,\n              textVerticalAlign: \"top\",\n              textAlign: \"left\",\n            },\n          });\n        }\n        return { type: \"group\", children };\n      },\n    },\n  ],\n  // Bolder outer frame vs. the thinner inner dividers gives the printed card a\n  // typographic hierarchy instead of one uniform line weight throughout.\n  graphic: [\n    {\n      type: \"rect\",\n      shape: { x: gridLeft, y: gridTop, width: available, height: available },\n      style: { fill: \"none\", stroke: outerFrameStroke, lineWidth: 5 },\n      z: 100,\n      silent: true,\n    },\n  ],\n});\n"}