{"spec_id":"heatmap-periodic-table","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nheatmap-periodic-table: Periodic Table Property Heatmap\nLibrary: altair 6.2.1 | Python 3.13.13\nQuality: 89/100 | Created: 2026-06-15\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove script directory from sys.path to avoid importing local altair.py\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nif _script_dir in sys.path:\n    sys.path.remove(_script_dir)\nsys.path[:] = [p for p in sys.path if os.path.abspath(p or \".\") != _script_dir]\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme tokens — Imprint palette, theme-adaptive chrome\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\"\n# Null tile: greyed element with no measured value\nTILE_NULL = \"#C0BFB8\" if THEME == \"light\" else \"#3A3A37\"\n# Text on colored tiles: near-white readable against both Imprint green and blue\nTEXT_ON_TILE = \"#F0EFE8\"\n\n# Imprint sequential colormap for continuous data (single-polarity IE values)\nCMAP_LOW = \"#009E73\"  # Imprint position 1 — low ionization energy\nCMAP_HIGH = \"#4467A3\"  # Imprint position 3 — high ionization energy\n\n# --- Element data: (symbol, atomic_number, group, display_period, ie_ev) ---\n# display_period 1-7 = main table; 8.5 = lanthanide row; 9.5 = actinide row\n# None for ie_ev = element has no well-measured first IE (superheavy elements)\nelements_data = [\n    # Period 1\n    (\"H\", 1, 1, 1, 13.598),\n    (\"He\", 2, 18, 1, 24.587),\n    # Period 2\n    (\"Li\", 3, 1, 2, 5.392),\n    (\"Be\", 4, 2, 2, 9.322),\n    (\"B\", 5, 13, 2, 8.298),\n    (\"C\", 6, 14, 2, 11.260),\n    (\"N\", 7, 15, 2, 14.534),\n    (\"O\", 8, 16, 2, 13.618),\n    (\"F\", 9, 17, 2, 17.422),\n    (\"Ne\", 10, 18, 2, 21.565),\n    # Period 3\n    (\"Na\", 11, 1, 3, 5.139),\n    (\"Mg\", 12, 2, 3, 7.646),\n    (\"Al\", 13, 13, 3, 5.986),\n    (\"Si\", 14, 14, 3, 8.151),\n    (\"P\", 15, 15, 3, 10.486),\n    (\"S\", 16, 16, 3, 10.360),\n    (\"Cl\", 17, 17, 3, 12.968),\n    (\"Ar\", 18, 18, 3, 15.760),\n    # Period 4\n    (\"K\", 19, 1, 4, 4.341),\n    (\"Ca\", 20, 2, 4, 6.113),\n    (\"Sc\", 21, 3, 4, 6.561),\n    (\"Ti\", 22, 4, 4, 6.828),\n    (\"V\", 23, 5, 4, 6.746),\n    (\"Cr\", 24, 6, 4, 6.767),\n    (\"Mn\", 25, 7, 4, 7.434),\n    (\"Fe\", 26, 8, 4, 7.902),\n    (\"Co\", 27, 9, 4, 7.881),\n    (\"Ni\", 28, 10, 4, 7.640),\n    (\"Cu\", 29, 11, 4, 7.726),\n    (\"Zn\", 30, 12, 4, 9.394),\n    (\"Ga\", 31, 13, 4, 5.999),\n    (\"Ge\", 32, 14, 4, 7.899),\n    (\"As\", 33, 15, 4, 9.815),\n    (\"Se\", 34, 16, 4, 9.752),\n    (\"Br\", 35, 17, 4, 11.814),\n    (\"Kr\", 36, 18, 4, 14.000),\n    # Period 5\n    (\"Rb\", 37, 1, 5, 4.177),\n    (\"Sr\", 38, 2, 5, 5.695),\n    (\"Y\", 39, 3, 5, 6.217),\n    (\"Zr\", 40, 4, 5, 6.634),\n    (\"Nb\", 41, 5, 5, 6.759),\n    (\"Mo\", 42, 6, 5, 7.092),\n    (\"Tc\", 43, 7, 5, 7.280),\n    (\"Ru\", 44, 8, 5, 7.361),\n    (\"Rh\", 45, 9, 5, 7.459),\n    (\"Pd\", 46, 10, 5, 8.337),\n    (\"Ag\", 47, 11, 5, 7.576),\n    (\"Cd\", 48, 12, 5, 8.994),\n    (\"In\", 49, 13, 5, 5.786),\n    (\"Sn\", 50, 14, 5, 7.344),\n    (\"Sb\", 51, 15, 5, 8.608),\n    (\"Te\", 52, 16, 5, 9.010),\n    (\"I\", 53, 17, 5, 10.451),\n    (\"Xe\", 54, 18, 5, 12.130),\n    # Period 6 main block (La-Lu pulled into f-block row)\n    (\"Cs\", 55, 1, 6, 3.894),\n    (\"Ba\", 56, 2, 6, 5.212),\n    # group 3 → placeholder (*) added separately\n    (\"Hf\", 72, 4, 6, 6.825),\n    (\"Ta\", 73, 5, 6, 7.549),\n    (\"W\", 74, 6, 6, 7.864),\n    (\"Re\", 75, 7, 6, 7.833),\n    (\"Os\", 76, 8, 6, 8.438),\n    (\"Ir\", 77, 9, 6, 8.967),\n    (\"Pt\", 78, 10, 6, 8.959),\n    (\"Au\", 79, 11, 6, 9.226),\n    (\"Hg\", 80, 12, 6, 10.438),\n    (\"Tl\", 81, 13, 6, 6.108),\n    (\"Pb\", 82, 14, 6, 7.417),\n    (\"Bi\", 83, 15, 6, 7.286),\n    (\"Po\", 84, 16, 6, 8.414),\n    (\"At\", 85, 17, 6, 9.310),\n    (\"Rn\", 86, 18, 6, 10.748),\n    # Period 7 main block (Ac-Lr pulled into f-block row)\n    (\"Fr\", 87, 1, 7, 4.073),\n    (\"Ra\", 88, 2, 7, 5.279),\n    # group 3 → placeholder (**) added separately\n    (\"Rf\", 104, 4, 7, None),\n    (\"Db\", 105, 5, 7, None),\n    (\"Sg\", 106, 6, 7, None),\n    (\"Bh\", 107, 7, 7, None),\n    (\"Hs\", 108, 8, 7, None),\n    (\"Mt\", 109, 9, 7, None),\n    (\"Ds\", 110, 10, 7, None),\n    (\"Rg\", 111, 11, 7, None),\n    (\"Cn\", 112, 12, 7, None),\n    (\"Nh\", 113, 13, 7, None),\n    (\"Fl\", 114, 14, 7, None),\n    (\"Mc\", 115, 15, 7, None),\n    (\"Lv\", 116, 16, 7, None),\n    (\"Ts\", 117, 17, 7, None),\n    (\"Og\", 118, 18, 7, None),\n    # Lanthanides — f-block row 1 (display_period=8.5, groups 3-17)\n    (\"La\", 57, 3, 8.5, 5.577),\n    (\"Ce\", 58, 4, 8.5, 5.539),\n    (\"Pr\", 59, 5, 8.5, 5.473),\n    (\"Nd\", 60, 6, 8.5, 5.525),\n    (\"Pm\", 61, 7, 8.5, 5.582),\n    (\"Sm\", 62, 8, 8.5, 5.644),\n    (\"Eu\", 63, 9, 8.5, 5.670),\n    (\"Gd\", 64, 10, 8.5, 6.150),\n    (\"Tb\", 65, 11, 8.5, 5.864),\n    (\"Dy\", 66, 12, 8.5, 5.939),\n    (\"Ho\", 67, 13, 8.5, 6.022),\n    (\"Er\", 68, 14, 8.5, 6.108),\n    (\"Tm\", 69, 15, 8.5, 6.184),\n    (\"Yb\", 70, 16, 8.5, 6.254),\n    (\"Lu\", 71, 17, 8.5, 5.426),\n    # Actinides — f-block row 2 (display_period=9.5, groups 3-17)\n    (\"Ac\", 89, 3, 9.5, 5.170),\n    (\"Th\", 90, 4, 9.5, 6.308),\n    (\"Pa\", 91, 5, 9.5, 5.890),\n    (\"U\", 92, 6, 9.5, 6.194),\n    (\"Np\", 93, 7, 9.5, 6.266),\n    (\"Pu\", 94, 8, 9.5, 6.026),\n    (\"Am\", 95, 9, 9.5, 5.974),\n    (\"Cm\", 96, 10, 9.5, 5.991),\n    (\"Bk\", 97, 11, 9.5, 6.198),\n    (\"Cf\", 98, 12, 9.5, 6.282),\n    (\"Es\", 99, 13, 9.5, 6.420),\n    (\"Fm\", 100, 14, 9.5, 6.500),\n    (\"Md\", 101, 15, 9.5, 6.580),\n    (\"No\", 102, 16, 9.5, 6.650),\n    (\"Lr\", 103, 17, 9.5, 4.900),\n    # Placeholder tiles for La-Lu / Ac-Lr gaps in the main table\n    (\"*\", None, 3, 6, None),\n    (\"**\", None, 3, 7, None),\n]\n\n# Build DataFrame\ndf = pd.DataFrame(elements_data, columns=[\"symbol\", \"atomic_number\", \"group\", \"display_period\", \"value\"])\n\n# Tile boundary columns (0.47 half-width leaves a small gap between tiles)\nHALF = 0.47\ndf[\"x1\"] = df[\"group\"] - HALF\ndf[\"x2\"] = df[\"group\"] + HALF\ndf[\"y1\"] = df[\"display_period\"] - HALF\ndf[\"y2\"] = df[\"display_period\"] + HALF\ndf[\"x_center\"] = df[\"group\"].astype(float)\ndf[\"y_center\"] = df[\"display_period\"].astype(float)\n# Atomic number left-aligned within tile\ndf[\"x_atomnum\"] = df[\"group\"] - HALF + 0.06\n\n# Display labels\ndf[\"atomic_label\"] = df[\"atomic_number\"].apply(lambda v: str(int(v)) if pd.notna(v) else \"\")\ndf[\"value_label\"] = df[\"value\"].apply(lambda v: f\"{v:.2f}\" if pd.notna(v) else \"\")\n\n# Text color: white-ish on colored tiles, soft ink on null/placeholder tiles\ndf[\"text_color\"] = np.where(df[\"value\"].notna(), TEXT_ON_TILE, INK_SOFT)\n\n# Tooltip labels\ndf[\"tooltip_symbol\"] = df[\"symbol\"]\ndf[\"tooltip_ie\"] = df[\"value\"].apply(lambda v: f\"{v:.3f} eV\" if pd.notna(v) else \"—\")\n\ndf_null = df[df[\"value\"].isna()].copy()\ndf_valid = df[df[\"value\"].notna()].copy()\n\n# Title with length-scaled font\ntitle_str = \"Ionization Energy · heatmap-periodic-table · python · altair · anyplot.ai\"\nn = len(title_str)\ntitle_fontsize = round(16 * 67 / n) if n > 67 else 16  # = 14\n\n# Shared axis scales — y reversed so period 1 is at the top\nx_scale = alt.Scale(domain=[0.5, 18.5])\ny_scale = alt.Scale(domain=[10.05, 0.5])  # reversed: large period → bottom\n\n# Null / placeholder tiles (grey)\nnull_rects = (\n    alt.Chart(df_null)\n    .mark_rect(stroke=PAGE_BG, strokeWidth=1.5)\n    .encode(\n        x=alt.X(\"x1:Q\", scale=x_scale, axis=None),\n        x2=\"x2:Q\",\n        y=alt.Y(\"y1:Q\", scale=y_scale, axis=None),\n        y2=\"y2:Q\",\n        color=alt.value(TILE_NULL),\n        tooltip=[alt.Tooltip(\"symbol:N\", title=\"Symbol\"), alt.Tooltip(\"tooltip_ie:N\", title=\"IE\")],\n    )\n)\n\n# Valid tiles colored by first ionization energy (Imprint sequential)\nvalid_rects = (\n    alt.Chart(df_valid)\n    .mark_rect(stroke=PAGE_BG, strokeWidth=1.5)\n    .encode(\n        x=alt.X(\"x1:Q\", scale=x_scale, axis=None),\n        x2=\"x2:Q\",\n        y=alt.Y(\"y1:Q\", scale=y_scale, axis=None),\n        y2=\"y2:Q\",\n        color=alt.Color(\n            \"value:Q\",\n            scale=alt.Scale(range=[CMAP_LOW, CMAP_HIGH], nice=False),\n            legend=alt.Legend(\n                title=\"1st Ionization Energy (eV)\",\n                orient=\"bottom\",\n                direction=\"horizontal\",\n                gradientLength=220,\n                gradientThickness=14,\n                titleFontSize=10,\n                labelFontSize=9,\n                titleColor=INK,\n                labelColor=INK_SOFT,\n                offset=8,\n            ),\n        ),\n        tooltip=[\n            alt.Tooltip(\"symbol:N\", title=\"Symbol\"),\n            alt.Tooltip(\"atomic_number:Q\", title=\"Atomic #\", format=\"d\"),\n            alt.Tooltip(\"tooltip_ie:N\", title=\"IE\"),\n        ],\n    )\n)\n\n# Atomic number — small text, top-left of each tile\natomic_text = (\n    alt.Chart(df)\n    .mark_text(align=\"left\", baseline=\"top\", fontSize=6, dy=-11)\n    .encode(\n        x=alt.X(\"x_atomnum:Q\", scale=x_scale, axis=None),\n        y=alt.Y(\"y_center:Q\", scale=y_scale, axis=None),\n        text=\"atomic_label:N\",\n        color=alt.Color(\"text_color:N\", scale=None, legend=None),\n    )\n)\n\n# Element symbol — bold, centered in tile\nsymbol_text = (\n    alt.Chart(df)\n    .mark_text(align=\"center\", baseline=\"middle\", fontSize=8, fontWeight=\"bold\", dy=-2)\n    .encode(\n        x=alt.X(\"x_center:Q\", scale=x_scale, axis=None),\n        y=alt.Y(\"y_center:Q\", scale=y_scale, axis=None),\n        text=\"symbol:N\",\n        color=alt.Color(\"text_color:N\", scale=None, legend=None),\n    )\n)\n\n# IE value — small text at bottom of colored tiles\nvalue_text = (\n    alt.Chart(df_valid)\n    .mark_text(align=\"center\", baseline=\"bottom\", fontSize=5.5, dy=12)\n    .encode(\n        x=alt.X(\"x_center:Q\", scale=x_scale, axis=None),\n        y=alt.Y(\"y_center:Q\", scale=y_scale, axis=None),\n        text=\"value_label:N\",\n        color=alt.value(TEXT_ON_TILE),\n    )\n)\n\n# Compose all layers\nchart = (\n    alt.layer(null_rects, valid_rects, atomic_text, symbol_text, value_text)\n    .properties(\n        width=620,\n        height=295,\n        background=PAGE_BG,\n        padding={\"left\": 0, \"right\": 0, \"top\": 0, \"bottom\": 0},\n        title=alt.Title(title_str, fontSize=title_fontsize, color=INK, offset=8),\n    )\n    .configure_view(continuousWidth=620, continuousHeight=295, fill=PAGE_BG, stroke=\"transparent\")\n    .configure_legend(fillColor=\"transparent\", strokeColor=\"transparent\")\n)\n\n# Save PNG and HTML\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\nchart.save(f\"plot-{THEME}.html\")\n\n# Pad PNG to exact 3200×1800 target (vl-convert may land slightly short)\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n"}