{"spec_id":"heatmap-periodic-table","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nheatmap-periodic-table: Periodic Table Property Heatmap\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 90/100 | Created: 2026-06-15\n\"\"\"\n\nimport os\n\nimport pandas as pd\nfrom lets_plot import *\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\n\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\"\nMISSING_FILL = \"#C4C4BE\" if THEME == \"light\" else \"#383835\"\n\n# Fixed luminance-based text colors — tile fill is theme-independent so text choice is too\nLIGHT_TEXT = \"#F0EFE8\"  # for dark tiles (relative luminance < 0.35)\nDARK_TEXT = \"#1A1A17\"  # for light tiles (relative luminance >= 0.35)\n\n# Data: (atomic_number, symbol, group_pos, period_pos, pauling_en)\n# period_pos 8.5 = lanthanide row below main table, 9.5 = actinide row\nELEMENTS = [\n    # Period 1\n    (1, \"H\", 1, 1, 2.20),\n    (2, \"He\", 18, 1, None),\n    # Period 2\n    (3, \"Li\", 1, 2, 0.98),\n    (4, \"Be\", 2, 2, 1.57),\n    (5, \"B\", 13, 2, 2.04),\n    (6, \"C\", 14, 2, 2.55),\n    (7, \"N\", 15, 2, 3.04),\n    (8, \"O\", 16, 2, 3.44),\n    (9, \"F\", 17, 2, 3.98),\n    (10, \"Ne\", 18, 2, None),\n    # Period 3\n    (11, \"Na\", 1, 3, 0.93),\n    (12, \"Mg\", 2, 3, 1.31),\n    (13, \"Al\", 13, 3, 1.61),\n    (14, \"Si\", 14, 3, 1.90),\n    (15, \"P\", 15, 3, 2.19),\n    (16, \"S\", 16, 3, 2.58),\n    (17, \"Cl\", 17, 3, 3.16),\n    (18, \"Ar\", 18, 3, None),\n    # Period 4\n    (19, \"K\", 1, 4, 0.82),\n    (20, \"Ca\", 2, 4, 1.00),\n    (21, \"Sc\", 3, 4, 1.36),\n    (22, \"Ti\", 4, 4, 1.54),\n    (23, \"V\", 5, 4, 1.63),\n    (24, \"Cr\", 6, 4, 1.66),\n    (25, \"Mn\", 7, 4, 1.55),\n    (26, \"Fe\", 8, 4, 1.83),\n    (27, \"Co\", 9, 4, 1.88),\n    (28, \"Ni\", 10, 4, 1.91),\n    (29, \"Cu\", 11, 4, 1.90),\n    (30, \"Zn\", 12, 4, 1.65),\n    (31, \"Ga\", 13, 4, 1.81),\n    (32, \"Ge\", 14, 4, 2.01),\n    (33, \"As\", 15, 4, 2.18),\n    (34, \"Se\", 16, 4, 2.55),\n    (35, \"Br\", 17, 4, 2.96),\n    (36, \"Kr\", 18, 4, 3.00),\n    # Period 5\n    (37, \"Rb\", 1, 5, 0.82),\n    (38, \"Sr\", 2, 5, 0.95),\n    (39, \"Y\", 3, 5, 1.22),\n    (40, \"Zr\", 4, 5, 1.33),\n    (41, \"Nb\", 5, 5, 1.60),\n    (42, \"Mo\", 6, 5, 2.16),\n    (43, \"Tc\", 7, 5, 1.90),\n    (44, \"Ru\", 8, 5, 2.20),\n    (45, \"Rh\", 9, 5, 2.20),\n    (46, \"Pd\", 10, 5, 2.20),\n    (47, \"Ag\", 11, 5, 1.93),\n    (48, \"Cd\", 12, 5, 1.69),\n    (49, \"In\", 13, 5, 1.78),\n    (50, \"Sn\", 14, 5, 1.96),\n    (51, \"Sb\", 15, 5, 2.05),\n    (52, \"Te\", 16, 5, 2.10),\n    (53, \"I\", 17, 5, 2.66),\n    (54, \"Xe\", 18, 5, 2.60),\n    # Period 6 — group 3 empty; La–Lu in f-block row below\n    (55, \"Cs\", 1, 6, 0.79),\n    (56, \"Ba\", 2, 6, 0.89),\n    (72, \"Hf\", 4, 6, 1.30),\n    (73, \"Ta\", 5, 6, 1.50),\n    (74, \"W\", 6, 6, 2.36),\n    (75, \"Re\", 7, 6, 1.90),\n    (76, \"Os\", 8, 6, 2.20),\n    (77, \"Ir\", 9, 6, 2.20),\n    (78, \"Pt\", 10, 6, 2.28),\n    (79, \"Au\", 11, 6, 2.54),\n    (80, \"Hg\", 12, 6, 2.00),\n    (81, \"Tl\", 13, 6, 1.62),\n    (82, \"Pb\", 14, 6, 2.33),\n    (83, \"Bi\", 15, 6, 2.02),\n    (84, \"Po\", 16, 6, 2.00),\n    (85, \"At\", 17, 6, 2.20),\n    (86, \"Rn\", 18, 6, 2.20),\n    # Period 7 — group 3 empty; Ac–Lr in f-block row below\n    (87, \"Fr\", 1, 7, 0.70),\n    (88, \"Ra\", 2, 7, 0.90),\n    (104, \"Rf\", 4, 7, None),\n    (105, \"Db\", 5, 7, None),\n    (106, \"Sg\", 6, 7, None),\n    (107, \"Bh\", 7, 7, None),\n    (108, \"Hs\", 8, 7, None),\n    (109, \"Mt\", 9, 7, None),\n    (110, \"Ds\", 10, 7, None),\n    (111, \"Rg\", 11, 7, None),\n    (112, \"Cn\", 12, 7, None),\n    (113, \"Nh\", 13, 7, None),\n    (114, \"Fl\", 14, 7, None),\n    (115, \"Mc\", 15, 7, None),\n    (116, \"Lv\", 16, 7, None),\n    (117, \"Ts\", 17, 7, None),\n    (118, \"Og\", 18, 7, None),\n    # Lanthanides: period_pos=8.5, groups 3–17\n    (57, \"La\", 3, 8.5, 1.10),\n    (58, \"Ce\", 4, 8.5, 1.12),\n    (59, \"Pr\", 5, 8.5, 1.13),\n    (60, \"Nd\", 6, 8.5, 1.14),\n    (61, \"Pm\", 7, 8.5, None),\n    (62, \"Sm\", 8, 8.5, 1.17),\n    (63, \"Eu\", 9, 8.5, None),\n    (64, \"Gd\", 10, 8.5, 1.20),\n    (65, \"Tb\", 11, 8.5, None),\n    (66, \"Dy\", 12, 8.5, 1.22),\n    (67, \"Ho\", 13, 8.5, 1.23),\n    (68, \"Er\", 14, 8.5, 1.24),\n    (69, \"Tm\", 15, 8.5, 1.25),\n    (70, \"Yb\", 16, 8.5, None),\n    (71, \"Lu\", 17, 8.5, 1.27),\n    # Actinides: period_pos=9.5, groups 3–17\n    (89, \"Ac\", 3, 9.5, 1.10),\n    (90, \"Th\", 4, 9.5, 1.30),\n    (91, \"Pa\", 5, 9.5, 1.50),\n    (92, \"U\", 6, 9.5, 1.38),\n    (93, \"Np\", 7, 9.5, 1.36),\n    (94, \"Pu\", 8, 9.5, 1.28),\n    (95, \"Am\", 9, 9.5, 1.13),\n    (96, \"Cm\", 10, 9.5, 1.28),\n    (97, \"Bk\", 11, 9.5, 1.30),\n    (98, \"Cf\", 12, 9.5, 1.30),\n    (99, \"Es\", 13, 9.5, 1.30),\n    (100, \"Fm\", 14, 9.5, 1.30),\n    (101, \"Md\", 15, 9.5, 1.30),\n    (102, \"No\", 16, 9.5, 1.30),\n    (103, \"Lr\", 17, 9.5, None),\n]\n\ndf = pd.DataFrame(ELEMENTS, columns=[\"atomic_number\", \"symbol\", \"group_pos\", \"period_pos\", \"electronegativity\"])\ndf[\"x\"] = df[\"group_pos\"]\ndf[\"y\"] = -df[\"period_pos\"]\ndf[\"label_num\"] = df[\"atomic_number\"].astype(str)\n\ndf_colored = df[df[\"electronegativity\"].notna()].copy()\ndf_missing = df[df[\"electronegativity\"].isna()].copy()\n\n# Luminance-adaptive text: lerp gradient endpoints in sRGB, compute WCAG relative luminance\n# Gradient: #009E73 (low EN) → #4467A3 (high EN)\nen_min = df_colored[\"electronegativity\"].min()\nen_max = df_colored[\"electronegativity\"].max()\nt = (df_colored[\"electronegativity\"] - en_min) / (en_max - en_min)\nr_srgb = (0x00 + t * (0x44 - 0x00)) / 255.0\ng_srgb = (0x9E + t * (0x67 - 0x9E)) / 255.0\nb_srgb = (0x73 + t * (0xA3 - 0x73)) / 255.0\nr_lin = (r_srgb / 12.92).where(r_srgb <= 0.04045, ((r_srgb + 0.055) / 1.055) ** 2.4)\ng_lin = (g_srgb / 12.92).where(g_srgb <= 0.04045, ((g_srgb + 0.055) / 1.055) ** 2.4)\nb_lin = (b_srgb / 12.92).where(b_srgb <= 0.04045, ((b_srgb + 0.055) / 1.055) ** 2.4)\nluminance = 0.2126 * r_lin + 0.7152 * g_lin + 0.0722 * b_lin\ndf_colored[\"sym_color\"] = luminance.apply(lambda lum: LIGHT_TEXT if lum < 0.35 else DARK_TEXT)\n\ndf_sym_light = df_colored[df_colored[\"sym_color\"] == LIGHT_TEXT]\ndf_sym_dark = df_colored[df_colored[\"sym_color\"] == DARK_TEXT]\n\ntitle = \"Electronegativity · heatmap-periodic-table · python · letsplot · anyplot.ai\"\ntitle_n = len(title)\ntitle_size = max(11, round(16 * 67 / title_n))\n\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_grid_major=element_blank(),\n    panel_grid_minor=element_blank(),\n    axis_title=element_blank(),\n    axis_text=element_blank(),\n    axis_ticks=element_blank(),\n    axis_line=element_blank(),\n    plot_title=element_text(color=INK, size=title_size),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=10),\n    legend_title=element_text(color=INK, size=11),\n)\n\nplot = (\n    ggplot()\n    # Grey tiles for elements with no defined electronegativity\n    + geom_tile(\n        mapping=aes(x=\"x\", y=\"y\"),\n        data=df_missing,\n        fill=MISSING_FILL,\n        color=PAGE_BG,\n        size=0.4,\n        width=0.93,\n        height=0.93,\n        tooltips=layer_tooltips().line(\"@symbol (#@atomic_number)\").line(\"EN: not defined\"),\n    )\n    # Colored tiles — Imprint sequential: green (low EN) → blue (high EN)\n    + geom_tile(\n        mapping=aes(x=\"x\", y=\"y\", fill=\"electronegativity\"),\n        data=df_colored,\n        color=PAGE_BG,\n        size=0.4,\n        width=0.93,\n        height=0.93,\n        tooltips=layer_tooltips()\n        .line(\"@symbol (#@atomic_number)\")\n        .line(\"Electronegativity: @electronegativity Pauling\"),\n    )\n    # Element symbols — luminance-adaptive: LIGHT_TEXT on dark tiles, DARK_TEXT on light tiles\n    + geom_text(\n        mapping=aes(x=\"x\", y=\"y\", label=\"symbol\"),\n        data=df_sym_light,\n        color=LIGHT_TEXT,\n        size=3.0,\n        fontface=\"bold\",\n        vjust=0.6,\n    )\n    + geom_text(\n        mapping=aes(x=\"x\", y=\"y\", label=\"symbol\"),\n        data=df_sym_dark,\n        color=DARK_TEXT,\n        size=3.0,\n        fontface=\"bold\",\n        vjust=0.6,\n    )\n    + geom_text(\n        mapping=aes(x=\"x\", y=\"y\", label=\"symbol\"), data=df_missing, color=INK_SOFT, size=3.0, fontface=\"bold\", vjust=0.6\n    )\n    # Atomic numbers — top-left corner; LIGHT_TEXT on colored tiles, INK_SOFT on grey tiles\n    + geom_text(\n        mapping=aes(x=\"x\", y=\"y\", label=\"label_num\"),\n        data=df_colored,\n        color=LIGHT_TEXT,\n        size=1.7,\n        nudge_x=-0.30,\n        nudge_y=0.30,\n        hjust=0,\n    )\n    + geom_text(\n        mapping=aes(x=\"x\", y=\"y\", label=\"label_num\"),\n        data=df_missing,\n        color=INK_SOFT,\n        size=1.7,\n        nudge_x=-0.30,\n        nudge_y=0.30,\n        hjust=0,\n    )\n    + scale_fill_gradient(low=\"#009E73\", high=\"#4467A3\", name=\"Pauling\\nEN\", na_value=MISSING_FILL)\n    + labs(title=title)\n    + ggsize(800, 450)\n    + anyplot_theme\n)\n\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}