Interactive Periodic Table
Abstract
Section titled “Abstract”The periodic table is a beautiful data-visualization problem: 118 elements, each with a dozen properties, arranged in a grid that itself encodes meaning (rows = periods, columns = groups). In this tutorial you build a Tkinter app where each element is a clickable button that pops up its details. Then you scale it from 10 elements to all 118, lay them out in the real periodic-table shape, color them by category (metals, nonmetals, noble gases), and add live search and filtering.
You will leave understanding:
- How to generate a grid of widgets from a data list with
grid(row=, column=). - The classic late-binding lambda bug in loops — and the
e=elementfix. - How to drive layout from data (atomic number → grid position).
- How to color-code and filter a dataset in a GUI.
Prerequisites
Section titled “Prerequisites”- Python 3.6 or above.
- A text editor or IDE.
- Tkinter (bundled with Python).
- Comfort with lists, dictionaries, and loops.
Getting Started
Section titled “Getting Started”Create the project
Section titled “Create the project”- Create a folder named
periodic-table. - Inside it, create
interactive_periodic_table.py.
Write the code
Section titled “Write the code”interactive_periodic_table.py
pch.viewSource"""
Interactive Periodic Table
A Python application that displays an interactive periodic table. Features include:
- Displaying information about elements when clicked.
- Searching for elements by name or symbol.
"""
import tkinter as tk
from tkinter import messagebox
# Sample data for the periodic table
elements = [
{"symbol": "H", "name": "Hydrogen", "atomic_number": 1, "atomic_mass": 1.008},
{"symbol": "He", "name": "Helium", "atomic_number": 2, "atomic_mass": 4.0026},
{"symbol": "Li", "name": "Lithium", "atomic_number": 3, "atomic_mass": 6.94},
{"symbol": "Be", "name": "Beryllium", "atomic_number": 4, "atomic_mass": 9.0122},
{"symbol": "B", "name": "Boron", "atomic_number": 5, "atomic_mass": 10.81},
{"symbol": "C", "name": "Carbon", "atomic_number": 6, "atomic_mass": 12.011},
{"symbol": "N", "name": "Nitrogen", "atomic_number": 7, "atomic_mass": 14.007},
{"symbol": "O", "name": "Oxygen", "atomic_number": 8, "atomic_mass": 15.999},
{"symbol": "F", "name": "Fluorine", "atomic_number": 9, "atomic_mass": 18.998},
{"symbol": "Ne", "name": "Neon", "atomic_number": 10, "atomic_mass": 20.180},
]
class InteractivePeriodicTable:
def __init__(self, root):
self.root = root
self.root.title("Interactive Periodic Table")
self.create_table()
def create_table(self):
"""Create the periodic table layout."""
for i, element in enumerate(elements):
button = tk.Button(
self.root,
text=element["symbol"],
width=10,
height=3,
command=lambda e=element: self.show_element_info(e),
)
button.grid(row=i // 5, column=i % 5, padx=5, pady=5)
def show_element_info(self, element):
"""Display information about the selected element."""
info = (
f"Name: {element['name']}\n"
f"Symbol: {element['symbol']}\n"
f"Atomic Number: {element['atomic_number']}\n"
f"Atomic Mass: {element['atomic_mass']}"
)
messagebox.showinfo("Element Information", info)
def main():
root = tk.Tk()
app = InteractivePeriodicTable(root)
root.mainloop()
if __name__ == "__main__":
main() Run it
Section titled “Run it”C:\Users\Your Name\periodic-table> python interactive_periodic_table.py
# A grid of element buttons appears. Click one to see its details.How it fits together
Section titled “How it fits together”Read from the top: this is what runs when you execute the file, and which function calls which. It is generated from the code, so it cannot drift from it.
flowchart TD RUN(["python interactive_periodic_table.py"]) InteractivePeriodicTable["InteractivePeriodicTable
class"] main("main") RUN --> main main --> InteractivePeriodicTable
Step-by-Step Explanation
Section titled “Step-by-Step Explanation”1. Elements as data
Section titled “1. Elements as data”elements = [
{"symbol": "H", "name": "Hydrogen", "atomic_number": 1, "atomic_mass": 1.008},
{"symbol": "He", "name": "Helium", "atomic_number": 2, "atomic_mass": 4.0026},
...
]Each element is a dict. The UI is generated from this list — add an element here and a button appears automatically. This data-drives-the-view idea is the heart of the project.
2. Building the grid
Section titled “2. Building the grid”for i, element in enumerate(elements):
button = tk.Button(
self.root, text=element["symbol"], width=10, height=3,
command=lambda e=element: self.show_element_info(e),
)
button.grid(row=i // 5, column=i % 5, padx=5, pady=5)grid places widgets in rows and columns. The integer math is the trick: i // 5 is the row, i % 5 is the column — so elements flow left-to-right, wrapping every 5. Change the 5 to change the columns per row.
3. The lambda gotcha (critical)
Section titled “3. The lambda gotcha (critical)”command=lambda e=element: self.show_element_info(e)Why e=element and not just lambda: self.show_element_info(element)? Because of late binding: a bare lambda captures the variable element, not its current value. By the time you click, the loop has finished and element points at the last item — so every button would show Neon. Binding e=element as a default argument snapshots the value now. This is the single most common Tkinter-loop bug.
4. Showing details
Section titled “4. Showing details”def show_element_info(self, element):
info = (
f"Name: {element['name']}\n"
f"Symbol: {element['symbol']}\n"
f"Atomic Number: {element['atomic_number']}\n"
f"Atomic Mass: {element['atomic_mass']}"
)
messagebox.showinfo("Element Information", info)A popup is fine to start; a side panel that updates in place feels far more like a real app (below).
Scale to All 118 Elements — in the Real Layout
Section titled “Scale to All 118 Elements — in the Real Layout”The actual periodic table isn’t a 5-wide wrap; it’s an 18-column grid with gaps. Store each element’s group (column) and period (row) and place it precisely:
# Each element gains "period" and "group" fields
for el in elements:
tk.Button(self.root, text=el["symbol"], width=4, height=2,
command=lambda e=el: self.show(e)).grid(
row=el["period"], column=el["group"], padx=1, pady=1)The lanthanides and actinides sit in two detached rows below — exactly as on a wall chart. Pull a full dataset from a JSON file rather than typing 118 dicts by hand.
Color by Category
Section titled “Color by Category”Chemistry uses color to group elements. Map categories to colors:
CATEGORY_COLORS = {
"alkali metal": "#ff6666", "noble gas": "#66ccff",
"nonmetal": "#99ff99", "metalloid": "#ffcc66", "transition metal": "#cccccc",
}
btn.config(bg=CATEGORY_COLORS.get(el["category"], "white"))Suddenly the table teaches — trends jump out visually.
Add Search and Filtering
Section titled “Add Search and Filtering”An Entry that highlights matches as you type:
def on_search(self, query):
q = query.lower()
for el, btn in self.buttons.items():
match = q in el["name"].lower() or q == el["symbol"].lower()
btn.config(relief="solid" if match and q else "raised")Filter by category with a dropdown to dim everything except, say, the noble gases.
Upgrade the Detail View
Section titled “Upgrade the Detail View”Replace the popup with a persistent side panel showing electron configuration, melting/boiling points, discovery year, and a short description. Update it on click instead of spawning dialogs — it feels like a reference tool, not a quiz.
Common Mistakes
Section titled “Common Mistakes”| Problem | Cause | Fix |
|---|---|---|
| Every button shows the last element | Late-binding lambda | Bind the value: lambda e=element: ... |
| Elements pile in one column | Wrong row/column math | Use i // cols and i % cols, or real group/period |
| Layout looks nothing like the chart | Used a simple wrap | Place by group/period with gaps |
| Window huge or buttons tiny | Fixed sizes vs. 118 cells | Smaller width/height; let the grid expand |
| Typing 118 dicts by hand is error-prone | Hard-coded data | Load elements from a JSON dataset |
| Search does nothing | Compared against symbol case-sensitively | Normalize with .lower() |
Variations to Try
Section titled “Variations to Try”- Full dataset — all 118 elements with 15+ properties from JSON.
- Trend heatmaps — color by electronegativity or atomic radius.
- Element quiz — “click the noble gases” timed challenge.
- Compare mode — select two elements and diff their properties.
- 3D Bohr model — draw electron shells for the selected element.
- Unit toggles — switch mass/temperature units.
- Export — save the selected element as a flashcard image.
Real-World Applications
Section titled “Real-World Applications”- Education — chemistry teaching and revision tools.
- Reference apps — quick element lookup for students and engineers.
- Data-viz practice — encoding multiple dimensions (color, position) at once.
- Material selection — filtering elements by property ranges.
Educational Value
Section titled “Educational Value”- Data-driven UI — generating widgets from a dataset.
- Grid layout math — mapping an index (or group/period) to position.
- Closures & late binding — the most-asked-about Python loop bug.
- Visual encoding — color and position carrying meaning.
Next Steps
Section titled “Next Steps”- Load all 118 elements from a JSON dataset.
- Lay them out in the real group/period grid.
- Color by category and add search + filtering.
- Swap the popup for a live detail panel.
Conclusion
Section titled “Conclusion”You built an interactive periodic table, dodged the infamous late-binding lambda trap, and learned to drive a UI entirely from data. Scaled to 118 color-coded, searchable elements in their true layout, it becomes a genuine reference and teaching tool — and the data-drives-the-view pattern you practiced powers dashboards, seating charts, and any grid-of-things UI you’ll build next. Full source on GitHub. Explore more visualization projects on Python Central Hub.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading