import dash
from dash import dcc, html, callback, Input, Output, State
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
import numpy as np
import dash_bootstrap_components as dbc
from datetime import datetime
from dash_iconify import DashIconify
# Cargar datos
df = pd.read_csv("henley_results_cleaned.csv", parse_dates=['date']).drop(['page_number','time'], axis=1).dropna()
# Procesar datos
df['year'] = pd.to_datetime(df['date'], errors='coerce').dt.year
df['five_year'] = (df['year'] // 5) * 5 # Agrupar por cada 5 años en lugar de décadas
df['finish_time_min'] = df['finish_time'] / 60 # Convertir a minutos
# Inicializar la aplicación con tema elegante y fuente moderna
app = dash.Dash(__name__, external_stylesheets=[
dbc.themes.FLATLY, "https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"],
meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}])
app.title = "Henley Royal Regatta"
# Paleta de colores elegante y moderna
color_palette = {
'primary': '#3c4b7a',
'secondary': '#92acfc',
'accent': '#f97316',
'neutral': '#f5f5f5',
'text': '#0f172a',
'highlight': '#06b6d4',
'background': '#f2f5fc',
'light_blue': '#bfdbfe',
'dark_blue': '#1e3a8a',
'red': '#ef4444',
'green': '#10b981',
}
# Layout del dashboard con estilo modernizado
app.layout = dbc.Container([
# Header con logo y título
dbc.Row([
dbc.Col([
html.Div([
html.Div([
DashIconify(icon="fa6-solid:flag-checkered", width=60, height=60,
style={'color': color_palette['accent'], 'marginRight': '20px'}),
], style={'display': 'flex', 'alignItems': 'center'}),
html.Div([
html.H1("HENLEY ROYAL REGATTA",
style={'fontFamily': 'Poppins, sans-serif', 'fontWeight': '700',
'color': color_palette['primary'], 'marginBottom': '0', 'letterSpacing': '1px'}),
html.H5("Exploring the History of the Most Prestigious Rowing Competition",
style={'color': color_palette['secondary'], 'fontWeight': '400', 'letterSpacing': '0.5px'})
])
], style={'display': 'flex', 'alignItems': 'center', 'marginBottom': '20px'})
], width=12)
]),
# Sección con botón que despliega modal con información
dbc.Row([
dbc.Col([
dbc.Button([
DashIconify(icon="mdi:information-outline", style={"marginRight": "8px"}),
"Get information about the Henley Regatta."
], id="open-regata-modal", color="primary", className="mb-3", style={'fontWeight': '500'})
], width=12, className="text-center")
], className="mb-4"),
# Modal con información sobre la regata
dbc.Modal([
dbc.ModalHeader(dbc.ModalTitle([
DashIconify(icon="mdi:boat-row", style={"marginRight": "10px"}),
"Information about the Regatta"
])),
dbc.ModalBody([
dbc.Row([
dbc.Col([
html.H4([
DashIconify(icon="fa6-solid:trophy", style={"marginRight": "10px", "color": color_palette['primary']}),
"Henly Reggate Race"
], style={'color': color_palette['primary'], 'fontWeight': 'bold', 'display': 'flex', 'alignItems': 'center'}),
html.P([
"The Henley Royal Regatta, held annually on the River Thames in England since 1839, is the world's most prestigious rowing competition. It features elite international rowers in head-to-head knockout races over a challenging course. ",
"Beyond the sport, it's a significant British social event steeped in tradition, attracting thousands of spectators and holding royal patronage. Winning at Henley is a mark of rowing excellence."
]),
html.Div([
DashIconify(icon="mdi:map-marker-path",style={"marginRight": "10px", "fontSize": "24px", "color": color_palette['secondary']}),
html.P([
"The boats compete over a 2,112-meter course, passing several key points: "
], style={'margin': '0', 'flex': '1'})
], style={'display': 'flex', 'alignItems': 'center', 'marginBottom': '10px'}),
html.Div(style={"marginLeft": "34px"}, children=[
html.Div([
DashIconify(icon="mdi:flag-variant", style={"marginRight": "10px", "color": color_palette['accent']}),
html.Span("Barrier (570m)", style={'fontWeight': 'bold', 'color': color_palette['accent']})
], style={'display': 'flex', 'alignItems': 'center', 'marginBottom': '8px'}),
html.Div([
DashIconify(icon="mdi:flag-variant-outline", style={"marginRight": "10px", "color": color_palette['accent']}),
html.Span("Fawley (1,100m)", style={'fontWeight': 'bold', 'color': color_palette['accent']})
], style={'display': 'flex', 'alignItems': 'center', 'marginBottom': '8px'}),
html.Div([
DashIconify(icon="mdi:flag-checkered", style={"marginRight": "10px", "color": color_palette['accent']}),
html.Span("Finish Line (2,112m)", style={'fontWeight': 'bold', 'color': color_palette['accent']})
], style={'display': 'flex', 'alignItems': 'center', 'marginBottom': '8px'})
])
], md=12)
])
]),
dbc.ModalFooter(
dbc.Button([
DashIconify(icon="mdi:close", style={"marginRight": "8px"}),
"Close"
], id="close-regata-modal", className="ms-auto")
)
], id="regata-modal", size="xl", is_open=False),
# Filtros modernizados con iconos
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardBody([
html.H5([
DashIconify(icon="mdi:filter-outline", style={"marginRight": "10px"}),
"Selecting Year Ranges"
], style={'color': color_palette['primary'], 'fontWeight': 'bold', 'marginBottom': '15px',
'textAlign': 'center', 'display': 'flex', 'alignItems': 'center', 'justifyContent': 'center'}),
# Añadir esto después del título del slider
html.Div([
html.Span("Data Visualization Note ", style={'fontWeight': 'bold'}),
dbc.Button(
DashIconify(icon="mdi:information-outline"),
id="slider-info-button",
color="link",
size="sm",
style={'padding': '0', 'marginLeft': '5px'}
),
dbc.Popover(
[
dbc.PopoverHeader("Data Grouping Information"),
dbc.PopoverBody(
"While you can select any specific year range, the timeline visualization groups data in 5-year periods for clearer trend analysis."
),
],
id="slider-info-popover",
target="slider-info-button",
trigger="hover",
placement="bottom",
)
], style={'display': 'flex', 'alignItems': 'center', 'justifyContent': 'center', 'marginBottom': '10px'}),
dbc.Row([
dbc.Col([
dcc.RangeSlider(
id='year-range-slider',
min=int(df['year'].min()),
max=int(df['year'].max()),
step=1,
marks={int(year): str(year) for year in range(int(df['year'].min()), int(df['year'].max()+1), 5)},
value=[int(df['year'].min()), int(df['year'].max())],
className="mt-2 mb-2",
tooltip={"placement": "bottom", "always_visible": True},
allowCross=False
),
], width=12),
]),
dbc.Row([
dbc.Col([
html.Div([
DashIconify(icon="mdi:trophy-outline", style={"marginRight": "8px"}),
html.Label("Cup:", style={'fontWeight': 'bold'})
], style={'display': 'flex', 'alignItems': 'center', 'marginBottom': '5px'}),
dcc.Dropdown(
id='cup-filter',
options=[{'label': cup, 'value': cup} for cup in sorted(df['cup'].unique())],
multi=False,
placeholder="All Cups",
style={'borderRadius': '8px'},
className="mb-2"
),
dbc.Button([
DashIconify(icon="mdi:information-outline", style={"marginRight": "5px"}),
"Info: Cups and Categoríes"
], id="cup-info-button", color="info", outline=True, size="sm", className="mt-1 mb-3",
style={'display': 'flex', 'alignItems': 'center'})
], width=5),
dbc.Col([
html.Div(
"OR",
style={'display': 'flex', 'justifyContent': 'center', 'alignItems': 'center', 'height': '100%',
'fontWeight': 'bold', 'fontSize': '1.2em', 'color': color_palette['primary']}
)
], width=2, style={'display': 'flex', 'alignItems': 'center', 'justifyContent': 'center'}),
dbc.Col([
html.Div([
DashIconify(icon="mdi:boat", style={"marginRight": "8px"}),
html.Label("Boat Class:", style={'fontWeight': 'bold'})
], style={'display': 'flex', 'alignItems': 'center', 'marginBottom': '5px'}),
dcc.Dropdown(
id='boat-filter',
options=[{'label': boat, 'value': boat} for boat in sorted(df['boatclass'].unique())],
multi=False,
placeholder="All Boats Classes",
style={'borderRadius': '8px'},
className="mb-2"
),
dbc.Button([
DashIconify(icon="mdi:information-outline", style={"marginRight": "5px"}),
"Info: Boat Classes"
], id="boat-info-button", color="info", outline=True, size="sm", className="mt-1 mb-3",
style={'display': 'flex', 'alignItems': 'center'})
], width=5),
], className="mb-4")
])
], className="shadow-sm", style={'borderRadius': '10px', 'border': 'none'})
], width=12)
], className="mb-4 mt-4"),
# Popovers con información
dbc.Popover(
[
dbc.PopoverHeader([
DashIconify(icon="mdi:trophy", style={"marginRight": "8px"}),
"Cups and Categoríes"
], style={'display': 'flex', 'alignItems': 'center'}),
dbc.PopoverBody([
html.P("Races are organized into different cups and categories:"),
html.Ul([
html.Li([
html.Strong("Grand Challenge Cup: "),
"The most prestigious cup for men's eight-oared crews"
], style={'display': 'flex', 'alignItems': 'center', 'marginBottom': '8px'}),
html.Li([
html.Strong("Diamond Sculls: "),
"Single Sculls Competition"
], style={'display': 'flex', 'alignItems': 'center', 'marginBottom': '8px'}),
html.Li([
html.Strong("Princess Elizabeth: "),
"For school eight-oared crews"
], style={'display': 'flex', 'alignItems': 'center'})
])
])
],
id="cup-info-popover",
target="cup-info-button",
trigger="click",
placement="bottom",
),
dbc.Popover(
[
dbc.PopoverHeader([
DashIconify(icon="mdi:boat", style={"marginRight": "8px"}),
"Boats Classes"
], style={'display': 'flex', 'alignItems': 'center'}),
dbc.PopoverBody([
html.P("Boats are classified according to the number of rowers:"),
html.Ul([
html.Li([
DashIconify(icon="mdi:account-group", style={"marginRight": "8px"}),
html.Strong("Eight (8): "),
"Coxed eight"
], style={'display': 'flex', 'alignItems': 'center', 'marginBottom': '8px'}),
html.Li([
DashIconify(icon="mdi:account-multiple", style={"marginRight": "8px"}),
html.Strong("Four (4): "),
"Fours (coxed or coxless)"
], style={'display': 'flex', 'alignItems': 'center', 'marginBottom': '8px'}),
html.Li([
DashIconify(icon="mdi:account-multiple-outline", style={"marginRight": "8px"}),
html.Strong("Pair (2): "),
"Pair"
], style={'display': 'flex', 'alignItems': 'center', 'marginBottom': '8px'}),
html.Li([
DashIconify(icon="mdi:account", style={"marginRight": "8px"}),
html.Strong("Single Scull (1x): "),
"Single scull"
], style={'display': 'flex', 'alignItems': 'center'})
])
])
],
id="boat-info-popover",
target="boat-info-button",
trigger="click",
placement="bottom",
),
# Estadísticas clave - Título con icono
dbc.Row([
dbc.Col([
html.H3([
DashIconify(icon="mdi:chart-box", style={"marginRight": "15px", "color": color_palette['primary']}),
"MAIN STATISTICS"
], style={'textAlign': 'center', 'color': color_palette['primary'], 'fontWeight': 'bold',
'marginBottom': '20px', 'display': 'flex', 'alignItems': 'center', 'justifyContent': 'center'})
], width=12)
]),
# Acordeón para estadísticas
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardBody([
dbc.Accordion(
[
dbc.AccordionItem(
[
dbc.Row(id='stats-cards')
],
title=[
DashIconify(icon="mdi:view-dashboard", style={"marginRight": "10px"}),
"See Stats"
],
),
],
start_collapsed=False, # Comienza expandido
id="stats-accordion",
),
])
], className="shadow-sm", style={'borderRadius': '10px', 'border': 'none'})
], width=12)
], className="mb-4"),
# Visualizaciones principales con iconos en los títulos
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader([
DashIconify(icon="mdi:chart-timeline-variant", style={"marginRight": "10px", "color": color_palette['primary']}),
html.H5("Top Winners Clubs over years", style={'color': color_palette['primary'], 'fontWeight': 'bold', 'display': 'inline'})
], style={'display': 'flex', 'alignItems': 'center'}),
dbc.CardBody([
html.H6("The progression of Winner clubs every five years", style={'color': color_palette['primary'], 'fontWeight': 'bold', 'display': 'inline'}),
html.Hr(),
dcc.Graph(id='timeline-chart', figure={}, style={'height': '500px'})
])
], className="shadow-sm", style={'borderRadius': '10px', 'border': 'none'})
], width=12)
], className="mb-4"),
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader([
DashIconify(icon="mdi:medal", style={"marginRight": "10px", "color": color_palette['primary']}),
html.H5("MOST DOMINANT CLUBS", style={'color': color_palette['primary'], 'fontWeight': 'bold', 'display': 'inline'})
], style={'display': 'flex', 'alignItems': 'center'}),
dbc.CardBody([
html.H6("TOP 10 Winners Clubs", style={'color': color_palette['primary'], 'fontWeight': 'bold', 'display': 'inline'}),
html.Hr(),
dcc.Graph(id='top-clubs-chart', figure={})
])
], className="shadow-sm", style={'borderRadius': '10px', 'border': 'none'})
], width=6),
dbc.Col([
dbc.Card([
dbc.CardHeader([
html.Div([
DashIconify(icon="mdi:chart-donut", style={"marginRight": "10px", "color": color_palette['primary']}),
html.H5("WAYS TO WIN", style={'color': color_palette['primary'],
'fontWeight': 'bold', 'display': 'inline', 'marginRight': '10px'})
], style={'display': 'flex', 'alignItems': 'center', 'flex': '1'}),
dbc.Button([
DashIconify(icon="mdi:information-outline", style={"marginRight": "5px"}),
"Info:Types of Victory (Verdict)"
], id="verdict-info-button", color="info", outline=True, size="sm",
style={'display': 'flex', 'alignItems': 'center'})
], style={'display': 'flex', 'alignItems': 'center', 'justifyContent': 'space-between'}),
dbc.CardBody([
html.H6("Breakdown of Verdicts by Cup", style={'color': color_palette['primary'], 'fontWeight': 'bold', 'display': 'inline'}),
html.Hr(),
dcc.Graph(id='verdict-chart', figure={})
])
], className="shadow-sm", style={'borderRadius': '10px', 'border': 'none'})
], width=6),
], className="mb-4"),
# Popover para tipos de victoria
dbc.Popover(
[
dbc.PopoverHeader([
DashIconify(icon="mdi:whistle", style={"marginRight": "8px"}),
"Types of Victory (Verdict)"
], style={'display': 'flex', 'alignItems': 'center'}),
dbc.PopoverBody([
html.P("The way a boat wins a race is described with various terms:"),
html.Ul([
html.Li([
DashIconify(icon="mdi:rocket-launch", style={"marginRight": "8px", "color": color_palette['green']}),
html.Strong("Easily: "),
"Won by clear water"
], style={'display': 'flex', 'alignItems': 'center', 'marginBottom': '8px'}),
html.Li([
DashIconify(icon="mdi:ruler", style={"marginRight": "8px", "color": color_palette['primary']}),
html.Strong("Lengths: "),
"Won by several lengths"
], style={'display': 'flex', 'alignItems': 'center', 'marginBottom': '8px'}),
html.Li([
DashIconify(icon="mdi:flag-outline", style={"marginRight": "8px", "color": color_palette['secondary']}),
html.Strong("Canvas: "),
"Won by about a canvas (1-2 meters)"
], style={'display': 'flex', 'alignItems': 'center', 'marginBottom': '8px'}),
html.Li([
DashIconify(icon="mdi:cancel", style={"marginRight": "8px", "color": color_palette['red']}),
html.Strong("Not Rowed Out: "),
"Race abandoned or disqualified"
], style={'display': 'flex', 'alignItems': 'center'})
])
]),
],
id="verdict-info-popover",
target="verdict-info-button",
trigger="click",
placement="bottom",
),
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader([
DashIconify(icon="mdi:timer", style={"marginRight": "10px", "color": color_palette['primary']}),
html.H5("Trends in Race Times", style={'color': color_palette['primary'], 'fontWeight': 'bold', 'display': 'inline'})
], style={'display': 'flex', 'alignItems': 'center'}),
dbc.CardBody([
dcc.Graph(id='times-chart', figure={})
])
], className="shadow-sm", style={'borderRadius': '10px', 'border': 'none'})
], width=6),
dbc.Col([
dbc.Card([
dbc.CardHeader([
DashIconify(icon="mdi:flag-variant", style={"marginRight": "10px", "color": color_palette['primary']}),
html.H5("Advantage at Key Points", style={'color': color_palette['primary'], 'fontWeight': 'bold', 'display': 'inline'})
], style={'display': 'flex', 'alignItems': 'center'}),
dbc.CardBody([
dcc.Graph(id='key-points-chart', figure={})
])
], className="shadow-sm", style={'borderRadius': '10px', 'border': 'none'})
], width=6),
], className="mb-4"),
# Footer modernizado
dbc.Row([
dbc.Col([
html.Div([
DashIconify(icon="mdi:rowing", style={"marginRight": "10px", "fontSize": "24px", "color": color_palette['secondary']}),
html.P("Dashboard Created with Dash-Python | Data Sourced: Data Is Plural and Dominic Goymour",
style={'textAlign': 'center', 'color': color_palette['primary'], 'margin': '0'})
], style={'display': 'flex', 'alignItems': 'center', 'justifyContent': 'center', 'padding': '20px'})
], width=12)
], style={'backgroundColor': color_palette['neutral'], 'borderRadius': '10px', 'marginTop': '20px'}),
], fluid=True, style={'fontFamily': 'Poppins, sans-serif', 'backgroundColor': '#fafafa', 'padding': '20px'})
# Callback para abrir y cerrar el modal
@app.callback(
Output("regata-modal", "is_open"),
[Input("open-regata-modal", "n_clicks"), Input("close-regata-modal", "n_clicks")],
[State("regata-modal", "is_open")],
)
def toggle_modal(n1, n2, is_open):
if n1 or n2:
return not is_open
return is_open
# Callback para actualizar estadísticas con iconos
@app.callback(
Output('stats-cards', 'children'),
[Input('year-range-slider', 'value'),
Input('cup-filter', 'value'),
Input('boat-filter', 'value')]
)
def update_stats_cards(year_range, selected_cups, selected_boats):
# Filtrar datos según selecciones
filtered_df = df.copy()
if year_range:
filtered_df = filtered_df[(filtered_df['year'] >= year_range[0]) & (filtered_df['year'] <= year_range[1])]
if selected_cups:
filtered_df = filtered_df[filtered_df['cup'] == selected_cups]
if selected_boats:
filtered_df = filtered_df[filtered_df['boatclass'] == selected_boats]
# Calcular estadísticas dinámicamente
total_races = len(filtered_df)
total_cups = filtered_df['cup'].nunique()
total_boats = filtered_df['boatclass'].nunique()
avg_time = filtered_df['finish_time'].mean() if not filtered_df['finish_time'].empty else 0
fastest_time = filtered_df['finish_time'].min() if not filtered_df['finish_time'].empty else 0
# Crear tarjetas para estadísticas principales con iconos
stat_cards = ([
dbc.Col(
dbc.Card([
dbc.CardBody([
html.Div([
DashIconify(icon="mdi:race", width=40, height=40, style={'color': color_palette['primary']}),
], style={'textAlign': 'center', 'marginBottom': '10px'}),
html.H4(f"{total_races:,}", className="card-title text-center",
style={'color': color_palette['primary'], 'fontSize': '2rem', 'fontWeight': '600'}),
html.P("Total Races", className="card-text text-center"),
])
], color="light", outline=True, className="h-100 shadow-sm",
style={'borderRadius': '12px', 'border': 'none', 'transition': 'all 0.3s'}),
width=12, md=4, lg=3
),
dbc.Col(
dbc.Card([
dbc.CardBody([
html.Div([
DashIconify(icon="mdi:trophy", width=40, height=40, style={'color': color_palette['primary']}),
], style={'textAlign': 'center', 'marginBottom': '10px'}),
html.H4(f"{total_cups}", className="card-title text-center",
style={'color': color_palette['primary'], 'fontSize': '2rem', 'fontWeight': '600'}),
html.P("Total Cups", className="card-text text-center"),
])
], color="light", outline=True, className="h-100 shadow-sm",
style={'borderRadius': '12px', 'border': 'none', 'transition': 'all 0.3s'}),
width=12, md=4, lg=3
),
dbc.Col(
dbc.Card([
dbc.CardBody([
html.Div([
DashIconify(icon="mdi:boat", width=40, height=40, style={'color': color_palette['primary']}),
], style={'textAlign': 'center', 'marginBottom': '10px'}),
html.H4(f"{total_boats}", className="card-title text-center",
style={'color': color_palette['primary'], 'fontSize': '2rem', 'fontWeight': '600'}),
html.P("Total Boat Classes", className="card-text text-center"),
])
], color="light", outline=True, className="h-100 shadow-sm",
style={'borderRadius': '12px', 'border': 'none', 'transition': 'all 0.3s'}),
width=12, md=4, lg=2
),
dbc.Col(
dbc.Card([
dbc.CardBody([
html.Div([
DashIconify(icon="mdi:clock-time-four", width=40, height=40, style={'color': color_palette['primary']}),
], style={'textAlign': 'center', 'marginBottom': '10px'}),
html.H4(f"{avg_time:.1f}s" if avg_time else "N/A", className="card-title text-center",
style={'color': color_palette['primary'], 'fontSize': '2rem', 'fontWeight': '600'}),
html.P("Average Time", className="card-text text-center"),
])
], color="light", outline=True, className="h-100 shadow-sm",
style={'borderRadius': '12px', 'border': 'none', 'transition': 'all 0.3s'}),
width=12, md=6, lg=2
),
dbc.Col(
dbc.Card([
dbc.CardBody([
html.Div([
DashIconify(icon="mdi:lightning-bolt", width=40, height=40, style={'color': color_palette['primary']}),
], style={'textAlign': 'center', 'marginBottom': '10px'}),
html.H4(f"{fastest_time:.1f}s" if fastest_time else "N/A",
className="card-title text-center", style={'color': color_palette['primary'], 'fontSize': '2rem', 'fontWeight': '600'}),
html.P("Fastest Time", className="card-text text-center"),
])
], color="light", outline=True, className="h-100 shadow-sm",
style={'borderRadius': '12px', 'border': 'none', 'transition': 'all 0.3s'}),
width=12, md=6, lg=2
),
])
return stat_cards
@app.callback(
[Output('timeline-chart', 'figure'),
Output('top-clubs-chart', 'figure'),
Output('verdict-chart', 'figure'),
Output('times-chart', 'figure'),
Output('key-points-chart', 'figure'),
Output('cup-filter', 'disabled'),
Output('boat-filter', 'disabled')],
[Input('year-range-slider', 'value'),
Input('cup-filter', 'value'),
Input('boat-filter', 'value')]
)
def update_all(year_range, selected_cups, selected_boats):
filtered_df = df.copy()
cup_disabled = False
boat_disabled = False
if year_range:
filtered_df = filtered_df[(filtered_df['year'] >= year_range[0]) & (filtered_df['year'] <= year_range[1])]
if selected_cups:
filtered_df = filtered_df[filtered_df['cup'] == selected_cups]
boat_disabled = True
elif selected_boats:
filtered_df = filtered_df[filtered_df['boatclass'] == selected_boats]
cup_disabled = True
# Obtener los top clubes para asignar colores consistentes
top_clubs_overall = filtered_df['winning_club'].value_counts().nlargest(10).index.tolist()
# Crear un diccionario de colores para los clubes
# Utilizamos una paleta de colores vibrante para distinguir bien entre clubes
club_colors = {}
color_palette_clubs = px.colors.diverging.balance
for i, club in enumerate(top_clubs_overall):
club_colors[club] = color_palette_clubs[i % len(color_palette_clubs)]
# 1. Timeline Chart - Victorias por cada 5 años
timeline_data = filtered_df.groupby(['five_year', 'winning_club']).size().reset_index(name='victories')
timeline_data = timeline_data[timeline_data['winning_club'].isin(top_clubs_overall)]
timeline_fig = px.area(
timeline_data,
x='five_year',
y='victories',
color='winning_club',
color_discrete_map=club_colors,
markers=True,
line_shape='spline',
)
timeline_fig.update_layout(
xaxis_title='5 Years Period',
yaxis_title='Númber of Wins',
legend_title='Club',
template='plotly_white',
legend=dict(orientation='h', yanchor='bottom', y=-0.3, xanchor='center', x=0.5),
xaxis=dict(tickmode='array', tickvals=sorted(timeline_data['five_year'].unique()),
ticktext=[f'{int(year)+4}' for year in sorted(timeline_data['five_year'].unique())])
)
# 2. Top Clubs Chart - Treemap de victorias por club
club_wins = filtered_df['winning_club'].value_counts().nlargest(10).reset_index()
club_wins.columns = ['club', 'wins']
# Crear un mapeo de colores para el treemap basado en los mismos colores del gráfico de área
treemap_color_map = {club: club_colors[club] for club in club_wins['club'] if club in club_colors}
top_clubs_fig = px.treemap(
club_wins,
path=['club'],
values='wins',
color='club', # Cambiar para usar clubes directamente como color
color_discrete_map=treemap_color_map, # Usar el mapeo de colores personalizado
)
top_clubs_fig.update_traces(
textinfo='label+value',
hovertemplate='<b>%{label}</b><br>Wins: %{value}<extra></extra>'
)
top_clubs_fig.update_layout(
margin=dict(l=0, r=0, t=30, b=0)
)
# 3. Verdict Chart - Sunburst de veredictos por copa
verdict_data = filtered_df.groupby(['cup', 'verdict']).size().reset_index(name='count')
verdict_data = verdict_data.sort_values('count', ascending=False)
verdict_fig = px.sunburst(
verdict_data,
path=['cup', 'verdict'],
values='count',
color='count',
color_continuous_scale='Blues',
)
verdict_fig.update_layout(
margin=dict(l=0, r=0, t=30, b=0),
coloraxis_showscale=False
)
# 4. Times Chart - Evolución de tiempos con bandas de confianza
times_data = filtered_df.groupby('year').agg({
'finish_time': ['mean', 'std', 'count'],
'barrier_time': ['mean'],
'fawley_time': ['mean']
}).reset_index()
times_data.columns = ['year', 'finish_mean', 'finish_std', 'finish_count', 'barrier_mean', 'fawley_mean']
# Calcular intervalos de confianza
times_data['finish_upper'] = times_data['finish_mean'] + 1.96 * times_data['finish_std'] / np.sqrt(times_data['finish_count'])
times_data['finish_lower'] = times_data['finish_mean'] - 1.96 * times_data['finish_std'] / np.sqrt(times_data['finish_count'])
times_fig = go.Figure()
# Añadir área de confianza
if not times_data.empty and not times_data['finish_mean'].isnull().all():
times_fig.add_trace(go.Scatter(
x=times_data['year'].tolist() + times_data['year'].tolist()[::-1],
y=times_data['finish_upper'].tolist() + times_data['finish_lower'].tolist()[::-1],
fill='toself',
fillcolor='rgba(1, 87, 155, 0.1)',
line=dict(color='rgba(255,255,255,0)'),
hoverinfo='skip',
showlegend=False
))
# Añadir líneas para cada tiempo
times_fig.add_trace(go.Scatter(
x=times_data['year'],
y=times_data['finish_mean'],
mode='lines+markers',
name='Final Time',
line=dict(color=color_palette['primary'], width=3),
marker=dict(size=7)
))
if not times_data['barrier_mean'].isnull().all():
times_fig.add_trace(go.Scatter(
x=times_data['year'],
y=times_data['barrier_mean'],
mode='lines+markers',
name='Barrier',
line=dict(color=color_palette['secondary'], width=2, dash='dash'),
marker=dict(size=5)
))
if not times_data['fawley_mean'].isnull().all():
times_fig.add_trace(go.Scatter(
x=times_data['year'],
y=times_data['fawley_mean'],
mode='lines+markers',
name='Fawley',
line=dict(color=color_palette['accent'], width=2, dash='dot'),
marker=dict(size=5)
))
times_fig.update_layout(
yaxis_title='Time (seconds)',
template='plotly_white',
legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=0.5),
margin=dict(l=0, r=0, t=30, b=0)
)
# 5. Key Points Chart - Gráfico de barras radial para ventaja en puntos clave
key_points_data = {
'stage': ['Barrier', 'Fawley'],
'winner_leading': [
(filtered_df['barrier_loser_leading'] == False).sum(),
(filtered_df['fawley_loser_leading'] == False).sum()
],
'comeback': [
(filtered_df['barrier_loser_leading'] == True).sum(),
(filtered_df['fawley_loser_leading'] == True).sum()
]
}
key_points_df = pd.DataFrame(key_points_data)
key_points_df['total'] = key_points_df['winner_leading'] + key_points_df['comeback']
key_points_df['winner_leading_pct'] = key_points_df['winner_leading'] / key_points_df['total'] * 100
key_points_df['comeback_pct'] = key_points_df['comeback'] / key_points_df['total'] * 100
key_points_fig = make_subplots(
rows=1, cols=2,
specs=[[{"type": "domain"}, {"type": "domain"}]],
subplot_titles=["Advantage in Barrier", "Advantage in Fawley"]
)
# Gráfico para Barrier
key_points_fig.add_trace(
go.Pie(
labels=['Winner Leading', 'Comeback'],
values=[key_points_df.loc[0, 'winner_leading'], key_points_df.loc[0, 'comeback']],
hole=.7,
textinfo='percent',
marker=dict(colors=[color_palette['primary'], color_palette['accent']]),
domain={'column': 0}
),
row=1, col=1
)
# Gráfico para Fawley
key_points_fig.add_trace(
go.Pie(
labels=['Winner Leading', 'Comeback'],
values=[key_points_df.loc[1, 'winner_leading'], key_points_df.loc[1, 'comeback']],
hole=.7,
textinfo='percent',
marker=dict(colors=[color_palette['primary'], color_palette['accent']]),
domain={'column': 1}
),
row=1, col=2
)
key_points_fig.add_annotation(
x=0.15, y=0.5,
text=f"{key_points_df.loc[0, 'winner_leading_pct']:.1f}%",
font=dict(size=30, color=color_palette['primary'], family="Arial Black"),
showarrow=False
)
key_points_fig.add_annotation(
x=0.85, y=0.5,
text=f"{key_points_df.loc[1, 'winner_leading_pct']:.1f}%",
font=dict(size=30, color=color_palette['primary'], family="Arial Black"),
showarrow=False
)
key_points_fig.update_layout(
# title_text="¿Qué tan determinante es liderar en puntos clave?",
legend=dict(orientation="h", yanchor="bottom", y=-0.1, xanchor="center", x=0.5),
margin=dict(l=0, r=0, t=30, b=0)
)
return timeline_fig, top_clubs_fig, verdict_fig, times_fig, key_points_fig,cup_disabled, boat_disabled
# Controlar toggles de popovers para información
app.clientside_callback(
"""
function(n_clicks) {
return true;
}
""",
Output("slider-info-popover", "is_open"),
[Input("slider-info-button", "n_clicks")],
prevent_initial_call=True,
)
app.clientside_callback(
"""
function(n_clicks) {
return true;
}
""",
Output("cup-info-popover", "is_open"),
[Input("cup-info-button", "n_clicks")],
prevent_initial_call=True,
)
app.clientside_callback(
"""
function(n_clicks) {
return true;
}
""",
Output("boat-info-popover", "is_open"),
[Input("boat-info-button", "n_clicks")],
prevent_initial_call=True,
)
app.clientside_callback(
"""
function(n_clicks) {
return true;
}
""",
Output("verdict-info-popover", "is_open"),
[Input("verdict-info-button", "n_clicks")],
prevent_initial_call=True,
)