import matplotlib.pyplot as plt import plotly.graph_objects as go import plotly.express as px import pandas as pd import numpy as np import streamlit as st from config import SOIL_TYPES, STRENGTH_PARAMETERS class SoilProfileVisualizer: def __init__(self): self.soil_colors = { "soft clay": "#8B4513", "medium clay": "#A0522D", "stiff clay": "#D2691E", "very stiff clay": "#CD853F", "hard clay": "#DEB887", "loose sand": "#F4A460", "medium dense sand": "#DAA520", "dense sand": "#B8860B", "very dense sand": "#CD853F", "soft silt": "#DDA0DD", "medium silt": "#BA55D3", "stiff silt": "#9370DB", "loose gravel": "#696969", "dense gravel": "#2F4F4F", "weathered rock": "#708090", "soft rock": "#2F4F4F", "hard rock": "#36454F" } def create_soil_profile_plot(self, soil_data): """Create interactive soil profile visualization""" if not soil_data or "soil_layers" not in soil_data: return None layers = soil_data["soil_layers"] fig = go.Figure() # Add soil layers for i, layer in enumerate(layers): depth_from = layer.get("depth_from", 0) depth_to = layer.get("depth_to", 0) soil_type = layer.get("soil_type", "unknown") description = layer.get("description", "") strength_value = layer.get("strength_value", "N/A") strength_param = layer.get("strength_parameter", "") # Get color color = self.soil_colors.get(soil_type.lower(), "#CCCCCC") # Create layer rectangle fig.add_shape( type="rect", x0=0, x1=1, y0=-depth_to, y1=-depth_from, fillcolor=color, line=dict(color="black", width=1), opacity=0.8 ) # Add layer text with enhanced parameters mid_depth = -(depth_from + depth_to) / 2 # Build text with available parameters text_lines = [f"{layer.get('consistency', '')} {soil_type}".strip()] # Add strength parameters if strength_param and strength_value is not None: text_lines.append(f"{strength_param}: {strength_value}") # Add calculated Su if available if layer.get("calculated_su"): text_lines.append(f"Su: {layer['calculated_su']:.0f} kPa*") # Add friction angle if available if layer.get("friction_angle"): text_lines.append(f"φ: {layer['friction_angle']:.1f}°*") fig.add_annotation( x=0.5, y=mid_depth, text="
".join(text_lines), showarrow=False, font=dict(size=9, color="white"), bgcolor="rgba(0,0,0,0.6)", bordercolor="white", borderwidth=1 ) # Add depth markers max_depth = max([layer.get("depth_to", 0) for layer in layers]) depth_ticks = list(range(0, int(max_depth) + 5, 5)) fig.update_layout( title="Soil Profile", xaxis=dict( range=[0, 1], showticklabels=False, showgrid=False, zeroline=False ), yaxis=dict( title="Depth (m)", range=[-max_depth - 2, 2], tickvals=[-d for d in depth_ticks], ticktext=[str(d) for d in depth_ticks], showgrid=True, gridcolor="lightgray" ), width=400, height=600, margin=dict(l=50, r=50, t=50, b=50) ) # Add water table if present if "water_table" in soil_data and soil_data["water_table"].get("depth"): wt_depth = soil_data["water_table"]["depth"] fig.add_hline( y=-wt_depth, line_dash="dash", line_color="blue", annotation_text="Water Table", annotation_position="right" ) return fig def create_strength_profile_plot(self, soil_data): """Create strength parameter vs depth plot""" if not soil_data or "soil_layers" not in soil_data: return None layers = soil_data["soil_layers"] depths = [] strengths = [] soil_types = [] for layer in layers: depth_from = layer.get("depth_from", 0) depth_to = layer.get("depth_to", 0) strength_value = layer.get("strength_value") soil_type = layer.get("soil_type", "") if strength_value is not None: mid_depth = (depth_from + depth_to) / 2 depths.append(mid_depth) strengths.append(strength_value) soil_types.append(soil_type) if not depths: return None fig = go.Figure() # Group by parameter type clay_depths = [] clay_strengths = [] sand_depths = [] sand_strengths = [] for i, soil_type in enumerate(soil_types): if "clay" in soil_type.lower(): clay_depths.append(depths[i]) clay_strengths.append(strengths[i]) else: sand_depths.append(depths[i]) sand_strengths.append(strengths[i]) # Add traces if clay_depths: # Create custom hover text for Su values clay_hover_text = [f"Depth: {d:.1f}m
Su: {s:.1f} kPa" for d, s in zip(clay_depths, clay_strengths)] fig.add_trace(go.Scatter( x=clay_strengths, y=clay_depths, mode='markers+lines', name='Su (kPa)', marker=dict(color='brown', size=8), line=dict(color='brown'), hovertemplate='%{customdata}', customdata=clay_hover_text )) if sand_depths: # Create custom hover text for SPT-N values sand_hover_text = [f"Depth: {d:.1f}m
SPT-N: {s:.0f} blows/30cm" for d, s in zip(sand_depths, sand_strengths)] fig.add_trace(go.Scatter( x=sand_strengths, y=sand_depths, mode='markers+lines', name='SPT-N (blows/30cm)', marker=dict(color='gold', size=8), line=dict(color='gold'), hovertemplate='%{customdata}', customdata=sand_hover_text )) # Determine primary axis title based on data if clay_depths and sand_depths: xaxis_title = "Strength Value (Su in kPa / SPT-N)" elif clay_depths: xaxis_title = "Undrained Shear Strength, Su (kPa)" elif sand_depths: xaxis_title = "SPT-N Value (blows/30cm)" else: xaxis_title = "Strength Value" fig.update_layout( title="Strength Parameters vs Depth", xaxis_title=xaxis_title, yaxis_title="Depth (m)", yaxis=dict(autorange='reversed'), width=500, height=600, showlegend=True, legend=dict( yanchor="top", y=0.99, xanchor="left", x=0.01 ) ) return fig def create_layer_summary_table(self, soil_data): """Create summary table of soil layers""" if not soil_data or "soil_layers" not in soil_data: return None layers = soil_data["soil_layers"] df_data = [] for layer in layers: # Build strength info with units strength_info = "" if layer.get("strength_parameter") and layer.get("strength_value") is not None: param = layer['strength_parameter'] value = layer['strength_value'] # Add units based on parameter type if param == "Su": strength_info = f"Su: {value:.1f} kPa" elif param == "SPT-N": strength_info = f"SPT-N: {value:.0f} blows/30cm" else: strength_info = f"{param}: {value}" # Add calculated parameters calc_params = [] if layer.get("calculated_su"): calc_params.append(f"Su: {layer['calculated_su']:.0f} kPa (calc)") if layer.get("friction_angle"): calc_params.append(f"φ: {layer['friction_angle']:.1f}° (calc)") if calc_params: strength_info += f" | {' | '.join(calc_params)}" df_data.append({ "Layer": layer.get("layer_id", ""), "Depth From (m)": layer.get("depth_from", ""), "Depth To (m)": layer.get("depth_to", ""), "Soil Type": f"{layer.get('consistency', '')} {layer.get('soil_type', '')}".strip(), "Description": layer.get("description", ""), "Strength Parameters": strength_info, "Color": layer.get("color", ""), "Moisture": layer.get("moisture", ""), "Notes": layer.get("su_source", "") or layer.get("friction_angle_source", "") or "" }) return pd.DataFrame(df_data) def export_profile_data(self, soil_data, format="csv"): """Export soil profile data""" df = self.create_layer_summary_table(soil_data) if format == "csv": return df.to_csv(index=False) elif format == "json": return df.to_json(orient="records", indent=2) else: return df.to_string(index=False)