# element_explorer_v2.
py
import wikipediaapi
import matplotlib.pyplot as plt
import numpy as np
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import plotly.graph_objects as go
from mendeleev import element
from pubchempy import get_compounds
from datetime import datetime
import os
import json
import csv
import sys
import webbrowser
import platform
# ---------------- Config / Storage ---------------- #
APP_NAME = "Element Explorer v2"
CACHE_FILE = "element_cache.json"
FAV_FILE = "favorites.json"
MAX_HISTORY = 25
# Make sure downloads path cross-platform
DOWNLOADS = os.path.join(os.path.expanduser("~"), "Downloads")
# ---------------- Hardcoded periodic table fallback ---------------- #
PERIODIC_TABLE = {
"Hydrogen": 1, "Helium": 2, "Lithium": 3, "Beryllium": 4, "Boron": 5,
"Carbon": 6, "Nitrogen": 7, "Oxygen": 8, "Fluorine": 9, "Neon": 10,
"Sodium": 11, "Magnesium": 12, "Aluminum": 13, "Silicon": 14, "Phosphorus":
15,
"Sulfur": 16, "Chlorine": 17, "Argon": 18, "Potassium": 19, "Calcium": 20,
# add more later if you want
}
# ---------------- Utilities: cache & favorites ---------------- #
def load_json_safe(path, default):
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return default
def save_json_safe(path, obj):
try:
with open(path, "w", encoding="utf-8") as f:
json.dump(obj, f, ensure_ascii=False, indent=2)
except Exception as e:
print("Failed to save JSON:", e)
cache = load_json_safe(CACHE_FILE, {})
favorites = load_json_safe(FAV_FILE, [])
def cache_put(name, data):
cache[name.lower()] = {"time": datetime.now().isoformat(), "data": data}
save_json_safe(CACHE_FILE, cache)
def cache_get(name):
return cache.get(name.lower(), {}).get("data")
def add_favorite(element_name):
if element_name not in favorites:
favorites.append(element_name)
save_json_safe(FAV_FILE, favorites)
return True
return False
def remove_favorite(element_name):
if element_name in favorites:
favorites.remove(element_name)
save_json_safe(FAV_FILE, favorites)
return True
return False
# ---------------- Fetch functions ---------------- #
def fetch_wikipedia_summary(element_name):
wiki = wikipediaapi.Wikipedia(language="en", user_agent=f"{APP_NAME}/1.0
(student project)")
page = wiki.page(element_name)
if page.exists():
summary = page.summary[:2000]
if "." in summary[1990:]:
return summary[:summary[:2000].rfind(".")+1]
return summary
return "No summary available."
def fetch_mendeleev_data(element_name):
try:
elem = element(element_name.capitalize())
data = {
"Atomic Number": elem.atomic_number,
"Symbol": elem.symbol,
"Atomic Mass": f"{elem.atomic_weight:.2f} u" if elem.atomic_weight else
"N/A",
"Density": f"{elem.density} g/cm³" if elem.density else "N/A",
"Electronegativity": f"{elem.en_pauling}" if elem.en_pauling else "N/A",
"Oxidation States": f"{', '.join(map(str, elem.oxistates))}" if
elem.oxistates else "N/A",
"Melting Point": f"{elem.melting_point} K" if elem.melting_point else
"N/A",
"Boiling Point": f"{elem.boiling_point} K" if elem.boiling_point else "N/A",
"Electronic Configuration": elem.ec if elem.ec else "N/A",
}
return data
except Exception:
return {}
def fetch_pubchem_data(element_name):
try:
compound = get_compounds(element_name, 'name')[0]
data = {
"Molecular Weight": f"{compound.molecular_weight} u",
"InChI Key": compound.inchikey,
"SMILES": compound.isomeric_smiles,
}
return data
except Exception:
return {}
def fetch_element_data(element_name):
# check cache
cached = cache_get(element_name)
if cached:
return cached
# fetch from sources
wikipedia_summary = fetch_wikipedia_summary(element_name)
mendeleev_data = fetch_mendeleev_data(element_name)
pubchem_data = fetch_pubchem_data(element_name)
atomic_number = PERIODIC_TABLE.get(element_name.capitalize(), None)
if atomic_number and "Atomic Number" not in mendeleev_data:
mendeleev_data["Atomic Number"] = str(atomic_number)
result = {"summary": wikipedia_summary, "details": {**mendeleev_data,
**pubchem_data}}
cache_put(element_name, result)
return result
# ---------------- Visualizations (unchanged) ---------------- #
def plot_bohr_model(atomic_number):
num_electrons = atomic_number
orbitals = []
max_electrons = [2, 8, 18, 32, 50, 72, 98]
for max_e in max_electrons:
if num_electrons <= 0:
break
if num_electrons >= max_e:
orbitals.append(max_e)
num_electrons -= max_e
else:
orbitals.append(num_electrons)
break
fig, ax = plt.subplots(figsize=(6, 6))
ax.set_xlim(-3, 3); ax.set_ylim(-3, 3); ax.set_aspect('equal'); ax.axis('off')
for i, electrons in enumerate(orbitals):
circle = plt.Circle((0,0), i+1, fill=False, color="#2A6F97", linewidth=1.5)
ax.add_artist(circle)
for j in range(electrons):
angle = 2*np.pi*j/electrons
x,y = (i+1)*np.cos(angle),(i+1)*np.sin(angle)
ax.plot(x,y,'o', color="#FF6B6B", markersize=6)
ax.plot(0,0,'o', color="#FFD166", markersize=15)
plt.title(f"Bohr Model — Z={atomic_number}")
plt.show()
def plot_orbital(orbital_type):
fig = go.Figure()
u = np.linspace(0, 2*np.pi, 100)
v = np.linspace(-1,1,100)
if orbital_type=='s':
x = np.outer(np.cos(u), np.sqrt(1-v**2))
y = np.outer(np.sin(u), np.sqrt(1-v**2))
z = np.outer(np.ones(np.size(u)), v)
fig.add_trace(go.Surface(z=z, x=x, y=y, colorscale='Blues'))
elif orbital_type=='p':
x = np.outer(np.sin(u), v)
y = np.outer(np.cos(u), v)
z = np.outer(np.ones(np.size(u)), v)
fig.add_trace(go.Surface(z=z, x=x, y=y, colorscale='Reds'))
elif orbital_type=='d':
x = np.outer(np.sin(u)**2 * np.cos(2*u), v)
y = np.outer(np.sin(u)**2 * np.sin(2*u), v)
z = np.outer(np.cos(u), v)
fig.add_trace(go.Surface(z=z, x=x, y=y, colorscale='Greens'))
elif orbital_type=='f':
x = np.outer(np.sin(u)**3 * np.cos(3*u), v)
y = np.outer(np.sin(u)**3 * np.sin(3*u), v)
z = np.outer(np.cos(u), v)
fig.add_trace(go.Surface(z=z, x=x, y=y, colorscale='Purples'))
fig.update_layout(title=f"{orbital_type.upper()} Orbital Visualization",
autosize=True)
fig.show()
# ---------------- Export helpers ---------------- #
def export_txt(report_text, element_name):
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{element_name}_Report_{timestamp}.txt"
filepath = os.path.join(DOWNLOADS, filename)
with open(filepath, "w", encoding="utf-8") as f:
f.write(report_text)
return filepath
def export_csv(details_dict, element_name):
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{element_name}_Report_{timestamp}.csv"
filepath = os.path.join(DOWNLOADS, filename)
with open(filepath, "w", newline='', encoding="utf-8") as csvfile:
w = csv.writer(csvfile)
w.writerow(["Property", "Value"])
for k,v in details_dict.items():
w.writerow([k, v])
return filepath
# ---------------- UI — build main window ---------------- #
root = tk.Tk()
root.title(APP_NAME)
root.geometry("1000x700")
root.configure(bg="#F4FAFF")
# Fonts and colors (cross-platform safe)
TITLE_FONT = ("Helvetica", 15, "bold")
LABEL_FONT = ("Helvetica", 11)
TEXT_FONT = ("Helvetica", 11)
BUTTON_FONT = ("Helvetica", 10, "bold")
ACCENT = "#4ECDC4"
ACCENT2 = "#FF6B6B"
# Layout frames
top_frame = ttk.Frame(root, padding=10)
top_frame.pack(fill=tk.X)
main_frame = ttk.Frame(root, padding=10)
main_frame.pack(fill=tk.BOTH, expand=True)
status_frame = ttk.Frame(root)
status_frame.pack(fill=tk.X, side=tk.BOTTOM)
# Title
title_lbl = tk.Label(top_frame, text=APP_NAME, font=("Helvetica", 18, "bold"),
bg="#F4FAFF", fg="#23395B")
title_lbl.pack(side=tk.LEFT)
# Search area
search_lbl = tk.Label(top_frame, text="Enter element name:",
font=LABEL_FONT, bg="#F4FAFF")
search_lbl.pack(side=tk.LEFT, padx=(20,6))
search_box = ttk.Entry(top_frame, font=TEXT_FONT, width=30)
search_box.pack(side=tk.LEFT)
search_box.bind("<Return>", lambda e: on_search())
# helper: status label
status_var = tk.StringVar(value="Ready")
status_lbl = tk.Label(status_frame, textvariable=status_var, anchor="w",
bg="#E9F6FF")
status_lbl.pack(fill=tk.X)
# History & Favorites column
left_col = ttk.Frame(main_frame)
left_col.pack(side=tk.LEFT, fill=tk.Y)
history_lbl = tk.Label(left_col, text="Search History", font=LABEL_FONT,
bg="#F4FAFF")
history_lbl.pack(anchor=tk.W)
history_list = tk.Listbox(left_col, height=10, width=25)
history_list.pack(pady=4)
history_list.bind("<<ListboxSelect>>", lambda e: on_history_select())
fav_lbl = tk.Label(left_col, text="Favorites", font=LABEL_FONT, bg="#F4FAFF")
fav_lbl.pack(anchor=tk.W, pady=(12,0))
fav_list = tk.Listbox(left_col, height=6, width=25)
fav_list.pack(pady=4)
fav_list.bind("<<ListboxSelect>>", lambda e: on_fav_select())
# populate favorites list initially
def refresh_fav_list():
fav_list.delete(0, tk.END)
for f in favorites:
fav_list.insert(tk.END, f)
refresh_fav_list()
# center column: result and visuals
center_col = ttk.Frame(main_frame)
center_col.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10)
result_text = tk.Text(center_col, wrap=tk.WORD, font=TEXT_FONT,
bg="#FFFFFF", height=20)
result_text.pack(fill=tk.BOTH, expand=True)
# small toolbar under result
toolbar = ttk.Frame(center_col)
toolbar.pack(fill=tk.X, pady=6)
def make_btn(text, cmd, bg=ACCENT):
b = tk.Button(toolbar, text=text, command=cmd, font=BUTTON_FONT,
bg=bg, fg="white", bd=0, padx=8, pady=6)
b.pack(side=tk.LEFT, padx=4)
# hover
b.bind("<Enter>", lambda e, b=b: b.configure(bg=ACCENT2 if bg==ACCENT
else ACCENT))
b.bind("<Leave>", lambda e, b=b: b.configure(bg=bg))
return b
# visualization buttons
bohr_button = make_btn("Show Bohr Model", lambda: on_bohr(), bg=ACCENT)
s_button = make_btn("S Orbital", lambda: plot_orbital('s'))
p_button = make_btn("P Orbital", lambda: plot_orbital('p'))
d_button = make_btn("D Orbital", lambda: plot_orbital('d'))
f_button = make_btn("F Orbital", lambda: plot_orbital('f'))
# right column: periodic grid + compare area
right_col = ttk.Frame(main_frame, width=320)
right_col.pack(side=tk.LEFT, fill=tk.Y, padx=(8,0))
# periodic quick grid (first 20) — visually attractive
grid_frame = ttk.LabelFrame(right_col, text="Periodic (Quick)", padding=8)
grid_frame.pack(fill=tk.X, pady=(0,10))
grid_elements =
["H","He","Li","Be","B","C","N","O","F","Ne","Na","Mg","Al","Si","P","S","Cl","Ar","K
","Ca"]
names_map = {k: v for v, k in zip(list(PERIODIC_TABLE.keys()),
list(PERIODIC_TABLE.keys()))} # we won't use mapping heavy here
def on_periodic_click(sym):
# try to get full name by searching cache or simple mapping
# simple approach: accept atomic symbol as name for fetch; mendeleev
accepts names mostly - we'll try common names for clicks
symbol_to_name = {
"H":"Hydrogen","He":"Helium","Li":"Lithium","Be":"Beryllium","B":"Boron","C":"Ca
rbon",
"N":"Nitrogen","O":"Oxygen","F":"Fluorine","Ne":"Neon","Na":"Sodium","Mg":"Ma
gnesium",
"Al":"Aluminum","Si":"Silicon","P":"Phosphorus","S":"Sulfur","Cl":"Chlorine","Ar":"
Argon",
"K":"Potassium","Ca":"Calcium"
}
name = symbol_to_name.get(sym, sym)
load_element(name)
# create grid of small buttons
r = 0; c = 0
for i, sym in enumerate(grid_elements):
b = tk.Button(grid_frame, text=sym, width=5, command=lambda s=sym:
on_periodic_click(s),
bg="#FFFFFF", fg="#23395B", bd=1)
b.grid(row=r, column=c, padx=3, pady=3)
c += 1
if c >= 5:
c = 0; r += 1
# compare frame
comp_frame = ttk.LabelFrame(right_col, text="Compare Elements", padding=8)
comp_frame.pack(fill=tk.X, pady=(4,10))
comp_a = ttk.Entry(comp_frame, width=12)
comp_b = ttk.Entry(comp_frame, width=12)
comp_a.grid(row=0, column=0, padx=4, pady=4)
comp_b.grid(row=0, column=1, padx=4, pady=4)
def on_compare():
a = comp_a.get().strip(); b = comp_b.get().strip()
if not a or not b:
messagebox.showerror("Compare", "Enter two elements to compare")
return
da = fetch_element_data(a); db = fetch_element_data(b)
# build simple compare popup
win = tk.Toplevel(root); win.title(f"Compare: {a} vs {b}");
win.geometry("700x420")
ta = tk.Text(win, wrap=tk.WORD, font=TEXT_FONT)
ta.pack(fill=tk.BOTH, expand=True)
ta.insert(tk.END, f"{a} — Summary:\n{da['summary']}\n\n")
ta.insert(tk.END, "Details:\n")
for k,v in da['details'].items():
ta.insert(tk.END, f"{k}: {v}\n")
ta.insert(tk.END, "\n\n---\n\n")
ta.insert(tk.END, f"{b} — Summary:\n{db['summary']}\n\n")
ta.insert(tk.END, "Details:\n")
for k,v in db['details'].items():
ta.insert(tk.END, f"{k}: {v}\n")
ta.configure(state="disabled")
compare_btn = tk.Button(comp_frame, text="Compare",
command=on_compare, bg=ACCENT, fg="white", font=BUTTON_FONT)
compare_btn.grid(row=1, column=0, columnspan=2, pady=6)
# favorites controls
fav_frame = ttk.LabelFrame(right_col, text="Favorites", padding=8)
fav_frame.pack(fill=tk.X, pady=(4,10))
fav_add_btn = tk.Button(fav_frame, text="Add Current", command=lambda:
on_add_fav(), bg="#FFB400", fg="white")
fav_add_btn.pack(fill=tk.X, pady=4)
fav_remove_btn = tk.Button(fav_frame, text="Remove Selected",
command=lambda: on_remove_fav(), bg="#FF6B6B", fg="white")
fav_remove_btn.pack(fill=tk.X)
# ---------------- Core behaviors ---------------- #
history = []
def push_history(name):
if not name: return
if name in history:
history.remove(name)
history.insert(0, name)
if len(history) > MAX_HISTORY:
history.pop()
history_list.delete(0, tk.END)
for h in history:
history_list.insert(tk.END, h)
def on_history_select():
sel = history_list.curselection()
if sel:
name = history_list.get(sel[0])
load_element(name)
def on_fav_select():
sel = fav_list.curselection()
if sel:
name = fav_list.get(sel[0])
load_element(name)
def on_add_fav():
cur = search_box.get().strip()
if not cur:
messagebox.showerror("Favorites", "Search/load an element first.")
return
if add_favorite(cur):
refresh_fav_list()
messagebox.showinfo("Favorites", f"Added {cur} to favorites.")
else:
messagebox.showinfo("Favorites", f"{cur} already in favorites.")
def on_remove_fav():
sel = fav_list.curselection()
if not sel:
messagebox.showerror("Favorites", "Select a favorite to remove.")
return
name = fav_list.get(sel[0])
if remove_favorite(name):
refresh_fav_list()
messagebox.showinfo("Favorites", f"Removed {name} from favorites.")
def load_element(name):
# main loader - fetch and populate result_text
if not name:
return
status_var.set(f"Loading {name} ...")
root.update_idletasks()
data = fetch_element_data(name)
if not data:
messagebox.showerror("Error", f"No data found for {name}")
status_var.set("Ready")
return
# display
result_text.delete(1.0, tk.END)
result_text.insert(tk.END, f"General Information:\n{data['summary']}\n\n")
result_text.insert(tk.END, "Detailed Data:\n")
for k,v in data['details'].items():
result_text.insert(tk.END, f"{k}: {v}\n")
result_text.see(tk.END)
search_box.delete(0, tk.END)
search_box.insert(0, name)
push_history(name)
status_var.set("Loaded.")
# update bohr button command if atomic number available
atomic = data['details'].get("Atomic Number")
try:
atomic_int = int(atomic)
bohr_button.configure(command=lambda a=atomic_int:
plot_bohr_model(a))
except Exception:
bohr_button.configure(command=lambda: messagebox.showerror("Bohr",
"Atomic number not available."))
def on_search():
name = search_box.get().strip()
if not name:
messagebox.showerror("Search", "Please enter an element name.")
return
load_element(name)
# keyboard shortcut: Enter
def bind_shortcuts():
root.bind("<Control-s>", lambda e: on_export_txt())
root.bind("<Control-c>", lambda e: on_copy_results())
# copy results to clipboard
def on_copy_results():
txt = result_text.get(1.0, tk.END)
try:
import pyperclip
pyperclip.copy(txt)
messagebox.showinfo("Copy", "Results copied to clipboard.")
except Exception:
# fallback
root.clipboard_clear()
root.clipboard_append(txt)
messagebox.showinfo("Copy", "Results copied to clipboard (Tk fallback).")
# export to TXT
def on_export_txt():
cur = search_box.get().strip() or "element"
txt = result_text.get(1.0, tk.END)
try:
path = export_txt(txt, cur)
messagebox.showinfo("Export TXT", f"Saved to Downloads:\n{path}")
except Exception as e:
messagebox.showerror("Export TXT", f"Failed: {e}")
# export CSV (details only)
def on_export_csv():
cur = search_box.get().strip() or "element"
# build details dict
data = fetch_element_data(cur)
if not data:
messagebox.showerror("Export CSV", "No data to export.")
return
try:
path = export_csv(data['details'], cur)
messagebox.showinfo("Export CSV", f"Saved to Downloads:\n{path}")
except Exception as e:
messagebox.showerror("Export CSV", f"Failed: {e}")
# hook buttons
make_btn("Export TXT (Downloads)", on_export_txt, bg="#2E86AB")
make_btn("Export CSV (Downloads)", on_export_csv, bg="#2A9D8F")
make_btn("Copy Results", on_copy_results, bg="#7B61FF")
# download / save button on toolbar replaced with text export to avoid pdf issues
download_btn = make_btn("Save Report (TXT)", on_export_txt, bg=ACCENT2)
# a tiny help / about
def on_about():
txt = f"{APP_NAME}\nA polished Element Explorer for CBSE project by you.\n\
nFeatures:\n- Wikipedia / Mendeleev / PubChem data\n- Bohr model + orbitals\n-
Quick periodic grid, compare, favorites, history\n- Export TXT / CSV (Downloads)\
n\nPlatform: {platform.system()} {platform.release()}\n"
messagebox.showinfo("About", txt)
make_btn("About", on_about, bg="#495867")
# load initial state
bind_shortcuts()
status_var.set("Ready")
# menu bar (optional nice touch)
menubar = tk.Menu(root)
filemenu = tk.Menu(menubar, tearoff=0)
filemenu.add_command(label="Export TXT (Ctrl-S)", command=on_export_txt)
filemenu.add_command(label="Export CSV", command=on_export_csv)
filemenu.add_separator()
filemenu.add_command(label="Exit", command=root.quit)
menubar.add_cascade(label="File", menu=filemenu)
helpmenu = tk.Menu(menubar, tearoff=0)
helpmenu.add_command(label="About", command=on_about)
menubar.add_cascade(label="Help", menu=helpmenu)
root.config(menu=menubar)
# run the loop
root.mainloop()