Spaces:
Sleeping
Sleeping
Upload 2 files
Browse files- app.py +689 -0
- requirements.txt +5 -0
app.py
ADDED
@@ -0,0 +1,689 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import plotly.graph_objects as go
|
3 |
+
import numpy as np
|
4 |
+
import pandas as pd
|
5 |
+
|
6 |
+
# Import mendeleev for comprehensive periodic table data
|
7 |
+
try:
|
8 |
+
from mendeleev.fetch import fetch_table
|
9 |
+
MENDELEEV_AVAILABLE = True
|
10 |
+
except ImportError:
|
11 |
+
print("mendeleev library not found. Please install it using: pip install mendeleev")
|
12 |
+
MENDELEEV_AVAILABLE = False
|
13 |
+
|
14 |
+
def load_periodic_data():
|
15 |
+
"""Load comprehensive periodic table data using mendeleev library"""
|
16 |
+
if not MENDELEEV_AVAILABLE:
|
17 |
+
return pd.DataFrame(), []
|
18 |
+
|
19 |
+
try:
|
20 |
+
# Get the full periodic table with all properties
|
21 |
+
df = fetch_table('elements')
|
22 |
+
|
23 |
+
# Get available columns and filter out non-numeric ones
|
24 |
+
numeric_columns = df.select_dtypes(include=[np.number]).columns.tolist()
|
25 |
+
|
26 |
+
# Remove non-property columns
|
27 |
+
exclude_cols = ['atomic_number', 'period', 'group_id', 'mass_number', 'mass', 'id']
|
28 |
+
numeric_columns = [col for col in numeric_columns if col not in exclude_cols]
|
29 |
+
|
30 |
+
return df, numeric_columns
|
31 |
+
except Exception as e:
|
32 |
+
print(f"Error loading mendeleev data: {e}")
|
33 |
+
return pd.DataFrame(), []
|
34 |
+
|
35 |
+
# Load data
|
36 |
+
elements_data, available_properties = load_periodic_data()
|
37 |
+
|
38 |
+
def is_continuous_correlative_property(prop_name, df):
|
39 |
+
"""Determine if a property is continuous AND correlative with atomic number (should be excluded from dropdown)"""
|
40 |
+
# Properties that are both continuous and strongly correlative with atomic number
|
41 |
+
continuous_correlative_properties = {
|
42 |
+
'atomic_weight', 'atomic_mass', 'mass', 'weight'
|
43 |
+
}
|
44 |
+
|
45 |
+
# Check if property name contains continuous correlative indicators
|
46 |
+
for cont_prop in continuous_correlative_properties:
|
47 |
+
if cont_prop in prop_name.lower():
|
48 |
+
return True
|
49 |
+
|
50 |
+
# Check if property is highly correlated with atomic number
|
51 |
+
if prop_name in df.columns and 'atomic_number' in df.columns:
|
52 |
+
data = df[[prop_name, 'atomic_number']].dropna()
|
53 |
+
if len(data) > 20:
|
54 |
+
correlation = data[prop_name].corr(data['atomic_number'])
|
55 |
+
# High correlation (>0.9) indicates strong relationship with atomic number
|
56 |
+
# Combined with high uniqueness indicates continuous correlative property
|
57 |
+
unique_ratio = len(data[prop_name].unique()) / len(data[prop_name])
|
58 |
+
if abs(correlation) > 0.9 and unique_ratio > 0.8:
|
59 |
+
return True
|
60 |
+
|
61 |
+
return False
|
62 |
+
|
63 |
+
def is_integer_property(prop_name, df):
|
64 |
+
"""Determine if a property should be treated as integer"""
|
65 |
+
integer_properties = {
|
66 |
+
'period', 'group_id', 'group', 'block_number', 'neutrons',
|
67 |
+
'electrons', 'protons', 'valence', 'oxidation_states'
|
68 |
+
}
|
69 |
+
|
70 |
+
# Check explicit integer properties
|
71 |
+
for int_prop in integer_properties:
|
72 |
+
if int_prop in prop_name.lower():
|
73 |
+
return True
|
74 |
+
|
75 |
+
# Check if all non-null values are integers
|
76 |
+
if prop_name in df.columns:
|
77 |
+
data = df[prop_name].dropna()
|
78 |
+
if len(data) > 5:
|
79 |
+
# Check if all values are close to integers
|
80 |
+
are_integers = np.allclose(data, np.round(data), rtol=0, atol=1e-10)
|
81 |
+
return are_integers
|
82 |
+
|
83 |
+
return False
|
84 |
+
|
85 |
+
def calculate_color_variance(data, use_log=False):
|
86 |
+
"""Calculate the effective color variance for a given scaling approach"""
|
87 |
+
if len(data) < 3:
|
88 |
+
return 0
|
89 |
+
|
90 |
+
if use_log:
|
91 |
+
# For log scale, need positive values
|
92 |
+
positive_data = data[data > 0]
|
93 |
+
if len(positive_data) < 3:
|
94 |
+
return 0
|
95 |
+
scaled_data = np.log10(positive_data)
|
96 |
+
else:
|
97 |
+
scaled_data = data
|
98 |
+
|
99 |
+
# Normalize to 0-1 range (simulating color mapping)
|
100 |
+
min_val, max_val = scaled_data.min(), scaled_data.max()
|
101 |
+
if max_val == min_val:
|
102 |
+
return 0
|
103 |
+
|
104 |
+
normalized = (scaled_data - min_val) / (max_val - min_val)
|
105 |
+
|
106 |
+
# Calculate effective variance - higher means better color distribution
|
107 |
+
return np.var(normalized)
|
108 |
+
|
109 |
+
def requires_log_scale(prop_name, df):
|
110 |
+
"""Improved heuristic to determine if logarithmic scale maximizes color palette utilization"""
|
111 |
+
if prop_name not in df.columns:
|
112 |
+
return False
|
113 |
+
|
114 |
+
data = df[prop_name].dropna()
|
115 |
+
if len(data) < 10:
|
116 |
+
return False
|
117 |
+
|
118 |
+
# Must have all positive values for log scale
|
119 |
+
if data.min() <= 0:
|
120 |
+
return False
|
121 |
+
|
122 |
+
# Properties that typically benefit from log scale (abundance-related)
|
123 |
+
log_scale_indicators = [
|
124 |
+
'abundance', 'concentration', 'ppm', 'ppb', 'radioactive',
|
125 |
+
'half_life', 'decay', 'isotope_abundance'
|
126 |
+
]
|
127 |
+
|
128 |
+
for indicator in log_scale_indicators:
|
129 |
+
if indicator in prop_name.lower():
|
130 |
+
return True
|
131 |
+
|
132 |
+
# Calculate color variance for both approaches
|
133 |
+
linear_variance = calculate_color_variance(data, use_log=False)
|
134 |
+
log_variance = calculate_color_variance(data, use_log=True)
|
135 |
+
|
136 |
+
# Use log scale if it provides significantly better color distribution
|
137 |
+
# Require at least 50% improvement to switch to log scale
|
138 |
+
improvement_threshold = 1.5
|
139 |
+
|
140 |
+
# Additional criteria for when log scale is beneficial:
|
141 |
+
# 1. Log scale provides better variance AND
|
142 |
+
# 2. Data has wide range (>2 orders of magnitude) OR high skewness
|
143 |
+
|
144 |
+
range_ratio = data.max() / data.min()
|
145 |
+
data_skewness = abs(data.skew()) if hasattr(data, 'skew') else 0
|
146 |
+
|
147 |
+
use_log_conditions = [
|
148 |
+
log_variance > linear_variance * improvement_threshold, # Log provides better color distribution
|
149 |
+
range_ratio > 100 or data_skewness > 2, # Data is suitable for log scaling
|
150 |
+
len(data) > 20 # Sufficient data points
|
151 |
+
]
|
152 |
+
|
153 |
+
return all(use_log_conditions)
|
154 |
+
|
155 |
+
def get_element_series_description(df):
|
156 |
+
"""Get element series description based on available data"""
|
157 |
+
# Try to find series-related columns
|
158 |
+
series_columns = []
|
159 |
+
for col in df.columns:
|
160 |
+
if any(term in col.lower() for term in ['series', 'group_name', 'category', 'family', 'type']):
|
161 |
+
series_columns.append(col)
|
162 |
+
|
163 |
+
# Prefer columns with descriptive names
|
164 |
+
if 'series' in df.columns:
|
165 |
+
return 'series'
|
166 |
+
elif 'group_name' in df.columns:
|
167 |
+
return 'group_name'
|
168 |
+
elif series_columns:
|
169 |
+
return series_columns[0]
|
170 |
+
|
171 |
+
# If no series column, try to create one from period and group
|
172 |
+
if 'period' in df.columns and 'group_id' in df.columns:
|
173 |
+
return 'period' # Fallback to period
|
174 |
+
|
175 |
+
return None
|
176 |
+
|
177 |
+
def create_element_series_mapping(df):
|
178 |
+
"""Create a mapping of element series if not available"""
|
179 |
+
if 'series' in df.columns:
|
180 |
+
return 'Element Series', 'series'
|
181 |
+
|
182 |
+
# Try other descriptive columns
|
183 |
+
descriptive_columns = {
|
184 |
+
'group_name': 'Element Group',
|
185 |
+
'category': 'Element Category',
|
186 |
+
'family': 'Element Family',
|
187 |
+
'type': 'Element Type'
|
188 |
+
}
|
189 |
+
|
190 |
+
for col, label in descriptive_columns.items():
|
191 |
+
if col in df.columns and df[col].notna().sum() > 50:
|
192 |
+
return label, col
|
193 |
+
|
194 |
+
# If no good series data, use period as fallback
|
195 |
+
if 'period' in df.columns:
|
196 |
+
return 'Period', 'period'
|
197 |
+
|
198 |
+
return None, None
|
199 |
+
|
200 |
+
def filter_relevant_properties(df, available_props):
|
201 |
+
"""Filter properties to keep only relevant ones with sufficient data, excluding continuous correlative properties"""
|
202 |
+
|
203 |
+
# Define curated properties with quality thresholds (these stay for internal use)
|
204 |
+
curated_properties = {
|
205 |
+
'atomic_weight': {'label': 'Atomic Mass (u)', 'min_data': 100, 'log_scale': False},
|
206 |
+
'density': {'label': 'Density (g/cmΒ³)', 'min_data': 50, 'log_scale': False},
|
207 |
+
'en_pauling': {'label': 'Electronegativity (Pauling)', 'min_data': 70, 'log_scale': False},
|
208 |
+
'atomic_radius': {'label': 'Atomic Radius (pm)', 'min_data': 50, 'log_scale': False},
|
209 |
+
'vdw_radius': {'label': 'Van der Waals Radius (pm)', 'min_data': 40, 'log_scale': False},
|
210 |
+
'covalent_radius': {'label': 'Covalent Radius (pm)', 'min_data': 40, 'log_scale': False},
|
211 |
+
'ionenergy': {'label': 'First Ionization Energy (eV)', 'min_data': 80, 'log_scale': False},
|
212 |
+
'electron_affinity': {'label': 'Electron Affinity (eV)', 'min_data': 40, 'log_scale': False},
|
213 |
+
'melting_point': {'label': 'Melting Point (K)', 'min_data': 70, 'log_scale': False},
|
214 |
+
'boiling_point': {'label': 'Boiling Point (K)', 'min_data': 60, 'log_scale': False},
|
215 |
+
'atomic_volume': {'label': 'Atomic Volume (cmΒ³/mol)', 'min_data': 40, 'log_scale': False},
|
216 |
+
'thermal_conductivity': {'label': 'Thermal Conductivity (W/mK)', 'min_data': 30, 'log_scale': False},
|
217 |
+
'c6': {'label': 'C6 Dispersion Coefficient', 'min_data': 30, 'log_scale': False},
|
218 |
+
'dipole_polarizability': {'label': 'Dipole Polarizability', 'min_data': 30, 'log_scale': False},
|
219 |
+
'period': {'label': 'Period', 'min_data': 100, 'log_scale': False},
|
220 |
+
'group_id': {'label': 'Group', 'min_data': 100, 'log_scale': False},
|
221 |
+
}
|
222 |
+
|
223 |
+
# Check which properties are available and have sufficient data
|
224 |
+
valid_properties = {}
|
225 |
+
dropdown_properties = {} # Separate dict for dropdown (excluding continuous correlative)
|
226 |
+
property_info = {}
|
227 |
+
|
228 |
+
# First, try to add element series as the default
|
229 |
+
default_label, default_property = create_element_series_mapping(df)
|
230 |
+
if default_property and default_property in df.columns:
|
231 |
+
non_null_count = df[default_property].notna().sum()
|
232 |
+
if non_null_count >= 50: # Lower threshold for series data
|
233 |
+
valid_properties[default_label] = default_property
|
234 |
+
dropdown_properties[default_label] = default_property
|
235 |
+
property_info[default_property] = {
|
236 |
+
'label': default_label,
|
237 |
+
'min_data': 50,
|
238 |
+
'log_scale': False,
|
239 |
+
'is_default': True
|
240 |
+
}
|
241 |
+
|
242 |
+
for prop_name, prop_config in curated_properties.items():
|
243 |
+
if prop_name in available_props:
|
244 |
+
# Count non-null values
|
245 |
+
non_null_count = df[prop_name].notna().sum()
|
246 |
+
if non_null_count >= prop_config['min_data']:
|
247 |
+
valid_properties[prop_config['label']] = prop_name
|
248 |
+
property_info[prop_name] = prop_config
|
249 |
+
|
250 |
+
# Only add to dropdown if not continuous and correlative
|
251 |
+
if not is_continuous_correlative_property(prop_name, df):
|
252 |
+
dropdown_properties[prop_config['label']] = prop_name
|
253 |
+
|
254 |
+
# Add any other properties with very good data coverage (>80 elements)
|
255 |
+
for prop in available_props:
|
256 |
+
if prop not in curated_properties:
|
257 |
+
non_null_count = df[prop].notna().sum()
|
258 |
+
if non_null_count > 80: # High threshold for uncurated properties
|
259 |
+
display_name = prop.replace('_', ' ').title()
|
260 |
+
log_scale = requires_log_scale(prop, df)
|
261 |
+
valid_properties[display_name] = prop
|
262 |
+
property_info[prop] = {'label': display_name, 'min_data': 80, 'log_scale': log_scale}
|
263 |
+
|
264 |
+
# Only add to dropdown if not continuous and correlative
|
265 |
+
if not is_continuous_correlative_property(prop, df):
|
266 |
+
dropdown_properties[display_name] = prop
|
267 |
+
|
268 |
+
return valid_properties, dropdown_properties, property_info
|
269 |
+
|
270 |
+
# Get valid properties
|
271 |
+
valid_properties, dropdown_properties, property_info = filter_relevant_properties(elements_data, available_properties)
|
272 |
+
|
273 |
+
def get_portland_like_colorscale(use_log=False):
|
274 |
+
"""Get Portland or Portland-like colorscale"""
|
275 |
+
# Portland is great - let's use variations of it
|
276 |
+
if use_log:
|
277 |
+
# For log scale, use a slightly adjusted Portland to handle the wider dynamic range
|
278 |
+
return 'Portland'
|
279 |
+
else:
|
280 |
+
return 'Portland'
|
281 |
+
|
282 |
+
def should_use_log_scale(property_name, df):
|
283 |
+
"""Determine if logarithmic scale should be used based on data distribution"""
|
284 |
+
if property_name not in df.columns:
|
285 |
+
return False
|
286 |
+
|
287 |
+
# Check if explicitly configured
|
288 |
+
if property_name in property_info:
|
289 |
+
configured_log = property_info[property_name].get('log_scale', False)
|
290 |
+
if configured_log:
|
291 |
+
return True
|
292 |
+
|
293 |
+
# Use improved heuristic
|
294 |
+
return requires_log_scale(property_name, df)
|
295 |
+
|
296 |
+
# Standard periodic table positions
|
297 |
+
ELEMENT_POSITIONS = {
|
298 |
+
# Period 1
|
299 |
+
1: (1, 1), 2: (18, 1),
|
300 |
+
# Period 2
|
301 |
+
3: (1, 2), 4: (2, 2), 5: (13, 2), 6: (14, 2), 7: (15, 2), 8: (16, 2), 9: (17, 2), 10: (18, 2),
|
302 |
+
# Period 3
|
303 |
+
11: (1, 3), 12: (2, 3), 13: (13, 3), 14: (14, 3), 15: (15, 3), 16: (16, 3), 17: (17, 3), 18: (18, 3),
|
304 |
+
# Period 4
|
305 |
+
19: (1, 4), 20: (2, 4), 21: (3, 4), 22: (4, 4), 23: (5, 4), 24: (6, 4), 25: (7, 4), 26: (8, 4),
|
306 |
+
27: (9, 4), 28: (10, 4), 29: (11, 4), 30: (12, 4), 31: (13, 4), 32: (14, 4), 33: (15, 4), 34: (16, 4), 35: (17, 4), 36: (18, 4),
|
307 |
+
# Period 5
|
308 |
+
37: (1, 5), 38: (2, 5), 39: (3, 5), 40: (4, 5), 41: (5, 5), 42: (6, 5), 43: (7, 5), 44: (8, 5),
|
309 |
+
45: (9, 5), 46: (10, 5), 47: (11, 5), 48: (12, 5), 49: (13, 5), 50: (14, 5), 51: (15, 5), 52: (16, 5), 53: (17, 5), 54: (18, 5),
|
310 |
+
# Period 6
|
311 |
+
55: (1, 6), 56: (2, 6),
|
312 |
+
# Lanthanides (period 6 continued)
|
313 |
+
57: (4, 9), 58: (5, 9), 59: (6, 9), 60: (7, 9), 61: (8, 9), 62: (9, 9), 63: (10, 9), 64: (11, 9),
|
314 |
+
65: (12, 9), 66: (13, 9), 67: (14, 9), 68: (15, 9), 69: (16, 9), 70: (17, 9), 71: (18, 9),
|
315 |
+
# Period 6 continued
|
316 |
+
72: (4, 6), 73: (5, 6), 74: (6, 6), 75: (7, 6), 76: (8, 6), 77: (9, 6), 78: (10, 6), 79: (11, 6),
|
317 |
+
80: (12, 6), 81: (13, 6), 82: (14, 6), 83: (15, 6), 84: (16, 6), 85: (17, 6), 86: (18, 6),
|
318 |
+
# Period 7
|
319 |
+
87: (1, 7), 88: (2, 7),
|
320 |
+
# Actinides (period 7 continued)
|
321 |
+
89: (4, 10), 90: (5, 10), 91: (6, 10), 92: (7, 10), 93: (8, 10), 94: (9, 10), 95: (10, 10), 96: (11, 10),
|
322 |
+
97: (12, 10), 98: (13, 10), 99: (14, 10), 100: (15, 10), 101: (16, 10), 102: (17, 10), 103: (18, 10),
|
323 |
+
# Period 7 continued
|
324 |
+
104: (4, 7), 105: (5, 7), 106: (6, 7), 107: (7, 7), 108: (8, 7), 109: (9, 7), 110: (10, 7), 111: (11, 7),
|
325 |
+
112: (12, 7), 113: (13, 7), 114: (14, 7), 115: (15, 7), 116: (16, 7), 117: (17, 7), 118: (18, 7)
|
326 |
+
}
|
327 |
+
|
328 |
+
def get_electronic_configuration(element):
|
329 |
+
"""Extract electronic configuration from element data"""
|
330 |
+
# Try different possible column names for electronic configuration
|
331 |
+
config_columns = ['electronic_configuration', 'electron_configuration', 'econf', 'ec']
|
332 |
+
|
333 |
+
for col in config_columns:
|
334 |
+
if col in element.index and pd.notna(element.get(col)):
|
335 |
+
return str(element[col])
|
336 |
+
|
337 |
+
# If no explicit electronic configuration column, try to construct it from other data
|
338 |
+
# This is a fallback - the mendeleev library should have this data
|
339 |
+
return None
|
340 |
+
|
341 |
+
def create_hover_text(element, selected_property, original_value, display_value):
|
342 |
+
"""Create detailed hover text for an element"""
|
343 |
+
|
344 |
+
def format_value(value, unit="", is_integer=False):
|
345 |
+
if pd.isna(value):
|
346 |
+
return "N/A"
|
347 |
+
if isinstance(value, (int, float)):
|
348 |
+
if is_integer:
|
349 |
+
return f"{int(round(value))} {unit}".strip()
|
350 |
+
elif abs(value) >= 1000:
|
351 |
+
return f"{value:.2e} {unit}".strip()
|
352 |
+
elif abs(value) >= 10:
|
353 |
+
return f"{value:.2f} {unit}".strip()
|
354 |
+
else:
|
355 |
+
return f"{value:.3f} {unit}".strip()
|
356 |
+
return str(value)
|
357 |
+
|
358 |
+
# Get property info
|
359 |
+
prop_config = property_info.get(selected_property, {})
|
360 |
+
prop_label = prop_config.get('label', selected_property.replace('_', ' ').title())
|
361 |
+
|
362 |
+
# Determine if this is an integer property
|
363 |
+
is_int_prop = is_integer_property(selected_property, elements_data)
|
364 |
+
|
365 |
+
# Determine units based on property name
|
366 |
+
if 'density' in selected_property.lower():
|
367 |
+
unit = "g/cmΒ³"
|
368 |
+
elif 'electronegativity' in selected_property.lower():
|
369 |
+
unit = ""
|
370 |
+
elif 'radius' in selected_property.lower():
|
371 |
+
unit = "pm"
|
372 |
+
elif 'energy' in selected_property.lower() or 'ionization' in selected_property.lower():
|
373 |
+
unit = "eV"
|
374 |
+
elif 'affinity' in selected_property.lower():
|
375 |
+
unit = "eV"
|
376 |
+
elif 'point' in selected_property.lower() or 'temperature' in selected_property.lower():
|
377 |
+
unit = "K"
|
378 |
+
elif 'weight' in selected_property.lower() or 'mass' in selected_property.lower():
|
379 |
+
unit = "u"
|
380 |
+
elif 'volume' in selected_property.lower():
|
381 |
+
unit = "cmΒ³/mol"
|
382 |
+
elif 'conductivity' in selected_property.lower():
|
383 |
+
unit = "W/mK"
|
384 |
+
else:
|
385 |
+
unit = ""
|
386 |
+
|
387 |
+
current_str = format_value(original_value, unit, is_int_prop)
|
388 |
+
|
389 |
+
# Build hover text with key properties
|
390 |
+
hover_lines = [
|
391 |
+
f"<b>{element.get('name', 'N/A')} ({element.get('symbol', 'N/A')})</b>",
|
392 |
+
f"<b>{prop_label}: {current_str}</b>",
|
393 |
+
"", # Empty line for separation
|
394 |
+
f"Atomic Number: {element.get('atomic_number', 'N/A')}",
|
395 |
+
]
|
396 |
+
|
397 |
+
# Add electronic configuration if available
|
398 |
+
electronic_config = get_electronic_configuration(element)
|
399 |
+
if electronic_config:
|
400 |
+
hover_lines.append(f"Electronic Configuration: {electronic_config}")
|
401 |
+
|
402 |
+
# Add key properties if available
|
403 |
+
key_properties = [
|
404 |
+
('atomic_weight', 'Atomic Weight', 'u', False),
|
405 |
+
('period', 'Period', '', True),
|
406 |
+
('group_id', 'Group', '', True),
|
407 |
+
('block', 'Block', '', False),
|
408 |
+
('en_pauling', 'Electronegativity', '', False),
|
409 |
+
('atomic_radius', 'Atomic Radius', 'pm', False),
|
410 |
+
('ionenergy', 'Ionization Energy', 'eV', False),
|
411 |
+
('melting_point', 'Melting Point', 'K', False),
|
412 |
+
('boiling_point', 'Boiling Point', 'K', False),
|
413 |
+
('density', 'Density', 'g/cmΒ³', False),
|
414 |
+
]
|
415 |
+
|
416 |
+
for prop_name, display_name, prop_unit, is_int in key_properties:
|
417 |
+
if prop_name in element.index and pd.notna(element.get(prop_name)):
|
418 |
+
value_str = format_value(element[prop_name], prop_unit, is_int)
|
419 |
+
hover_lines.append(f"{display_name}: {value_str}")
|
420 |
+
|
421 |
+
return "<br>".join(hover_lines)
|
422 |
+
|
423 |
+
def create_periodic_table_figure(selected_property_label):
|
424 |
+
"""Create the periodic table figure for the given property"""
|
425 |
+
|
426 |
+
if not MENDELEEV_AVAILABLE or elements_data.empty:
|
427 |
+
fig = go.Figure()
|
428 |
+
fig.add_annotation(
|
429 |
+
text="Mendeleev library not available. Please install: pip install mendeleev",
|
430 |
+
showarrow=False,
|
431 |
+
font=dict(size=16)
|
432 |
+
)
|
433 |
+
return fig
|
434 |
+
|
435 |
+
# Get the actual property name from the label
|
436 |
+
selected_property = valid_properties.get(selected_property_label)
|
437 |
+
if not selected_property:
|
438 |
+
fig = go.Figure()
|
439 |
+
fig.add_annotation(
|
440 |
+
text=f"Property '{selected_property_label}' not available",
|
441 |
+
showarrow=False,
|
442 |
+
font=dict(size=16)
|
443 |
+
)
|
444 |
+
return fig
|
445 |
+
|
446 |
+
# Filter out elements without the selected property
|
447 |
+
property_data = elements_data[selected_property].dropna()
|
448 |
+
|
449 |
+
if property_data.empty:
|
450 |
+
fig = go.Figure()
|
451 |
+
fig.add_annotation(
|
452 |
+
text=f"No data available for {selected_property_label}",
|
453 |
+
showarrow=False,
|
454 |
+
font=dict(size=16)
|
455 |
+
)
|
456 |
+
return fig
|
457 |
+
|
458 |
+
# Determine if we should use log scale
|
459 |
+
use_log = should_use_log_scale(selected_property, elements_data)
|
460 |
+
|
461 |
+
# Prepare data for visualization
|
462 |
+
if use_log:
|
463 |
+
# For log scale, we need positive values
|
464 |
+
positive_data = property_data[property_data > 0]
|
465 |
+
if positive_data.empty:
|
466 |
+
use_log = False
|
467 |
+
viz_data = property_data
|
468 |
+
min_value = property_data.min()
|
469 |
+
max_value = property_data.max()
|
470 |
+
else:
|
471 |
+
viz_data = np.log10(positive_data)
|
472 |
+
min_value = viz_data.min()
|
473 |
+
max_value = viz_data.max()
|
474 |
+
else:
|
475 |
+
viz_data = property_data
|
476 |
+
min_value = property_data.min()
|
477 |
+
max_value = property_data.max()
|
478 |
+
|
479 |
+
# Initialize data lists
|
480 |
+
hover_texts = []
|
481 |
+
element_symbols = []
|
482 |
+
atomic_numbers = []
|
483 |
+
x_positions = []
|
484 |
+
y_positions = []
|
485 |
+
element_values = []
|
486 |
+
|
487 |
+
# Process each element
|
488 |
+
for _, element in elements_data.iterrows():
|
489 |
+
atomic_num = element['atomic_number']
|
490 |
+
|
491 |
+
# Skip elements without position data
|
492 |
+
if atomic_num not in ELEMENT_POSITIONS:
|
493 |
+
continue
|
494 |
+
|
495 |
+
x_pos, y_pos = ELEMENT_POSITIONS[atomic_num]
|
496 |
+
x_positions.append(x_pos)
|
497 |
+
y_positions.append(11 - y_pos) # Invert y-axis for correct table layout
|
498 |
+
element_symbols.append(element['symbol'])
|
499 |
+
atomic_numbers.append(atomic_num)
|
500 |
+
|
501 |
+
# Get property value
|
502 |
+
original_value = element[selected_property]
|
503 |
+
|
504 |
+
if pd.notna(original_value):
|
505 |
+
if use_log and original_value > 0:
|
506 |
+
display_value = np.log10(original_value)
|
507 |
+
else:
|
508 |
+
display_value = original_value
|
509 |
+
else:
|
510 |
+
display_value = np.nan
|
511 |
+
|
512 |
+
element_values.append(display_value)
|
513 |
+
|
514 |
+
# Create comprehensive hover text
|
515 |
+
hover_text = create_hover_text(element, selected_property, original_value, display_value)
|
516 |
+
hover_texts.append(hover_text)
|
517 |
+
|
518 |
+
# Create the figure
|
519 |
+
fig = go.Figure()
|
520 |
+
|
521 |
+
# Add scatter plot
|
522 |
+
fig.add_trace(go.Scatter(
|
523 |
+
x=x_positions,
|
524 |
+
y=y_positions,
|
525 |
+
mode='markers+text',
|
526 |
+
text=element_symbols,
|
527 |
+
hoverinfo='text',
|
528 |
+
hovertext=hover_texts,
|
529 |
+
textfont=dict(
|
530 |
+
family="Arial, sans-serif",
|
531 |
+
size=14,
|
532 |
+
color="white",
|
533 |
+
weight="bold",
|
534 |
+
),
|
535 |
+
hoverlabel=dict(
|
536 |
+
bgcolor="rgba(255,255,255,0.95)",
|
537 |
+
font_size=12,
|
538 |
+
font_family="Arial, sans-serif",
|
539 |
+
bordercolor="black"
|
540 |
+
),
|
541 |
+
marker=dict(
|
542 |
+
symbol='square',
|
543 |
+
color=element_values,
|
544 |
+
size=45,
|
545 |
+
colorscale=get_portland_like_colorscale(use_log),
|
546 |
+
cmin=min_value,
|
547 |
+
cmax=max_value,
|
548 |
+
colorbar=dict(
|
549 |
+
title=f"{selected_property_label}{'<br>(log scale)' if use_log else ''}",
|
550 |
+
thickness=20,
|
551 |
+
x=1.02
|
552 |
+
),
|
553 |
+
showscale=True,
|
554 |
+
line=dict(color='black', width=1)
|
555 |
+
)
|
556 |
+
))
|
557 |
+
|
558 |
+
# Add atomic number annotations
|
559 |
+
for i in range(len(x_positions)):
|
560 |
+
fig.add_annotation(
|
561 |
+
x=x_positions[i],
|
562 |
+
y=y_positions[i] + 0.3,
|
563 |
+
text=str(atomic_numbers[i]),
|
564 |
+
showarrow=False,
|
565 |
+
font=dict(
|
566 |
+
family="Arial, sans-serif",
|
567 |
+
size=8,
|
568 |
+
color="white",
|
569 |
+
weight="bold",
|
570 |
+
)
|
571 |
+
)
|
572 |
+
|
573 |
+
# Add lanthanide and actinide labels
|
574 |
+
fig.add_annotation(x=3, y=2, text="Lanthanides", showarrow=False,
|
575 |
+
font=dict(size=10, weight="bold"))
|
576 |
+
fig.add_annotation(x=3, y=1, text="Actinides", showarrow=False,
|
577 |
+
font=dict(size=10, weight="bold"))
|
578 |
+
|
579 |
+
# Update layout
|
580 |
+
title_text = f'<b>Periodic Table by {selected_property_label}</b>'
|
581 |
+
if use_log:
|
582 |
+
title_text += '<br><span style="font-size:14px;">(Logarithmic Color Scale)</span>'
|
583 |
+
|
584 |
+
fig.update_layout(
|
585 |
+
title=dict(
|
586 |
+
text=title_text,
|
587 |
+
x=0.5,
|
588 |
+
font=dict(size=24)
|
589 |
+
),
|
590 |
+
xaxis=dict(
|
591 |
+
range=[0, 19],
|
592 |
+
showgrid=False,
|
593 |
+
zeroline=False,
|
594 |
+
showticklabels=False,
|
595 |
+
visible=False
|
596 |
+
),
|
597 |
+
yaxis=dict(
|
598 |
+
range=[0, 12],
|
599 |
+
showgrid=False,
|
600 |
+
zeroline=False,
|
601 |
+
showticklabels=False,
|
602 |
+
visible=False
|
603 |
+
),
|
604 |
+
plot_bgcolor='white',
|
605 |
+
paper_bgcolor='#f8f9fa',
|
606 |
+
width=1200,
|
607 |
+
height=800,
|
608 |
+
margin=dict(l=20, r=100, t=100, b=20)
|
609 |
+
)
|
610 |
+
|
611 |
+
return fig
|
612 |
+
|
613 |
+
# Create Gradio interface
|
614 |
+
def create_gradio_app():
|
615 |
+
"""Create the Gradio interface"""
|
616 |
+
|
617 |
+
if not MENDELEEV_AVAILABLE or not dropdown_properties:
|
618 |
+
def error_interface():
|
619 |
+
return "β Mendeleev library not available or no valid properties found. Please install: pip install mendeleev"
|
620 |
+
|
621 |
+
return gr.Interface(
|
622 |
+
fn=error_interface,
|
623 |
+
inputs=[],
|
624 |
+
outputs=gr.Textbox(label="Error"),
|
625 |
+
title="Periodic Table Dashboard - Error"
|
626 |
+
)
|
627 |
+
|
628 |
+
# Get property options for dropdown (excluding continuous correlative properties)
|
629 |
+
property_options = list(dropdown_properties.keys())
|
630 |
+
|
631 |
+
# Set default to element series if available, otherwise first property
|
632 |
+
default_property = None
|
633 |
+
for label, prop_name in dropdown_properties.items():
|
634 |
+
if property_info.get(prop_name, {}).get('is_default', False):
|
635 |
+
default_property = label
|
636 |
+
break
|
637 |
+
|
638 |
+
if not default_property and property_options:
|
639 |
+
default_property = property_options[0]
|
640 |
+
|
641 |
+
with gr.Blocks(title="Interactive Periodic Table", theme=gr.themes.Soft()) as app:
|
642 |
+
gr.Markdown("# π§ͺ Interactive Periodic Table")
|
643 |
+
|
644 |
+
with gr.Row():
|
645 |
+
with gr.Column(scale=1):
|
646 |
+
property_dropdown = gr.Dropdown(
|
647 |
+
choices=property_options,
|
648 |
+
value=default_property,
|
649 |
+
label="Select Property to Colourize",
|
650 |
+
)
|
651 |
+
with gr.Row():
|
652 |
+
plot_output = gr.Plot(show_label=False)
|
653 |
+
with gr.Row():
|
654 |
+
gr.Markdown(f"""
|
655 |
+
**π¬ Data Source:** [Mendeleev Library](https://mendeleev.readthedocs.io/)
|
656 |
+
""")
|
657 |
+
|
658 |
+
# Update plot when dropdown changes
|
659 |
+
property_dropdown.change(
|
660 |
+
fn=create_periodic_table_figure,
|
661 |
+
inputs=[property_dropdown],
|
662 |
+
outputs=[plot_output]
|
663 |
+
)
|
664 |
+
|
665 |
+
# Initialize with first property
|
666 |
+
app.load(
|
667 |
+
fn=create_periodic_table_figure,
|
668 |
+
inputs=[property_dropdown],
|
669 |
+
outputs=[plot_output]
|
670 |
+
)
|
671 |
+
|
672 |
+
return app
|
673 |
+
|
674 |
+
# Create and run the app
|
675 |
+
if __name__ == "__main__":
|
676 |
+
print(f"π Starting Gradio app with {len(dropdown_properties)} properties (excluding continuous correlative)")
|
677 |
+
if dropdown_properties:
|
678 |
+
print("π Available dropdown properties:")
|
679 |
+
for label, prop_name in dropdown_properties.items():
|
680 |
+
log_note = " (log scale)" if should_use_log_scale(prop_name, elements_data) else ""
|
681 |
+
int_note = " (integer)" if is_integer_property(prop_name, elements_data) else ""
|
682 |
+
default_note = " (DEFAULT)" if property_info.get(prop_name, {}).get('is_default', False) else ""
|
683 |
+
data_count = elements_data[prop_name].notna().sum()
|
684 |
+
print(f" β’ {label}: {data_count} elements{log_note}{int_note}{default_note}")
|
685 |
+
|
686 |
+
print(f"\nπ Total valid properties (including continuous correlative): {len(valid_properties)}")
|
687 |
+
|
688 |
+
app = create_gradio_app()
|
689 |
+
app.launch()
|
requirements.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
plotly
|
2 |
+
gradio
|
3 |
+
pandas
|
4 |
+
numpy
|
5 |
+
lxml
|