{"spec_id":"map-tilegrid","library":"d3","language":"javascript","code":"// anyplot.ai\n// map-tilegrid: Tile Grid Map for Equal-Area Geographic Comparison\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-05\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 110, right: 60, bottom: 170, left: 60 };\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Renewable energy share of gross final energy consumption (%), EU-27, 2022.\n// Countries are laid out on a tile grid that approximates their geographic\n// position, so every country gets identical visual weight regardless of area.\nconst data = [\n  { code: \"SE\", value: 66, row: 0, col: 4 },\n  { code: \"FI\", value: 47, row: 0, col: 5 },\n  { code: \"IE\", value: 13, row: 1, col: 0 },\n  { code: \"DK\", value: 41, row: 1, col: 3 },\n  { code: \"EE\", value: 38, row: 1, col: 6 },\n  { code: \"NL\", value: 15, row: 2, col: 2 },\n  { code: \"DE\", value: 20, row: 2, col: 3 },\n  { code: \"PL\", value: 16, row: 2, col: 4 },\n  { code: \"LV\", value: 43, row: 2, col: 6 },\n  { code: \"BE\", value: 13, row: 3, col: 1 },\n  { code: \"LU\", value: 11, row: 3, col: 2 },\n  { code: \"CZ\", value: 18, row: 3, col: 4 },\n  { code: \"SK\", value: 19, row: 3, col: 5 },\n  { code: \"LT\", value: 29, row: 3, col: 6 },\n  { code: \"FR\", value: 20, row: 4, col: 1 },\n  { code: \"AT\", value: 36, row: 4, col: 4 },\n  { code: \"HU\", value: 15, row: 4, col: 5 },\n  { code: \"RO\", value: 24, row: 4, col: 6 },\n  { code: \"PT\", value: 34, row: 5, col: 0 },\n  { code: \"ES\", value: 22, row: 5, col: 1 },\n  { code: \"IT\", value: 19, row: 5, col: 3 },\n  { code: \"SI\", value: 26, row: 5, col: 4 },\n  { code: \"HR\", value: 32, row: 5, col: 5 },\n  { code: \"BG\", value: 19, row: 5, col: 6 },\n  { code: \"MT\", value: 11, row: 6, col: 3 },\n  { code: \"GR\", value: 22, row: 6, col: 5 },\n  { code: \"CY\", value: 18, row: 6, col: 7 },\n];\n\nconst gridCols = d3.max(data, (d) => d.col) + 1;\nconst gridRows = d3.max(data, (d) => d.row) + 1;\nconst gap = 8;\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3\n  .select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\n\n// --- Tile layout (equal-area squares, centered in the plot area) ------------\nconst innerW = width - margin.left - margin.right;\nconst innerH = height - margin.top - margin.bottom;\nconst tileSize = Math.min(\n  (innerW - gap * (gridCols - 1)) / gridCols,\n  (innerH - gap * (gridRows - 1)) / gridRows,\n);\nconst gridWidth = gridCols * tileSize + gap * (gridCols - 1);\nconst gridHeight = gridRows * tileSize + gap * (gridRows - 1);\nconst offsetX = margin.left + (innerW - gridWidth) / 2;\nconst offsetY = margin.top + (innerH - gridHeight) / 2;\n\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${offsetX},${offsetY})`);\n\n// --- Color scale (sequential — single-polarity share-of-total data) --------\nconst [minValue, maxValue] = d3.extent(data, (d) => d.value);\nconst color = d3\n  .scaleSequential(d3.interpolateRgbBasis(t.seq))\n  .domain([minValue, maxValue]);\n\n// Perceived luminance decides whether tile text needs light or dark ink so it\n// stays legible against every fill the sequential scale can produce.\nfunction textInkFor(hex) {\n  const rgb = d3.rgb(hex);\n  const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;\n  return luminance > 0.55 ? \"#1A1A17\" : \"#FAF8F1\";\n}\n\n// --- Tiles --------------------------------------------------------------------\nconst tiles = g\n  .selectAll(\"g.tile\")\n  .data(data)\n  .join(\"g\")\n  .attr(\"class\", \"tile\")\n  .attr(\n    \"transform\",\n    (d) => `translate(${d.col * (tileSize + gap)},${d.row * (tileSize + gap)})`,\n  );\n\ntiles\n  .append(\"rect\")\n  .attr(\"width\", tileSize)\n  .attr(\"height\", tileSize)\n  .attr(\"rx\", 6)\n  .attr(\"fill\", (d) => color(d.value))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\ntiles\n  .append(\"text\")\n  .attr(\"x\", tileSize / 2)\n  .attr(\"y\", tileSize / 2 - tileSize * 0.06)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"middle\")\n  .attr(\"fill\", (d) => textInkFor(color(d.value)))\n  .style(\"font-size\", `${Math.max(16, tileSize * 0.22)}px`)\n  .style(\"font-weight\", \"700\")\n  .text((d) => d.code);\n\ntiles\n  .append(\"text\")\n  .attr(\"x\", tileSize / 2)\n  .attr(\"y\", tileSize / 2 + tileSize * 0.24)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"middle\")\n  .attr(\"fill\", (d) => textInkFor(color(d.value)))\n  .style(\"font-size\", `${Math.max(12, tileSize * 0.15)}px`)\n  .style(\"font-weight\", \"400\")\n  .text((d) => `${d.value}%`);\n\n// --- Legend (colorbar: value-to-color mapping) -------------------------------\nconst legendWidth = gridWidth;\nconst legendHeight = 22;\nconst legendX = offsetX;\nconst legendY = offsetY + gridHeight + 56;\n\nconst gradientId = \"imprint-seq-gradient\";\nconst gradientStops = d3.range(0, 1.0001, 0.1);\nsvg\n  .append(\"defs\")\n  .append(\"linearGradient\")\n  .attr(\"id\", gradientId)\n  .attr(\"x1\", \"0%\")\n  .attr(\"x2\", \"100%\")\n  .selectAll(\"stop\")\n  .data(gradientStops)\n  .join(\"stop\")\n  .attr(\"offset\", (d) => `${d * 100}%`)\n  .attr(\"stop-color\", (d) => color(minValue + d * (maxValue - minValue)));\n\nconst legend = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${legendX},${legendY})`);\n\nlegend\n  .append(\"rect\")\n  .attr(\"width\", legendWidth)\n  .attr(\"height\", legendHeight)\n  .attr(\"rx\", 4)\n  .attr(\"fill\", `url(#${gradientId})`)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1);\n\nlegend\n  .append(\"text\")\n  .attr(\"x\", 0)\n  .attr(\"y\", legendHeight + 26)\n  .attr(\"text-anchor\", \"start\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(`${minValue}%`);\n\nlegend\n  .append(\"text\")\n  .attr(\"x\", legendWidth)\n  .attr(\"y\", legendHeight + 26)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(`${maxValue}%`);\n\nlegend\n  .append(\"text\")\n  .attr(\"x\", legendWidth / 2)\n  .attr(\"y\", -14)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"Renewable energy share of consumption (%)\");\n\n// --- Title --------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 52)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"map-tilegrid · javascript · d3 · anyplot.ai\");\n"}