{"spec_id":"heatmap-periodic-table","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nheatmap-periodic-table: Periodic Table Property Heatmap\nLibrary: plotly 6.8.0 | Python 3.13.13\nQuality: 85/100 | Created: 2026-06-15\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove the script's own directory from sys.path to prevent self-import of\n# 'plotly' (this file is named plotly.py, which shadows the installed package).\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if p not in (\"\", _script_dir)]\n\nimport numpy as np\nimport plotly.graph_objects as go\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint sequential colormap (single-polarity, green→blue) — for IE1\nimprint_seq = [[0.0, \"#009E73\"], [1.0, \"#4467A3\"]]\nGREY_TILE = \"#8A8A82\" if THEME == \"light\" else \"#5A5A55\"\n# Grey tile luminance: light #8A8A82 ≈ 0.272 (use dark text), dark #5A5A55 ≈ 0.117 (use white)\nGREY_TEXT = \"#1A1A17\" if THEME == \"light\" else \"#FFFFFF\"\n\n# Element data: (Z, symbol, x_grid 0-17, y_grid 0-9, ie1_kJ_mol or None)\n# y=0-6: periods 1-7; y=7: visual gap row; y=8: lanthanides; y=9: actinides\nELEMENTS = [\n    # Period 1\n    (1, \"H\", 0, 0, 1312.0),\n    (2, \"He\", 17, 0, 2372.3),\n    # Period 2\n    (3, \"Li\", 0, 1, 520.2),\n    (4, \"Be\", 1, 1, 899.5),\n    (5, \"B\", 12, 1, 800.6),\n    (6, \"C\", 13, 1, 1086.5),\n    (7, \"N\", 14, 1, 1402.3),\n    (8, \"O\", 15, 1, 1313.9),\n    (9, \"F\", 16, 1, 1681.0),\n    (10, \"Ne\", 17, 1, 2080.7),\n    # Period 3\n    (11, \"Na\", 0, 2, 495.8),\n    (12, \"Mg\", 1, 2, 737.7),\n    (13, \"Al\", 12, 2, 577.5),\n    (14, \"Si\", 13, 2, 786.5),\n    (15, \"P\", 14, 2, 1011.8),\n    (16, \"S\", 15, 2, 999.6),\n    (17, \"Cl\", 16, 2, 1251.2),\n    (18, \"Ar\", 17, 2, 1520.6),\n    # Period 4\n    (19, \"K\", 0, 3, 418.8),\n    (20, \"Ca\", 1, 3, 589.8),\n    (21, \"Sc\", 2, 3, 633.1),\n    (22, \"Ti\", 3, 3, 658.8),\n    (23, \"V\", 4, 3, 650.9),\n    (24, \"Cr\", 5, 3, 652.9),\n    (25, \"Mn\", 6, 3, 717.3),\n    (26, \"Fe\", 7, 3, 762.5),\n    (27, \"Co\", 8, 3, 760.4),\n    (28, \"Ni\", 9, 3, 737.1),\n    (29, \"Cu\", 10, 3, 745.5),\n    (30, \"Zn\", 11, 3, 906.4),\n    (31, \"Ga\", 12, 3, 578.8),\n    (32, \"Ge\", 13, 3, 762.0),\n    (33, \"As\", 14, 3, 947.0),\n    (34, \"Se\", 15, 3, 941.0),\n    (35, \"Br\", 16, 3, 1139.9),\n    (36, \"Kr\", 17, 3, 1350.8),\n    # Period 5\n    (37, \"Rb\", 0, 4, 403.0),\n    (38, \"Sr\", 1, 4, 549.5),\n    (39, \"Y\", 2, 4, 600.0),\n    (40, \"Zr\", 3, 4, 640.1),\n    (41, \"Nb\", 4, 4, 652.1),\n    (42, \"Mo\", 5, 4, 684.3),\n    (43, \"Tc\", 6, 4, 702.0),\n    (44, \"Ru\", 7, 4, 710.2),\n    (45, \"Rh\", 8, 4, 719.7),\n    (46, \"Pd\", 9, 4, 804.4),\n    (47, \"Ag\", 10, 4, 731.0),\n    (48, \"Cd\", 11, 4, 867.8),\n    (49, \"In\", 12, 4, 558.3),\n    (50, \"Sn\", 13, 4, 708.6),\n    (51, \"Sb\", 14, 4, 834.0),\n    (52, \"Te\", 15, 4, 869.3),\n    (53, \"I\", 16, 4, 1008.4),\n    (54, \"Xe\", 17, 4, 1170.4),\n    # Period 6 — f-block gap at x=2; La-Lu in lanthanide row (y=8)\n    (55, \"Cs\", 0, 5, 375.7),\n    (56, \"Ba\", 1, 5, 502.9),\n    (72, \"Hf\", 3, 5, 658.5),\n    (73, \"Ta\", 4, 5, 761.0),\n    (74, \"W\", 5, 5, 770.0),\n    (75, \"Re\", 6, 5, 760.0),\n    (76, \"Os\", 7, 5, 840.0),\n    (77, \"Ir\", 8, 5, 880.0),\n    (78, \"Pt\", 9, 5, 870.0),\n    (79, \"Au\", 10, 5, 890.1),\n    (80, \"Hg\", 11, 5, 1007.1),\n    (81, \"Tl\", 12, 5, 589.4),\n    (82, \"Pb\", 13, 5, 715.6),\n    (83, \"Bi\", 14, 5, 703.0),\n    (84, \"Po\", 15, 5, 812.1),\n    (85, \"At\", 16, 5, 920.0),\n    (86, \"Rn\", 17, 5, 1037.0),\n    # Period 7 — f-block gap at x=2; Ac-Lr in actinide row (y=9)\n    (87, \"Fr\", 0, 6, 380.0),\n    (88, \"Ra\", 1, 6, 509.3),\n    (104, \"Rf\", 3, 6, 580.0),\n    (105, \"Db\", 4, 6, None),\n    (106, \"Sg\", 5, 6, None),\n    (107, \"Bh\", 6, 6, None),\n    (108, \"Hs\", 7, 6, None),\n    (109, \"Mt\", 8, 6, None),\n    (110, \"Ds\", 9, 6, None),\n    (111, \"Rg\", 10, 6, None),\n    (112, \"Cn\", 11, 6, None),\n    (113, \"Nh\", 12, 6, None),\n    (114, \"Fl\", 13, 6, None),\n    (115, \"Mc\", 14, 6, None),\n    (116, \"Lv\", 15, 6, None),\n    (117, \"Ts\", 16, 6, None),\n    (118, \"Og\", 17, 6, None),\n    # Lanthanides (y=8, x=2 to x=16)\n    (57, \"La\", 2, 8, 538.1),\n    (58, \"Ce\", 3, 8, 534.4),\n    (59, \"Pr\", 4, 8, 527.0),\n    (60, \"Nd\", 5, 8, 533.1),\n    (61, \"Pm\", 6, 8, 540.0),\n    (62, \"Sm\", 7, 8, 544.5),\n    (63, \"Eu\", 8, 8, 547.1),\n    (64, \"Gd\", 9, 8, 593.4),\n    (65, \"Tb\", 10, 8, 565.8),\n    (66, \"Dy\", 11, 8, 573.0),\n    (67, \"Ho\", 12, 8, 581.0),\n    (68, \"Er\", 13, 8, 589.3),\n    (69, \"Tm\", 14, 8, 596.7),\n    (70, \"Yb\", 15, 8, 603.4),\n    (71, \"Lu\", 16, 8, 523.5),\n    # Actinides (y=9, x=2 to x=16)\n    (89, \"Ac\", 2, 9, 499.0),\n    (90, \"Th\", 3, 9, 587.0),\n    (91, \"Pa\", 4, 9, 568.0),\n    (92, \"U\", 5, 9, 597.6),\n    (93, \"Np\", 6, 9, 604.5),\n    (94, \"Pu\", 7, 9, 584.7),\n    (95, \"Am\", 8, 9, 578.0),\n    (96, \"Cm\", 9, 9, 581.0),\n    (97, \"Bk\", 10, 9, 601.0),\n    (98, \"Cf\", 11, 9, 608.0),\n    (99, \"Es\", 12, 9, 619.0),\n    (100, \"Fm\", 13, 9, 627.0),\n    (101, \"Md\", 14, 9, 635.0),\n    (102, \"No\", 15, 9, 641.6),\n    (103, \"Lr\", 16, 9, 443.0),\n]\n\n# Build 2D value grids (10 rows × 18 cols)\nNROWS, NCOLS = 10, 18\nz_colored = np.full((NROWS, NCOLS), np.nan)\nz_grey = np.full((NROWS, NCOLS), np.nan)\n\nie1_all = []\nfor _Z, _sym, xg, yg, ie1 in ELEMENTS:\n    if ie1 is not None:\n        z_colored[yg, xg] = ie1\n        ie1_all.append(ie1)\n    else:\n        z_grey[yg, xg] = 1.0\n\nie1_min = min(ie1_all)\nie1_max = max(ie1_all)\n\n\ndef _srgb_lin(c):\n    \"\"\"sRGB channel value (0–255) to linear light.\"\"\"\n    n = c / 255.0\n    return n / 12.92 if n <= 0.04045 else ((n + 0.055) / 1.055) ** 2.4\n\n\ndef _tile_text(ie1_val):\n    \"\"\"Return dark or white text color based on imprint_seq tile background luminance.\"\"\"\n    t = (ie1_val - ie1_min) / (ie1_max - ie1_min)\n    r, g, b = t * 68, 158 + t * (103 - 158), 115 + t * (163 - 115)\n    lum = 0.2126 * _srgb_lin(r) + 0.7152 * _srgb_lin(g) + 0.0722 * _srgb_lin(b)\n    # Crossover at lum≈0.19: dark text (#1A1A17) on lighter green tiles,\n    # white text (#FFFFFF) on darker blue tiles — theme-independent (data colors are fixed)\n    return \"#1A1A17\" if lum > 0.19 else \"#FFFFFF\"\n\n\n# Title with font-size scaled for length\ntitle = \"First Ionization Energy · heatmap-periodic-table · python · plotly · anyplot.ai\"\nn_chars = len(title)\nratio = 67 / n_chars if n_chars > 67 else 1.0\ntitle_fs = max(11, round(16 * ratio))\n\n# Figure\nfig = go.Figure()\n\n# Layer 1: grey tiles for elements with no IE1 data\nfig.add_trace(\n    go.Heatmap(\n        z=z_grey,\n        colorscale=[[0.0, GREY_TILE], [1.0, GREY_TILE]],\n        showscale=False,\n        zmin=0,\n        zmax=1,\n        xgap=3,\n        ygap=3,\n        hoverinfo=\"skip\",\n    )\n)\n\n# Layer 2: Imprint-colored tiles for elements with known IE1\nfig.add_trace(\n    go.Heatmap(\n        z=z_colored,\n        colorscale=imprint_seq,\n        zmin=ie1_min,\n        zmax=ie1_max,\n        xgap=3,\n        ygap=3,\n        colorbar={\n            \"title\": {\"text\": \"IE₁ (kJ/mol)\", \"font\": {\"size\": 10, \"color\": INK}, \"side\": \"right\"},\n            \"tickfont\": {\"size\": 9, \"color\": INK_SOFT},\n            \"tickcolor\": INK_SOFT,\n            \"thickness\": 14,\n            \"len\": 0.68,\n            \"y\": 0.5,\n            \"x\": 1.01,\n            \"bgcolor\": ELEVATED_BG,\n            \"bordercolor\": INK_MUTED,\n            \"borderwidth\": 0.5,\n        },\n        hoverinfo=\"skip\",\n    )\n)\n\n# Build annotations: period/group orientation labels + element tiles\nannotations = []\n\n# Period numbers (1–7) on the left edge — orient readers within the table\nfor period in range(7):\n    annotations.append(\n        {\n            \"x\": -0.80,\n            \"y\": period,\n            \"text\": str(period + 1),\n            \"font\": {\"size\": 4.5, \"color\": INK_MUTED, \"family\": \"Arial\"},\n            \"showarrow\": False,\n            \"xref\": \"x\",\n            \"yref\": \"y\",\n            \"xanchor\": \"center\",\n            \"yanchor\": \"middle\",\n        }\n    )\n\n# Group numbers (1–18) along the top edge\nfor grp in range(18):\n    annotations.append(\n        {\n            \"x\": grp,\n            \"y\": -0.72,\n            \"text\": str(grp + 1),\n            \"font\": {\"size\": 3.8, \"color\": INK_MUTED, \"family\": \"Arial\"},\n            \"showarrow\": False,\n            \"xref\": \"x\",\n            \"yref\": \"y\",\n            \"xanchor\": \"center\",\n            \"yanchor\": \"middle\",\n        }\n    )\n\n# Element tiles: symbol (centered, bold) + atomic number (top-left corner)\nfor Z, sym, xg, yg, ie1 in ELEMENTS:\n    txt_col = _tile_text(ie1) if ie1 is not None else GREY_TEXT\n\n    # Symbol — centered in tile, bold\n    annotations.append(\n        {\n            \"x\": xg,\n            \"y\": yg,\n            \"text\": f\"<b>{sym}</b>\",\n            \"font\": {\"size\": 6.5, \"color\": txt_col, \"family\": \"Arial\"},\n            \"showarrow\": False,\n            \"xref\": \"x\",\n            \"yref\": \"y\",\n            \"xanchor\": \"center\",\n            \"yanchor\": \"middle\",\n        }\n    )\n\n    # Atomic number — top-left corner of tile (raised to 5.0 px for mobile legibility)\n    annotations.append(\n        {\n            \"x\": xg - 0.38,\n            \"y\": yg - 0.33,\n            \"text\": str(Z),\n            \"font\": {\"size\": 5.0, \"color\": txt_col, \"family\": \"Arial\"},\n            \"showarrow\": False,\n            \"xref\": \"x\",\n            \"yref\": \"y\",\n            \"xanchor\": \"left\",\n            \"yanchor\": \"top\",\n        }\n    )\n\n# Series labels in empty left cells of the f-block rows\nannotations.extend(\n    [\n        {\n            \"x\": 0.5,\n            \"y\": 8,\n            \"text\": \"Lanthanides\",\n            \"font\": {\"size\": 4.5, \"color\": INK_MUTED, \"family\": \"Arial\"},\n            \"showarrow\": False,\n            \"xref\": \"x\",\n            \"yref\": \"y\",\n            \"xanchor\": \"center\",\n            \"yanchor\": \"middle\",\n        },\n        {\n            \"x\": 0.5,\n            \"y\": 9,\n            \"text\": \"Actinides\",\n            \"font\": {\"size\": 4.5, \"color\": INK_MUTED, \"family\": \"Arial\"},\n            \"showarrow\": False,\n            \"xref\": \"x\",\n            \"yref\": \"y\",\n            \"xanchor\": \"center\",\n            \"yanchor\": \"middle\",\n        },\n    ]\n)\n\nfig.update_layout(\n    title={\n        \"text\": title,\n        \"font\": {\"size\": title_fs, \"color\": INK},\n        \"x\": 0.47,\n        \"xanchor\": \"center\",\n        \"y\": 0.98,\n        \"yanchor\": \"top\",\n    },\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    autosize=False,\n    width=800,\n    height=450,\n    margin={\"l\": 45, \"r\": 100, \"t\": 60, \"b\": 30},\n    xaxis={\"showticklabels\": False, \"showgrid\": False, \"zeroline\": False, \"showline\": False, \"range\": [-1.1, 17.5]},\n    yaxis={\n        \"showticklabels\": False,\n        \"showgrid\": False,\n        \"zeroline\": False,\n        \"showline\": False,\n        \"range\": [9.5, -1.1],  # extended top for group number row; reversed: period 1 at top\n    },\n    annotations=annotations,\n)\n\n# Thin separator lines framing the f-block gap — makes the separation intentional\nfor y_sep in [6.6, 7.4]:\n    fig.add_shape(\n        type=\"line\", x0=1.6, x1=16.4, y0=y_sep, y1=y_sep, line={\"color\": INK_MUTED, \"width\": 0.8}, xref=\"x\", yref=\"y\"\n    )\n\n# Save\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}