# Dash Plotly App for Four-Column Sankey Diagram
# Visualizes rowing competition crew progression through stages
from dash import Dash, Input, Output, callback, dcc, html
import dash_bootstrap_components as dbc
import os
import sys
# Try to import existing modules, fallback to embedded versions for deploy environments
MODULES_AVAILABLE = False
IMPORT_ERROR = None
try:
from data_processing import load_and_process_excel
from crew_tracking import track_crew_progression_with_slag
from visualization import create_four_column_cumulative_sankey
MODULES_AVAILABLE = True
print("✓ Successfully imported local modules")
except ImportError as e:
IMPORT_ERROR = str(e)
print(f"✗ Import error: {e}")
print(f"Python path: {sys.path}")
# For embedded/deployed environments where separate files aren't available
import pandas as pd
import plotly.graph_objects as go
from collections import defaultdict
import re
def load_and_process_excel(file_path):
"""Fallback: Simple Excel loader"""
try:
df = pd.read_excel(file_path, header=None)
return {
'voorwedstrijden': pd.DataFrame(),
'challenges': pd.DataFrame(),
'halve finales': pd.DataFrame(),
'finales': pd.DataFrame()
}
except Exception as e:
return None
def create_four_column_cumulative_sankey(all_race_data):
"""Fallback visualization function"""
fig = go.Figure()
return fig
# Initialize app with Bootstrap theme
# Available themes: BOOTSTRAP, CERULEAN, COSMO, CYBORG, DARKLY, FLATLY, JOURNAL,
# LITERA, LUMEN, LUX, MATERIA, MINTY, MORPH, PULSE, QUARTZ, SANDSTONE, SIMPLEX,
# SKETCHY, SLATE, SOLAR, SPACELAB, SUPERHERO, UNITED, VAPOR, YETI, ZEPHYR
app = Dash(__name__, external_stylesheets=[dbc.themes.COSMO])
# ===== DATA LOADING =====
def load_competition_data(excel_file):
"""Load and process competition data from Excel file"""
try:
# Load the Excel file and split by race types
race_dataframes = load_and_process_excel(excel_file)
# Check if loading failed
if race_dataframes is None:
return None, f"Failed to load Excel file '{excel_file}'. File may not be accessible in this environment."
# Process race data for crew tracking
all_race_data = {}
for race_type, df in race_dataframes.items():
# Skip empty dataframes
if df.empty:
all_race_data[race_type] = {}
continue
# Convert dataframe rows to crew entries
crew_entries = []
for _, row in df.iterrows():
crew_entry = {
'pos': row.get('pos.', ''),
'code': row.get('code', ''),
'ploeg': row.get('ploeg', ''),
'veld': row.get('veld', ''),
'baan': row.get('baan', ''),
'race_name': row.get('race_name', ''),
'race_number': row.get('race_number', ''),
'race_sub_number': row.get('race_sub_number', ''),
'race_type': row.get('race_type', ''),
'crew_member': row.get('crew_member', ''),
'crew_unique_id': row.get('crew_unique_id', '')
}
crew_entries.append(crew_entry)
# Group by race_name for the structure expected by visualization
race_data = {}
for entry in crew_entries:
race_name = entry['race_name']
if race_name and race_name != '':
if race_name not in race_data:
race_data[race_name] = []
race_data[race_name].append(entry)
all_race_data[race_type] = race_data
return all_race_data
except Exception as e:
import traceback
print(f"Error loading {excel_file}: {e}")
print(traceback.format_exc())
return None, str(e)
# Detect available Excel files
available_files = []
for filename in ['VSc2x.xlsx', 'MSc2x.xlsx', 'MSc4-.xlsx']:
if os.path.exists(filename):
available_files.append(filename)
# Default file
default_file = available_files[0] if available_files else 'VSc2x.xlsx'
# ===== DASH APP LAYOUT =====
# Navbar
navbar = dbc.Navbar(
dbc.Container([
dbc.Row([
dbc.Col(html.I(className="bi bi-water me-2"), width="auto"),
dbc.Col(dbc.NavbarBrand("SuperCup progressie analyse", className="ms-2")),
], align="center", className="g-0"),
]),
color="primary",
dark=True,
className="mb-4",
)
# Info card
info_card = dbc.Card([
dbc.CardHeader(html.H5("Over de visualisatie", className="mb-0")),
dbc.CardBody([
html.P("Deze Sankey-diagram toont de voortgang van roeiteams door de competitie-fases:", className="mb-3"),
dbc.Row([
dbc.Col([
dbc.Badge("1", color="primary", className="me-2"),
html.Strong("Veld"), " - Klasse van het team"
], width=12, className="mb-2"),
dbc.Col([
dbc.Badge("2", color="primary", className="me-2"),
html.Strong("Halve Finales"), " - Verdeling van teams in halve finales, let op dat de klasses los zijn getoond maar samen starten in de bovenste of onderstande halve finales"
], width=12, className="mb-2"),
dbc.Col([
dbc.Badge("3", color="primary", className="me-2"),
html.Strong("Finales"), " - Verdeling van teams in finales"
], width=12, className="mb-2"),
dbc.Col([
dbc.Badge("4", color="primary", className="me-2"),
html.Strong("Eindklassering"), " - Eindklasseringen"
], width=12, className="mb-2"),
]),
html.Hr(),
html.P([
html.I(className="bi bi-info-circle me-2"),
"Kleuren vertegenwoordigen verschillende veldcategorieën. Beweeg de muis over de grafiek om de aantallen te zien."
], className="mb-0 text-muted small"),
])
], className="mb-4")
# Controls card
controls_card = dbc.Card([
dbc.CardHeader(html.H5("Veldselectie", className="mb-0")),
dbc.CardBody([
dbc.Label("Selecteer veld:", className="fw-bold mb-2"),
dcc.Dropdown(
id='competition-dropdown',
options=[{'label': f.replace('.xlsx', ''), 'value': f} for f in available_files],
value=default_file,
clearable=False,
className="mb-0"
)
])
], className="mb-4")
# Main layout
app.layout = dbc.Container([
navbar,
dbc.Row([
dbc.Col([
info_card,
controls_card,
], width=12, lg=3, className="mb-3 mb-lg-0"), # Add margin on mobile
dbc.Col([
dcc.Loading(
id="loading",
type="default",
children=[
html.Div(id='graph-container')
]
),
], width=12, lg=9),
]),
], fluid=True, className="p-2 p-md-4") # Smaller padding on mobile
@callback(
Output('graph-container', 'children'),
Input('competition-dropdown', 'value')
)
def update_graph(selected_file):
"""Update the Sankey diagram when a different competition is selected"""
if not selected_file or not os.path.exists(selected_file):
return dbc.Alert([
html.I(className="bi bi-exclamation-triangle me-2"),
f"Error: File '{selected_file}' not found in this environment."
], color="danger")
try:
# Load the data
result = load_competition_data(selected_file)
# Check if error was returned
if isinstance(result, tuple):
all_race_data, error_msg = result
return dbc.Alert([
html.H5([html.I(className="bi bi-x-circle me-2"), "Error Loading Data"], className="alert-heading"),
html.P(f"Failed to load '{selected_file}'"),
html.Hr(),
html.P(error_msg, className="mb-0 small"),
], color="danger")
all_race_data = result
if all_race_data is None:
return dbc.Alert([
html.I(className="bi bi-exclamation-triangle me-2"),
f"Error loading data from '{selected_file}'. Check the console for details."
], color="warning")
# Create the Sankey diagram using the existing visualization function
fig = create_four_column_cumulative_sankey(all_race_data)
# Style the figure to match Bootstrap COSMO theme
fig.update_layout(
height=700,
template="plotly_white", # Clean white background
font=dict(
family="'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
size=12,
color="#373a3c" # COSMO text color
),
title=dict(
font=dict(size=16, color="#2780e3"), # COSMO primary blue
x=0.5,
xanchor='center'
),
paper_bgcolor='white',
plot_bgcolor='white',
margin=dict(t=80, l=60, r=60, b=60),
# Responsive settings
autosize=True,
modebar=dict(
orientation='v', # Vertical toolbar on mobile
bgcolor='rgba(255,255,255,0.7)'
)
)
return dbc.Card([
dbc.CardBody([
dcc.Graph(
id='sankey-diagram',
figure=fig,
config={
'displayModeBar': True,
'displaylogo': False,
'responsive': True, # Enable responsive mode
'modeBarButtonsToRemove': ['lasso2d', 'select2d'],
'toImageButtonOptions': {
'format': 'png',
'filename': f'sankey_{selected_file.replace(".xlsx", "")}',
'height': 700,
'width': 1200,
'scale': 2
}
},
style={'width': '100%', 'height': '700px'}, # Responsive container
className='responsive-graph'
)
], className="p-0")
], className="shadow-sm")
except Exception as e:
import traceback
traceback.print_exc()
return dbc.Alert([
html.H5([html.I(className="bi bi-bug me-2"), "Error Creating Visualization"], className="alert-heading"),
html.P(str(e)),
html.Hr(),
html.P("Check the console for detailed traceback.", className="mb-0 small text-muted")
], color="danger")
if __name__ == '__main__':
app.run_server(debug=True)