{"spec_id":"heatmap-periodic-table","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// heatmap-periodic-table: Periodic Table Property Heatmap\n// Library: highcharts 12.6.0 | JavaScript 22.22.3\n// Quality: 87/100 | Created: 2026-06-15\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst isDark = window.ANYPLOT_THEME === 'dark';\nconst W = window.ANYPLOT_SIZE.width;   // 1200 (square)\nconst H = window.ANYPLOT_SIZE.height;  // 1200\n\n// --- Color utilities ---\nfunction hexToRgb(hex) {\n  return [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];\n}\n\nfunction colorAt(frac) {\n  const a = hexToRgb(t.seq[0]);   // #009E73 brand green\n  const b = hexToRgb(t.seq[1]);   // #4467A3 blue\n  const r = Math.round(a[0] + (b[0] - a[0]) * frac);\n  const g = Math.round(a[1] + (b[1] - a[1]) * frac);\n  const bl = Math.round(a[2] + (b[2] - a[2]) * frac);\n  return 'rgb(' + r + ',' + g + ',' + bl + ')';\n}\n\nfunction lum(rgbStr) {\n  return rgbStr.match(/\\d+/g).map(Number).reduce((acc, c, i) => {\n    const v = c / 255;\n    const lin = v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);\n    return acc + lin * [0.2126, 0.7152, 0.0722][i];\n  }, 0);\n}\n\n// --- Element data: [symbol, Z, group, period, IE1 kJ/mol | null] ---\n// period 8 = lanthanide f-block row; period 9 = actinide f-block row\n// group in f-block rows: 3=La/Ac, 4=Ce/Th, ..., 17=Lu/Lr\nconst elements = [\n  // Period 1\n  ['H', 1, 1, 1, 1312], ['He', 2, 18, 1, 2372],\n  // Period 2\n  ['Li', 3, 1, 2, 520], ['Be', 4, 2, 2, 900], ['B', 5, 13, 2, 800], ['C', 6, 14, 2, 1086],\n  ['N', 7, 15, 2, 1402], ['O', 8, 16, 2, 1314], ['F', 9, 17, 2, 1681], ['Ne', 10, 18, 2, 2081],\n  // Period 3\n  ['Na', 11, 1, 3, 496], ['Mg', 12, 2, 3, 738], ['Al', 13, 13, 3, 578], ['Si', 14, 14, 3, 786],\n  ['P', 15, 15, 3, 1012], ['S', 16, 16, 3, 1000], ['Cl', 17, 17, 3, 1251], ['Ar', 18, 18, 3, 1521],\n  // Period 4\n  ['K', 19, 1, 4, 419], ['Ca', 20, 2, 4, 590], ['Sc', 21, 3, 4, 633], ['Ti', 22, 4, 4, 659],\n  ['V', 23, 5, 4, 651], ['Cr', 24, 6, 4, 653], ['Mn', 25, 7, 4, 717], ['Fe', 26, 8, 4, 762],\n  ['Co', 27, 9, 4, 760], ['Ni', 28, 10, 4, 737], ['Cu', 29, 11, 4, 745], ['Zn', 30, 12, 4, 906],\n  ['Ga', 31, 13, 4, 579], ['Ge', 32, 14, 4, 762], ['As', 33, 15, 4, 947], ['Se', 34, 16, 4, 941],\n  ['Br', 35, 17, 4, 1140], ['Kr', 36, 18, 4, 1351],\n  // Period 5\n  ['Rb', 37, 1, 5, 403], ['Sr', 38, 2, 5, 550], ['Y', 39, 3, 5, 600], ['Zr', 40, 4, 5, 640],\n  ['Nb', 41, 5, 5, 652], ['Mo', 42, 6, 5, 684], ['Tc', 43, 7, 5, 702], ['Ru', 44, 8, 5, 711],\n  ['Rh', 45, 9, 5, 720], ['Pd', 46, 10, 5, 804], ['Ag', 47, 11, 5, 731], ['Cd', 48, 12, 5, 868],\n  ['In', 49, 13, 5, 558], ['Sn', 50, 14, 5, 709], ['Sb', 51, 15, 5, 834], ['Te', 52, 16, 5, 869],\n  ['I', 53, 17, 5, 1008], ['Xe', 54, 18, 5, 1170],\n  // Period 6 main (group 3 left for La placeholder)\n  ['Cs', 55, 1, 6, 376], ['Ba', 56, 2, 6, 503],\n  ['Hf', 72, 4, 6, 659], ['Ta', 73, 5, 6, 761], ['W', 74, 6, 6, 770], ['Re', 75, 7, 6, 760],\n  ['Os', 76, 8, 6, 840], ['Ir', 77, 9, 6, 880], ['Pt', 78, 10, 6, 870], ['Au', 79, 11, 6, 890],\n  ['Hg', 80, 12, 6, 1007], ['Tl', 81, 13, 6, 589], ['Pb', 82, 14, 6, 716], ['Bi', 83, 15, 6, 703],\n  ['Po', 84, 16, 6, 812], ['At', 85, 17, 6, 930], ['Rn', 86, 18, 6, 1037],\n  // Period 7 main (group 3 left for Ac placeholder)\n  ['Fr', 87, 1, 7, 380], ['Ra', 88, 2, 7, 509],\n  ['Rf', 104, 4, 7, 580], ['Db', 105, 5, 7, null], ['Sg', 106, 6, 7, null], ['Bh', 107, 7, 7, null],\n  ['Hs', 108, 8, 7, null], ['Mt', 109, 9, 7, null], ['Ds', 110, 10, 7, null], ['Rg', 111, 11, 7, null],\n  ['Cn', 112, 12, 7, 1155], ['Nh', 113, 13, 7, null], ['Fl', 114, 14, 7, null], ['Mc', 115, 15, 7, null],\n  ['Lv', 116, 16, 7, null], ['Ts', 117, 17, 7, null], ['Og', 118, 18, 7, null],\n  // Lanthanides — period 8, group 3..17 (La=3, Ce=4, ..., Lu=17)\n  ['La', 57, 3, 8, 538], ['Ce', 58, 4, 8, 528], ['Pr', 59, 5, 8, 523], ['Nd', 60, 6, 8, 530],\n  ['Pm', 61, 7, 8, 536], ['Sm', 62, 8, 8, 543], ['Eu', 63, 9, 8, 547], ['Gd', 64, 10, 8, 593],\n  ['Tb', 65, 11, 8, 565], ['Dy', 66, 12, 8, 572], ['Ho', 67, 13, 8, 581], ['Er', 68, 14, 8, 589],\n  ['Tm', 69, 15, 8, 597], ['Yb', 70, 16, 8, 603], ['Lu', 71, 17, 8, 524],\n  // Actinides — period 9, group 3..17 (Ac=3, Th=4, ..., Lr=17)\n  ['Ac', 89, 3, 9, 499], ['Th', 90, 4, 9, 587], ['Pa', 91, 5, 9, 568], ['U', 92, 6, 9, 598],\n  ['Np', 93, 7, 9, 605], ['Pu', 94, 8, 9, 585], ['Am', 95, 9, 9, 578], ['Cm', 96, 10, 9, 581],\n  ['Bk', 97, 11, 9, 601], ['Cf', 98, 12, 9, 608], ['Es', 99, 13, 9, 619], ['Fm', 100, 14, 9, 627],\n  ['Md', 101, 15, 9, 635], ['No', 102, 16, 9, 642], ['Lr', 103, 17, 9, 479],\n];\n\nconst ieValues = elements.map(e => e[4]).filter(v => v !== null);\nconst minIE = Math.min(...ieValues);  // 376 (Cs)\nconst maxIE = Math.max(...ieValues);  // 2372 (He)\n\n// --- Layout (CSS px; mount = 1200×1200 square) ---\nconst PITCH = 64;\nconst TILE = PITCH - 2;   // 62px tiles with 2px gap\nconst GRID_LEFT = Math.round((W - 18 * PITCH) / 2);  // center 18-column grid\n\nconst TITLE_H = 64;\nconst GRP_H = 20;              // group-number row above tiles\nconst MAIN_H = 7 * PITCH;     // 448px\nconst FGAP_H = Math.round(PITCH * 0.55);  // 35px gap before f-block\nconst FBLOCK_H = 2 * PITCH;   // 128px\nconst CB_H = 22;               // colorbar height\nconst CB_LABEL_H = 44;         // two label rows (tick values + property name)\nconst CONTENT_H = TITLE_H + GRP_H + MAIN_H + FGAP_H + FBLOCK_H + 30 + CB_H + CB_LABEL_H;\nconst TOP = Math.round((H - CONTENT_H) / 2);\n\nconst TITLE_Y = TOP + Math.round(TITLE_H * 0.72);\nconst GRP_TOP = TOP + TITLE_H;\nconst GRID_TOP = GRP_TOP + GRP_H;\nconst FBLOCK_TOP = GRID_TOP + MAIN_H + FGAP_H;\nconst CB_TOP = FBLOCK_TOP + FBLOCK_H + 30;\n\n// --- Render via Highcharts SVG renderer ---\nconst chart = Highcharts.chart('container', {\n  chart: {\n    backgroundColor: 'transparent',\n    animation: false,\n    style: { fontFamily: 'inherit' },\n    margin: [0, 0, 0, 0],\n    spacing: [0, 0, 0, 0],\n  },\n  title: { text: '' },\n  credits: { enabled: false },\n  xAxis: { visible: false },\n  yAxis: { visible: false },\n  legend: { enabled: false },\n  tooltip: { enabled: false },\n  plotOptions: { series: { animation: false } },\n  series: [],\n});\n\nconst R = chart.renderer;\n\n// --- Title ---\nconst titleText = 'First Ionization Energy · heatmap-periodic-table · javascript · highcharts · anyplot.ai';\nconst titleFS = Math.max(Math.round(22 * 67 / titleText.length), 12);\nR.text(titleText, W / 2, TITLE_Y)\n  .attr({ align: 'center', zIndex: 5 })\n  .css({ color: t.ink, fontSize: titleFS + 'px', fontWeight: '600' })\n  .add();\n\n// --- Group numbers (1–18) above main table ---\nfor (var g = 1; g <= 18; g++) {\n  R.text(String(g), GRID_LEFT + (g - 1) * PITCH + PITCH / 2, GRP_TOP + 14)\n    .attr({ align: 'center', zIndex: 3 })\n    .css({ color: t.inkSoft, fontSize: '9px' })\n    .add();\n}\n\n// --- Draw a single element tile ---\nfunction drawTile(x, y, sym, z, ie) {\n  const frac = ie !== null ? (ie - minIE) / (maxIE - minIE) : null;\n  const fill = frac !== null\n    ? colorAt(frac)\n    : (isDark ? '#383834' : '#C0BFB8');\n  const textCol = frac !== null\n    ? (lum(fill) > 0.18 ? '#1A1A17' : '#F0EFE8')\n    : (isDark ? '#6B6A63' : '#8A8A83');\n\n  const numFS = Math.round(TILE * 0.145);  // ~9px\n  const symFS = Math.round(TILE * 0.265);  // ~16px\n\n  R.rect(x, y, TILE, TILE, 4)   // 4px rounded corners\n    .attr({ fill: fill, zIndex: 1 })\n    .add();\n\n  // Atomic number — top-left corner\n  R.text(String(z), x + 3, y + numFS + 2)\n    .attr({ zIndex: 3 })\n    .css({ color: textCol, fontSize: numFS + 'px', lineHeight: '1' })\n    .add();\n\n  // Element symbol — centered\n  R.text(sym, x + TILE / 2, y + TILE / 2 + Math.round(symFS * 0.38))\n    .attr({ align: 'center', zIndex: 3 })\n    .css({ color: textCol, fontSize: symFS + 'px', fontWeight: '700' })\n    .add();\n}\n\n// --- Main table and f-block tiles ---\nelements.forEach(function(el) {\n  var sym = el[0], z = el[1], grp = el[2], per = el[3], ie = el[4];\n  var x, y;\n  if (per <= 7) {\n    x = GRID_LEFT + (grp - 1) * PITCH;\n    y = GRID_TOP + (per - 1) * PITCH;\n  } else {\n    // f-block: group 3..17 aligns with main table columns\n    x = GRID_LEFT + (grp - 1) * PITCH;\n    y = FBLOCK_TOP + (per - 8) * PITCH;\n  }\n  drawTile(x, y, sym, z, ie);\n});\n\n// --- Placeholder tiles at group 3 for periods 6 and 7 ---\n[\n  [6, '57–71'],\n  [7, '89–103'],\n].forEach(function(pair) {\n  var per = pair[0], label = pair[1];\n  var x = GRID_LEFT + 2 * PITCH;\n  var y = GRID_TOP + (per - 1) * PITCH;\n  var fill = isDark ? '#383834' : '#C0BFB8';\n  var col = isDark ? '#6B6A63' : '#8A8A83';\n  R.rect(x, y, TILE, TILE, 4)\n    .attr({ fill: fill, zIndex: 1 })\n    .add();\n  R.text(label, x + TILE / 2, y + TILE / 2 + 5)\n    .attr({ align: 'center', zIndex: 3 })\n    .css({ color: col, fontSize: '9px' })\n    .add();\n});\n\n// --- Connector lines: placeholder tiles → f-block rows (dashed) ---\nvar connColor = isDark ? '#4A4A44' : '#BEBDB6';\nvar connX = GRID_LEFT + 2 * PITCH + Math.round(TILE / 2);\n\n// Period 6 placeholder → Lanthanide row\nR.path(['M', connX, GRID_TOP + 5 * PITCH + TILE + 1, 'L', connX, FBLOCK_TOP - 1])\n  .attr({ stroke: connColor, 'stroke-width': 1, 'stroke-dasharray': '3,3', zIndex: 1 })\n  .add();\n\n// Period 7 placeholder → Actinide row\nR.path(['M', connX, GRID_TOP + 6 * PITCH + TILE + 1, 'L', connX, FBLOCK_TOP + PITCH - 1])\n  .attr({ stroke: connColor, 'stroke-width': 1, 'stroke-dasharray': '3,3', zIndex: 1 })\n  .add();\n\n// --- Period numbers (1–7) to the left of main table ---\nfor (var per = 1; per <= 7; per++) {\n  R.text(String(per), GRID_LEFT - 4, GRID_TOP + (per - 1) * PITCH + Math.round(PITCH / 2) + 4)\n    .attr({ align: 'right', zIndex: 3 })\n    .css({ color: t.inkSoft, fontSize: '10px' })\n    .add();\n}\n\n// --- F-block row labels (\"Ln\" / \"An\") ---\nR.text('Ln', GRID_LEFT - 4, FBLOCK_TOP + Math.round(PITCH / 2) + 4)\n  .attr({ align: 'right', zIndex: 3 })\n  .css({ color: t.inkSoft, fontSize: '10px' })\n  .add();\nR.text('An', GRID_LEFT - 4, FBLOCK_TOP + PITCH + Math.round(PITCH / 2) + 4)\n  .attr({ align: 'right', zIndex: 3 })\n  .css({ color: t.inkSoft, fontSize: '10px' })\n  .add();\n\n// --- Colorbar ---\nconst CB_W = Math.round(W * 0.52);\nconst CB_LEFT = Math.round((W - CB_W) / 2);\nconst STOPS = 60;\n\nfor (var i = 0; i < STOPS; i++) {\n  var frac = i / (STOPS - 1);\n  R.rect(\n    CB_LEFT + Math.round(i * CB_W / STOPS),\n    CB_TOP,\n    Math.ceil(CB_W / STOPS) + 1,\n    CB_H,\n    0\n  )\n    .attr({ fill: colorAt(frac), zIndex: 1 })\n    .add();\n}\n\n// Colorbar border\nR.rect(CB_LEFT, CB_TOP, CB_W, CB_H, 0)\n  .attr({ fill: 'none', stroke: t.inkSoft, 'stroke-width': 1, zIndex: 2 })\n  .add();\n\n// Colorbar tick marks and labels\nvar tickBotY = CB_TOP + CB_H;\nvar valLabelY = tickBotY + 15;\nvar propLabelY = tickBotY + 33;\n\n// Endpoint tick marks\nR.path(['M', CB_LEFT, tickBotY, 'L', CB_LEFT, tickBotY + 5])\n  .attr({ stroke: t.inkSoft, 'stroke-width': 1, zIndex: 3 }).add();\nR.path(['M', CB_LEFT + CB_W, tickBotY, 'L', CB_LEFT + CB_W, tickBotY + 5])\n  .attr({ stroke: t.inkSoft, 'stroke-width': 1, zIndex: 3 }).add();\n\n// Endpoint value labels\nR.text(String(minIE), CB_LEFT, valLabelY)\n  .attr({ align: 'left', zIndex: 3 })\n  .css({ color: t.inkSoft, fontSize: '10px' })\n  .add();\nR.text(String(maxIE), CB_LEFT + CB_W, valLabelY)\n  .attr({ align: 'right', zIndex: 3 })\n  .css({ color: t.inkSoft, fontSize: '10px' })\n  .add();\n\n// Intermediate tick marks at 1000, 1500, 2000 kJ/mol\n[1000, 1500, 2000].forEach(function(val) {\n  var frac = (val - minIE) / (maxIE - minIE);\n  var tx = CB_LEFT + Math.round(frac * CB_W);\n  R.path(['M', tx, tickBotY, 'L', tx, tickBotY + 5])\n    .attr({ stroke: t.inkSoft, 'stroke-width': 1, zIndex: 3 })\n    .add();\n  R.text(String(val), tx, valLabelY)\n    .attr({ align: 'center', zIndex: 3 })\n    .css({ color: t.inkSoft, fontSize: '10px' })\n    .add();\n});\n\n// Property name label\nR.text('First Ionization Energy (kJ/mol)', W / 2, propLabelY)\n  .attr({ align: 'center', zIndex: 3 })\n  .css({ color: t.inkSoft, fontSize: '11px' })\n  .add();\n"}