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