Download notebook (.ipynb)

Mendeleev’s Periodic Table of Elements#

The notebook is inspired by this and this examples.

The data is available under the Creative Commons Attribution-ShareAlike 4.0 International Public License (CC BY-SA 4.0). For more details, see here or visit Data Explorer.

import pandas as pd

from lets_plot import *
LetsPlot.setup_html()
def get_elements_df():
    df = pd.read_csv("https://raw.githubusercontent.com/JetBrains/lets-plot-docs/master/data/chemical_elements.csv", encoding_errors='ignore')
    # Fixes and updates in data
    df.loc[df["Element"] == "Francium", "Type"] = "Alkali Metal"
    df.loc[df["Element"] == "Radium", "Type"] = "Alkaline Earth Metal"
    df.loc[df["Element"] == "Astatine", "Type"] = "Halogen"
    df.loc[df["Element"] == "Radon", "Type"] = "Noble Gas"
    df.loc[df["Atomic Number"] == 113, "Element"] = "Nihonium"
    df.loc[df["Atomic Number"] == 113, "Symbol"] = "Nh"
    df.loc[df["Atomic Number"] == 113, "Type"] = "Metal"
    df.loc[df["Atomic Number"] == 114, "Element"] = "Flerovium"
    df.loc[df["Atomic Number"] == 114, "Symbol"] = "Fl"
    df.loc[df["Atomic Number"] == 114, "Type"] = "Metal"
    df.loc[df["Atomic Number"] == 115, "Element"] = "Moscovium"
    df.loc[df["Atomic Number"] == 115, "Symbol"] = "Mc"
    df.loc[df["Atomic Number"] == 115, "Type"] = "Metal"
    df.loc[df["Atomic Number"] == 116, "Element"] = "Livermorium"
    df.loc[df["Atomic Number"] == 116, "Symbol"] = "Lv"
    df.loc[df["Atomic Number"] == 116, "Type"] = "Metal"
    df.loc[df["Atomic Number"] == 117, "Element"] = "Tennessine"
    df.loc[df["Atomic Number"] == 117, "Symbol"] = "Ts"
    df.loc[df["Atomic Number"] == 117, "Type"] = "Halogen"
    df.loc[df["Atomic Number"] == 118, "Element"] = "Oganesson"
    df.loc[df["Atomic Number"] == 118, "Symbol"] = "Og"
    df.loc[df["Atomic Number"] == 118, "Type"] = "Noble Gas"
    df.loc[df["Type"] == "Transactinide", "Type"] = "Transition Metal"
    return df

def prepare_top_df(filtered_df):
    return filtered_df.assign(
        X=lambda df: df["Group"],
        Y=lambda df: df["Period"],
    )

def prepare_bottom_df(filtered_df):
    import numpy as np

    nrows = 2
    hshift = 3
    vshift = 2.5
    return filtered_df.assign(
        X=np.tile(np.arange(len(filtered_df) // nrows), nrows) + hshift,
        Y=filtered_df["Period"] + vshift
    )

def get_extra_top_df():
    return pd.DataFrame({
        "X": [3, 3],
        "Y": [6, 7],
        "Type": ["Lanthanide", "Actinide"],
        "Range": ["57-71", "89-103"],
    })

def get_table_key_df(df, x, y, *, atomic_number):
    return df[df["Atomic Number"] == atomic_number].assign(X=[x], Y=[y])

def get_group_df(df):
    result = df.groupby(
        "Group"
    ).agg(
        Y=("Period", 'min')
    ).reset_index()
    result["Group"] = result["Group"].astype(int)
    return result

def get_period_df(min_value, max_value):
    return {
        "X": [0] * (max_value - min_value + 1),
        "Period": list(range(min_value, max_value + 1)),
    }

def get_annotations_df(x, y):
    return {
        "X": [x+.8, x+1, x+.4],
        "Y": [y-.9, y+.1, y+1],
        "Label": ["Atomic Number", "Symbol", "Atomic Mass"],
    }

elements_df = get_elements_df()
print(elements_df.shape)
elements_df.head()
(118, 23)
Atomic Number Element Symbol Atomic Weight Period Group Phase Most Stable Crystal Type Ionic Radius ... Density Melting Point (K) Boiling Point (K) Isotopes Discoverer Year of Discovery Specific Heat Capacity Electron Configuration Display Row Display Column
0 1 Hydrogen H 1.007940 1 1 gas NaN Nonmetal 0.012 ... 0.000090 14.175 20.28 3.0 Cavendish 1766.0 14.304 1s1 1 1
1 2 Helium He 4.002602 1 18 gas NaN Noble Gas NaN ... 0.000179 NaN 4.22 5.0 Janssen 1868.0 5.193 1s2 1 18
2 3 Lithium Li 6.941000 2 1 solid bcc Alkali Metal 0.760 ... 0.534000 453.850 1615.00 5.0 Arfvedson 1817.0 3.582 [He] 2s1 2 1
3 4 Beryllium Be 9.012182 2 2 solid hex Alkaline Earth Metal 0.350 ... 1.850000 1560.150 2742.00 6.0 Vaulquelin 1798.0 1.825 [He] 2s2 2 2
4 5 Boron B 10.811000 2 13 solid rho Metalloid 0.230 ... 2.340000 2573.150 4200.00 6.0 Gay-Lussac 1808.0 1.026 [He] 2s2 2p1 2 13

5 rows × 23 columns

tile_side = .95
tile_ratio = 1.2
table_key_size_ratio = 1.5
table_key_x, table_key_y = 10.25, 1.25
bottom_filter = lambda df: (df["Type"] == "Actinide")|(df["Type"] == "Lanthanide")

top_df = prepare_top_df(elements_df[~bottom_filter(elements_df)])
bottom_df = prepare_bottom_df(elements_df[bottom_filter(elements_df)])
extra_top_df = get_extra_top_df()
table_key_df = get_table_key_df(elements_df, table_key_x, table_key_y, atomic_number=78)
group_df = get_group_df(top_df)
period_df = get_period_df(1, 7)
annotations_df = get_annotations_df(table_key_x, table_key_y)
def inner_text(df, *, ratio=1):
    if 'Range' in df.columns:
        return geom_text(aes(label="Range"), data=df, nudge_y=.05*ratio, size=5*ratio, fontface='bold') + \
            geom_text(aes(label="Type"), data=df, nudge_y=-.2*ratio, size=4*ratio)
    else:
        return geom_text(aes(label="Atomic Number"), data=df, nudge_x=-.37*ratio, nudge_y=.37*ratio, hjust='left', vjust='top', size=5*ratio) + \
            geom_text(aes(label="Symbol"), data=df, nudge_y=.05*ratio, size=7*ratio, fontface='bold') + \
            geom_text(aes(label="Atomic Weight"), data=df, nudge_y=-.2*ratio, size=4*ratio, label_format=".3~f")

def table_key_annotations(x, y):
    table_key_arrow = arrow(angle=30, length=4, type='closed')
    return geom_curve(x=x+.7, y=y-.9, xend=x-.3, yend=y-.6, curvature=.4, ncp=1, arrow=table_key_arrow) + \
        geom_segment(x=x+.9, y=y+.1, xend=x+.3, yend=y-.1, curvature=.3, arrow=table_key_arrow) + \
        geom_curve(x=x+.3, y=y+1, xend=x, yend=y+.5, curvature=-.4, ncp=1, arrow=table_key_arrow)

element_tooltips = layer_tooltips().title("@Element\n(@Type)")\
                                   .line("@|@{Atomic Number}")\
                                   .line("Atomic Mass|@{Atomic Weight}")\
                                   .line("@|@{Electron Configuration}")
table_theme = theme(plot_title=element_text(size=26, face='bold', margin=[30, 0, 5, 0], hjust=.5), \
                    plot_caption=element_text(size=18), \
                    plot_background=element_rect(color='black', size=3), \
                    legend_position=[.36, .85], \
                    legend_background='blank')

ggplot(mapping=aes("X", "Y", fill="Type")) + \
    geom_tile(data=top_df, color='black', size=.25, width=tile_side, height=tile_side, tooltips=element_tooltips) + \
    geom_tile(data=extra_top_df, color='black', size=.25, width=tile_side, height=tile_side, tooltips='none') + \
    geom_tile(data=bottom_df, color='black', size=.25, width=tile_side, height=tile_side, tooltips=element_tooltips) + \
    geom_tile(data=table_key_df, color='black', size=.25, width=table_key_size_ratio*tile_side, height=table_key_size_ratio*tile_side, tooltips='none') + \
    inner_text(top_df) + \
    inner_text(extra_top_df) + \
    inner_text(bottom_df) + \
    inner_text(table_key_df, ratio=table_key_size_ratio) + \
    geom_text(aes("Group", "Y", label="Group"), data=group_df, \
              color='gray', nudge_y=.525, vjust='bottom', size=6) + \
    geom_text(aes("X", "Period", label="Period"), data=period_df, \
              color='gray', nudge_x=.375, vjust='right', size=6) + \
    geom_text(aes(label="Label"), data=annotations_df, hjust=0) + \
    table_key_annotations(table_key_x, table_key_y) + \
    scale_y_reverse() + \
    scale_fill_brewer(name='', type='qual', palette='Set2', guide=guide_legend(ncol=2)) + \
    coord_fixed(ratio=tile_ratio) + \
    labs(title="Periodic Table of Chemical Elements", caption="© 1869, Dmitri Mendeleev") + \
    ggsize(1000, 700) + \
    theme_void() + table_theme