import pandas as pd import matplotlib.pyplot as plt from flask import Blueprint, render_template_string, request, jsonify, Response import os from sklearn.decomposition import PCA from sklearn.preprocessing import StandardScaler import numpy as np from scipy.stats import linregress from scipy.optimize import curve_fit import json import glob # Create Blueprint co2_plot_bp = Blueprint('co2_plot', __name__, url_prefix='/co2') # Atomic weights for conversion between atomic and weight fractions ATOMIC_WEIGHTS = { 'Ag': 107.8682, 'Au': 196.966569, 'Cd': 112.411, 'Cu': 63.546, 'Ga': 69.723, 'Hg': 200.59, 'In': 114.818, 'Mn': 54.938044, 'Mo': 95.96, 'Nb': 92.90637, 'Ni': 58.6934, 'Pd': 106.42, 'Pt': 195.084, 'Rh': 102.90550, 'Sn': 118.710, 'Tl': 204.38, 'W': 183.84, 'Zn': 65.38 } # Voltage conversion functions def lin_fxn(x, a, b): return a*x+b def fit_lin(X, Y): params, covariance = curve_fit(lin_fxn, X, Y) a_fit, b_fit = params return (a_fit, b_fit) def est_x(x, X, Y): fit = fit_lin(X, Y) x = fit[0]*x+fit[1] return x def load_calibration_data_for_voltage_conversion(custom_params=None): """Load calibration data for voltage conversion from full cell to half cell.""" # Default experiment conditions: Neutral CO2RR in 4cm2 cell, Sputtered Copper Catalyst, 0.1M Bicarbonate - ref electrode (3M kcl) 230mV vs SHE default_params = { 'ref_pot': 0.23, # V Ag/AgCl electrode 'cathode_pH': 12.5, 'anode_pH': 3, 'geo_area': 4, # cm2 'membrane_loss': 0.1, # V # Note: anode_measured_potential_vs_ref is now interpolated from calibration data } # Use custom parameters if provided, otherwise use defaults if custom_params: params = {**default_params, **custom_params} else: params = default_params ref_pot = params['ref_pot'] cathode_pH = params['cathode_pH'] anode_pH = params['anode_pH'] Nern_pH_loss = (cathode_pH-anode_pH)*0.059 geo_area = params['geo_area'] membrane_loss = params['membrane_loss'] # Measurements from calibration work j = np.array([50,100,200]) cathode_pot = np.array([-1.62,-2.0,-2.3]) cathode_R = np.array([0.48,0.34,0.3]) anode_pot = np.array([1.3,1.35,1.4]) anode_R = np.array([0,0,0]) #almost negligible fullcell_pot = np.array([3,3.4,3.7]) fullcell_R = np.array([0.47,0.35,0.3]) n = len(cathode_pot) cathode_pot_corr = np.zeros(n) anode_pot_corr = np.zeros(n) cathode_overpot = np.zeros(n) anode_overpot = np.zeros(n) fullcell_pot_corr = np.zeros(n) for i in range(0, n): cathode_pot_corr[i] = correct_potential(cathode_pot[i], cathode_R[i], cathode_pH, j[i], geo_area, ref_pot) anode_pot_corr[i] = correct_potential(anode_pot[i], anode_R[i], anode_pH, j[i], geo_area, ref_pot) cathode_overpot[i] = get_overpotential(cathode_pot_corr[i],0.08) anode_overpot[i] = get_overpotential(anode_pot_corr[i], 1.23) fullcell_pot_corr[i] = fullcell_pot[i]-fullcell_R[i]*j[i]/1000*geo_area conditions_dict = { 'ref pot': ref_pot, 'cathode pH': cathode_pH, 'anode pH': anode_pH, 'Nern pH loss': Nern_pH_loss, 'geo area': geo_area, 'membrane loss': membrane_loss, } measurements_dict = { 'j': j, 'cathode pot': cathode_pot, 'cathode R': cathode_R, 'anode pot': anode_pot, 'anode R': anode_R, 'fullcell pot': fullcell_pot, 'fullcell R': fullcell_R, } data_dict = { 'cathode pot corr': cathode_pot_corr, 'anode pot corr': anode_pot_corr, 'cathode overpot': cathode_overpot, 'anode overpot': anode_overpot, 'fullcell pot corr': fullcell_pot_corr } return {'measurements': measurements_dict, 'conditions': conditions_dict, 'extracted params': data_dict} def she2rhe(ushe, pH, ref_pot): ushe = ushe+ref_pot+(0.059*pH) return ushe def rhe2she(urhe, pH, ref_pot): urhe = urhe - (0.059 * pH) return urhe def correct_potential(pot, R, pH, j, area, ref_pot): if pot<0: corrected_pot = she2rhe(pot+j/1000*area*R,pH, ref_pot) else: corrected_pot = she2rhe(pot-j/1000*area*R,pH, ref_pot) return corrected_pot def interpolate_cathode_R(current_density): """ Interpolate cathode resistance R from log(j) vs R calibration data. Calibration data: j = [50, 100, 200] mA/cm² R = [0.48, 0.34, 0.3] ohm Fits log(j) vs R and interpolates R for given current density. """ # Calibration data j_array = np.array([50, 100, 200]) # mA/cm² R_array = np.array([0.48, 0.34, 0.3]) # ohm # Convert to log scale for j log_j = np.log10(j_array) # Fit linear relationship: R = a * log10(j) + b fit_params = np.polyfit(log_j, R_array, 1) a, b = fit_params # Interpolate R for given current density (convert mA/cm² to mA/cm², already in correct units) if current_density <= 0: # Use minimum R if current density is too small return R_array[-1] # Use the smallest R (at highest j) log_j_input = np.log10(current_density) R_interpolated = a * log_j_input + b # Clamp to reasonable bounds (between min and max R values) R_interpolated = np.clip(R_interpolated, R_array.min(), R_array.max()) return R_interpolated def interpolate_anode_potential_vs_ref(current_density): """ Interpolate anode measured potential vs reference from log(j) vs anode_pot calibration data. Calibration data: j = [50, 100, 200] mA/cm² anode_pot = [1.3, 1.35, 1.4] V Fits log(j) vs anode_pot and interpolates anode_pot for given current density. """ # Calibration data j_array = np.array([50, 100, 200]) # mA/cm² anode_pot_array = np.array([1.3, 1.35, 1.4]) # V # Convert to log scale for j log_j = np.log10(j_array) # Fit linear relationship: anode_pot = a * log10(j) + b fit_params = np.polyfit(log_j, anode_pot_array, 1) a, b = fit_params # Interpolate anode_pot for given current density if current_density <= 0: # Use minimum anode_pot if current density is too small return anode_pot_array[0] # Use the smallest anode_pot (at lowest j) log_j_input = np.log10(current_density) anode_pot_interpolated = a * log_j_input + b # Clamp to reasonable bounds (between min and max anode_pot values) anode_pot_interpolated = np.clip(anode_pot_interpolated, anode_pot_array.min(), anode_pot_array.max()) return anode_pot_interpolated def cell2rhe(vcell, ref_pot, anode_pH, membrane_loss, Nern_pH_loss, current_density, geo_area, custom_anode_potential_vs_ref=None, custom_R_cathode=None): """ Convert full cell voltage to cathode potential vs RHE. Steps (matching notebook example): 1. Interpolate anode measured potential vs reference from calibration data (or use custom value) 2. Convert anode measured potential (vs reference) to RHE: V_anode_RHE = anode_measured_potential_vs_ref + ref_pot + 0.059 * anode_pH 3. Calculate cathode RHE (before IR correction): V_cathode_RHE = (V_anode_RHE + membrane_loss + Nern_pH_loss) - full_cell_V 4. Interpolate cathode resistance from calibration data (or use custom value) 5. Apply IR correction: V_cathode_RHE = V_cathode_RHE - (i/1000 * R * A) where i is current density in A/cm², R is interpolated resistance, A is geometric area Parameters: ----------- custom_anode_potential_vs_ref : float, optional Custom anode measured potential vs reference (V). If provided, overrides interpolation. custom_R_cathode : float, optional Custom cathode resistance (Ω). If provided, overrides interpolation. """ # Step 1: Interpolate anode measured potential vs reference (or use custom value) if custom_anode_potential_vs_ref is not None: anode_measured_potential_vs_ref = custom_anode_potential_vs_ref else: anode_measured_potential_vs_ref = interpolate_anode_potential_vs_ref(current_density) # Step 2: Convert anode measured potential to RHE v_anode_rhe = anode_measured_potential_vs_ref + ref_pot + 0.059 * anode_pH # Step 3: Calculate cathode RHE with membrane and Nernst pH losses (before IR correction) v_cathode_rhe = (v_anode_rhe + membrane_loss + Nern_pH_loss) - vcell # Step 4: Interpolate cathode resistance from calibration data (or use custom value) if custom_R_cathode is not None: R = custom_R_cathode else: R = interpolate_cathode_R(current_density) # current_density in mA/cm², R in ohm # Step 5: Apply IR correction # Convert current density from mA/cm² to A/cm² and apply IR correction # i/1000 converts mA/cm² to A/cm² IR_drop = (current_density / 1000.0) * R * geo_area v_cathode_rhe = v_cathode_rhe - IR_drop return v_cathode_rhe def get_overpotential(pot, pot_theory): overpot = abs(pot-pot_theory) return overpot def fullcell2halfcell(vcell, current_density, custom_params=None): ''' Main function to convert a voltage value from full cell to half cell vs she or rhe Parameters: ----------- vcell : float Full cell voltage (V) current_density : float Current density (mA/cm²) custom_params : dict, optional Custom parameters for voltage conversion ''' cali_dict = load_calibration_data_for_voltage_conversion(custom_params) # Extract custom values if provided custom_anode_pot = custom_params.get('anode_measured_potential_vs_ref') if custom_params else None custom_R = custom_params.get('R_cathode') if custom_params else None urhe = cell2rhe(vcell, cali_dict['conditions']['ref pot'], cali_dict['conditions']['anode pH'], cali_dict['conditions']['membrane loss'], cali_dict['conditions']['Nern pH loss'], current_density, cali_dict['conditions']['geo area'], custom_anode_potential_vs_ref=custom_anode_pot, custom_R_cathode=custom_R) ushe = rhe2she(urhe, cali_dict['conditions']['cathode pH'], cali_dict['conditions']['ref pot']) return ushe, urhe def convert_atomic_to_weight_fraction(df, element_columns): """ Convert atomic fraction to weight fraction for elemental compositions. """ df_converted = df.copy() for col in element_columns: if col in df_converted.columns and col in ATOMIC_WEIGHTS: df_converted[col] = df_converted[col] * ATOMIC_WEIGHTS[col] # Normalize to get weight fractions (0-1 scale) for idx, row in df_converted.iterrows(): total_weight = sum(row[col] for col in element_columns if col in df_converted.columns and col in ATOMIC_WEIGHTS) if total_weight > 0: for col in element_columns: if col in df_converted.columns and col in ATOMIC_WEIGHTS: df_converted.at[idx, col] = row[col] / total_weight return df_converted def filter_df_by_current_density(df, target_current_density, tolerance=10): """ Filter dataframe to get data close to a specific current density value. """ # Filter data within tolerance of target current density filtered_df = df[abs(df['current density'] - target_current_density) <= tolerance].copy() return filtered_df def generate_df_at_voltage(df, voltage_col='voltage_mean', cd_col='current density', fe_prefix='fe_', group_cols=None, target_voltage=3.0): """ Generate a dataframe interpolated at a specific voltage value. """ if group_cols is None: # Default: group by 'source' and all columns containing 'xrf' in their name xrf_cols = [col for col in df.columns if 'xrf' in col] group_cols = ['source'] + xrf_cols # Handle both _mean/_std suffix format and simple format fe_mean_cols = [col for col in df.columns if col.startswith(fe_prefix) and col.endswith('_mean')] fe_std_cols = [col for col in df.columns if col.startswith(fe_prefix) and col.endswith('_std')] # If no _mean columns found, look for simple fe_ columns (without _mean suffix) if not fe_mean_cols: fe_mean_cols = [col for col in df.columns if col.startswith(fe_prefix) and not col.endswith('_std')] # Get elemental composition columns (Ag, Au, Cu, etc.) # Handle both voltage_mean and voltage column names voltage_cols_to_exclude = ['voltage_mean', 'voltage_std', 'voltage'] composition_col = 'xrf composition' if 'xrf composition' in df.columns else 'target composition' element_cols = [col for col in df.columns if col not in ['source', 'current density', composition_col, 'rep'] + voltage_cols_to_exclude and not col.startswith('fe_') and not col.endswith('std')] results = [] for group_keys, group_df in df.groupby(group_cols): if not isinstance(group_keys, tuple): group_keys = (group_keys,) # Current density fit log_cds = np.log(group_df[cd_col].replace(0, np.nan).dropna().values) valid_idx = group_df[cd_col].replace(0, np.nan).dropna().index voltages_for_fit = group_df.loc[valid_idx, voltage_col].values if len(voltages_for_fit) >= 2: slope, intercept, _, _, _ = linregress(voltages_for_fit, log_cds) pred_log_cd = slope * target_voltage + intercept pred_cd = np.exp(pred_log_cd) else: pred_cd = np.nan fe_pred_dict = {} # MEAN for fe_col in fe_mean_cols: fe_vals = group_df[fe_col].values mask = ~np.isnan(fe_vals) if np.sum(mask) >= 2: slope_fe, intercept_fe, _, _, _ = linregress(group_df[voltage_col].values[mask], fe_vals[mask]) pred_fe = slope_fe * target_voltage + intercept_fe else: pred_fe = np.nan fe_pred_dict[fe_col] = pred_fe # STD for fe_col in fe_std_cols: fe_vals = group_df[fe_col].values mask = ~np.isnan(fe_vals) if np.sum(mask) >= 2: slope_fe, intercept_fe, _, _, _ = linregress(group_df[voltage_col].values[mask], fe_vals[mask]) pred_fe = slope_fe * target_voltage + intercept_fe else: pred_fe = np.nan fe_pred_dict[fe_col] = pred_fe # Get elemental composition values (these don't change with voltage, so take the first value) element_dict = {} for element_col in element_cols: element_vals = group_df[element_col].dropna() if not element_vals.empty: element_dict[element_col] = element_vals.iloc[0] else: element_dict[element_col] = np.nan row = dict(zip(group_cols, group_keys)) row['current density'] = pred_cd row.update(fe_pred_dict) row.update(element_dict) # Add elemental composition columns results.append(row) return pd.DataFrame(results) def load_xrd_data(sample_id, data_type="raw"): """ Load XRD data for a specific sample ID from Data/XRD or Data/CustomXRD directory. Args: sample_id: The sample ID to load data_type: Either "raw" (.xy files) or "normalized" (.csv files) Returns the XRD data as a list of [x, y] pairs or None if not found. """ try: # First check for custom XRD data, then fall back to original custom_xrd_base = "Data/CustomXRD" original_xrd_base = "Data/XRD" # Construct potential file paths if data_type == "raw": custom_path = f"{custom_xrd_base}/raw/{sample_id}.xy" original_path = f"{original_xrd_base}/raw/{sample_id}.xy" elif data_type == "normalized": custom_path = f"{custom_xrd_base}/normalized/{sample_id}.csv" original_path = f"{original_xrd_base}/normalized/{sample_id}.csv" else: print(f"Invalid data type: {data_type}") return None # Check custom XRD first, then original xrd_file_path = None if os.path.exists(custom_path): xrd_file_path = custom_path print(f"DEBUG: Using custom XRD file: {custom_path}") elif os.path.exists(original_path): xrd_file_path = original_path print(f"DEBUG: Using original XRD file: {original_path}") else: print(f"XRD file not found in custom or original locations for sample {sample_id} ({data_type})") return None # Read the file data = [] with open(xrd_file_path, 'r') as f: lines = f.readlines() # Skip the first line (header) for line_num, line in enumerate(lines[1:], 2): # Start from line 2 line = line.strip() if line and not line.startswith('#'): # Skip empty lines and comments try: # Handle different separators (space, tab, comma) parts = line.replace(',', ' ').split() if len(parts) >= 2: x_val = float(parts[0]) y_val = float(parts[1]) data.append([x_val, y_val]) except ValueError: # Skip lines that can't be parsed as numbers if line_num <= 10: # Only log first few errors to avoid spam print(f"Warning: Could not parse line {line_num} in {xrd_file_path}: {line}") continue if not data: print(f"No valid data found in XRD file: {xrd_file_path}") return None print(f"Loaded XRD data for sample {sample_id} ({data_type}): {len(data)} data points") return data except Exception as e: print(f"Error loading XRD data: {e}") return None def load_original_data(): """Load the original data from CSV file or current data from dashboard""" try: # First try to load current data from dashboard current_data_file = "Data/current_data_co2.json" if os.path.exists(current_data_file): with open(current_data_file, 'r') as f: saved_data = json.load(f) if isinstance(saved_data, dict) and 'data' in saved_data and 'columns' in saved_data: current_data = saved_data['data'] column_order = saved_data['columns'] df = pd.DataFrame(current_data, columns=column_order) elif isinstance(saved_data, list): df = pd.DataFrame(saved_data) else: df = pd.DataFrame(saved_data) # Filter for CO2R reaction if reaction column exists if 'reaction' in df.columns: df = df[df['reaction'] == 'CO2R'].copy() df = df.drop('reaction', axis=1) print(f"DEBUG: Available columns after loading CO2R data: {list(df.columns)}") print(f"DEBUG: Data shape: {df.shape}") print(f"DEBUG: Voltage columns present: {[col for col in df.columns if 'voltage' in col.lower()]}") return df except Exception as e: print(f"Could not load current data: {e}") # Fallback to original CSV data try: df = pd.read_csv("Data/DashboardData.csv") if 'reaction' in df.columns: df = df[df['reaction'] == 'CO2R'].copy() df = df.drop('reaction', axis=1) return df except Exception as e: print(f"Could not load CSV data: {e}") return pd.DataFrame() def calculate_pca_components(df): """Calculate PCA components from elemental composition data.""" if df.empty or len(df) < 2: df['PCA1'] = np.nan df['PCA2'] = np.nan return df # Get only elemental composition columns voltage_cols_to_exclude = ['voltage_mean', 'voltage_std', 'voltage'] composition_col = 'xrf composition' if 'xrf composition' in df.columns else 'target composition' element_cols = [col for col in df.columns if col not in ['sample id', 'source', 'batch number', 'batch date', 'current density', composition_col, 'target composition', 'xrf composition', 'rep'] + voltage_cols_to_exclude and not col.startswith('fe_') and not col.endswith('std')] # Filter out non-numeric columns numeric_element_cols = [] for col in element_cols: try: if pd.to_numeric(df[col], errors='coerce').notna().sum() >= 2: numeric_element_cols.append(col) except: continue if len(numeric_element_cols) < 2: df['PCA1'] = np.nan df['PCA2'] = np.nan return df # Prepare data for PCA pca_data = df[numeric_element_cols].copy() for col in pca_data.columns: pca_data[col] = pd.to_numeric(pca_data[col], errors='coerce') pca_data = pca_data.fillna(0) if pca_data.sum().sum() == 0: df['PCA1'] = 0 df['PCA2'] = 0 return df try: # Standardize and apply PCA scaler = StandardScaler() pca_data_scaled = scaler.fit_transform(pca_data) pca = PCA(n_components=2) pca_components = pca.fit_transform(pca_data_scaled) df['PCA1'] = pca_components[:, 0] df['PCA2'] = pca_components[:, 1] except Exception as e: print(f"PCA calculation failed: {e}") df['PCA1'] = np.nan df['PCA2'] = np.nan return df def format_column_name(column_name): """Format column names to be more readable.""" if column_name in ['voltage_mean', 'voltage']: return 'Full Cell Voltage (V)' elif column_name == 'voltage_she': return 'Est. Half-cell potential vs SHE (V)' elif column_name == 'voltage_rhe': return 'Est. Half-cell potential vs RHE (V)' elif column_name == 'cost_per_gram': return 'Cost per kg' elif column_name.startswith('fe_'): base_name = column_name.replace('fe_', '').replace('_mean', '') if base_name == 'h2': return 'Faradaic Efficiency H₂' elif base_name == 'co': return 'Faradaic Efficiency CO' elif base_name == 'ch4': return 'Faradaic Efficiency CH₄' elif base_name == 'c2h4': return 'Faradaic Efficiency C₂H₄' elif base_name == 'gas_total': return 'Faradaic Efficiency Gas Total' elif base_name == 'liquid': return 'Faradaic Efficiency Liquid' else: return 'Faradaic Efficiency ' + base_name.upper() elif column_name.startswith('max_partial_current_'): base_name = column_name.replace('max_partial_current_', '').replace('_mean', '').replace('_std', '') species_map = { 'h2': 'H₂', 'co': 'CO', 'ch4': 'CH₄', 'c2h4': 'C₂H₄', 'gas_total': 'Gas Total', 'liquid': 'Liquid' } species_label = species_map.get(base_name, base_name.upper()) return f'Max Partial Current {species_label}' elif column_name.startswith('partial_current_'): base_name = column_name.replace('partial_current_', '').replace('_mean', '').replace('_std', '') species_map = { 'h2': 'H₂', 'co': 'CO', 'ch4': 'CH₄', 'c2h4': 'C₂H₄', 'gas_total': 'Gas Total', 'liquid': 'Liquid' } species_label = species_map.get(base_name, base_name.upper()) return f'Partial Current {species_label}' elif column_name in ['PCA1', 'PCA2']: return column_name elif column_name in ['Ag', 'Au', 'Cd', 'Cu', 'Ga', 'Hg', 'In', 'Ni', 'Pd', 'Pt', 'Rh', 'Sn', 'Tl', 'Zn']: return column_name else: return column_name @co2_plot_bp.route('/') def co2_plot_main(): """Main CO2R plot page""" # Load and process data current_df = load_original_data() if current_df.empty: return "
Please ensure CO2R data is available in the main dashboard.
" # Calculate PCA components df_with_pca = calculate_pca_components(current_df) # Identify element columns voltage_cols_to_exclude = ['voltage_mean', 'voltage_std', 'voltage'] composition_col = 'xrf composition' if 'xrf composition' in df_with_pca.columns else 'target composition' element_cols = [col for col in df_with_pca.columns if col not in ['sample id', 'source', 'batch number', 'batch date', 'current density', composition_col, 'target composition', 'xrf composition', 'PCA1', 'PCA2', 'rep'] + voltage_cols_to_exclude and not col.startswith('fe_') and not col.endswith('std')] # Move partial current columns to the end for dropdown ordering element_pc_cols = [c for c in element_cols if c.startswith('partial_current_') or c.startswith('max_partial_current_')] element_non_pc_cols = [c for c in element_cols if c not in element_pc_cols] element_cols = element_non_pc_cols + element_pc_cols # Add PCA1 as first option if available if 'PCA1' in df_with_pca.columns: element_cols.insert(0, 'PCA1') if 'Cu' in df_with_pca.columns and 'Cu' not in element_cols: element_cols.insert(0, 'Cu') # Identify FE columns for y-axis options fe_cols = [col for col in df_with_pca.columns if col.startswith('fe_') and col.endswith('_mean')] if not fe_cols: fe_cols = [col for col in df_with_pca.columns if col.startswith('fe_') and not col.endswith('_std')] # Default y-axis to CO FE if available default_y_col = 'fe_co_mean' if 'fe_co_mean' in fe_cols else (fe_cols[0] if fe_cols else 'voltage_mean') # Generate dropdown options element_options = ''.join([f'' for col in element_cols]) fe_options = ''.join([f'' for col in fe_cols]) # Add voltage options to y-axis if 'voltage_mean' in df_with_pca.columns: fe_options += f'' if 'voltage' in df_with_pca.columns: fe_options += f'' # Create comprehensive x-axis and z-axis options from original comprehensive_x_axis_options = element_cols.copy() if 'PCA2' in df_with_pca.columns: comprehensive_x_axis_options.append('PCA2') if 'voltage_mean' in df_with_pca.columns: comprehensive_x_axis_options.append('voltage_mean') elif 'voltage' in df_with_pca.columns: comprehensive_x_axis_options.append('voltage') comprehensive_x_axis_options.extend(fe_cols) # Reorder to move partial current options to the bottom comp_pc_cols = [c for c in comprehensive_x_axis_options if c.startswith('partial_current_') or c.startswith('max_partial_current_')] comp_non_pc_cols = [c for c in comprehensive_x_axis_options if c not in comp_pc_cols] comprehensive_x_axis_options = comp_non_pc_cols + comp_pc_cols # Y-axis options: same as x-axis options y_axis_options = comprehensive_x_axis_options.copy() # Ensure partial current options remain at the bottom for y-axis as well y_pc_cols = [c for c in y_axis_options if c.startswith('partial_current_') or c.startswith('max_partial_current_')] y_non_pc_cols = [c for c in y_axis_options if c not in y_pc_cols] y_axis_options = y_non_pc_cols + y_pc_cols # Create z-axis options (for color control) - same as y-axis options with "Default" as first option z_axis_options = ['default_colors'] # Default option for current blue/red/black coloring z_axis_options.extend(y_axis_options) # Add all y-axis options (already ordered) # Generate all dropdown options x_axis_options_html = ''.join([f'' for col in comprehensive_x_axis_options]) y_axis_options_html = ''.join([f'' for col in y_axis_options]) z_axis_options_html = ''.join([f'' for col in z_axis_options]) # Current density options current_density_options = [50, 100, 150, 200, 300] default_current_density = 100 # Create the comprehensive HTML template from original interactive plot html_template = f'''/Data/XRD/raw/ or /Data/CustomXRD/raw/ directoriessample_001.xy for raw or sample_001.csv for normalized)