Spaces:
Build error
Build error
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
|
| 2 |
import math
|
| 3 |
from typing import Dict, List, Optional
|
| 4 |
|
|
@@ -8,6 +7,9 @@ import numpy as np
|
|
| 8 |
import matplotlib.pyplot as plt
|
| 9 |
from periodictable import elements
|
| 10 |
|
|
|
|
|
|
|
|
|
|
| 11 |
NUMERIC_PROPS = [
|
| 12 |
("mass", "Atomic mass (u)"),
|
| 13 |
("density", "Density (g/cm^3)"),
|
|
@@ -109,23 +111,33 @@ def build_elements_df() -> pd.DataFrame:
|
|
| 109 |
return df
|
| 110 |
|
| 111 |
DF = build_elements_df()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
MAX_GROUP = 18
|
| 113 |
MAX_PERIOD = 7
|
| 114 |
-
|
| 115 |
GRID: List[List[Optional[int]]] = [[None for _ in range(MAX_GROUP)] for _ in range(MAX_PERIOD)]
|
|
|
|
| 116 |
for _, row in DF.iterrows():
|
| 117 |
-
period
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
| 119 |
continue
|
| 120 |
-
|
|
|
|
| 121 |
|
| 122 |
-
LAN = [z for z in DF["Z"] if 57 <= z <= 71]
|
| 123 |
-
ACT = [z for z in DF["Z"] if 89 <= z <= 103]
|
| 124 |
|
|
|
|
|
|
|
|
|
|
| 125 |
def plot_trend(trend_df: pd.DataFrame, prop_key: str, Z: int, symbol: str):
|
| 126 |
fig, ax = plt.subplots()
|
| 127 |
ax.scatter(trend_df["Z"], trend_df[prop_key])
|
| 128 |
-
# highlight
|
| 129 |
sel = trend_df.loc[trend_df["Z"] == Z, prop_key]
|
| 130 |
if not sel.empty and not pd.isna(sel.values[0]):
|
| 131 |
ax.scatter([Z], [sel.values[0]], s=80)
|
|
@@ -144,16 +156,15 @@ def plot_heatmap(property_key: str):
|
|
| 144 |
z = GRID[r][c]
|
| 145 |
if z is None:
|
| 146 |
continue
|
| 147 |
-
val = DF.loc[DF[
|
| 148 |
if not pd.isna(val):
|
| 149 |
grid_vals[r, c] = float(val)
|
| 150 |
-
|
| 151 |
fig, ax = plt.subplots()
|
| 152 |
im = ax.imshow(grid_vals, origin="upper", aspect="auto")
|
| 153 |
ax.set_xticks(range(MAX_GROUP))
|
| 154 |
-
ax.set_xticklabels([str(i) for i in range(1, MAX_GROUP+1)])
|
| 155 |
ax.set_yticks(range(MAX_PERIOD))
|
| 156 |
-
ax.set_yticklabels([str(i) for i in range(1, MAX_PERIOD+1)])
|
| 157 |
ax.set_xlabel("Group")
|
| 158 |
ax.set_ylabel("Period")
|
| 159 |
ax.set_title(f"Periodic heatmap: {prop_label}")
|
|
@@ -161,6 +172,9 @@ def plot_heatmap(property_key: str):
|
|
| 161 |
fig.tight_layout()
|
| 162 |
return fig
|
| 163 |
|
|
|
|
|
|
|
|
|
|
| 164 |
def element_info(z_or_symbol: str):
|
| 165 |
try:
|
| 166 |
if z_or_symbol.isdigit():
|
|
@@ -172,29 +186,33 @@ def element_info(z_or_symbol: str):
|
|
| 172 |
except Exception:
|
| 173 |
return f"Unknown element: {z_or_symbol}", None, None
|
| 174 |
|
| 175 |
-
row = DF.loc[DF[
|
| 176 |
-
symbol = row[
|
| 177 |
|
| 178 |
facts = []
|
| 179 |
facts.extend(CURATED_FACTS.get(symbol, []))
|
| 180 |
-
facts.append(GROUP_FACTS.get(row[
|
| 181 |
facts = [f for f in facts if f]
|
| 182 |
|
| 183 |
props_lines = [
|
| 184 |
f"{row['name']} ({symbol}), Z = {Z}",
|
| 185 |
-
f"Period {int(row['period'])
|
|
|
|
|
|
|
| 186 |
f"Atomic mass: {row['mass'] if row['mass'] else '—'} u",
|
| 187 |
f"Density: {row['density'] if row['density'] else '—'} g/cm³",
|
| 188 |
f"Electronegativity: {row['electronegativity'] if row['electronegativity'] else '—'} (Pauling)",
|
| 189 |
-
f"Melting point: {row['melting_point'] if row['melting_point'] else '—'} K |
|
| 190 |
-
f"
|
|
|
|
|
|
|
| 191 |
f"Radioactive: {'Yes' if row['is_radioactive'] else 'No'}",
|
| 192 |
]
|
| 193 |
info_text = "\n".join(props_lines)
|
| 194 |
facts_text = "\n• ".join(["Interesting facts:"] + facts) if facts else "No fact on file—still cool though!"
|
| 195 |
|
| 196 |
-
prop_key =
|
| 197 |
-
trend_df = DF[[
|
| 198 |
fig = plot_trend(trend_df, prop_key, Z, symbol)
|
| 199 |
|
| 200 |
return info_text, facts_text, fig
|
|
@@ -203,20 +221,45 @@ def handle_button_click(z: int):
|
|
| 203 |
return element_info(str(z))
|
| 204 |
|
| 205 |
def search_element(query: str):
|
| 206 |
-
query = (query or
|
| 207 |
if not query:
|
| 208 |
return gr.update(), gr.update(), gr.update()
|
| 209 |
return element_info(query)
|
| 210 |
|
|
|
|
|
|
|
|
|
|
| 211 |
with gr.Blocks(title="Interactive Periodic Table") as demo:
|
| 212 |
gr.Markdown("# 🧪 Interactive Periodic Table\nClick an element or search by symbol/name/atomic number.")
|
| 213 |
|
| 214 |
with gr.Row():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
with gr.Column(scale=2):
|
| 216 |
gr.Markdown("### Main Table")
|
|
|
|
| 217 |
with gr.Row():
|
| 218 |
for g in range(1, 19):
|
| 219 |
gr.Markdown(f"**{g}**")
|
|
|
|
|
|
|
| 220 |
for r in range(MAX_PERIOD):
|
| 221 |
with gr.Row():
|
| 222 |
for c in range(MAX_GROUP):
|
|
@@ -224,41 +267,26 @@ with gr.Blocks(title="Interactive Periodic Table") as demo:
|
|
| 224 |
if z is None:
|
| 225 |
gr.Button("", interactive=False)
|
| 226 |
else:
|
| 227 |
-
sym = DF.loc[DF[
|
| 228 |
btn = gr.Button(sym)
|
| 229 |
-
btn.click(handle_button_click, inputs=[gr.Number(z, visible=False)],
|
| 230 |
-
|
| 231 |
|
| 232 |
gr.Markdown("### f-block (lanthanides & actinides)")
|
| 233 |
with gr.Row():
|
| 234 |
for z in LAN:
|
| 235 |
-
sym = DF.loc[DF[
|
| 236 |
btn = gr.Button(sym)
|
| 237 |
-
btn.click(handle_button_click, inputs=[gr.Number(z, visible=False)],
|
| 238 |
-
|
| 239 |
with gr.Row():
|
| 240 |
for z in ACT:
|
| 241 |
-
sym = DF.loc[DF[
|
| 242 |
btn = gr.Button(sym)
|
| 243 |
-
btn.click(handle_button_click, inputs=[gr.Number(z, visible=False)],
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
with gr.Column(scale=1):
|
| 247 |
-
search = gr.Textbox(label="Search (symbol/name/Z)", placeholder="e.g., C, Iron, 79")
|
| 248 |
-
info = gr.Textbox(label="Properties", lines=10, interactive=False)
|
| 249 |
-
facts = gr.Markdown("Select an element to see fun facts.")
|
| 250 |
-
trend = gr.Matplotlib()
|
| 251 |
-
|
| 252 |
-
search.submit(search_element, inputs=[search], outputs=[info, facts, trend])
|
| 253 |
-
|
| 254 |
-
gr.Markdown("### Trend heatmap")
|
| 255 |
-
prop = gr.Dropdown(choices=[k for k, _ in NUMERIC_PROPS], value="electronegativity", label="Property")
|
| 256 |
-
heat = gr.Matplotlib()
|
| 257 |
-
|
| 258 |
-
def heatmap_callback(property_key):
|
| 259 |
-
return plot_heatmap(property_key)
|
| 260 |
-
prop.change(heatmap_callback, inputs=[prop], outputs=[heat])
|
| 261 |
-
heat.update(plot_heatmap("electronegativity"))
|
| 262 |
|
| 263 |
if __name__ == "__main__":
|
| 264 |
demo.launch()
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import math
|
| 2 |
from typing import Dict, List, Optional
|
| 3 |
|
|
|
|
| 7 |
import matplotlib.pyplot as plt
|
| 8 |
from periodictable import elements
|
| 9 |
|
| 10 |
+
# -----------------------------
|
| 11 |
+
# Data
|
| 12 |
+
# -----------------------------
|
| 13 |
NUMERIC_PROPS = [
|
| 14 |
("mass", "Atomic mass (u)"),
|
| 15 |
("density", "Density (g/cm^3)"),
|
|
|
|
| 111 |
return df
|
| 112 |
|
| 113 |
DF = build_elements_df()
|
| 114 |
+
|
| 115 |
+
# -----------------------------
|
| 116 |
+
# Safe grid mapping (skip None period/group)
|
| 117 |
+
# -----------------------------
|
| 118 |
MAX_GROUP = 18
|
| 119 |
MAX_PERIOD = 7
|
|
|
|
| 120 |
GRID: List[List[Optional[int]]] = [[None for _ in range(MAX_GROUP)] for _ in range(MAX_PERIOD)]
|
| 121 |
+
|
| 122 |
for _, row in DF.iterrows():
|
| 123 |
+
period = row["period"]
|
| 124 |
+
group = row["group"]
|
| 125 |
+
z = int(row["Z"])
|
| 126 |
+
# Skip rows with missing period or group
|
| 127 |
+
if pd.isna(period) or group is None:
|
| 128 |
continue
|
| 129 |
+
period = int(period)
|
| 130 |
+
GRID[period - 1][group - 1] = z
|
| 131 |
|
| 132 |
+
LAN = [int(z) for z in DF["Z"] if 57 <= int(z) <= 71]
|
| 133 |
+
ACT = [int(z) for z in DF["Z"] if 89 <= int(z) <= 103]
|
| 134 |
|
| 135 |
+
# -----------------------------
|
| 136 |
+
# Plotting helpers (Matplotlib)
|
| 137 |
+
# -----------------------------
|
| 138 |
def plot_trend(trend_df: pd.DataFrame, prop_key: str, Z: int, symbol: str):
|
| 139 |
fig, ax = plt.subplots()
|
| 140 |
ax.scatter(trend_df["Z"], trend_df[prop_key])
|
|
|
|
| 141 |
sel = trend_df.loc[trend_df["Z"] == Z, prop_key]
|
| 142 |
if not sel.empty and not pd.isna(sel.values[0]):
|
| 143 |
ax.scatter([Z], [sel.values[0]], s=80)
|
|
|
|
| 156 |
z = GRID[r][c]
|
| 157 |
if z is None:
|
| 158 |
continue
|
| 159 |
+
val = DF.loc[DF["Z"] == z, property_key].values[0]
|
| 160 |
if not pd.isna(val):
|
| 161 |
grid_vals[r, c] = float(val)
|
|
|
|
| 162 |
fig, ax = plt.subplots()
|
| 163 |
im = ax.imshow(grid_vals, origin="upper", aspect="auto")
|
| 164 |
ax.set_xticks(range(MAX_GROUP))
|
| 165 |
+
ax.set_xticklabels([str(i) for i in range(1, MAX_GROUP + 1)])
|
| 166 |
ax.set_yticks(range(MAX_PERIOD))
|
| 167 |
+
ax.set_yticklabels([str(i) for i in range(1, MAX_PERIOD + 1)])
|
| 168 |
ax.set_xlabel("Group")
|
| 169 |
ax.set_ylabel("Period")
|
| 170 |
ax.set_title(f"Periodic heatmap: {prop_label}")
|
|
|
|
| 172 |
fig.tight_layout()
|
| 173 |
return fig
|
| 174 |
|
| 175 |
+
# -----------------------------
|
| 176 |
+
# Callbacks
|
| 177 |
+
# -----------------------------
|
| 178 |
def element_info(z_or_symbol: str):
|
| 179 |
try:
|
| 180 |
if z_or_symbol.isdigit():
|
|
|
|
| 186 |
except Exception:
|
| 187 |
return f"Unknown element: {z_or_symbol}", None, None
|
| 188 |
|
| 189 |
+
row = DF.loc[DF["Z"] == Z].iloc[0].to_dict()
|
| 190 |
+
symbol = row["symbol"]
|
| 191 |
|
| 192 |
facts = []
|
| 193 |
facts.extend(CURATED_FACTS.get(symbol, []))
|
| 194 |
+
facts.append(GROUP_FACTS.get(row["category"], None))
|
| 195 |
facts = [f for f in facts if f]
|
| 196 |
|
| 197 |
props_lines = [
|
| 198 |
f"{row['name']} ({symbol}), Z = {Z}",
|
| 199 |
+
f"Period {int(row['period']) if not pd.isna(row['period']) else '—'}, "
|
| 200 |
+
f"Group {row['group'] if row['group'] is not None else '—'}, "
|
| 201 |
+
f"Block {row['block']} | Category: {row['category'].replace('-', ' ').title()}",
|
| 202 |
f"Atomic mass: {row['mass'] if row['mass'] else '—'} u",
|
| 203 |
f"Density: {row['density'] if row['density'] else '—'} g/cm³",
|
| 204 |
f"Electronegativity: {row['electronegativity'] if row['electronegativity'] else '—'} (Pauling)",
|
| 205 |
+
f"Melting point: {row['melting_point'] if row['melting_point'] else '—'} K | "
|
| 206 |
+
f"Boiling point: {row['boiling_point'] if row['boiling_point'] else '—'} K",
|
| 207 |
+
f"vdW radius: {row['vdw_radius'] if row['vdw_radius'] else '—'} pm | "
|
| 208 |
+
f"Covalent radius: {row['covalent_radius'] if row['covalent_radius'] else '—'} pm",
|
| 209 |
f"Radioactive: {'Yes' if row['is_radioactive'] else 'No'}",
|
| 210 |
]
|
| 211 |
info_text = "\n".join(props_lines)
|
| 212 |
facts_text = "\n• ".join(["Interesting facts:"] + facts) if facts else "No fact on file—still cool though!"
|
| 213 |
|
| 214 |
+
prop_key = "electronegativity" if not pd.isna(row["electronegativity"]) else "mass"
|
| 215 |
+
trend_df = DF[["Z", "symbol", prop_key]].dropna()
|
| 216 |
fig = plot_trend(trend_df, prop_key, Z, symbol)
|
| 217 |
|
| 218 |
return info_text, facts_text, fig
|
|
|
|
| 221 |
return element_info(str(z))
|
| 222 |
|
| 223 |
def search_element(query: str):
|
| 224 |
+
query = (query or "").strip()
|
| 225 |
if not query:
|
| 226 |
return gr.update(), gr.update(), gr.update()
|
| 227 |
return element_info(query)
|
| 228 |
|
| 229 |
+
# -----------------------------
|
| 230 |
+
# UI
|
| 231 |
+
# -----------------------------
|
| 232 |
with gr.Blocks(title="Interactive Periodic Table") as demo:
|
| 233 |
gr.Markdown("# 🧪 Interactive Periodic Table\nClick an element or search by symbol/name/atomic number.")
|
| 234 |
|
| 235 |
with gr.Row():
|
| 236 |
+
# Create the inspector FIRST so button clicks can target these components
|
| 237 |
+
with gr.Column(scale=1):
|
| 238 |
+
gr.Markdown("### Inspector")
|
| 239 |
+
search = gr.Textbox(label="Search (symbol/name/Z)", placeholder="e.g., C, Iron, 79")
|
| 240 |
+
info = gr.Textbox(label="Properties", lines=10, interactive=False)
|
| 241 |
+
facts = gr.Markdown("Select an element to see fun facts.")
|
| 242 |
+
trend = gr.Matplotlib()
|
| 243 |
+
|
| 244 |
+
search.submit(search_element, inputs=[search], outputs=[info, facts, trend])
|
| 245 |
+
|
| 246 |
+
gr.Markdown("### Trend heatmap")
|
| 247 |
+
prop = gr.Dropdown(choices=[k for k, _ in NUMERIC_PROPS], value="electronegativity", label="Property")
|
| 248 |
+
heat = gr.Matplotlib()
|
| 249 |
+
prop.change(lambda k: plot_heatmap(k), inputs=[prop], outputs=[heat])
|
| 250 |
+
|
| 251 |
+
# Initialize heatmap on load
|
| 252 |
+
demo.load(lambda: plot_heatmap("electronegativity"), outputs=[heat])
|
| 253 |
+
|
| 254 |
+
# Now build the grid of buttons
|
| 255 |
with gr.Column(scale=2):
|
| 256 |
gr.Markdown("### Main Table")
|
| 257 |
+
# Headers (groups 1-18)
|
| 258 |
with gr.Row():
|
| 259 |
for g in range(1, 19):
|
| 260 |
gr.Markdown(f"**{g}**")
|
| 261 |
+
|
| 262 |
+
# Element grid
|
| 263 |
for r in range(MAX_PERIOD):
|
| 264 |
with gr.Row():
|
| 265 |
for c in range(MAX_GROUP):
|
|
|
|
| 267 |
if z is None:
|
| 268 |
gr.Button("", interactive=False)
|
| 269 |
else:
|
| 270 |
+
sym = DF.loc[DF["Z"] == z, "symbol"].values[0]
|
| 271 |
btn = gr.Button(sym)
|
| 272 |
+
btn.click(handle_button_click, inputs=[gr.Number(z, visible=False)],
|
| 273 |
+
outputs=[info, facts, trend])
|
| 274 |
|
| 275 |
gr.Markdown("### f-block (lanthanides & actinides)")
|
| 276 |
with gr.Row():
|
| 277 |
for z in LAN:
|
| 278 |
+
sym = DF.loc[DF["Z"] == z, "symbol"].values[0]
|
| 279 |
btn = gr.Button(sym)
|
| 280 |
+
btn.click(handle_button_click, inputs=[gr.Number(z, visible=False)],
|
| 281 |
+
outputs=[info, facts, trend])
|
| 282 |
with gr.Row():
|
| 283 |
for z in ACT:
|
| 284 |
+
sym = DF.loc[DF["Z"] == z, "symbol"].values[0]
|
| 285 |
btn = gr.Button(sym)
|
| 286 |
+
btn.click(handle_button_click, inputs=[gr.Number(z, visible=False)],
|
| 287 |
+
outputs=[info, facts, trend])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
|
| 289 |
if __name__ == "__main__":
|
| 290 |
demo.launch()
|
| 291 |
+
|
| 292 |
+
|