{"spec_id":"heatmap-periodic-table","library":"muix","language":"javascript","code":"// anyplot.ai\n// heatmap-periodic-table: Periodic Table Property Heatmap\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// Quality: 88/100 | Created: 2026-06-15\n//# anyplot-orientation: square\n// anyplot.ai\n// heatmap-periodic-table: Periodic Table Property Heatmap\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-06-15\n\nimport { ChartContainer } from \"@mui/x-charts\";\nimport Box from \"@mui/material/Box\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Element data: [Z, symbol, row (1-7 main, 8=lanthanides, 9=actinides), col (1-18), en|null]\n// Pauling electronegativity scale; null = no well-defined value\nconst ELEMENTS = [\n  [1,\"H\",1,1,2.20],[2,\"He\",1,18,null],\n  [3,\"Li\",2,1,0.98],[4,\"Be\",2,2,1.57],[5,\"B\",2,13,2.04],[6,\"C\",2,14,2.55],\n  [7,\"N\",2,15,3.04],[8,\"O\",2,16,3.44],[9,\"F\",2,17,3.98],[10,\"Ne\",2,18,null],\n  [11,\"Na\",3,1,0.93],[12,\"Mg\",3,2,1.31],[13,\"Al\",3,13,1.61],[14,\"Si\",3,14,1.90],\n  [15,\"P\",3,15,2.19],[16,\"S\",3,16,2.58],[17,\"Cl\",3,17,3.16],[18,\"Ar\",3,18,null],\n  [19,\"K\",4,1,0.82],[20,\"Ca\",4,2,1.00],[21,\"Sc\",4,3,1.36],[22,\"Ti\",4,4,1.54],\n  [23,\"V\",4,5,1.63],[24,\"Cr\",4,6,1.66],[25,\"Mn\",4,7,1.55],[26,\"Fe\",4,8,1.83],\n  [27,\"Co\",4,9,1.88],[28,\"Ni\",4,10,1.91],[29,\"Cu\",4,11,1.90],[30,\"Zn\",4,12,1.65],\n  [31,\"Ga\",4,13,1.81],[32,\"Ge\",4,14,2.01],[33,\"As\",4,15,2.18],[34,\"Se\",4,16,2.55],\n  [35,\"Br\",4,17,2.96],[36,\"Kr\",4,18,3.00],\n  [37,\"Rb\",5,1,0.82],[38,\"Sr\",5,2,0.95],[39,\"Y\",5,3,1.22],[40,\"Zr\",5,4,1.33],\n  [41,\"Nb\",5,5,1.60],[42,\"Mo\",5,6,2.16],[43,\"Tc\",5,7,1.90],[44,\"Ru\",5,8,2.20],\n  [45,\"Rh\",5,9,2.28],[46,\"Pd\",5,10,2.20],[47,\"Ag\",5,11,1.93],[48,\"Cd\",5,12,1.69],\n  [49,\"In\",5,13,1.78],[50,\"Sn\",5,14,1.96],[51,\"Sb\",5,15,2.05],[52,\"Te\",5,16,2.10],\n  [53,\"I\",5,17,2.66],[54,\"Xe\",5,18,2.60],\n  [55,\"Cs\",6,1,0.79],[56,\"Ba\",6,2,0.89],\n  [72,\"Hf\",6,4,1.30],[73,\"Ta\",6,5,1.50],[74,\"W\",6,6,2.36],[75,\"Re\",6,7,1.90],\n  [76,\"Os\",6,8,2.20],[77,\"Ir\",6,9,2.20],[78,\"Pt\",6,10,2.28],[79,\"Au\",6,11,2.54],\n  [80,\"Hg\",6,12,2.00],[81,\"Tl\",6,13,1.62],[82,\"Pb\",6,14,2.33],[83,\"Bi\",6,15,2.02],\n  [84,\"Po\",6,16,2.00],[85,\"At\",6,17,2.20],[86,\"Rn\",6,18,null],\n  [87,\"Fr\",7,1,0.70],[88,\"Ra\",7,2,0.90],\n  [104,\"Rf\",7,4,null],[105,\"Db\",7,5,null],[106,\"Sg\",7,6,null],[107,\"Bh\",7,7,null],\n  [108,\"Hs\",7,8,null],[109,\"Mt\",7,9,null],[110,\"Ds\",7,10,null],[111,\"Rg\",7,11,null],\n  [112,\"Cn\",7,12,null],[113,\"Nh\",7,13,null],[114,\"Fl\",7,14,null],[115,\"Mc\",7,15,null],\n  [116,\"Lv\",7,16,null],[117,\"Ts\",7,17,null],[118,\"Og\",7,18,null],\n  // Lanthanides (f-block row 1, cols 3-17)\n  [57,\"La\",8,3,1.10],[58,\"Ce\",8,4,1.12],[59,\"Pr\",8,5,1.13],[60,\"Nd\",8,6,1.14],\n  [61,\"Pm\",8,7,null],[62,\"Sm\",8,8,1.17],[63,\"Eu\",8,9,null],[64,\"Gd\",8,10,1.20],\n  [65,\"Tb\",8,11,null],[66,\"Dy\",8,12,1.22],[67,\"Ho\",8,13,1.23],[68,\"Er\",8,14,1.24],\n  [69,\"Tm\",8,15,1.25],[70,\"Yb\",8,16,null],[71,\"Lu\",8,17,1.27],\n  // Actinides (f-block row 2, cols 3-17)\n  [89,\"Ac\",9,3,1.10],[90,\"Th\",9,4,1.30],[91,\"Pa\",9,5,1.50],[92,\"U\",9,6,1.38],\n  [93,\"Np\",9,7,1.36],[94,\"Pu\",9,8,1.28],[95,\"Am\",9,9,1.30],[96,\"Cm\",9,10,1.30],\n  [97,\"Bk\",9,11,1.30],[98,\"Cf\",9,12,1.30],[99,\"Es\",9,13,1.30],[100,\"Fm\",9,14,1.30],\n  [101,\"Md\",9,15,1.30],[102,\"No\",9,16,1.30],[103,\"Lr\",9,17,null],\n];\n\nconst EN_MIN = 0.70;\nconst EN_MAX = 3.98;\n\n// Imprint sequential colormap: #009E73 → #4467A3\nconst C0 = [0, 158, 115];\nconst C1 = [68, 103, 163];\n\nfunction tileColor(en) {\n  if (en === null) return null;\n  const norm = (en - EN_MIN) / (EN_MAX - EN_MIN);\n  const r = Math.round(C0[0] + norm * (C1[0] - C0[0]));\n  const g = Math.round(C0[1] + norm * (C1[1] - C0[1]));\n  const b = Math.round(C0[2] + norm * (C1[2] - C0[2]));\n  return `rgb(${r},${g},${b})`;\n}\n\n// WCAG-based luminance to pick readable text on colored tile\nfunction tileTextColor(en) {\n  if (en === null) return t.inkSoft;\n  const norm = (en - EN_MIN) / (EN_MAX - EN_MIN);\n  const r = C0[0] + norm * (C1[0] - C0[0]);\n  const g = C0[1] + norm * (C1[1] - C0[1]);\n  const b = C0[2] + norm * (C1[2] - C0[2]);\n  const lin = [r / 255, g / 255, b / 255].map(\n    (v) => (v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4))\n  );\n  const lum = 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2];\n  return lum > 0.18 ? \"#1A1A17\" : \"#F0EFE8\";\n}\n\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n  const THEME = window.ANYPLOT_THEME;\n\n  const NULL_FILL = THEME === \"light\" ? \"#CCCBC3\" : \"#3E3E3A\";\n\n  const GAP = 2;\n  const LEFT = 16;\n  const TILE = Math.floor((W - LEFT * 2 - 17 * GAP) / 18);\n  const F_GAP = 20;\n  const CB_H = 16;\n\n  // Vertical centering: compute yOffset so blank space is balanced top/bottom.\n  // Content block spans from title-top (~TITLE_Y_BASE - TITLE_FONT) to colorbar note.\n  const TITLE_FONT = 20;\n  const TITLE_Y_BASE = 46;\n  const SUBTITLE_Y_BASE = 68;\n  const INITIAL_TABLE_TOP = 88; // subtitle baseline + 20px gap\n\n  // Distance from TABLE_TOP to bottom of last f-block row:\n  // tileY(9)+TILE = TABLE_TOP + 8*(TILE+GAP) + F_GAP + TILE\n  const tableBottomOff = 8 * (TILE + GAP) + TILE + F_GAP;\n  // Colorbar note (last element) is at TABLE_TOP + tableBottomOff + 38 + CB_H + 46\n  const contentBottomOff = tableBottomOff + 38 + CB_H + 46;\n\n  const contentTop = TITLE_Y_BASE - TITLE_FONT; // approx 26px\n  const contentBottomNoShift = INITIAL_TABLE_TOP + contentBottomOff;\n  const contentHeight = contentBottomNoShift - contentTop;\n  const yOffset = Math.max(0, Math.round((H - contentHeight) / 2 - contentTop));\n\n  const TABLE_TOP = INITIAL_TABLE_TOP + yOffset;\n  const titleY = TITLE_Y_BASE + yOffset;\n  const subtitleY = SUBTITLE_Y_BASE + yOffset;\n\n  function tileX(col) {\n    return LEFT + (col - 1) * (TILE + GAP);\n  }\n  function tileY(row) {\n    if (row <= 7) return TABLE_TOP + (row - 1) * (TILE + GAP);\n    const fTop = TABLE_TOP + 7 * (TILE + GAP) + F_GAP;\n    return fTop + (row - 8) * (TILE + GAP);\n  }\n\n  const tableBottom = tileY(9) + TILE;\n  const cbTop = tableBottom + 38;\n  const cbWidth = Math.round(W * 0.52);\n  const cbLeft = Math.round((W - cbWidth) / 2);\n\n  const cbTicks = [\n    { val: 0.70, label: \"0.70\" },\n    { val: 1.50, label: \"1.50\" },\n    { val: 2.50, label: \"2.50\" },\n    { val: 3.50, label: \"3.50\" },\n    { val: 3.98, label: \"3.98\" },\n  ];\n\n  // ChartContainer is the MUI X Charts composition root; its SVG surface\n  // hosts the periodic table tiles drawn as direct SVG children.\n  return (\n    <Box sx={{ width: W, height: H, bgcolor: t.pageBg }}>\n      <ChartContainer width={W} height={H} series={[]} skipAnimation>\n        <defs>\n          {/* Imprint sequential gradient for colorbar */}\n          <linearGradient id=\"pt-imprint-seq\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\">\n            <stop offset=\"0%\" stopColor=\"#009E73\" />\n            <stop offset=\"100%\" stopColor=\"#4467A3\" />\n          </linearGradient>\n        </defs>\n\n        {/* Title */}\n        <text\n          x={W / 2}\n          y={titleY}\n          textAnchor=\"middle\"\n          fontSize={20}\n          fontWeight=\"500\"\n          fill={t.ink}\n          fontFamily=\"Inter, Roboto, sans-serif\"\n        >\n          heatmap-periodic-table · javascript · muix · anyplot.ai\n        </text>\n        <text\n          x={W / 2}\n          y={subtitleY}\n          textAnchor=\"middle\"\n          fontSize={13}\n          fill={t.inkSoft}\n          fontFamily=\"Inter, Roboto, sans-serif\"\n        >\n          Pauling Electronegativity — periodic trends across all elements\n        </text>\n\n        {/* Placeholder tiles at period 6 & 7, group 3 (f-block location markers) */}\n        {[{ pr: 6, lbl: \"57–71\" }, { pr: 7, lbl: \"89–103\" }].map(({ pr, lbl }) => {\n          const x = tileX(3);\n          const y = tileY(pr);\n          return (\n            <g key={`ph${pr}`}>\n              <rect\n                x={x} y={y} width={TILE} height={TILE}\n                fill=\"none\"\n                stroke={t.inkSoft}\n                strokeWidth={0.75}\n                strokeDasharray=\"3,2\"\n                rx={1}\n              />\n              <text\n                x={x + TILE / 2}\n                y={y + TILE / 2 + 3}\n                textAnchor=\"middle\"\n                fontSize={7}\n                fill={t.inkSoft}\n                fontFamily=\"monospace\"\n              >\n                {lbl}\n              </text>\n            </g>\n          );\n        })}\n\n        {/* Element tiles */}\n        {ELEMENTS.map(([Z, sym, row, col, en]) => {\n          const x = tileX(col);\n          const y = tileY(row);\n          const fill = tileColor(en) ?? NULL_FILL;\n          const txt = tileTextColor(en);\n          return (\n            <g key={Z}>\n              <rect x={x} y={y} width={TILE} height={TILE} fill={fill} rx={1} />\n              {/* Atomic number — top-left corner */}\n              <text\n                x={x + 2} y={y + 8}\n                fontSize={7}\n                fill={txt}\n                fontFamily=\"monospace\"\n                opacity={0.9}\n              >\n                {Z}\n              </text>\n              {/* Element symbol — centered */}\n              <text\n                x={x + TILE / 2}\n                y={y + TILE / 2 + 5}\n                textAnchor=\"middle\"\n                fontSize={13}\n                fontWeight=\"700\"\n                fill={txt}\n                fontFamily=\"Inter, Roboto, sans-serif\"\n              >\n                {sym}\n              </text>\n              {/* Electronegativity value — bottom */}\n              {en !== null && (\n                <text\n                  x={x + TILE / 2}\n                  y={y + TILE - 4}\n                  textAnchor=\"middle\"\n                  fontSize={8}\n                  fill={txt}\n                  fontFamily=\"monospace\"\n                  opacity={0.88}\n                >\n                  {en.toFixed(2)}\n                </text>\n              )}\n            </g>\n          );\n        })}\n\n        {/* Colorbar label */}\n        <text\n          x={W / 2}\n          y={cbTop - 10}\n          textAnchor=\"middle\"\n          fontSize={12}\n          fill={t.ink}\n          fontFamily=\"Inter, Roboto, sans-serif\"\n        >\n          Electronegativity (Pauling Scale)\n        </text>\n\n        {/* Colorbar gradient rect */}\n        <rect\n          x={cbLeft}\n          y={cbTop}\n          width={cbWidth}\n          height={CB_H}\n          fill=\"url(#pt-imprint-seq)\"\n          rx={2}\n        />\n\n        {/* Colorbar ticks and labels */}\n        {cbTicks.map(({ val, label }) => {\n          const nx = cbLeft + ((val - EN_MIN) / (EN_MAX - EN_MIN)) * cbWidth;\n          return (\n            <g key={val}>\n              <line\n                x1={nx} y1={cbTop + CB_H}\n                x2={nx} y2={cbTop + CB_H + 4}\n                stroke={t.inkSoft}\n                strokeWidth={1}\n              />\n              <text\n                x={nx}\n                y={cbTop + CB_H + 15}\n                textAnchor=\"middle\"\n                fontSize={10}\n                fill={t.inkSoft}\n                fontFamily=\"monospace\"\n              >\n                {label}\n              </text>\n            </g>\n          );\n        })}\n\n        {/* Low / High labels */}\n        <text\n          x={cbLeft}\n          y={cbTop + CB_H + 30}\n          textAnchor=\"start\"\n          fontSize={10}\n          fill={t.inkSoft}\n          fontFamily=\"Inter, Roboto, sans-serif\"\n        >\n          Low (Fr = 0.70)\n        </text>\n        <text\n          x={cbLeft + cbWidth}\n          y={cbTop + CB_H + 30}\n          textAnchor=\"end\"\n          fontSize={10}\n          fill={t.inkSoft}\n          fontFamily=\"Inter, Roboto, sans-serif\"\n        >\n          High (F = 3.98)\n        </text>\n        <text\n          x={cbLeft + cbWidth / 2}\n          y={cbTop + CB_H + 46}\n          textAnchor=\"middle\"\n          fontSize={10}\n          fill={t.inkSoft}\n          fontFamily=\"Inter, Roboto, sans-serif\"\n          fontStyle=\"italic\"\n        >\n          Grey tiles = no defined value (noble gases, synthetic elements)\n        </text>\n      </ChartContainer>\n    </Box>\n  );\n}\n"}