{"spec_id":"heatmap-periodic-table","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nheatmap-periodic-table: Periodic Table Property Heatmap\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 87/100 | Created: 2026-06-15\n\"\"\"\n\nimport os\nimport sys\n\n\nsys.path.pop(0)  # prevent this file shadowing the installed pygal package\n\nimport pygal\nfrom pygal.style import Style\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\"\nGREY_TILE = \"#CCCBC3\" if THEME == \"light\" else \"#3A3A36\"\n\nFONT = \"DejaVu Sans, Helvetica, Arial, sans-serif\"\n\n# Imprint sequential colormap: brand green → blue (single-polarity continuous data)\nSEQ_START = \"#009E73\"\nSEQ_END = \"#4467A3\"\n\n# Element data: (symbol, atomic_number, grid_col[1-18], grid_row[1-9], en_or_None)\n# grid_row 1-7 = periods 1-7 in the main 18-column table\n# grid_row 8   = lanthanide f-block row (Ce-Lu at cols 4-17)\n# grid_row 9   = actinide  f-block row (Th-Lr at cols 4-17)\n# La sits at (col=3, row=6) and Ac at (col=3, row=7) in the main table;\n# Ce-Lu and Th-Lr are detached below following the standard IUPAC layout.\nELEMENTS = [\n    # Period 1\n    (\"H\", 1, 1, 1, 2.20),\n    (\"He\", 2, 18, 1, None),\n    # Period 2\n    (\"Li\", 3, 1, 2, 0.98),\n    (\"Be\", 4, 2, 2, 1.57),\n    (\"B\", 5, 13, 2, 2.04),\n    (\"C\", 6, 14, 2, 2.55),\n    (\"N\", 7, 15, 2, 3.04),\n    (\"O\", 8, 16, 2, 3.44),\n    (\"F\", 9, 17, 2, 3.98),\n    (\"Ne\", 10, 18, 2, None),\n    # Period 3\n    (\"Na\", 11, 1, 3, 0.93),\n    (\"Mg\", 12, 2, 3, 1.31),\n    (\"Al\", 13, 13, 3, 1.61),\n    (\"Si\", 14, 14, 3, 1.90),\n    (\"P\", 15, 15, 3, 2.19),\n    (\"S\", 16, 16, 3, 2.58),\n    (\"Cl\", 17, 17, 3, 3.16),\n    (\"Ar\", 18, 18, 3, None),\n    # Period 4\n    (\"K\", 19, 1, 4, 0.82),\n    (\"Ca\", 20, 2, 4, 1.00),\n    (\"Sc\", 21, 3, 4, 1.36),\n    (\"Ti\", 22, 4, 4, 1.54),\n    (\"V\", 23, 5, 4, 1.63),\n    (\"Cr\", 24, 6, 4, 1.66),\n    (\"Mn\", 25, 7, 4, 1.55),\n    (\"Fe\", 26, 8, 4, 1.83),\n    (\"Co\", 27, 9, 4, 1.88),\n    (\"Ni\", 28, 10, 4, 1.91),\n    (\"Cu\", 29, 11, 4, 1.90),\n    (\"Zn\", 30, 12, 4, 1.65),\n    (\"Ga\", 31, 13, 4, 1.81),\n    (\"Ge\", 32, 14, 4, 2.01),\n    (\"As\", 33, 15, 4, 2.18),\n    (\"Se\", 34, 16, 4, 2.55),\n    (\"Br\", 35, 17, 4, 2.96),\n    (\"Kr\", 36, 18, 4, 3.00),\n    # Period 5\n    (\"Rb\", 37, 1, 5, 0.82),\n    (\"Sr\", 38, 2, 5, 0.95),\n    (\"Y\", 39, 3, 5, 1.22),\n    (\"Zr\", 40, 4, 5, 1.33),\n    (\"Nb\", 41, 5, 5, 1.60),\n    (\"Mo\", 42, 6, 5, 2.16),\n    (\"Tc\", 43, 7, 5, 1.90),\n    (\"Ru\", 44, 8, 5, 2.20),\n    (\"Rh\", 45, 9, 5, 2.28),\n    (\"Pd\", 46, 10, 5, 2.20),\n    (\"Ag\", 47, 11, 5, 1.93),\n    (\"Cd\", 48, 12, 5, 1.69),\n    (\"In\", 49, 13, 5, 1.78),\n    (\"Sn\", 50, 14, 5, 1.96),\n    (\"Sb\", 51, 15, 5, 2.05),\n    (\"Te\", 52, 16, 5, 2.10),\n    (\"I\", 53, 17, 5, 2.66),\n    (\"Xe\", 54, 18, 5, 2.60),\n    # Period 6 — La holds col 3; Hf-Rn are d/p block\n    (\"Cs\", 55, 1, 6, 0.79),\n    (\"Ba\", 56, 2, 6, 0.89),\n    (\"La\", 57, 3, 6, 1.10),\n    (\"Hf\", 72, 4, 6, 1.30),\n    (\"Ta\", 73, 5, 6, 1.50),\n    (\"W\", 74, 6, 6, 2.36),\n    (\"Re\", 75, 7, 6, 1.90),\n    (\"Os\", 76, 8, 6, 2.20),\n    (\"Ir\", 77, 9, 6, 2.20),\n    (\"Pt\", 78, 10, 6, 2.28),\n    (\"Au\", 79, 11, 6, 2.54),\n    (\"Hg\", 80, 12, 6, 2.00),\n    (\"Tl\", 81, 13, 6, 1.62),\n    (\"Pb\", 82, 14, 6, 2.33),\n    (\"Bi\", 83, 15, 6, 2.02),\n    (\"Po\", 84, 16, 6, 2.00),\n    (\"At\", 85, 17, 6, 2.20),\n    (\"Rn\", 86, 18, 6, 2.20),\n    # Period 7 — Ac holds col 3; Rf-Og are transactinides (no EN)\n    (\"Fr\", 87, 1, 7, 0.70),\n    (\"Ra\", 88, 2, 7, 0.90),\n    (\"Ac\", 89, 3, 7, 1.10),\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    # Lanthanide f-block row (row 8, cols 4-17 = Ce through Lu)\n    (\"Ce\", 58, 4, 8, 1.12),\n    (\"Pr\", 59, 5, 8, 1.13),\n    (\"Nd\", 60, 6, 8, 1.14),\n    (\"Pm\", 61, 7, 8, 1.13),\n    (\"Sm\", 62, 8, 8, 1.17),\n    (\"Eu\", 63, 9, 8, 1.20),\n    (\"Gd\", 64, 10, 8, 1.20),\n    (\"Tb\", 65, 11, 8, 1.10),\n    (\"Dy\", 66, 12, 8, 1.22),\n    (\"Ho\", 67, 13, 8, 1.23),\n    (\"Er\", 68, 14, 8, 1.24),\n    (\"Tm\", 69, 15, 8, 1.25),\n    (\"Yb\", 70, 16, 8, 1.10),\n    (\"Lu\", 71, 17, 8, 1.27),\n    # Actinide f-block row (row 9, cols 4-17 = Th through Lr)\n    (\"Th\", 90, 4, 9, 1.30),\n    (\"Pa\", 91, 5, 9, 1.50),\n    (\"U\", 92, 6, 9, 1.38),\n    (\"Np\", 93, 7, 9, 1.36),\n    (\"Pu\", 94, 8, 9, 1.28),\n    (\"Am\", 95, 9, 9, 1.30),\n    (\"Cm\", 96, 10, 9, 1.30),\n    (\"Bk\", 97, 11, 9, 1.30),\n    (\"Cf\", 98, 12, 9, 1.30),\n    (\"Es\", 99, 13, 9, 1.30),\n    (\"Fm\", 100, 14, 9, 1.30),\n    (\"Md\", 101, 15, 9, 1.30),\n    (\"No\", 102, 16, 9, 1.30),\n    (\"Lr\", 103, 17, 9, 1.30),\n]\n\nen_values = [e[4] for e in ELEMENTS if e[4] is not None]\nEN_MIN = min(en_values)  # 0.70  (Fr)\nEN_MAX = max(en_values)  # 3.98  (F)\n\n\ndef lerp_hex(c0, c1, t):\n    r0, g0, b0 = int(c0[1:3], 16), int(c0[3:5], 16), int(c0[5:7], 16)\n    r1, g1, b1 = int(c1[1:3], 16), int(c1[3:5], 16), int(c1[5:7], 16)\n    r = int(round(r0 + (r1 - r0) * t))\n    g = int(round(g0 + (g1 - g0) * t))\n    b = int(round(b0 + (b1 - b0) * t))\n    return f\"#{r:02X}{g:02X}{b:02X}\"\n\n\ndef tile_color(en):\n    if en is None:\n        return GREY_TILE\n    t = (en - EN_MIN) / (EN_MAX - EN_MIN)\n    return lerp_hex(SEQ_START, SEQ_END, t)\n\n\ndef relative_luminance(hex_c):\n    vals = [int(hex_c[i : i + 2], 16) / 255 for i in (1, 3, 5)]\n    lin = [v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4 for v in vals]\n    return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2]\n\n\ndef ink_for_tile(hex_c):\n    return \"#F0EFE8\" if relative_luminance(hex_c) < 0.35 else \"#1A1A17\"\n\n\n# Canvas and layout — landscape 3200×1800 (periodic table is inherently wider than tall)\nCANVAS_W = 3200\nCANVAS_H = 1800\nTILE = 155  # tile side length (px)\nGAP = 8  # gap between tiles (px)\nSTRIDE = TILE + GAP  # 163\nGRID_W = 17 * STRIDE + TILE  # 2926 px\nLEFT = (CANVAS_W - GRID_W) // 2  # 137 px left edge of column 1\nTOP = 118  # y of the top edge of period-1 tiles\nFBLOCK_EXTRA = 40  # extra gap between period-7 row and the f-block rows\n\nTITLE_TEXT = \"heatmap-periodic-table · python · pygal · anyplot.ai\"\n\n# Pygal Style — carries all theme tokens into the rendering engine\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"),\n    title_font_size=66,\n    label_font_size=44,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    stroke_width=2.5,\n)\n\n\ndef tile_xy(col, row):\n    \"\"\"Return (x, y) top-left corner for a tile at (col, row), 1-indexed.\"\"\"\n    x = LEFT + (col - 1) * STRIDE\n    if row <= 7:\n        y = TOP + (row - 1) * STRIDE\n    else:\n        row7_bottom = TOP + 6 * STRIDE + TILE\n        y = row7_bottom + FBLOCK_EXTRA + (row - 8) * STRIDE\n    return x, y\n\n\nclass PeriodicHeatmap(pygal.Bar):\n    \"\"\"Custom pygal chart type for periodic table heatmaps.\n\n    Subclasses pygal.Bar to leverage its SVG rendering engine, Style\n    theming, and render_to_png() infrastructure. The _plot() override\n    draws the canonical IUPAC 18×7 grid with f-block rows using\n    pygal's svg.node() API instead of the default bar chart.\n    \"\"\"\n\n    def _plot(self):\n        \"\"\"Draw the periodic table using pygal's SVG node API.\"\"\"\n        root = self.svg.root\n\n        # Full-canvas background — covers any pygal chrome drawn by _decorate()\n        self.svg.node(root, \"rect\", x=0, y=0, width=CANVAS_W, height=CANVAS_H, fill=PAGE_BG)\n\n        # Title\n        n = self.svg.node(\n            root,\n            \"text\",\n            attrib={\n                \"x\": str(CANVAS_W // 2),\n                \"y\": \"72\",\n                \"text-anchor\": \"middle\",\n                \"fill\": INK,\n                \"style\": f\"font-size:66px;font-weight:500;font-family:{FONT}\",\n            },\n        )\n        n.text = TITLE_TEXT\n\n        # Group numbers 1–18 above the tiles\n        for g in range(1, 19):\n            gx = LEFT + (g - 1) * STRIDE + TILE / 2\n            n = self.svg.node(\n                root,\n                \"text\",\n                attrib={\n                    \"x\": f\"{gx:.1f}\",\n                    \"y\": f\"{TOP - 20:.1f}\",\n                    \"text-anchor\": \"middle\",\n                    \"fill\": INK_MUTED,\n                    \"style\": f\"font-size:28px;font-family:{FONT}\",\n                },\n            )\n            n.text = str(g)\n\n        # Period numbers 1–7 to the left of the main grid\n        for p in range(1, 8):\n            px = LEFT - 30\n            py = TOP + (p - 1) * STRIDE + TILE / 2 + 11\n            n = self.svg.node(\n                root,\n                \"text\",\n                attrib={\n                    \"x\": f\"{px:.1f}\",\n                    \"y\": f\"{py:.1f}\",\n                    \"text-anchor\": \"end\",\n                    \"fill\": INK_MUTED,\n                    \"style\": f\"font-size:28px;font-family:{FONT}\",\n                },\n            )\n            n.text = str(p)\n\n        # f-block row labels (\"Ln\" and \"An\") to the left of the f-block rows\n        for fb_row, label in [(8, \"Ln\"), (9, \"An\")]:\n            _, fy = tile_xy(4, fb_row)\n            n = self.svg.node(\n                root,\n                \"text\",\n                attrib={\n                    \"x\": f\"{LEFT - 30:.1f}\",\n                    \"y\": f\"{fy + TILE / 2 + 11:.1f}\",\n                    \"text-anchor\": \"end\",\n                    \"fill\": INK_MUTED,\n                    \"style\": f\"font-size:28px;font-style:italic;font-family:{FONT}\",\n                },\n            )\n            n.text = label\n\n        # Thin dashed connector lines from period 6/7 group-3 tile to f-block rows\n        for main_row, fb_row in [(6, 8), (7, 9)]:\n            mx, my = tile_xy(3, main_row)\n            _, fy = tile_xy(4, fb_row)\n            self.svg.node(\n                root,\n                \"line\",\n                attrib={\n                    \"x1\": f\"{mx + TILE / 2:.1f}\",\n                    \"y1\": f\"{my + TILE:.1f}\",\n                    \"x2\": f\"{LEFT + 3 * STRIDE + TILE / 2:.1f}\",\n                    \"y2\": f\"{fy:.1f}\",\n                    \"stroke\": INK_MUTED,\n                    \"stroke-width\": \"1.5\",\n                    \"stroke-dasharray\": \"6,5\",\n                    \"stroke-opacity\": \"0.50\",\n                },\n            )\n\n        # Element tiles\n        for sym, an, col, row, en in ELEMENTS:\n            x, y = tile_xy(col, row)\n            fill = tile_color(en)\n            ink = ink_for_tile(fill)\n\n            self.svg.node(root, \"rect\", x=x, y=y, width=TILE, height=TILE, fill=fill, rx=3, ry=3)\n\n            # Atomic number — top-left corner, small\n            n = self.svg.node(\n                root, \"text\", x=x + 7, y=y + 22, fill=ink, style=f\"font-size:20px;opacity:0.80;font-family:{FONT}\"\n            )\n            n.text = str(an)\n\n            # Element symbol — centred, prominent\n            n = self.svg.node(\n                root,\n                \"text\",\n                attrib={\n                    \"x\": f\"{x + TILE / 2:.1f}\",\n                    \"y\": f\"{y + TILE / 2 + 22:.1f}\",\n                    \"text-anchor\": \"middle\",\n                    \"fill\": ink,\n                    \"style\": f\"font-size:50px;font-weight:700;font-family:{FONT}\",\n                },\n            )\n            n.text = sym\n\n            # EN value — bottom-centre, small (only when known)\n            if en is not None:\n                n = self.svg.node(\n                    root,\n                    \"text\",\n                    attrib={\n                        \"x\": f\"{x + TILE / 2:.1f}\",\n                        \"y\": f\"{y + TILE - 11:.1f}\",\n                        \"text-anchor\": \"middle\",\n                        \"fill\": ink,\n                        \"style\": f\"font-size:20px;opacity:0.80;font-family:{FONT}\",\n                    },\n                )\n                n.text = f\"{en:.2f}\"\n\n        # Colorbar — placed below the f-block rows\n        _, row9_y = tile_xy(4, 9)\n        cb_label_y = row9_y + TILE + 42\n        cb_x = LEFT\n        cb_w = GRID_W\n        cb_h = 42\n        cb_y = cb_label_y + 32\n\n        # Gradient definition\n        defs = self.svg.node(root, \"defs\")\n        grad = self.svg.node(\n            defs, \"linearGradient\", attrib={\"id\": \"cbGrad\", \"x1\": \"0\", \"y1\": \"0\", \"x2\": \"1\", \"y2\": \"0\"}\n        )\n        self.svg.node(grad, \"stop\", attrib={\"offset\": \"0%\", \"stop-color\": SEQ_START})\n        self.svg.node(grad, \"stop\", attrib={\"offset\": \"100%\", \"stop-color\": SEQ_END})\n\n        # Colorbar label\n        n = self.svg.node(\n            root,\n            \"text\",\n            attrib={\n                \"x\": f\"{cb_x + cb_w / 2:.1f}\",\n                \"y\": f\"{cb_label_y:.1f}\",\n                \"text-anchor\": \"middle\",\n                \"fill\": INK,\n                \"style\": f\"font-size:32px;font-family:{FONT}\",\n            },\n        )\n        n.text = \"Pauling Electronegativity\"\n\n        # Colorbar gradient rectangle\n        self.svg.node(\n            root,\n            \"rect\",\n            attrib={\n                \"x\": str(cb_x),\n                \"y\": str(cb_y),\n                \"width\": str(cb_w),\n                \"height\": str(cb_h),\n                \"fill\": \"url(#cbGrad)\",\n                \"rx\": \"4\",\n            },\n        )\n\n        # Colorbar tick marks and labels\n        for val in [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5]:\n            if EN_MIN <= val <= EN_MAX:\n                t = (val - EN_MIN) / (EN_MAX - EN_MIN)\n                tx = cb_x + t * cb_w\n                self.svg.node(\n                    root,\n                    \"line\",\n                    attrib={\n                        \"x1\": f\"{tx:.1f}\",\n                        \"y1\": f\"{cb_y + cb_h:.1f}\",\n                        \"x2\": f\"{tx:.1f}\",\n                        \"y2\": f\"{cb_y + cb_h + 10:.1f}\",\n                        \"stroke\": INK_SOFT,\n                        \"stroke-width\": \"1.5\",\n                    },\n                )\n                n = self.svg.node(\n                    root,\n                    \"text\",\n                    attrib={\n                        \"x\": f\"{tx:.1f}\",\n                        \"y\": f\"{cb_y + cb_h + 36:.1f}\",\n                        \"text-anchor\": \"middle\",\n                        \"fill\": INK_SOFT,\n                        \"style\": f\"font-size:26px;font-family:{FONT}\",\n                    },\n                )\n                n.text = f\"{val:.1f}\"\n\n        # Colorbar end labels (min / max values)\n        for x_pos, val in [(cb_x, EN_MIN), (cb_x + cb_w, EN_MAX)]:\n            n = self.svg.node(\n                root,\n                \"text\",\n                attrib={\n                    \"x\": f\"{x_pos:.1f}\",\n                    \"y\": f\"{cb_y + cb_h + 36:.1f}\",\n                    \"text-anchor\": \"middle\",\n                    \"fill\": INK_SOFT,\n                    \"style\": f\"font-size:26px;font-family:{FONT}\",\n                },\n            )\n            n.text = f\"{val:.2f}\"\n\n\n# Instantiate using pygal's Bar as base — provides SVG engine, Style theming,\n# render_to_png(), and render() without needing manual cairosvg calls\nchart = PeriodicHeatmap(\n    width=CANVAS_W,\n    height=CANVAS_H,\n    title=None,\n    show_legend=False,\n    show_x_labels=False,\n    show_y_labels=False,\n    show_x_guides=False,\n    show_y_guides=False,\n    style=custom_style,\n    margin=0,\n)\n\n# Add a minimal data point so pygal's _draw() calls _plot() (requires non-empty series)\nchart.add(\"EN\", [1.0])\n\n# Render PNG using pygal's built-in render_to_png() (uses cairosvg internally)\nchart.render_to_png(f\"plot-{THEME}.png\")\n\n# Render interactive HTML — pygal's render() returns SVG with embedded JS tooltips\nwith open(f\"plot-{THEME}.html\", \"wb\") as fh:\n    fh.write(chart.render())\n"}