{"spec_id":"heatmap-periodic-table","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nheatmap-periodic-table: Periodic Table Property Heatmap\nLibrary: bokeh 3.9.1 | Python 3.13.13\nQuality: 88/100 | Created: 2026-06-15\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Remove this script's own directory from sys.path so `import bokeh` resolves\n# to the installed package rather than this file (which shares its name).\n_this_dir = str(Path(__file__).resolve().parent)\nsys.path[:] = [p for p in sys.path if p != _this_dir]\n\nfrom bokeh.io import output_file, save\nfrom bokeh.models import BasicTicker, ColorBar, ColumnDataSource, LinearColorMapper, Range1d\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nAMBER = \"#DDCC77\"\n\n\n# Imprint sequential colormap: brand-green → blue (single-polarity continuous)\ndef _lerp_hex(c0, c1, t):\n    r0, g0, b0 = (int(c0[i : i + 2], 16) for i in (1, 3, 5))\n    r1, g1, b1 = (int(c1[i : i + 2], 16) for i in (1, 3, 5))\n    return \"#{:02X}{:02X}{:02X}\".format(\n        int(round(r0 + (r1 - r0) * t)), int(round(g0 + (g1 - g0) * t)), int(round(b0 + (b1 - b0) * t))\n    )\n\n\nIMPRINT_SEQ = [_lerp_hex(\"#009E73\", \"#4467A3\", t / 255.0) for t in range(256)]\nGREY_TILE = \"#C8C6BE\" if THEME == \"light\" else \"#3C3B36\"\n\n# Focal elements: He (Z=2, IE=2372 — highest) and Fr (Z=87, IE=393 — lowest)\nFOCAL_Z = {2, 87}\n\n\ndef _lum(hx):\n    def _lin(c):\n        return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4\n\n    return sum(\n        w * _lin(int(hx[i : i + 2], 16) / 255.0) for w, i in zip((0.2126, 0.7152, 0.0722), (1, 3, 5), strict=False)\n    )\n\n\n# Element symbols (index = atomic number)\nSYMBOLS = [\n    \"\",\n    \"H\",\n    \"He\",\n    \"Li\",\n    \"Be\",\n    \"B\",\n    \"C\",\n    \"N\",\n    \"O\",\n    \"F\",\n    \"Ne\",\n    \"Na\",\n    \"Mg\",\n    \"Al\",\n    \"Si\",\n    \"P\",\n    \"S\",\n    \"Cl\",\n    \"Ar\",\n    \"K\",\n    \"Ca\",\n    \"Sc\",\n    \"Ti\",\n    \"V\",\n    \"Cr\",\n    \"Mn\",\n    \"Fe\",\n    \"Co\",\n    \"Ni\",\n    \"Cu\",\n    \"Zn\",\n    \"Ga\",\n    \"Ge\",\n    \"As\",\n    \"Se\",\n    \"Br\",\n    \"Kr\",\n    \"Rb\",\n    \"Sr\",\n    \"Y\",\n    \"Zr\",\n    \"Nb\",\n    \"Mo\",\n    \"Tc\",\n    \"Ru\",\n    \"Rh\",\n    \"Pd\",\n    \"Ag\",\n    \"Cd\",\n    \"In\",\n    \"Sn\",\n    \"Sb\",\n    \"Te\",\n    \"I\",\n    \"Xe\",\n    \"Cs\",\n    \"Ba\",\n    \"La\",\n    \"Ce\",\n    \"Pr\",\n    \"Nd\",\n    \"Pm\",\n    \"Sm\",\n    \"Eu\",\n    \"Gd\",\n    \"Tb\",\n    \"Dy\",\n    \"Ho\",\n    \"Er\",\n    \"Tm\",\n    \"Yb\",\n    \"Lu\",\n    \"Hf\",\n    \"Ta\",\n    \"W\",\n    \"Re\",\n    \"Os\",\n    \"Ir\",\n    \"Pt\",\n    \"Au\",\n    \"Hg\",\n    \"Tl\",\n    \"Pb\",\n    \"Bi\",\n    \"Po\",\n    \"At\",\n    \"Rn\",\n    \"Fr\",\n    \"Ra\",\n    \"Ac\",\n    \"Th\",\n    \"Pa\",\n    \"U\",\n    \"Np\",\n    \"Pu\",\n    \"Am\",\n    \"Cm\",\n    \"Bk\",\n    \"Cf\",\n    \"Es\",\n    \"Fm\",\n    \"Md\",\n    \"No\",\n    \"Lr\",\n    \"Rf\",\n    \"Db\",\n    \"Sg\",\n    \"Bh\",\n    \"Hs\",\n    \"Mt\",\n    \"Ds\",\n    \"Rg\",\n    \"Cn\",\n    \"Nh\",\n    \"Fl\",\n    \"Mc\",\n    \"Lv\",\n    \"Ts\",\n    \"Og\",\n]\n\n# First ionization energies in kJ/mol\nIE = {\n    1: 1312,\n    2: 2372,\n    3: 520,\n    4: 900,\n    5: 801,\n    6: 1086,\n    7: 1402,\n    8: 1314,\n    9: 1681,\n    10: 2081,\n    11: 496,\n    12: 738,\n    13: 578,\n    14: 786,\n    15: 1012,\n    16: 1000,\n    17: 1251,\n    18: 1521,\n    19: 419,\n    20: 590,\n    21: 633,\n    22: 659,\n    23: 651,\n    24: 653,\n    25: 717,\n    26: 762,\n    27: 760,\n    28: 737,\n    29: 746,\n    30: 906,\n    31: 579,\n    32: 762,\n    33: 947,\n    34: 941,\n    35: 1140,\n    36: 1351,\n    37: 403,\n    38: 550,\n    39: 600,\n    40: 640,\n    41: 652,\n    42: 685,\n    43: 702,\n    44: 711,\n    45: 720,\n    46: 805,\n    47: 731,\n    48: 868,\n    49: 558,\n    50: 709,\n    51: 834,\n    52: 869,\n    53: 1008,\n    54: 1170,\n    55: 376,\n    56: 503,\n    57: 538,\n    58: 534,\n    59: 527,\n    60: 533,\n    61: 540,\n    62: 545,\n    63: 547,\n    64: 593,\n    65: 566,\n    66: 573,\n    67: 581,\n    68: 589,\n    69: 597,\n    70: 603,\n    71: 524,\n    72: 659,\n    73: 761,\n    74: 770,\n    75: 760,\n    76: 840,\n    77: 880,\n    78: 870,\n    79: 890,\n    80: 1007,\n    81: 589,\n    82: 716,\n    83: 703,\n    84: 812,\n    85: 920,\n    86: 1037,\n    87: 393,\n    88: 509,\n    89: 499,\n    90: 587,\n    91: 568,\n    92: 598,\n    93: 605,\n    94: 585,\n    95: 578,\n    96: 581,\n    97: 601,\n    98: 608,\n    99: 619,\n    100: 627,\n    101: 635,\n    102: 642,\n    103: 470,\n}\n\n\ndef _build_grid():\n    \"\"\"Return {atomic_number: (col, row)} using standard periodic table layout.\n    Lanthanides/actinides placed in detached f-block rows below the main body.\n    \"\"\"\n    g = {}\n    g[1] = (1, 1)\n    g[2] = (18, 1)\n    g[3] = (1, 2)\n    g[4] = (2, 2)\n    for z in range(5, 11):\n        g[z] = (z + 8, 2)  # B→13 … Ne→18\n    g[11] = (1, 3)\n    g[12] = (2, 3)\n    for z in range(13, 19):\n        g[z] = (z, 3)  # Al→13 … Ar→18\n    for z in range(19, 37):\n        g[z] = (z - 18, 4)\n    for z in range(37, 55):\n        g[z] = (z - 36, 5)\n    g[55] = (1, 6)\n    g[56] = (2, 6)\n    for z in range(72, 87):\n        g[z] = (z - 68, 6)  # Hf→4 … Rn→18\n    g[87] = (1, 7)\n    g[88] = (2, 7)\n    for z in range(104, 119):\n        g[z] = (z - 100, 7)  # Rf→4 … Og→18\n    # Lanthanides (La–Lu, Z=57–71): f-block row 1 at display y-row 8.5\n    for i, z in enumerate(range(57, 72)):\n        g[z] = (i + 3, 8.5)\n    # Actinides (Ac–Lr, Z=89–103): f-block row 2 at display y-row 9.5\n    for i, z in enumerate(range(89, 104)):\n        g[z] = (i + 3, 9.5)\n    return g\n\n\nGRID = _build_grid()\n\nie_min = min(IE.values())\nie_max = max(IE.values())\n\n# Build unified data arrays for all tiles (colored + grey) in a single pass\nxs, ys, fcs, tcs, syms, nstrs, nxs, nys, ie_strs, ivys = [], [], [], [], [], [], [], [], [], []\nfocal_xs, focal_ys = [], []\n\nfor z in range(1, 119):\n    if z not in GRID:\n        continue\n    cx, cy = GRID[z]\n    sym = SYMBOLS[z] if z < len(SYMBOLS) else \"\"\n    yd = -cy  # negate: period 1 → y=-1 (near top of Range1d upper bound)\n\n    if z in IE:\n        ie = IE[z]\n        nrm = (ie - ie_min) / (ie_max - ie_min)\n        idx = min(255, int(round(nrm * 255)))\n        fc = IMPRINT_SEQ[idx]\n        tc = \"#F0EFE8\" if _lum(fc) < 0.35 else \"#1A1A17\"\n        ie_str = str(ie)\n    else:\n        fc = GREY_TILE\n        tc = INK_MUTED\n        ie_str = \"\"\n\n    xs.append(cx)\n    ys.append(yd)\n    fcs.append(fc)\n    tcs.append(tc)\n    syms.append(sym)\n    nstrs.append(str(z))\n    nxs.append(cx - 0.37)\n    nys.append(yd + 0.37)\n    ie_strs.append(ie_str)\n    ivys.append(yd - 0.27)\n\n    if z in FOCAL_Z:\n        focal_xs.append(cx)\n        focal_ys.append(yd)\n\n# Figure — square canvas (2400×2400) for symmetric grid layout\ntitle_str = \"heatmap-periodic-table · python · bokeh · anyplot.ai\"\nn_chars = len(title_str)\ntitle_pt = f\"{max(34, round(50 * 67 / n_chars))}pt\"\n\np = figure(\n    width=2400,\n    height=2400,\n    title=title_str,\n    toolbar_location=None,\n    x_range=Range1d(0.3, 18.7),\n    y_range=Range1d(-10.4, 0.2),\n    min_border_bottom=330,\n    min_border_left=80,\n    min_border_top=130,\n    min_border_right=80,\n)\n\n# Linear color mapper (Imprint sequential palette) — used for ColorBar\nmapper = LinearColorMapper(palette=IMPRINT_SEQ, low=ie_min, high=ie_max)\n\n# Single ColumnDataSource for all tiles (colored + grey unified)\nsrc = ColumnDataSource(\n    {\"x\": xs, \"y\": ys, \"fc\": fcs, \"tc\": tcs, \"s\": syms, \"nstr\": nstrs, \"nx\": nxs, \"ny\": nys, \"ie\": ie_strs, \"ivy\": ivys}\n)\n\n# All tile rectangles (precomputed fill colors; seamless PAGE_BG borders)\np.rect(x=\"x\", y=\"y\", width=0.88, height=0.88, source=src, fill_color={\"field\": \"fc\"}, line_color=PAGE_BG, line_width=2)\n\n# Amber border highlights for focal elements: He (highest IE) and Fr (lowest IE)\nsrc_focal = ColumnDataSource({\"x\": focal_xs, \"y\": focal_ys})\np.rect(x=\"x\", y=\"y\", width=0.88, height=0.88, source=src_focal, fill_alpha=0, line_color=AMBER, line_width=3)\n\n# Element symbol — bold, centered\np.text(\n    x=\"x\",\n    y=\"y\",\n    text=\"s\",\n    source=src,\n    text_align=\"center\",\n    text_baseline=\"middle\",\n    text_color={\"field\": \"tc\"},\n    text_font_size=\"20pt\",\n    text_font_style=\"bold\",\n)\n# Atomic number — small, top-left of tile\np.text(\n    x=\"nx\",\n    y=\"ny\",\n    text=\"nstr\",\n    source=src,\n    text_align=\"left\",\n    text_baseline=\"top\",\n    text_color={\"field\": \"tc\"},\n    text_font_size=\"13pt\",\n)\n# IE value — bottom of tile (13pt for balance; empty string for grey tiles)\np.text(\n    x=\"x\",\n    y=\"ivy\",\n    text=\"ie\",\n    source=src,\n    text_align=\"center\",\n    text_baseline=\"bottom\",\n    text_color={\"field\": \"tc\"},\n    text_font_size=\"13pt\",\n)\n\n# Colorbar — horizontal, placed below plot area\ncolor_bar = ColorBar(\n    color_mapper=mapper,\n    ticker=BasicTicker(desired_num_ticks=9),\n    label_standoff=14,\n    border_line_color=None,\n    orientation=\"horizontal\",\n    title=\"First Ionization Energy (kJ/mol)\",\n    title_text_color=INK,\n    title_text_font_size=\"28pt\",\n    title_standoff=14,\n    major_label_text_color=INK_SOFT,\n    major_label_text_font_size=\"22pt\",\n    background_fill_color=PAGE_BG,\n    bar_line_color=None,\n    height=60,\n    padding=20,\n)\np.add_layout(color_bar, \"below\")\n\n# Chrome — theme-adaptive\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\np.outline_line_alpha = 0  # fully suppress dark-mode frame outline artifact\n\np.title.text_color = INK\np.title.text_font_size = title_pt\np.title.text_font_style = \"normal\"\np.title.align = \"center\"\n\np.xaxis.visible = False\np.yaxis.visible = False\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = None\n\n# Save interactive HTML, then screenshot via headless Chrome\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\nW, H = 2400, 2400\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\n# CDP override forces exact W×H viewport regardless of outer window chrome\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}