| """Plotly visualization helpers for the PFAS-SBEAD dashboard.""" |
|
|
| from __future__ import annotations |
|
|
| import numpy as np |
| import pandas as pd |
| import plotly.express as px |
| import plotly.graph_objects as go |
| from plotly.subplots import make_subplots |
|
|
|
|
| COLOR_SCHEME = px.colors.qualitative.Set2 |
| TEMPLATE = "plotly_white" |
|
|
|
|
| def degradation_scatter(df: pd.DataFrame) -> go.Figure: |
| """Scatter: PFAS degradation vs voltage, colored by current density.""" |
| fig = px.scatter( |
| df, |
| x="voltage_V", |
| y="PFAS_degradation_pct", |
| color="current_density_A_m2", |
| size="HRT_days", |
| hover_data=["OLR_kg_m3_d", "pH", "AI_score"], |
| title="PFAS Degradation vs Applied Voltage", |
| labels={ |
| "voltage_V": "Voltage (V)", |
| "PFAS_degradation_pct": "PFAS Degradation (%)", |
| "current_density_A_m2": "Current Density (A/m²)", |
| "HRT_days": "HRT (days)", |
| }, |
| color_continuous_scale="Viridis", |
| template=TEMPLATE, |
| ) |
| fig.update_layout(height=450) |
| return fig |
|
|
|
|
| def ai_score_distribution(df: pd.DataFrame) -> go.Figure: |
| """Histogram of AI optimization scores.""" |
| fig = px.histogram( |
| df, |
| x="AI_score", |
| nbins=25, |
| title="AI Optimization Score Distribution", |
| labels={"AI_score": "AI Score"}, |
| color_discrete_sequence=[COLOR_SCHEME[0]], |
| template=TEMPLATE, |
| ) |
| fig.update_layout(height=350) |
| return fig |
|
|
|
|
| def mass_balance_sunburst(mb_df: pd.DataFrame) -> go.Figure: |
| """Average mass balance breakdown as a pie chart.""" |
| avg = mb_df[ |
| ["remaining_in_water_ug_L", "adsorbed_sludge_ug_L", |
| "adsorbed_electrode_ug_L", "short_chain_products_ug_L", "mineralized_PFAS_ug_L"] |
| ].mean() |
| labels = ["Remaining in Water", "Adsorbed on Sludge", "Adsorbed on Electrode", |
| "Short-Chain Products", "Mineralized (Degraded)"] |
| fig = go.Figure(data=[go.Pie( |
| labels=labels, |
| values=avg.values, |
| hole=0.4, |
| marker_colors=COLOR_SCHEME[:5], |
| )]) |
| fig.update_layout( |
| title="Average PFAS Mass Balance Distribution", |
| template=TEMPLATE, |
| height=400, |
| ) |
| return fig |
|
|
|
|
| def feature_importance_bar(imp_df: pd.DataFrame) -> go.Figure: |
| """Horizontal bar chart of feature importances.""" |
| fig = px.bar( |
| imp_df.head(10), |
| x="importance", |
| y="feature", |
| orientation="h", |
| title="Top Feature Importances (SHAP-proxy)", |
| labels={"importance": "Importance", "feature": ""}, |
| color="importance", |
| color_continuous_scale="Blues", |
| template=TEMPLATE, |
| ) |
| fig.update_layout(height=400, showlegend=False) |
| return fig |
|
|
|
|
| def degradation_heatmap(df: pd.DataFrame) -> go.Figure: |
| """Heatmap: voltage vs OLR bins, showing mean degradation.""" |
| df_copy = df.copy() |
| df_copy["OLR_bin"] = pd.cut(df_copy["OLR_kg_m3_d"], bins=6) |
| df_copy["voltage_bin"] = pd.cut(df_copy["voltage_V"], bins=6) |
| pivot = df_copy.pivot_table( |
| values="PFAS_degradation_pct", |
| index="voltage_bin", |
| columns="OLR_bin", |
| aggfunc="mean", |
| ) |
| fig = px.imshow( |
| pivot.values, |
| x=[str(c) for c in pivot.columns], |
| y=[str(i) for i in pivot.index], |
| color_continuous_scale="YlOrRd", |
| title="PFAS Degradation Heatmap: Voltage × OLR", |
| labels={"x": "OLR Bin (kg/m³/d)", "y": "Voltage Bin (V)", "color": "Degradation (%)"}, |
| template=TEMPLATE, |
| ) |
| fig.update_layout(height=420) |
| return fig |
|
|
|
|
| def dual_axis_performance(df: pd.DataFrame) -> go.Figure: |
| """Dual-axis: degradation and fluoride release vs experiment.""" |
| fig = make_subplots(specs=[[{"secondary_y": True}]]) |
| fig.add_trace( |
| go.Scatter( |
| x=df["experiment_id"], |
| y=df["PFAS_degradation_pct"], |
| mode="lines+markers", |
| name="PFAS Degradation (%)", |
| marker=dict(size=5, color=COLOR_SCHEME[0]), |
| ), |
| secondary_y=False, |
| ) |
| fig.add_trace( |
| go.Scatter( |
| x=df["experiment_id"], |
| y=df["fluoride_release_mg_L"], |
| mode="lines+markers", |
| name="Fluoride Release (mg/L)", |
| marker=dict(size=5, color=COLOR_SCHEME[1]), |
| ), |
| secondary_y=True, |
| ) |
| fig.update_layout( |
| title="Degradation & Fluoride Release Across Experiments", |
| template=TEMPLATE, |
| height=400, |
| legend=dict(orientation="h", yanchor="bottom", y=1.02), |
| ) |
| fig.update_yaxes(title_text="PFAS Degradation (%)", secondary_y=False) |
| fig.update_yaxes(title_text="Fluoride Release (mg/L)", secondary_y=True) |
| return fig |
|
|
|
|
| def stability_radar(df: pd.DataFrame) -> go.Figure: |
| """Radar chart of average stability indicators.""" |
| cols = ["pH_drop", "current_instability_index"] |
| vfa_norm = df["VFA_accumulation_mg_L"] / df["VFA_accumulation_mg_L"].max() |
| orp_norm = df["ORP_drift_mV"].abs() / df["ORP_drift_mV"].abs().max() |
|
|
| values = [ |
| df["pH_drop"].mean() / 1.5, |
| vfa_norm.mean(), |
| orp_norm.mean(), |
| df["current_instability_index"].mean() / 0.5, |
| ] |
| categories = ["pH Drop", "VFA Accumulation", "ORP Drift", "Current Instability"] |
| values.append(values[0]) |
| categories.append(categories[0]) |
|
|
| fig = go.Figure(data=go.Scatterpolar( |
| r=values, |
| theta=categories, |
| fill="toself", |
| fillcolor="rgba(255, 99, 71, 0.2)", |
| line_color="tomato", |
| )) |
| fig.update_layout( |
| polar=dict(radialaxis=dict(visible=True, range=[0, 1])), |
| title="Reactor Stability Indicators (Normalized)", |
| template=TEMPLATE, |
| height=400, |
| ) |
| return fig |
|
|
|
|
| def sensitivity_bar(sens_df: pd.DataFrame) -> go.Figure: |
| """Bar chart of sensitivity analysis.""" |
| fig = px.bar( |
| sens_df, |
| x="feature", |
| y="correlation_with_AI_score", |
| color="correlation_with_AI_score", |
| color_continuous_scale="RdYlGn", |
| title="Sensitivity Analysis: Feature Correlation with AI Score", |
| labels={"feature": "", "correlation_with_AI_score": "Correlation"}, |
| template=TEMPLATE, |
| ) |
| fig.update_layout(height=400, xaxis_tickangle=-45) |
| return fig |
|
|
|
|
| def energy_vs_degradation(df: pd.DataFrame) -> go.Figure: |
| """Scatter: energy input vs degradation with instability flag.""" |
| fig = px.scatter( |
| df, |
| x="energy_input_kWh_d", |
| y="PFAS_degradation_pct", |
| color="instability_flag", |
| symbol="instability_flag", |
| title="Energy Input vs PFAS Degradation (Instability Highlighted)", |
| labels={ |
| "energy_input_kWh_d": "Energy Input (kWh/d)", |
| "PFAS_degradation_pct": "PFAS Degradation (%)", |
| "instability_flag": "Instability", |
| }, |
| color_discrete_map={0: COLOR_SCHEME[0], 1: "red"}, |
| template=TEMPLATE, |
| ) |
| fig.update_layout(height=400) |
| return fig |
|
|
|
|
| def optimization_pareto(df: pd.DataFrame) -> go.Figure: |
| """Pareto front: degradation vs energy showing trade-off.""" |
| fig = px.scatter( |
| df, |
| x="energy_input_kWh_d", |
| y="PFAS_degradation_pct", |
| color="AI_score", |
| size="fluoride_release_mg_L", |
| hover_data=["voltage_V", "HRT_days", "OLR_kg_m3_d"], |
| title="Optimization Landscape: Degradation vs Energy Trade-off", |
| labels={ |
| "energy_input_kWh_d": "Energy Input (kWh/d)", |
| "PFAS_degradation_pct": "PFAS Degradation (%)", |
| "AI_score": "AI Score", |
| }, |
| color_continuous_scale="Plasma", |
| template=TEMPLATE, |
| ) |
| fig.update_layout(height=450) |
| return fig |
|
|