import json
import os
import tkinter as tk
from tkinter import ttk, messagebox
import webbrowser
from functools import partial
import threading
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D # noqa: F401 unused import (needed by matplotlib 3D)
import numpy as np
# ----------------- Config -----------------
DATA_FILE = "elements.json"
GRID_COLUMNS = 18
GRID_ROWS = 9 # we'll use rows 1-7 + an extra for Lanthanides/Actinides placeholder
# ----------------- Default JSON Creator -----------------
DEFAULT_ELEMENTS = {
# symbol: data
"H": {"name": "Hydrogen", "atomic_number": 1, "atomic_mass": 1.008, "group": 1, "period": 1,
"category": "Nonmetal", "uses": ["Fuel", "Chemical feedstock"], "electron_configuration": "1s1"},
"He": {"name": "Helium", "atomic_number": 2, "atomic_mass": 4.0026, "group": 18, "period": 1,
"category": "Noble Gas", "uses": ["Balloons", "Cryogenics"], "electron_configuration": "1s2"},
"Li": {"name": "Lithium", "atomic_number": 3, "atomic_mass": 6.94, "group": 1, "period": 2,
"category": "Alkali Metal", "uses": ["Batteries"], "electron_configuration": "[He] 2s1"},
"Be": {"name": "Beryllium", "atomic_number": 4, "atomic_mass": 9.0122, "group": 2, "period": 2,
"category": "Alkaline Earth Metal", "uses": ["Aerospace alloys"], "electron_configuration": "[He] 2s2"},
"B": {"name": "Boron", "atomic_number": 5, "atomic_mass": 10.81, "group": 13, "period": 2,
"category": "Metalloid", "uses": ["Glass", "Detergents"], "electron_configuration": "[He] 2s2 2p1"},
"C": {"name": "Carbon", "atomic_number": 6, "atomic_mass": 12.011, "group": 14, "period": 2,
"category": "Nonmetal", "uses": ["Organic chemistry", "Materials"], "electron_configuration": "[He] 2s2 2p2"},
"N": {"name": "Nitrogen", "atomic_number": 7, "atomic_mass": 14.007, "group": 15, "period": 2,
"category": "Nonmetal", "uses": ["Fertilizers", "Industrial gas"], "electron_configuration": "[He] 2s2 2p3"},
"O": {"name": "Oxygen", "atomic_number": 8, "atomic_mass": 15.999, "group": 16, "period": 2,
"category": "Nonmetal", "uses": ["Respiration", "Combustion"], "electron_configuration": "[He] 2s2 2p4"},
"F": {"name": "Fluorine", "atomic_number": 9, "atomic_mass": 18.998, "group": 17, "period": 2,
"category": "Halogen", "uses": ["Toothpaste (fluoride)"], "electron_configuration": "[He] 2s2 2p5"},
"Ne": {"name": "Neon", "atomic_number": 10, "atomic_mass": 20.1797, "group": 18, "period": 2,
"category": "Noble Gas", "uses": ["Neon signs"], "electron_configuration": "[He] 2s2 2p6"},
"Na": {"name": "Sodium", "atomic_number": 11, "atomic_mass": 22.989, "group": 1, "period": 3,
"category": "Alkali Metal", "uses": ["Salt", "Chemicals"], "electron_configuration": "[Ne] 3s1"},
"Mg": {"name": "Magnesium", "atomic_number": 12, "atomic_mass": 24.305, "group": 2, "period": 3,
"category": "Alkaline Earth Metal", "uses": ["Alloys", "Electronics"], "electron_configuration": "[Ne] 3s2"},
"Al": {"name": "Aluminium", "atomic_number": 13, "atomic_mass": 26.9815, "group": 13, "period": 3,
"category": "Post-transition Metal", "uses": ["Packaging", "Aerospace"], "electron_configuration": "[Ne] 3s2 3p1"},
"Si": {"name": "Silicon", "atomic_number": 14, "atomic_mass": 28.085, "group": 14, "period": 3,
"category": "Metalloid", "uses": ["Semiconductors"], "electron_configuration": "[Ne] 3s2 3p2"},
"P": {"name": "Phosphorus", "atomic_number": 15, "atomic_mass": 30.9738, "group": 15, "period": 3,
"category": "Nonmetal", "uses": ["Fertilizers"], "electron_configuration": "[Ne] 3s2 3p3"},
"S": {"name": "Sulfur", "atomic_number": 16, "atomic_mass": 32.06, "group": 16, "period": 3,
"category": "Nonmetal", "uses": ["Sulfuric acid", "Fertilizers"], "electron_configuration": "[Ne] 3s2 3p4"},
"Cl": {"name": "Chlorine", "atomic_number": 17, "atomic_mass": 35.45, "group": 17, "period": 3,
"category": "Halogen", "uses": ["Water purification"], "electron_configuration": "[Ne] 3s2 3p5"},
"Ar": {"name": "Argon", "atomic_number": 18, "atomic_mass": 39.948, "group": 18, "period": 3,
"category": "Noble Gas", "uses": ["Lighting", "Welding"], "electron_configuration": "[Ne] 3s2 3p6"},
"K": {"name": "Potassium", "atomic_number": 19, "atomic_mass": 39.0983, "group": 1, "period": 4,
"category": "Alkali Metal", "uses": ["Fertilizers"], "electron_configuration": "[Ar] 4s1"},
"Ca": {"name": "Calcium", "atomic_number": 20, "atomic_mass": 40.078, "group": 2, "period": 4,
"category": "Alkaline Earth Metal", "uses": ["Bones", "Construction"], "electron_configuration": "[Ar] 4s2"}
}
def ensure_data_file():
if not os.path.exists(DATA_FILE):
with open(DATA_FILE, "w", encoding="utf-8") as f:
json.dump(DEFAULT_ELEMENTS, f, indent=2)
print(f"Created default {DATA_FILE} with first 20 elements. You can edit/expand it.")
# ----------------- Utility: Load Data -----------------
def load_elements():
ensure_data_file()
with open(DATA_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
# Normalize: attach group/period if missing
elements = {}
for sym, info in data.items():
elements[sym] = info
return elements
# ----------------- Orbital Visualizer -----------------
def show_orbital_visual(element_data):
"""
Very simple 3D shells visualization.
Each shell (n) is drawn as a translucent sphere surface of radius proportional to n.
Electron counts per shell approximated from electron_configuration (simple parse).
"""
# parse approximate shells from electron_configuration string by counting numbers in brackets or 's','p','d'
cfg = element_data.get("electron_configuration", "")
# crude parsing to get principal quantum numbers occurrences like 1s2 2s2 2p6 etc.
shells = []
import re
matches = re.findall(r'(\d)(?:[spdf])\d*', cfg)
if matches:
# get unique shell numbers and sort
shells = sorted(set(int(m) for m in matches))
else:
# fallback: estimate shells from atomic number
an = element_data.get("atomic_number", 1)
# very rough heuristic: shell count = ceil(sqrt(Z)/2) — just for visualization
shells = list(range(1, int(max(1, round((an**0.5)/2))) + 2))
fig = plt.figure(figsize=(6,6))
ax = fig.add_subplot(111, projection='3d')
ax.set_facecolor("white")
ax._axis3don = False
# Draw concentric sphere surfaces for shells
for n in shells:
# create sphere
u = np.linspace(0, 2 * np.pi, 60)
v = np.linspace(0, np.pi, 30)
r = 0.6 * n # radius scale
x = r * np.outer(np.cos(u), np.sin(v))
y = r * np.outer(np.sin(u), np.sin(v))
z = r * np.outer(np.ones_like(u), np.cos(v))
ax.plot_surface(x, y, z, color=plt.cm.viridis(n/ max(1, len(shells))), alpha=0.15, linewidth=0)
# put some electron points randomly distributed on shell
np.random.seed(n*10)
m = min(12, 4 + n*4)
theta = np.random.uniform(0, 2*np.pi, m)
phi = np.random.uniform(0, np.pi, m)
ex = r * np.sin(phi) * np.cos(theta)
ey = r * np.sin(phi) * np.sin(theta)
ez = r * np.cos(phi)
ax.scatter(ex, ey, ez, s=30, color=plt.cm.viridis(n / max(1, len(shells))))
ax.set_title(f"Shells for {element_data.get('name', '')} ({element_data.get('symbol','')})", pad=20)
max_r = 0.6 * (max(shells) + 1)
ax.set_xlim(-max_r, max_r)
ax.set_ylim(-max_r, max_r)
ax.set_zlim(-max_r, max_r)
plt.show()
# ----------------- GUI -----------------
class PeriodicTableApp:
def __init__(self, root):
self.root = root
self.root.title("Interactive Periodic Table")
self.elements = load_elements() # key: symbol
# create a mapping by atomic_number for quick lookup
self.by_atomic = {info["atomic_number"]: sym for sym, info in self.elements.items() if "atomic_number" in info}
self.buttons = {} # symbol -> button
self.build_ui()
def build_ui(self):
top = tk.Frame(self.root)
top.pack(padx=8, pady=8, anchor="w")
tk.Label(top, text="Search (symbol / name / atomic #):").pack(side="left")
self.search_var = tk.StringVar()
search_entry = tk.Entry(top, textvariable=self.search_var)
search_entry.pack(side="left", padx=4)
search_entry.bind("<Return>", lambda e: self.do_search())
tk.Button(top, text="Search", command=self.do_search).pack(side="left", padx=4)
tk.Button(top, text="Open elements.json", command=self.open_data_file).pack(side="left", padx=6)
# Table frame
table_frame = tk.Frame(self.root)
table_frame.pack(padx=8, pady=8)
# create empty grid
for r in range(1, 8): # periods 1..7
for c in range(1, GRID_COLUMNS + 1):
placeholder = tk.Frame(table_frame, width=46, height=36, bd=0)
placeholder.grid(row=r, column=c, padx=1, pady=1)
# Place element buttons based on group/period columns
for sym, info in self.elements.items():
group = info.get("group")
period = info.get("period")
if group is None or period is None:
# skip elements without position (lanthanides/actinides not positioned)
continue
r = period
c = group
btn = tk.Button(table_frame, text=f"{sym}\n{info.get('atomic_number','')}",
width=6, height=3, command=partial(self.show_element_popup, sym))
btn.grid(row=r, column=c, padx=1, pady=1)
self.buttons[sym] = btn
# Add legend / info area
info_frame = tk.Frame(self.root)
info_frame.pack(fill="x", padx=8, pady=6)
tk.Label(info_frame, text="Click an element to view details and 3D shell visualization.", fg="gray").pack(side="left")
def open_data_file(self):
path = os.path.abspath(DATA_FILE)
if os.path.exists(path):
webbrowser.open(f"file://{path}")
else:
messagebox.showinfo("Info", f"{DATA_FILE} not found.")
def do_search(self):
q = self.search_var.get().strip().lower()
if not q:
return
# search by symbol
sym_match = None
for sym, info in self.elements.items():
if sym.lower() == q:
sym_match = sym
break
if sym_match:
self.flash_button(sym_match)
self.show_element_popup(sym_match)
return
# search by name
for sym, info in self.elements.items():
if info.get("name", "").lower() == q:
self.flash_button(sym)
self.show_element_popup(sym)
return
# search by atomic number
if q.isdigit():
an = int(q)
sym = self.by_atomic.get(an)
if sym:
self.flash_button(sym)
self.show_element_popup(sym)
return
# partial name match
for sym, info in self.elements.items():
if q in info.get("name", "").lower():
self.flash_button(sym)
self.show_element_popup(sym)
return
messagebox.showinfo("Search", "No matching element found.")
def flash_button(self, sym):
btn = self.buttons.get(sym)
if not btn:
return
orig = btn.cget("bg")
def _flash():
for _ in range(4):
btn.config(bg="yellow")
btn.update()
self.root.after(200)
btn.config(bg=orig)
btn.update()
self.root.after(200)
threading.Thread(target=_flash, daemon=True).start()
def show_element_popup(self, sym):
info = self.elements.get(sym)
if not info:
messagebox.showerror("Error", "Element data not found.")
return
popup = tk.Toplevel(self.root)
popup.title(f"{sym} - {info.get('name','')}")
popup.geometry("420x320")
left = tk.Frame(popup)
left.pack(side="left", fill="both", expand=True, padx=8, pady=8)
tk.Label(left, text=f"{info.get('name','')} ({sym})", font=("Arial", 16, "bold")).pack(anchor="w")
tk.Label(left, text=f"Atomic Number: {info.get('atomic_number','')}", font=("Arial", 12)).pack(anchor="w")
tk.Label(left, text=f"Atomic Mass: {info.get('atomic_mass','')}", font=("Arial", 12)).pack(anchor="w")
tk.Label(left, text=f"Category: {info.get('category','')}", font=("Arial", 12)).pack(anchor="w")
tk.Label(left, text=f"Electron Configuration: {info.get('electron_configuration','')}", font=("Arial", 11)).pack(anchor="w", pady=6)
uses = info.get("uses", [])
tk.Label(left, text="Uses / Applications:", font=("Arial", 12, "underline")).pack(anchor="w", pady=(6,0))
uses_text = tk.Text(left, height=6, wrap="word")
uses_text.pack(fill="both", expand=True)
uses_text.insert("1.0", "\n".join(uses) if isinstance(uses, list) else str(uses))
uses_text.config(state="disabled")
# right side controls
right = tk.Frame(popup)
right.pack(side="right", fill="y", padx=6, pady=6)
tk.Button(right, text="Visualize Shells (3D)", command=lambda: threading.Thread(target=show_orbital_visual, args=(info,), daemon=True).start()).pack(pady=6)
tk.Button(right, text="Close", command=popup.destroy).pack(pady=6)
# ----------------- Run App -----------------
def main():
root = tk.Tk()
app = PeriodicTableApp(root)
root.mainloop()
if __name__ == "__main__":
main()
No comments:
Post a Comment