{"spec_id":"map-tilegrid","library":"muix","language":"javascript","code":"// anyplot.ai\n// map-tilegrid: Tile Grid Map for Equal-Area Geographic Comparison\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-05\n\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\nimport { ScatterChart } from \"@mui/x-charts/ScatterChart\";\nimport { ContinuousColorLegend } from \"@mui/x-charts/ChartsLegend\";\n\nconst tokens = window.ANYPLOT_TOKENS;\n\n// --- Data: renewable electricity share (%) by European country, placed on an\n// approximate compass tile grid (row 0 = north, col 0 = west). Every country\n// gets one equally-sized tile, so a small nation (Estonia) reads with the\n// same visual weight as a large one (France) — the point of a tile grid. ----\nconst countries = [\n  { abbr: \"IS\", name: \"Iceland\", row: 0, col: 0, value: 85 },\n  { abbr: \"NO\", name: \"Norway\", row: 0, col: 3, value: 98 },\n  { abbr: \"SE\", name: \"Sweden\", row: 0, col: 5, value: 65 },\n  { abbr: \"FI\", name: \"Finland\", row: 0, col: 7, value: 55 },\n  { abbr: \"IE\", name: \"Ireland\", row: 1, col: 0, value: 40 },\n  { abbr: \"GB\", name: \"United Kingdom\", row: 1, col: 1, value: 43 },\n  { abbr: \"DK\", name: \"Denmark\", row: 1, col: 4, value: 62 },\n  { abbr: \"EE\", name: \"Estonia\", row: 1, col: 8, value: 32 },\n  { abbr: \"NL\", name: \"Netherlands\", row: 2, col: 2, value: 37 },\n  { abbr: \"DE\", name: \"Germany\", row: 2, col: 4, value: 46 },\n  { abbr: \"PL\", name: \"Poland\", row: 2, col: 6, value: 18 },\n  { abbr: \"LT\", name: \"Lithuania\", row: 2, col: 8, value: 28 },\n  { abbr: \"BE\", name: \"Belgium\", row: 3, col: 1, value: 24 },\n  { abbr: \"CZ\", name: \"Czechia\", row: 3, col: 4, value: 17 },\n  { abbr: \"SK\", name: \"Slovakia\", row: 3, col: 5, value: 23 },\n  { abbr: \"UA\", name: \"Ukraine\", row: 3, col: 8, value: 9 },\n  { abbr: \"FR\", name: \"France\", row: 4, col: 1, value: 25 },\n  { abbr: \"CH\", name: \"Switzerland\", row: 4, col: 3, value: 72 },\n  { abbr: \"AT\", name: \"Austria\", row: 4, col: 4, value: 78 },\n  { abbr: \"HU\", name: \"Hungary\", row: 4, col: 5, value: 14 },\n  { abbr: \"RO\", name: \"Romania\", row: 4, col: 7, value: 43 },\n  { abbr: \"PT\", name: \"Portugal\", row: 5, col: 0, value: 61 },\n  { abbr: \"ES\", name: \"Spain\", row: 5, col: 1, value: 46 },\n  { abbr: \"IT\", name: \"Italy\", row: 5, col: 4, value: 35 },\n  { abbr: \"HR\", name: \"Croatia\", row: 5, col: 5, value: 55 },\n  { abbr: \"RS\", name: \"Serbia\", row: 5, col: 6, value: 30 },\n  { abbr: \"BG\", name: \"Bulgaria\", row: 5, col: 7, value: 19 },\n  { abbr: \"GR\", name: \"Greece\", row: 6, col: 6, value: 40 },\n];\n\nconst N_COLS = Math.max(...countries.map((c) => c.col)) + 1;\nconst N_ROWS = Math.max(...countries.map((c) => c.row)) + 1;\nconst colCategories = Array.from({ length: N_COLS }, (_, i) => String(i));\nconst rowCategories = Array.from({ length: N_ROWS }, (_, i) => String(i));\n\nconst points = countries.map((c) => ({\n  id: c.abbr,\n  x: String(c.col),\n  y: String(c.row),\n  z: c.value,\n}));\n\nconst shares = countries.map((c) => c.value);\nconst minShare = Math.min(...shares);\nconst maxShare = Math.max(...shares);\n\n// Perceived luminance of the tile fill decides whether the abbreviation reads\n// better in dark or light ink — the fill itself always comes from the\n// continuous Imprint scale, never a custom hex.\nfunction textColorFor(hex) {\n  const n = parseInt(hex.slice(1), 16);\n  const r = (n >> 16) & 255;\n  const g = (n >> 8) & 255;\n  const b = n & 255;\n  const toLinear = (c) => {\n    const cs = c / 255;\n    return cs <= 0.03928 ? cs / 12.92 : ((cs + 0.055) / 1.055) ** 2.4;\n  };\n  const luminance = 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);\n  return luminance > 0.45 ? \"#1A1A17\" : \"#F0EFE8\";\n}\n\n// Custom scatter mark: opaque square tiles (instead of the default circles)\n// sized from the band scales' own bandwidth, so every tile is identical in\n// area regardless of how few or many neighbors share its row/column.\nfunction CountryTile(props) {\n  const { series, xScale, yScale, colorGetter, color } = props;\n  const cellWidth = xScale.bandwidth();\n  const cellHeight = yScale.bandwidth();\n  const tileSize = Math.min(cellWidth, cellHeight);\n  const fontSize = tileSize * 0.32;\n\n  return (\n    <g>\n      {series.data.map((point, i) => {\n        const cx = (xScale(point.x) ?? 0) + cellWidth / 2;\n        const cy = (yScale(point.y) ?? 0) + cellHeight / 2;\n        const fill = colorGetter ? colorGetter(i) : color;\n        return (\n          <g key={point.id}>\n            <rect\n              x={cx - tileSize / 2}\n              y={cy - tileSize / 2}\n              width={tileSize}\n              height={tileSize}\n              rx={tileSize * 0.1}\n              fill={fill}\n              stroke={tokens.pageBg}\n              strokeWidth={3}\n            />\n            <text\n              x={cx}\n              y={cy}\n              textAnchor=\"middle\"\n              dominantBaseline=\"central\"\n              fontSize={fontSize}\n              fontWeight={600}\n              fill={textColorFor(fill)}\n            >\n              {point.id}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const TITLE_BLOCK_HEIGHT = 100;\n  const chartWidth = width;\n  const chartHeight = height - TITLE_BLOCK_HEIGHT;\n\n  // ContinuousColorLegend positions its bounding box (bar + both labels)\n  // flush against the *full* width passed to ScatterChart — it ignores\n  // margin.right entirely, so the tile grid's margin math can't push it\n  // inward. Render the chart itself slightly narrower than the mount so a\n  // safety strip of true background is always left between the legend's\n  // rightmost label glyph and the physical canvas edge.\n  const RIGHT_SAFETY = 12;\n  const renderWidth = chartWidth - RIGHT_SAFETY;\n\n  // Reserve a slice on the right for the continuous colorbar legend, then\n  // fit the largest uniform tile pitch that keeps every tile the same size\n  // (the \"equal area\" rule the spec calls out as the whole point of a tile\n  // grid map) inside whatever space remains. The legend itself is only as\n  // wide as its longest label (its bar and labels are stacked, not summed),\n  // so the reserve only needs to cover that plus a small gap.\n  const PAD = 28;\n  const LEGEND_RESERVE = 64;\n  const availW = renderWidth - PAD * 2 - LEGEND_RESERVE;\n  const availH = chartHeight - PAD * 2;\n  const cellPitch = Math.min(availW / N_COLS, availH / N_ROWS);\n  const gridWidth = cellPitch * N_COLS;\n  const gridHeight = cellPitch * N_ROWS;\n\n  const marginLeft = PAD + (availW - gridWidth) / 2;\n  const marginTop = PAD + (availH - gridHeight) / 2;\n  const marginRight = renderWidth - marginLeft - gridWidth;\n  const marginBottom = chartHeight - marginTop - gridHeight;\n\n  return (\n    <Box sx={{ width, height, bgcolor: tokens.pageBg, display: \"flex\", flexDirection: \"column\" }}>\n      <Box\n        sx={{\n          height: TITLE_BLOCK_HEIGHT,\n          display: \"flex\",\n          flexDirection: \"column\",\n          justifyContent: \"center\",\n          alignItems: \"center\",\n        }}\n      >\n        <Typography sx={{ color: tokens.ink, fontSize: 22, fontWeight: 500, lineHeight: 1.2, fontFamily: \"inherit\" }}>\n          map-tilegrid · javascript · muix · anyplot.ai\n        </Typography>\n        <Typography sx={{ color: tokens.inkSoft, fontSize: 13, lineHeight: 1.2, fontFamily: \"inherit\", pt: \"4px\" }}>\n          Renewable electricity share by country — equal tile weight, not land area\n        </Typography>\n      </Box>\n      <Box sx={{ flex: 1, display: \"flex\" }}>\n        <ScatterChart\n          width={renderWidth}\n          height={chartHeight}\n          skipAnimation\n          disableVoronoi\n          series={[\n            {\n              id: \"renewables\",\n              type: \"scatter\",\n              data: points,\n              label: \"Renewable electricity share\",\n              xAxisId: \"col\",\n              yAxisId: \"row\",\n              zAxisId: \"share\",\n              valueFormatter: (value, { dataIndex }) =>\n                `${countries[dataIndex].name}: ${value.z}% renewable`,\n            },\n          ]}\n          xAxis={[{ id: \"col\", scaleType: \"band\", data: colCategories, categoryGapRatio: 0.12 }]}\n          yAxis={[{ id: \"row\", scaleType: \"band\", data: rowCategories, categoryGapRatio: 0.12 }]}\n          zAxis={[\n            {\n              id: \"share\",\n              min: minShare,\n              max: maxShare,\n              colorMap: { type: \"continuous\", min: minShare, max: maxShare, color: [tokens.seq[0], tokens.seq[1]] },\n            },\n          ]}\n          bottomAxis={null}\n          leftAxis={null}\n          margin={{ top: marginTop, right: marginRight, bottom: marginBottom, left: marginLeft }}\n          slots={{ scatter: CountryTile }}\n          slotProps={{ legend: { hidden: true } }}\n        >\n          <ContinuousColorLegend\n            axisId=\"share\"\n            axisDirection=\"z\"\n            position={{ horizontal: \"right\", vertical: \"middle\" }}\n            direction=\"column\"\n            length=\"55%\"\n            thickness={14}\n            minLabel={({ formattedValue }) => `${formattedValue}%`}\n            maxLabel={({ formattedValue }) => `${formattedValue}%`}\n            // MUI X measures this label off-screen against `document.body`\n            // (not `#container`), so \"inherit\" would pick up the browser's\n            // default serif font instead of the sans-serif stack the label\n            // actually renders in — an explicit family keeps the measured\n            // and rendered widths in sync.\n            labelStyle={{\n              fontSize: 13,\n              fill: tokens.inkSoft,\n              fontFamily: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif',\n            }}\n          />\n        </ScatterChart>\n      </Box>\n    </Box>\n  );\n}\n"}