mirix commited on
Commit
ceadb69
Β·
verified Β·
1 Parent(s): 4280fa4

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +689 -0
  2. 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