daniel-saed commited on
Commit
6e6feed
·
verified ·
1 Parent(s): 6175303

Upload 18 files

Browse files
Files changed (18) hide show
  1. .gitattributes +12 -35
  2. DOVAL.csv +3 -0
  3. DOVAL.parquet +3 -0
  4. DROAD.csv +3 -0
  5. DROAD.parquet +3 -0
  6. Dockerfile +24 -0
  7. FORMULA.csv +3 -0
  8. FORMULA.parquet +3 -0
  9. OVAL.csv +3 -0
  10. OVAL.parquet +3 -0
  11. ROAD.csv +3 -0
  12. ROAD.parquet +3 -0
  13. app.py +1720 -0
  14. app1.py +23 -0
  15. assets/ADDCN___.TTF +0 -0
  16. assets/custom.css +63 -0
  17. assets/style.css +212 -0
  18. requirements.txt +0 -0
.gitattributes CHANGED
@@ -1,35 +1,12 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
3
+ DOVAL.csv filter=lfs diff=lfs merge=lfs -text
4
+ DOVAL.parquet filter=lfs diff=lfs merge=lfs -text
5
+ DROAD.csv filter=lfs diff=lfs merge=lfs -text
6
+ DROAD.parquet filter=lfs diff=lfs merge=lfs -text
7
+ FORMULA.csv filter=lfs diff=lfs merge=lfs -text
8
+ FORMULA.parquet filter=lfs diff=lfs merge=lfs -text
9
+ OVAL.csv filter=lfs diff=lfs merge=lfs -text
10
+ OVAL.parquet filter=lfs diff=lfs merge=lfs -text
11
+ ROAD.csv filter=lfs diff=lfs merge=lfs -text
12
+ ROAD.parquet filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
DOVAL.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:75983946fe84b06601918e9f280d3015eb29edae9072fc62fae7d402b5954c9b
3
+ size 17777622
DOVAL.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:36ccad97fcc7f66cf36ba174a950364b7b4a118016bcbe97655c22cd75a6dad9
3
+ size 8215507
DROAD.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c96c5f0273fa92e61054e0d3401c80bfada1b6766c68922aa8625d59a8705ecc
3
+ size 20184116
DROAD.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ea4229286b42160df0004c1960352c572697f67182ca803eb73ba2ad74d68ea0
3
+ size 9008231
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # 2. Establece el directorio de trabajo dentro del contenedor.
4
+ WORKDIR /app
5
+
6
+ # 3. Copia primero el archivo de requisitos.
7
+ # Esto aprovecha el caché de Docker: si no cambias tus requisitos,
8
+ # no se volverán a instalar en cada despliegue, haciéndolo más rápido.
9
+ COPY requirements.txt .
10
+
11
+ # 4. Instala las dependencias.
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # 5. Copia el resto de los archivos de tu proyecto al contenedor.
15
+ # Esto incluye app.py, los archivos .csv y la carpeta 'assets'.
16
+ COPY . .
17
+
18
+ # 6. Expón el puerto que usará la aplicación. Hugging Face usa 7860 por defecto.
19
+ EXPOSE 7860
20
+
21
+ # 7. El comando para iniciar la aplicación.
22
+ # Usamos Gunicorn para ejecutar el 'server' de tu 'app.py'.
23
+ # El host 0.0.0.0 es crucial para que sea accesible desde fuera del contenedor.
24
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "1", "--threads", "4", "app:server"]
FORMULA.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e22bd25c608db76070a2da6f98d1a789d3b45581b7c5742be39c9c18a1f76d3e
3
+ size 18550025
FORMULA.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:51b8b7792c00f3ddb83b443cd9e82fc081b29f6991067c469f6d7e46e5dfe1fe
3
+ size 8519049
OVAL.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:894cd7d8befaaa886e973eab4126aeb6da27d98da1312daf46056b15051cd44c
3
+ size 38100470
OVAL.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b552296c9db5497e8658a4c8ea95424acbc9933c86cbf73241645bf6aa441cae
3
+ size 17391737
ROAD.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c6a9be95f3b32ca25af39d32ed4530b2f32dcd324562055f882fd6fc6ec6afdd
3
+ size 24480683
ROAD.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9ce4dc82dc87bce11b0cf67b650b8086e5f923a071583cef86ee68ede27db578
3
+ size 11157634
app.py ADDED
@@ -0,0 +1,1720 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dash
2
+ from dash import html, dcc, dash_table, Input, Output, State
3
+ import plotly.express as px
4
+ import pandas as pd
5
+ import numpy as np
6
+ import plotly.graph_objects as go
7
+ import pycountry_convert as pc
8
+ import pycountry
9
+ import gunicorn
10
+
11
+ iracing_ragions = {
12
+ 'US':['US'],
13
+ 'Mexico':['MX'],
14
+ 'Brazil':['BR'],
15
+ 'Canada':['CA'],
16
+ 'Atlantic':['GL'],
17
+ 'Japan':['JP'],
18
+ 'South America':['AR','PE','UY','CL','PY','BO','EC','CO','VE','GY','PA','CR','NI','HN','GT','BZ','SV','JM','DO','BS'],
19
+ 'Iberia':['ES','PT','AD'],
20
+ 'International':['RU','IL'],
21
+ 'France':['FR'],
22
+ 'UK & I':['GB','IE'], # <-- CORREGIDO: ' IE' -> 'IE' y nombre
23
+ 'Africa':['ZA','BW','ZW','ZM','CD','GA','BI','RW','UG','KE','SO','MG','SZ','NG','GH','CI','BF','NE','GW','GM','SN','MR','EH','MA','DZ','LY','TN','EG','DJ'],
24
+ 'Italy':['IT'],
25
+ 'Central EU':['PL','CZ','SK','HU','SI','HR','RS','ME','AL','RO','MD','UA','BY','EE','LV','LT'],
26
+ 'Finland':['FI'],
27
+ 'DE-AT-CH':['CH','AT','DE'], # <-- CORRECCIÓN: Eliminado el ''
28
+ 'Scandinavia':['DK','SE','NO'],
29
+ 'Australia & NZ':['AU','NZ'],
30
+ 'Asia':['SA','JO','IQ','YE','OM','AE','QA','IN','PK','AF','NP','BD','MM','TH','KH','VN','MY','ID','CN','PH','KR','MN','KZ','KG','UZ','TJ','AF','TM','LK'],
31
+ 'Benelux':['NL','BE','LU']
32
+ }
33
+
34
+ def load_and_process_data(filename):
35
+ """Función para cargar y pre-procesar un archivo de disciplina."""
36
+ print(f"Loading and processing {filename}...")
37
+ df = pd.read_csv(filename)
38
+ '''filename_parquet = filename.replace('.csv', '.parquet')
39
+ df = pd.read_parquet(filename_parquet)'''
40
+ df = df[df['IRATING'] > 1]
41
+ df = df[df['STARTS'] > 1]
42
+ df = df[df['CLASS'].str.contains('D|C|B|A|P|R', na=False)]
43
+
44
+ country_to_region_map = {country: region for region, countries in iracing_ragions.items() for country in countries}
45
+ df['REGION'] = df['LOCATION'].map(country_to_region_map).fillna('International')
46
+
47
+ df['Rank World'] = df['IRATING'].rank(method='first', ascending=False).fillna(0).astype(int)
48
+ df['Rank Region'] = df.groupby('REGION')['IRATING'].rank(method='first', ascending=False).fillna(0).astype(int)
49
+ df['Rank Country'] = df.groupby('LOCATION')['IRATING'].rank(method='first', ascending=False).fillna(0).astype(int)
50
+
51
+ df['CLASS'] = df['CLASS'].str[0]
52
+ print(f"Finished processing {filename}.")
53
+ return df
54
+
55
+ def create_irating_trend_line_chart(df):
56
+ """
57
+ Crea un gráfico de líneas que muestra el promedio de carreras corridas
58
+ para diferentes rangos de iRating, eliminando valores atípicos.
59
+ """
60
+ # --- 1. Eliminar Outliers ---
61
+ # Calculamos el percentil 99 para las carreras y filtramos para
62
+ # que unos pocos pilotos con miles de carreras no desvíen el promedio.
63
+ max_starts = df['STARTS'].quantile(0.95)
64
+ min_starts = df['STARTS'].quantile(0.001)
65
+ print(max_starts)
66
+ print(min_starts)
67
+ df_filtered = df[df['STARTS'] <= max_starts]
68
+ df_filtered = df[df['STARTS'] >= min_starts]
69
+
70
+ # --- 2. Agrupar iRating en Bins ---
71
+ # Creamos los rangos de 0 a 11000, en pasos de 1000.
72
+ bins = list(range(0, 12000, 1000))
73
+ labels = [f'{i/1000:.0f}k - {i/1000+1-0.001:.1f}k' for i in bins[:-1]]
74
+
75
+ df_filtered['irating_bin'] = pd.cut(df_filtered['IRATING'], bins=bins, labels=labels, right=False)
76
+
77
+ # --- 3. Calcular el promedio de carreras por bin ---
78
+ trend_data = df_filtered.groupby('irating_bin').agg(
79
+ avg_starts=('STARTS', 'mean'),
80
+ num_pilots=('DRIVER', 'count')
81
+ ).reset_index()
82
+
83
+ # --- 4. Crear el Gráfico ---
84
+ fig = go.Figure(data=go.Scatter(
85
+ x=trend_data['irating_bin'],
86
+ y=trend_data['avg_starts'],
87
+ mode='lines+markers', # Líneas y puntos en cada dato
88
+ marker=dict(
89
+ color='rgba(0, 111, 255, 1)',
90
+ size=8,
91
+ line=dict(width=1, color='white')
92
+ ),
93
+ line=dict(color='rgba(0, 111, 255, 0.7)'),
94
+
95
+ customdata=trend_data['num_pilots'],
96
+ hovertemplate=(
97
+ "<b>iRating Range:</b> %{x}<br>" +
98
+ "<b>Average Races:</b> %{y:.0f}<br>" +
99
+ "<b>Drivers in this range:</b> %{customdata:,}<extra></extra>"
100
+ )
101
+ ))
102
+
103
+ fig.update_layout(
104
+ title=dict(
105
+ text='<b>iRating Range By Avg. Races</b>',
106
+ font=dict(color='white', size=14),
107
+ x=0.5,
108
+ xanchor='center'
109
+ ),
110
+ template='plotly_dark',
111
+ paper_bgcolor='rgba(11,11,19,1)',
112
+ plot_bgcolor='rgba(11,11,19,1)',
113
+ font=GLOBAL_FONT,
114
+ xaxis=dict(
115
+ title_text='iRating Range', # Texto del título
116
+ title_font=dict(size=12), # Estilo de la fuente del título
117
+ showgrid=True,
118
+ gridwidth=1,
119
+ gridcolor='rgba(255,255,255,0.1)'
120
+ ),
121
+ yaxis=dict(
122
+ title_text='Avg. Races', # Texto del título
123
+ title_font=dict(size=12), # Estilo de la fuente del título
124
+ showgrid=True,
125
+ gridwidth=1,
126
+ gridcolor='rgba(255,255,255,0.1)'
127
+ ),
128
+ margin=dict(l=10, r=10, t=50, b=10),
129
+ )
130
+ return fig
131
+
132
+ def calculate_competitiveness(df):
133
+ """
134
+ Calcula el iRating promedio de los 100 mejores pilotos para cada región y país.
135
+ Descarta grupos con menos de 100 pilotos.
136
+ """
137
+ # --- Cálculo para Regiones ---
138
+ # Filtramos regiones con al menos 100 pilotos
139
+ region_counts = df['REGION'].value_counts()
140
+ valid_regions = region_counts[region_counts >= 100].index
141
+
142
+ region_scores = {}
143
+ for region in valid_regions:
144
+ # Tomamos el top 100 por iRating y calculamos el promedio
145
+ top_100 = df[df['REGION'] == region].nlargest(100, 'IRATING')
146
+ region_scores[region] = top_100['IRATING'].mean()
147
+
148
+ # Convertimos a DataFrame, ordenamos y tomamos el top 10
149
+ top_regions_df = pd.DataFrame(list(region_scores.items()), columns=['REGION', 'avg_irating'])
150
+ top_regions_df = top_regions_df.sort_values('avg_irating', ascending=False)
151
+
152
+ # --- Cálculo para Países ---
153
+ # Mismo proceso para países
154
+ country_counts = df['LOCATION'].value_counts()
155
+ valid_countries = country_counts[country_counts >= 100].index
156
+
157
+ country_scores = {}
158
+ for country in valid_countries:
159
+ top_100 = df[df['LOCATION'] == country].nlargest(100, 'IRATING')
160
+ country_scores[country] = top_100['IRATING'].mean()
161
+
162
+ top_countries_df = pd.DataFrame(list(country_scores.items()), columns=['LOCATION', 'avg_irating'])
163
+ top_countries_df = top_countries_df.sort_values('avg_irating', ascending=False)
164
+
165
+ return top_regions_df, top_countries_df
166
+
167
+ def create_region_bubble_chart(df):
168
+
169
+ df = df[df['REGION'] != 'Atlantic']
170
+ region_stats = df.groupby('REGION').agg(
171
+ avg_starts=('STARTS', 'mean'),
172
+ avg_irating=('IRATING', 'mean'),
173
+ num_pilots=('DRIVER', 'count')
174
+ ).reset_index()
175
+
176
+ # --- ¡CLAVE PARA EL ORDEN! ---
177
+ # Al ordenar de menor a mayor, Plotly dibuja las burbujas pequeñas al final,
178
+ # asegurando que queden por encima de las grandes. ¡Esto ya está correcto!
179
+ region_stats = region_stats.sort_values('num_pilots', ascending=True)
180
+ hover_text_pilots = (region_stats['num_pilots'] / 1000).round(1).astype(str) + 'k'
181
+
182
+ fig = go.Figure()
183
+ fig.add_trace(go.Scatter(
184
+ x=region_stats['avg_irating'],
185
+ y=region_stats['avg_starts'],
186
+ mode='markers+text',
187
+ text=region_stats['REGION'],
188
+ textposition='top center',
189
+ # --- MODIFICACIÓN: Añadimos fondo al texto ---
190
+ textfont=dict(
191
+ size=8,
192
+ color='rgba(255, 255, 255, 0.9)',
193
+ family='Lato, sans-serif'
194
+ ),
195
+ # --- FIN DE LA MODIFICACIÓN ---
196
+
197
+ marker=dict(
198
+ size=region_stats['num_pilots'],
199
+ sizemode='area',
200
+ sizeref=2.*max(region_stats['num_pilots'])/(50.**2.3),
201
+ sizemin=6,
202
+ # --- MODIFICACIÓN: Coloreamos por número de pilotos ---
203
+ color=region_stats['num_pilots'],
204
+ # Usamos la misma escala de colores que el mapa para coherencia
205
+ colorscale=[
206
+ [0.0, '#050A28'],
207
+ [0.05, '#0A1950'],
208
+ [0.15, '#0050B4'],
209
+ [0.3, '#006FFF'],
210
+ [0.5, '#3C96FF'],
211
+ [0.7, '#82BEFF'],
212
+ [1.0, '#DCEBFF']
213
+ ],
214
+ cmin=0,
215
+ cmax=20000,
216
+ showscale=False
217
+
218
+ # --- FIN DE LA MODIFICACIÓN ---
219
+ ),
220
+ customdata=np.stack((hover_text_pilots, region_stats['num_pilots']), axis=-1),
221
+ hovertemplate=(
222
+ "<b>%{text}</b><br>" +
223
+ "Avg. iRating: %{x:.0f}<br>" +
224
+ "Avg. Races: %{y:.1f}<br>" +
225
+ "Driver Qty: %{customdata[0]} (%{customdata[1]:,})<extra></extra>"
226
+ )
227
+ ))
228
+
229
+ fig.update_layout(
230
+ title=dict(
231
+ text='<b>Regions (Avg. iRating, Avg. Races, Qty. Drivers)</b>',
232
+ font=dict(color='white', size=14),
233
+ x=0.5,
234
+ xanchor='center'
235
+ ),
236
+ font=GLOBAL_FONT,
237
+ #xaxis_title='Avg. iRating',
238
+ #yaxis_title='Avg. Races',
239
+ template='plotly_dark',
240
+
241
+ paper_bgcolor='rgba(11,11,19,1)',
242
+
243
+ plot_bgcolor='rgba(11,11,19,1)',
244
+ # --- AÑADIMOS ESTILO DE GRID IGUAL AL HISTOGRAMA ---
245
+ xaxis=dict(
246
+ title_text='Avg. iRating', # Texto del título
247
+ title_font=dict(size=12), # Estilo de la fuente del título
248
+ showgrid=True,
249
+ gridwidth=1,
250
+ gridcolor='rgba(255,255,255,0.1)'
251
+ ),
252
+ yaxis=dict(
253
+ title_text='Avg. Races', # Texto del título
254
+ title_font=dict(size=12), # Estilo de la fuente del título
255
+ showgrid=True,
256
+ gridwidth=1,
257
+ gridcolor='rgba(255,255,255,0.1)'
258
+ ),
259
+ # --- FIN DEL ESTILO DE GRID ---
260
+ margin=dict(l=10, r=10, t=50, b=10),
261
+ )
262
+ return fig
263
+
264
+ def create_kpi_global(filtered_df, filter_context="World"):
265
+ total_pilots = len(filtered_df)
266
+ avg_irating = filtered_df['IRATING'].mean() if total_pilots > 0 else 0
267
+ avg_starts = filtered_df['STARTS'].mean() if total_pilots > 0 else 0
268
+ avg_wins = filtered_df['WINS'].mean() if total_pilots > 0 else 0
269
+
270
+ fig = go.Figure()
271
+ kpis = [
272
+ {'value': total_pilots, 'title': f"Drivers {filter_context}", 'format': ',.0f'},
273
+ {'value': avg_irating, 'title': "Average iRating", 'format': ',.0f'},
274
+ {'value': avg_starts, 'title': "Average Starts", 'format': '.1f'},
275
+ {'value': avg_wins, 'title': "Average Wins", 'format': '.2f'}
276
+ ]
277
+ for i, kpi in enumerate(kpis):
278
+ fig.add_trace(go.Indicator(
279
+ mode="number",
280
+ value=kpi['value'],
281
+ number={'valueformat': kpi['format'], 'font': {'size': 20}},
282
+ # --- MODIFICACIÓN: Añadimos <b> para poner el texto en negrita ---
283
+ title={"text": f"<b>{kpi['title']}</b>", 'font': {'size': 16}},
284
+ domain={'row': 0, 'column': i}
285
+ ))
286
+ fig.update_layout(
287
+ grid={'rows': 1, 'columns': 4, 'pattern': "independent"},
288
+ template='plotly_dark',
289
+ paper_bgcolor='rgba(0,0,0,0)',
290
+ plot_bgcolor='#323232',
291
+ margin=dict(l=20, r=20, t=50, b=10),
292
+ height=60,
293
+ font=GLOBAL_FONT
294
+ )
295
+ return fig
296
+
297
+ def create_kpi_pilot(filtered_df, pilot_info=None, filter_context="World"):
298
+ fig = go.Figure()
299
+
300
+ title_text = "Select a Driver"
301
+
302
+ # Si NO hay información del piloto, creamos una figura vacía y ocultamos los ejes.
303
+ if pilot_info is None:
304
+ fig.update_layout(
305
+ paper_bgcolor='rgba(0,0,0,0)',
306
+ plot_bgcolor='rgba(0,0,0,0)',
307
+ xaxis_visible=False,
308
+ yaxis_visible=False,
309
+ height=50,
310
+ annotations=[
311
+ dict(
312
+ text="<b>Select or search a driver</b>",
313
+ xref="paper",
314
+ yref="paper",
315
+ x=0.5,
316
+ y=0.5,
317
+ showarrow=False,
318
+ font=dict(
319
+ size=12,
320
+ color="grey"
321
+ )
322
+ )
323
+ ]
324
+ )
325
+ return fig
326
+ # --- FIN DE LA
327
+
328
+ # Si SÍ hay información del piloto, procedemos como antes.
329
+ pilot_name = pilot_info.get('DRIVER', 'Piloto')
330
+ title_text = f"<b>{pilot_name}</b>"
331
+
332
+ rank_world = pilot_info.get('Rank World', 0)
333
+ rank_region = pilot_info.get('Rank Region', 0)
334
+ rank_country = pilot_info.get('Rank Country', 0)
335
+ percentil_world = (1 - (rank_world / len(df))) * 100 if len(df) > 0 else 0
336
+ region_df = df[df['REGION'] == pilot_info.get('REGION')]
337
+ percentil_region = (1 - (rank_region / len(region_df))) * 100 if len(region_df) > 0 else 0
338
+ country_df = df[df['LOCATION'] == pilot_info.get('LOCATION')]
339
+ percentil_country = (1 - (rank_country / len(country_df))) * 100 if len(country_df) > 0 else 0
340
+
341
+ kpis_piloto = [
342
+ {'rank': rank_world, 'percentil': percentil_world, 'title': "World Rank"},
343
+ {'rank': rank_region, 'percentil': percentil_region, 'title': "Region Rank "},
344
+ {'rank': rank_country, 'percentil': percentil_country, 'title': "Country Rank"}
345
+ ]
346
+
347
+ for i, kpi in enumerate(kpis_piloto):
348
+ fig.add_trace(go.Indicator(
349
+ mode="number",
350
+ value=kpi['rank'],
351
+ number={'prefix': "#", 'font': {'size': 12}},
352
+ # Eliminamos el <br> y ajustamos el texto para que esté en una línea
353
+ title={"text": f"{kpi['title']} <span style='font-size:12px;color:gray'>(Top {100-kpi['percentil']:.2f}%)</span>", 'font': {'size': 12}},
354
+ domain={'row': 0, 'column': i}
355
+ ))
356
+ fig.update_layout(
357
+ title={
358
+ 'text': title_text,
359
+ 'y':0.95, 'x':0.5, 'xanchor': 'center', 'yanchor': 'top',
360
+ 'font': {'size': 14}
361
+ },
362
+ grid={'rows': 1, 'columns': 3, 'pattern': "independent"},
363
+ template='plotly_dark',
364
+ paper_bgcolor='rgba(0,0,0,0)',
365
+ plot_bgcolor='rgba(0,0,0,0)',
366
+ margin=dict(l=20, r=20, t=40, b=10),
367
+ height=60,
368
+ font=GLOBAL_FONT
369
+ )
370
+ return fig
371
+
372
+ def create_density_heatmap(df):
373
+ # --- 1. Preparación de datos ---
374
+ num_bins_tendencia = 50
375
+ df_copy = df.copy()
376
+ df_copy['irating_bin'] = pd.cut(df_copy['IRATING'], bins=num_bins_tendencia)
377
+ # MODIFICACIÓN: Añadimos 'mean' al cálculo de agregación
378
+ stats_per_bin = df_copy.groupby('irating_bin')['AVG_INC'].agg(['max', 'min', 'mean']).reset_index()
379
+ stats_per_bin['irating_mid'] = stats_per_bin['irating_bin'].apply(lambda b: b.mid)
380
+ stats_per_bin = stats_per_bin.sort_values('irating_mid').dropna()
381
+
382
+ # --- 2. CÁLCULO DE LA REGRESIÓN LINEAL ---
383
+ # Coeficientes para máximos y mínimos (sin cambios)
384
+ max_coeffs = np.polyfit(stats_per_bin['irating_mid'], stats_per_bin['max'], 1)
385
+ max_line_func = np.poly1d(max_coeffs)
386
+ min_coeffs = np.polyfit(stats_per_bin['irating_mid'], stats_per_bin['min'], 1)
387
+ min_line_func = np.poly1d(min_coeffs)
388
+
389
+ # NUEVO: Coeficientes para la línea de promedios
390
+ mean_coeffs = np.polyfit(stats_per_bin['irating_mid'], stats_per_bin['mean'], 1)
391
+ mean_line_func = np.poly1d(mean_coeffs)
392
+
393
+ # Generamos los puntos Y para las líneas rectas
394
+ x_trend = stats_per_bin['irating_mid']
395
+ y_trend_max = max_line_func(x_trend)
396
+ y_trend_min = min_line_func(x_trend)
397
+ y_trend_mean = mean_line_func(x_trend) # NUEVO
398
+
399
+ # --- 3. Creación de las trazas del gráfico ---
400
+ heatmap_trace = go.Histogram2d(
401
+ x=df['IRATING'],
402
+ y=df['AVG_INC'],
403
+ colorscale='Plasma',
404
+ nbinsx=100, nbinsy=100, zmin=0, zmax=50,
405
+ name='Densidad'
406
+ )
407
+
408
+ max_line_trace = go.Scatter(
409
+ x=x_trend, y=y_trend_max, mode='lines',
410
+ name='Tendencia Máximo AVG_INC',
411
+ line=dict(color='red', width=1, dash='dash')
412
+ )
413
+
414
+ min_line_trace = go.Scatter(
415
+ x=x_trend, y=y_trend_min, mode='lines',
416
+ name='Tendencia Mínimo AVG_INC',
417
+ line=dict(color='lime', width=1, dash='dash')
418
+ )
419
+
420
+ # NUEVO: Traza para la línea de promedio
421
+ mean_line_trace = go.Scatter(
422
+ x=x_trend,
423
+ y=y_trend_mean,
424
+ mode='lines',
425
+ name='Tendencia Promedio AVG_INC',
426
+ line=dict(color='black', width=2, dash='solid')
427
+ )
428
+
429
+ # --- 4. Combinación de las trazas en una sola figura ---
430
+ # MODIFICACIÓN: Añadimos la nueva traza a la lista de datos
431
+ fig = go.Figure(data=[heatmap_trace, max_line_trace, min_line_trace, mean_line_trace])
432
+
433
+ fig.update_layout(
434
+ title='Densidad de Pilotos: iRating vs. AVG_INC',
435
+ xaxis_title='iRating',
436
+ yaxis_title='Incidents Per Race',
437
+ template='plotly_dark',
438
+ paper_bgcolor='rgba(0,0,0,0)',
439
+ plot_bgcolor='rgba(0,0,0,0)',
440
+ xaxis=dict(range=[0, 12000], showgrid=True, gridwidth=1, gridcolor='rgba(255,255,255,0.1)'),
441
+ yaxis=dict(range=[0,25], showgrid=True, gridwidth=1, gridcolor='rgba(255,255,255,0.1)'),
442
+ legend=dict(yanchor="top", y=0.99, xanchor="right", x=0.99)
443
+ )
444
+ return fig
445
+
446
+ def country_to_continent_code(country_code):
447
+ try:
448
+ # Convierte el código de país de 2 letras a código de continente
449
+ continent_code = pc.country_alpha2_to_continent_code(country_code)
450
+ return continent_code
451
+ except (KeyError, TypeError):
452
+ # Devuelve 'Otros' si el código no se encuentra o es inválido
453
+ return 'Otros'
454
+
455
+ def create_continent_map(df, selected_region='ALL', selected_country='ALL'):
456
+ # La preparación de datos es la misma
457
+ country_counts = df['LOCATION'].value_counts().reset_index()
458
+ country_counts.columns = ['LOCATION_2_LETTER', 'PILOTOS']
459
+
460
+ def alpha2_to_alpha3(code):
461
+ try:
462
+ return pycountry.countries.get(alpha_2=code).alpha_3
463
+ except (LookupError, AttributeError):
464
+ return None
465
+
466
+ country_counts['LOCATION_3_LETTER'] = country_counts['LOCATION_2_LETTER'].apply(alpha2_to_alpha3)
467
+ country_counts.dropna(subset=['LOCATION_3_LETTER'], inplace=True)
468
+
469
+ # Lógica de coloreado y hover avanzada (sin cambios)
470
+ show_scale = True
471
+ color_column = 'PILOTOS'
472
+ color_scale = [
473
+ [0.0, '#050A28'], # 1. Azul casi negro
474
+ [0.05, '#0A1950'], # 2. Azul marino oscuro
475
+ [0.15, '#0050B4'], # 3. Azul estándar
476
+ [0.3, '#006FFF'], # 4. Azul Eléctrico (punto focal)
477
+ [0.5, '#3C96FF'], # 5. Azul brillante
478
+ [0.7, '#82BEFF'], # 6. Azul claro (cielo)
479
+ [1.0, '#DCEBFF'] # 7. Resplandor azulado (casi blanco)
480
+ ]
481
+ range_color_val = [0, 20000]
482
+
483
+ # Creación del mapa base
484
+ fig = px.choropleth(
485
+ country_counts,
486
+ locations="LOCATION_3_LETTER",
487
+ locationmode="ISO-3",
488
+ color=color_column,
489
+ # --- CORRECCIÓN CLAVE AQUÍ ---
490
+ # Ya no usamos hover_name, pasamos todo a custom_data
491
+ custom_data=['LOCATION_2_LETTER', 'PILOTOS'],
492
+ color_continuous_scale=color_scale,
493
+ projection="natural earth",
494
+ range_color=range_color_val
495
+ )
496
+
497
+ # Actualizamos la plantilla del hover para usar las variables correctas de custom_data
498
+ fig.update_traces(
499
+ hovertemplate="<b>%{customdata[0]}</b><br>Drivers: %{customdata[1]}<extra></extra>"
500
+ )
501
+
502
+ # Lógica de zoom dinámico (sin cambios)
503
+ if selected_country != 'ALL' and selected_country in country_coords:
504
+ zoom_level = 4 if selected_country not in ['US', 'CA', 'AU', 'BR', 'AR'] else 3
505
+ fig.update_geos(center=country_coords[selected_country], projection_scale=zoom_level)
506
+ elif selected_region != 'ALL':
507
+ countries_in_region = iracing_ragions.get(selected_region, [])
508
+ lats = [country_coords[c]['lat'] for c in countries_in_region if c in country_coords]
509
+ lons = [country_coords[c]['lon'] for c in countries_in_region if c in country_coords]
510
+ if lats and lons:
511
+ center_lat = sum(lats) / len(lats)
512
+ center_lon = sum(lons) / len(lons)
513
+ zoom_level = 2
514
+ if len(countries_in_region) < 5: zoom_level = 4
515
+ elif len(countries_in_region) < 15: zoom_level = 3
516
+ fig.update_geos(center={'lat': center_lat, 'lon': center_lon}, projection_scale=zoom_level)
517
+ else:
518
+ fig.update_geos(center={'lat': 20, 'lon': 0}, projection_scale=1)
519
+ else:
520
+ fig.update_geos(center={'lat': 20, 'lon': 0}, projection_scale=1)
521
+
522
+ fig.update_layout(
523
+ template='plotly_dark',
524
+ # --- BLOQUE MODIFICADO ---
525
+ paper_bgcolor='rgba(0,0,0,0)', # Fondo de toda la figura (transparente)
526
+ plot_bgcolor='rgba(0,0,0,0)', # Fondo del área del mapa (transparente)
527
+ # --- FIN DE LA MODIFICACIÓN ---
528
+ geo=dict(
529
+ bgcolor='rgba(0,0,0,0)', # Fondo específico del globo (transparente)
530
+ lakecolor='#4E5D6C',
531
+ landcolor='#323232',
532
+ subunitcolor='rgba(0,0,0,0)',
533
+ showframe=False, # <-- Oculta el marco exterior del globo
534
+ showcoastlines=False # <-- Oculta las líneas de la costa
535
+ ),
536
+ margin={"r":0,"t":40,"l":0,"b":0},
537
+ coloraxis_showscale=show_scale,
538
+ coloraxis_colorbar=dict(
539
+ title='Drivers',
540
+ orientation='h',
541
+ yanchor='bottom',
542
+ y=-0.05,
543
+ xanchor='center',
544
+ x=0.5,
545
+ len=0.5,
546
+ thickness=10
547
+ )
548
+ )
549
+ return fig
550
+
551
+ def create_histogram_with_percentiles(df, column='IRATING', bin_width=100, highlight_irating=None, highlight_name=None):
552
+ # Crear bins específicos de 100 en 100
553
+ min_val = df[column].min()
554
+ max_val = df[column].max()
555
+ bin_edges = np.arange(min_val, max_val + bin_width, bin_width)
556
+
557
+ hist, bin_edges = np.histogram(df[column], bins=bin_edges)
558
+ bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
559
+ bin_widths = bin_edges[1:] - bin_edges[:-1]
560
+ total = len(df)
561
+ hover_text = []
562
+
563
+ # Transformación personalizada para hacer más visibles los valores pequeños
564
+ hist_transformed = []
565
+ for i in range(len(hist)):
566
+ # Percentil: % de pilotos con menos iRating que el límite inferior del bin superior
567
+ below = (df[column] < bin_edges[i+1]).sum()
568
+ percentile = below / total * 100
569
+ top_percent = 100 - percentile
570
+ hover_text.append(
571
+ f"Range: {int(bin_edges[i])}-{int(bin_edges[i+1])}<br>"
572
+ f"Drivers: {hist[i]}<br>"
573
+ f"Top: {top_percent:.2f}%"
574
+ )
575
+
576
+ # Transformación: valores pequeños (0-50) los amplificamos
577
+ if hist[i] <= 50 and hist[i] > 0:
578
+ hist_transformed.append(hist[i] + 2)
579
+ else:
580
+ hist_transformed.append(hist[i])
581
+
582
+ fig = go.Figure(data=go.Bar(
583
+ x=bin_centers,
584
+ y=hist_transformed, # <-- Usamos los valores transformados para visualización
585
+ width=bin_widths * 1,
586
+ hovertext=hover_text, # <-- Pero en el hover mostramos los valores reales
587
+ hovertemplate='%{hovertext}<extra></extra>',
588
+ marker=dict(color=hist, colorscale=[
589
+ [0.0, "#FFFFFF"], # Empieza con un azul claro (LightSkyBlue)
590
+ [0.1, "#A0B8EC"],
591
+ [0.3, "#668CDF"], # Pasa por un cian muy pálido (LightCyan)
592
+ [1.0, "rgba(0,111,255,1)"] # Termina en blanco
593
+ ])
594
+ ))
595
+
596
+ # --- NUEVO: Lógica para añadir la línea de señalización ---
597
+ if highlight_irating is not None and highlight_name is not None:
598
+ fig.add_vline(
599
+ x=highlight_irating,
600
+ line_width=.5,
601
+ line_dash="dot",
602
+ annotation_position="top left", # Mejor posición para texto vertical
603
+ annotation_textangle=-90, # <-- AÑADE ESTA LÍNEA PARA ROTAR EL TEXTO
604
+ line_color="white",
605
+ annotation_text=f"<b>{highlight_name}</b>",
606
+ annotation_font_size=10,
607
+ annotation_font_color="white"
608
+ )
609
+ # --- FIN DEL BLOQUE NUEVO ---
610
+
611
+ fig.update_layout(
612
+ title=dict(
613
+ text='<b>iRating Histogram</b>',
614
+ font=dict(color='white', size=14),
615
+ x=0.5,
616
+ xanchor='center'
617
+ ),
618
+ font=GLOBAL_FONT,
619
+ xaxis=dict(
620
+ title_text='iRating', # Texto del título
621
+ title_font=dict(size=12), # Estilo de la fuente del título
622
+ showgrid=True,
623
+ gridwidth=1,
624
+ gridcolor='rgba(255,255,255,0.1)'
625
+ ),
626
+ yaxis=dict(
627
+ title_text='Qty. Drivers', # Texto del título
628
+ title_font=dict(size=12), # Estilo de la fuente del título
629
+ showgrid=True,
630
+ gridwidth=1,
631
+ gridcolor='rgba(255,255,255,0.1)'
632
+ ),
633
+ template='plotly_dark',
634
+ hovermode='x unified',
635
+ paper_bgcolor='rgba(18,18,26,.5)',
636
+ plot_bgcolor='rgba(255,255,255,0)',
637
+
638
+
639
+ # --- MODIFICACIÓN: Reducir márgenes y tamaño de fuentes ---
640
+ margin=dict(l=10, r=10, t=50, b=10) # Reduce los márgenes (izquierda, derecha, arriba, abajo) # Reduce el tamaño del título principal
641
+
642
+ )
643
+
644
+ return fig
645
+
646
+ def create_correlation_heatmap(df):
647
+ # Seleccionar solo columnas numéricas para la correlación
648
+ numeric_df = df.select_dtypes(include=np.number)
649
+ corr_matrix = numeric_df.corr()
650
+
651
+ fig = go.Figure(data=go.Heatmap(
652
+ z=corr_matrix.values,
653
+ x=corr_matrix.columns,
654
+ y=corr_matrix.columns,
655
+ colorscale='RdBu_r', # Rojo-Azul invertido (Rojo=positivo, Azul=negativo)
656
+ zmin=-1, zmax=1,
657
+ text=corr_matrix.values,
658
+ texttemplate="%{text:.2f}",
659
+ textfont={"size":12}
660
+ ))
661
+
662
+ fig.update_layout(
663
+ title='🔁 Correlation between Variables',
664
+ template='plotly_dark',
665
+ margin=dict(l=40, r=20, t=40, b=20)
666
+ )
667
+ return fig
668
+
669
+ def flag_img(code):
670
+ url = f"https://flagcdn.com/16x12/{code.lower()}.png"
671
+ # La función ahora asume que si el código llega aquí, es válido.
672
+ # La comprobación se hará una sola vez al crear el diccionario.
673
+ return f'![{code}]({url})'
674
+
675
+ GLOBAL_FONT = {'family': "Lato, sans-serif"}
676
+
677
+ DISCIPLINE_DATAFRAMES = {
678
+ 'ROAD.csv': load_and_process_data('ROAD.csv'),
679
+ 'FORMULA.csv': load_and_process_data('FORMULA.csv'),
680
+ 'OVAL.csv': load_and_process_data('OVAL.csv'),
681
+ 'DROAD.csv': load_and_process_data('DROAD.csv'),
682
+ 'DOVAL.csv': load_and_process_data('DOVAL.csv')
683
+ }
684
+
685
+ country_coords = {
686
+ 'ES': {'lat': 40.4, 'lon': -3.7}, 'US': {'lat': 39.8, 'lon': -98.5},
687
+ 'BR': {'lat': -14.2, 'lon': -51.9}, 'DE': {'lat': 51.1, 'lon': 10.4},
688
+ 'FR': {'lat': 46.2, 'lon': 2.2}, 'IT': {'lat': 41.8, 'lon': 12.5},
689
+ 'GB': {'lat': 55.3, 'lon': -3.4}, 'PT': {'lat': 39.3, 'lon': -8.2},
690
+ 'NL': {'lat': 52.1, 'lon': 5.2}, 'AU': {'lat': -25.2, 'lon': 133.7},
691
+ 'JP': {'lat': 36.2, 'lon': 138.2}, 'CA': {'lat': 56.1, 'lon': -106.3},
692
+ 'AR': {'lat': -38.4, 'lon': -63.6}, 'MX': {'lat': 23.6, 'lon': -102.5},
693
+ 'CL': {'lat': -35.6, 'lon': -71.5}, 'BE': {'lat': 50.5, 'lon': 4.4},
694
+ 'FI': {'lat': 61.9, 'lon': 25.7}, 'SE': {'lat': 60.1, 'lon': 18.6},
695
+ 'NO': {'lat': 60.4, 'lon': 8.4}, 'DK': {'lat': 56.2, 'lon': 9.5},
696
+ 'IE': {'lat': 53.4, 'lon': -8.2}, 'CH': {'lat': 46.8, 'lon': 8.2},
697
+ 'AT': {'lat': 47.5, 'lon': 14.5}, 'PL': {'lat': 51.9, 'lon': 19.1},
698
+ }
699
+
700
+
701
+
702
+ # --- 1. Carga y Preparación de Datos ---
703
+ df = DISCIPLINE_DATAFRAMES['ROAD.csv']
704
+ df = df[df['IRATING'] > 1]
705
+ df = df[df['STARTS'] > 1]
706
+ #df = df[df['STARTS'] < 2000]
707
+ df = df[df['CLASS'].str.contains('D|C|B|A|P|R', na=False)]
708
+
709
+ # --- AÑADE ESTE BLOQUE PARA CREAR LA COLUMNA 'REGION' ---
710
+ # 1. Invertir el diccionario iracing_ragions para un mapeo rápido
711
+ country_to_region_map = {country: region
712
+ for region, countries in iracing_ragions.items()
713
+ for country in countries}
714
+
715
+ # 2. Crear la nueva columna 'REGION' usando el mapa
716
+ df['REGION'] = df['LOCATION'].map(country_to_region_map)
717
+
718
+ # 3. Rellenar los países no encontrados con un valor por defecto
719
+ df['REGION'].fillna('International', inplace=True)
720
+ # --- FIN DEL BLOQUE AÑADIDO ---
721
+
722
+ # --- AÑADE ESTE BLOQUE PARA CALCULAR LOS RANKINGS ---
723
+ # --- CORRECCIÓN: Cambiamos method='dense' por method='first' para rankings únicos ---
724
+ df['Rank World'] = df['IRATING'].rank(method='first', ascending=False).fillna(0).astype(int)
725
+ df['Rank Region'] = df.groupby('REGION')['IRATING'].rank(method='first', ascending=False).fillna(0).astype(int)
726
+ df['Rank Country'] = df.groupby('LOCATION')['IRATING'].rank(method='first', ascending=False).fillna(0).astype(int)
727
+ # --- FIN DEL BLOQUE AÑADIDO ---
728
+
729
+ df['CONTINENT'] = df['LOCATION'].apply(country_to_continent_code)
730
+
731
+ df['CLASS'] = df['CLASS'].str[0]
732
+
733
+ #df = df[df['IRATING'] < 10000]
734
+
735
+ # --- MODIFICACIÓN: Definimos las columnas que queremos en la tabla ---
736
+ # Usaremos esta lista más adelante para construir el df_table dinámicamente
737
+ TABLE_COLUMNS = ['DRIVER', 'IRATING', 'LOCATION', 'Rank World', 'Rank Region', 'Rank Country']
738
+ df_table = df[TABLE_COLUMNS]
739
+
740
+ df_for_graphs = df.copy() # Usamos una copia completa para los gráficos
741
+
742
+ # --- MODIFICACIÓN: Nos aseguramos de que el df principal tenga todas las columnas necesarias ---
743
+ df = df[['DRIVER','IRATING','LOCATION','STARTS','WINS','AVG_START_POS','AVG_FINISH_POS','AVG_INC','TOP25PCNT', 'REGION', 'Rank World', 'Rank Region', 'Rank Country','CLASS']]
744
+
745
+ # Aplica solo el emoji/código si está in country_flags, si no deja el valor original
746
+ # OJO: Esta línea ahora debe ir dentro del callback, ya que las columnas cambian
747
+ # df_table['LOCATION'] = df_table['LOCATION'].map(lambda x: flag_img(x) if x in country_flags else x)
748
+
749
+ #df['LOCATION'] = 'a'
750
+ density_heatmap = dcc.Graph(
751
+ id='density-heatmap',
752
+ style={'height': '30vh', 'borderRadius': '15px', 'overflow': 'hidden'},
753
+ figure=create_density_heatmap(df_for_graphs)
754
+ )
755
+ correlation_heatmap = dcc.Graph(
756
+ id='correlation-heatmap',
757
+ style={'height': '70vh'}, # Ajusta la altura para que quepan los 3 gráficos
758
+ # Usamos las columnas numéricas del dataframe original
759
+ figure=create_correlation_heatmap(df[['IRATING', 'STARTS', 'WINS','TOP25PCNT','AVG_INC','AVG_FINISH_POS']])
760
+ )
761
+
762
+
763
+ kpi_global = dcc.Graph(id='kpi-global', style={'height': '6vh', 'marginBottom': '0px', 'marginTop': '20px'})
764
+ kpi_pilot = dcc.Graph(id='kpi-pilot', style={'height': '3hv', 'marginBottom': '10px'})
765
+
766
+ histogram_irating = dcc.Graph(
767
+ id='histogram-plot',
768
+ # --- MODIFICACIÓN: Ajustamos el estilo del contenedor del gráfico ---
769
+ style={
770
+ 'height': '26vh',
771
+ 'borderRadius': '10px', # Coincide con el radio de los filtros
772
+ 'border': '1px solid #4A4A4A', # Coincide con el borde de los filtros
773
+ 'overflow': 'hidden'
774
+ },
775
+ # --- FIN DE LA MODIFICACIÓN ---
776
+ figure=create_histogram_with_percentiles(df, 'IRATING', 100) # 100 = ancho de cada bin
777
+ )
778
+
779
+ # --- MODIFICACIÓN: Simplificamos la definición inicial de la tabla ---
780
+ # Las columnas se generarán dinámicamente en el callback
781
+ interactive_table = dash_table.DataTable(
782
+ id='datatable-interactiva',
783
+ # columns se define en el callback
784
+ data=[], # Inicialmente vacía
785
+ sort_action="custom",
786
+ sort_mode="single",
787
+ page_action="custom",
788
+ page_current=0,
789
+ page_size=20,
790
+ page_count=len(df_table) // 20 + (1 if len(df_table) % 20 > 0 else 0),
791
+ virtualization=False,
792
+ style_as_list_view=True,
793
+ active_cell={'row': 21,'column':1},
794
+
795
+
796
+ # --- ELIMINAMOS selected_rows Y AÑADIMOS active_cell ---
797
+ # selected_rows=[], # <-- ELIMINAR ESTA LÍNEA
798
+
799
+ style_table={
800
+ #'tableLayout': 'fixed', # <-- DESCOMENTA O AÑADE ESTA LÍNEA
801
+ 'overflowX': 'auto',
802
+ 'height': '70vh',
803
+ 'minHeight': '0',
804
+ 'width': '100%',
805
+ 'borderRadius': '15px',
806
+ 'overflow': 'hidden',
807
+ 'backgroundColor': 'rgba(11,11,19,1)',
808
+ 'textOverflow': 'ellipsis',
809
+ 'border': '1px solid #4A4A4A'
810
+
811
+ },
812
+
813
+ style_cell={
814
+ 'textAlign': 'center',
815
+ 'padding': '1px',
816
+ 'backgroundColor': 'rgba(11,11,19,1)',
817
+ 'color': 'rgb(255, 255, 255,.8)',
818
+ 'border': '1px solid rgba(255, 255, 255, 0)',
819
+ 'overflow': 'hidden',
820
+ 'textOverflow': 'ellipsis',
821
+ 'whiteSpace': 'nowrap', # <-- AÑADE ESTA LÍNEA
822
+ 'maxWidth': 0
823
+ },
824
+ style_header={
825
+ 'backgroundColor': 'rgba(30,30,38,1)',
826
+ 'fontWeight': 'bold',
827
+ 'color': 'white',
828
+ 'border': 'none',
829
+ 'textAlign': 'center',
830
+ 'fontSize': 10
831
+ },
832
+ # --- AÑADIMOS ESTILO PARA LA FILA SELECCIONADA Y LAS CLASES ---
833
+ style_data_conditional=[
834
+ {
835
+ 'if': {'state': 'active'},
836
+ 'backgroundColor': 'rgba(0, 111, 255, 0.3)',
837
+ 'border': '1px solid rgba(0, 111, 255)'
838
+ },
839
+ {
840
+ 'if': {'state': 'selected'},
841
+ 'backgroundColor': 'rgba(0, 111, 255, 0)',
842
+ 'border': '1px solid rgba(0, 111, 255,0)'
843
+ },
844
+ # --- REGLAS MEJORADAS CON BORDES REDONDEADOS ---
845
+ {'if': {'filter_query': '{CLASS} contains "P"','column_id': 'CLASS'},
846
+ 'backgroundColor': 'rgba(54,54,62,255)', 'color': 'rgba(166,167,171,255)', 'fontWeight': 'bold','border': '1px solid rgba(134,134,142,255)'},
847
+
848
+ {'if': {'filter_query': '{CLASS} contains "A"','column_id': 'CLASS'},
849
+ 'backgroundColor': 'rgba(0,42,102,255)', 'color': 'rgba(107,163,238,255)', 'fontWeight': 'bold','border': '1px solid rgba(35,104,195,255)'},
850
+
851
+ {'if': {'filter_query': '{CLASS} contains "B"','column_id': 'CLASS'},
852
+ 'backgroundColor': 'rgba(24,84,14,255)', 'color': 'rgba(139,224,105,255)', 'fontWeight': 'bold','border': '1px solid rgba(126,228,103,255)'},
853
+
854
+ {'if': {'filter_query': '{CLASS} contains "C"','column_id': 'CLASS'},
855
+ 'backgroundColor': 'rgba(81,67,6,255)', 'color': 'rgba(224,204,109,255)', 'fontWeight': 'bold','border': '1px solid rgba(220,193,76,255)'},
856
+
857
+ {'if': {'filter_query': '{CLASS} contains "D"','column_id': 'CLASS'},
858
+ 'backgroundColor': 'rgba(102,40,3,255)', 'color': 'rgba(255,165,105,255)', 'fontWeight': 'bold','border': '1px solid rgba(208,113,55,255)'},
859
+
860
+ {'if': {'filter_query': '{CLASS} contains "R"','column_id': 'CLASS'},
861
+ 'backgroundColor': 'rgba(91,19,20,255)', 'color': 'rgba(225,125,123,255)', 'fontWeight': 'bold','border': '1px solid rgba(172,62,61,255)'},
862
+ ],
863
+ # --- MODIFICACIÓN: Forzamos el ancho de las columnas ---
864
+ style_cell_conditional=[
865
+ {'if': {'column_id': 'CLASS'}, 'width': '5%', 'minWidth': '5%', 'maxWidth': '5%'},
866
+ {'if': {'column_id': 'Rank World'}, 'width': '10%', 'minWidth': '10%', 'maxWidth': '10%'},
867
+ {'if': {'column_id': 'Rank Region'}, 'width': '10%', 'minWidth': '10%', 'maxWidth': '10%'},
868
+ {'if': {'column_id': 'Rank Country'}, 'width': '10%', 'minWidth': '10%', 'maxWidth': '10%'},
869
+ {'if': {'column_id': 'DRIVER'}, 'width': '30%', 'minWidth': '30%', 'maxWidth': '30%', 'textAlign': 'left'},
870
+ {'if': {'column_id': 'IRATING'}, 'width': '10%', 'minWidth': '10%', 'maxWidth': '10%'},
871
+ {'if': {'column_id': 'LOCATION'}, 'width': '10%', 'minWidth': '10%', 'maxWidth': '10%'},
872
+ {'if': {'column_id': 'WINS'}, 'width': '5%', 'minWidth': '5%', 'maxWidth': '5%'},
873
+ {'if': {'column_id': 'STARTS'}, 'width': '5%', 'minWidth': '5%', 'maxWidth': '5%'},
874
+ {'if': {'column_id': 'REGION'}, 'width': '15%', 'minWidth': '15%', 'maxWidth': '15%'},
875
+ ],
876
+ style_data={
877
+ 'whiteSpace':'normal',
878
+ 'textAlign': 'center',
879
+ 'fontSize': 10,},
880
+ # Renderizado virtual
881
+ )
882
+
883
+ scatter_irating_starts = dcc.Graph(
884
+ id='scatter-irating',
885
+ style={'height': '20vh','borderRadius': '15px','overflow': 'hidden'},
886
+ # Usamos go.Scattergl en lugar de px.scatter para un rendimiento masivo
887
+ figure=go.Figure(data=go.Scattergl(
888
+ x=df['IRATING'],
889
+ y=df['STARTS'],
890
+ mode='markers',
891
+ marker=dict(
892
+ color='rgba(0,111,255,.3)', # Color semitransparente
893
+ size=5,
894
+ line=dict(width=0)
895
+ ),
896
+ # Desactivamos el hover para máxima velocidad
897
+ hoverinfo='none'
898
+ )).update_layout(
899
+ title='Relación iRating vs. Carreras Iniciadas',
900
+ xaxis_title='iRating',
901
+ yaxis_title='Carreras Iniciadas (Starts)',
902
+ template='plotly_dark',
903
+ paper_bgcolor='rgba(30,30,38,1)', # Fondo de toda la figura (transparente)
904
+ plot_bgcolor='rgba(30,30,38,1)', # Fondo del área de las barras (transparente)
905
+ # --- FIN DE LAS LÍNEAS AÑADIDAS ---
906
+ xaxis=dict(showgrid=True, gridwidth=1, gridcolor='rgba(255,255,255,0.1)'),
907
+ yaxis=dict(showgrid=True, gridwidth=1, gridcolor='rgba(255,255,255,0.1)')
908
+ ),
909
+ # Hacemos el gráfico estático (no interactivo) para que sea aún más rápido
910
+ config={'staticPlot': True}
911
+ )
912
+
913
+ continent_map = dcc.Graph(
914
+ id='continent-map',
915
+ style={'height': '55vh'},
916
+ figure=create_continent_map(df_for_graphs)
917
+ )
918
+
919
+ region_bubble_chart = dcc.Graph(
920
+ id='region-bubble-chart',
921
+ style={'height': '33vh','borderRadius': '10px','border': '1px solid #4A4A4A',
922
+ 'overflow': 'hidden'},
923
+ figure=create_region_bubble_chart(df)
924
+ )
925
+
926
+ # --- 3. Inicialización de la App ---
927
+ app = dash.Dash(__name__)
928
+ server = app.server # <-- AÑADE ESTA LÍNEA
929
+
930
+ # Layout principal
931
+ app.layout = html.Div(
932
+ #style={'height': '100vh', 'display': 'flex', 'flexDirection': 'column', 'backgroundColor': '#1E1E1E'},
933
+ style={},
934
+ children=[
935
+
936
+
937
+ # --- CONTENEDOR PRINCIPAL CON 3 COLUMNAS ---
938
+ html.Div(
939
+ id='main-content-container',
940
+ #style={'display': 'flex', 'flex': 1, 'minHeight': 0, 'padding': '0 10px 10px 10px'},
941
+ style={'display': 'flex', 'padding': '0px 10px 10px 10px'},
942
+ children=[
943
+
944
+ # --- COLUMNA IZQUIERDA (FILTROS Y TABLA) ---
945
+ html.Div(
946
+ id='left-column',
947
+ style={'width': '25%', 'padding': '1%', 'display': 'flex', 'flexDirection': 'column'},
948
+ children=[
949
+ # Contenedor de Filtros
950
+ html.Div(
951
+ style={'display': 'flex', 'width': '60%', 'justifyContent': 'space-between', 'margin': '0 auto 10px auto'},
952
+ children=[
953
+ # --- MODIFICACIÓN: Centramos el texto del contenedor del filtro de Región ---
954
+ html.Div([
955
+ html.Label("Region:", style={'color': 'white', 'fontSize': 2}),
956
+ dcc.Dropdown(
957
+ id='region-filter',
958
+ options=[{'label': 'All', 'value': 'ALL'}] +
959
+ [{'label': region, 'value': region} for region in sorted(iracing_ragions.keys())],
960
+ value='ALL',
961
+ className='iracing-dropdown',
962
+ # --- AÑADIMOS ESTILO INICIAL ---
963
+
964
+ )
965
+ ], style={'flex': 1, 'marginRight': '10px', 'textAlign': 'center'}),
966
+
967
+ # --- MODIFICACIÓN: Centramos el texto del contenedor del filtro de País ---
968
+ html.Div([
969
+ html.Label("Country:", style={'color': 'white', 'fontSize': 10}),
970
+ dcc.Dropdown(
971
+ id='country-filter',
972
+ options=[{'label': 'All', 'value': 'ALL'}],
973
+ value='ALL',
974
+ className='iracing-dropdown',
975
+ # --- AÑADIMOS ESTILO INICIAL ---
976
+
977
+ )
978
+ ], style={'flex': 1, 'marginRight': '10px', 'textAlign': 'center'}),
979
+
980
+
981
+ ]
982
+ ),
983
+ # Contenedor de la Tabla
984
+ html.Div(
985
+ [
986
+ html.Label("Search Driver:", style={'color': 'white', 'fontSize': 10}),
987
+ dcc.Dropdown(
988
+ id='pilot-search-dropdown',
989
+ options=[],
990
+ placeholder='Search Driver...',
991
+ className='iracing-dropdown',
992
+ searchable=True,
993
+ clearable=True,
994
+ search_value='',
995
+ # Se elimina el estilo de aquí para aplicarlo al contenedor.
996
+ )
997
+ ],
998
+ # --- MODIFICACIÓN: Centramos el texto del contenedor de búsqueda ---
999
+ style={'width': '60%', 'marginBottom': '10px', 'margin': '0 auto 10px auto', 'color':'white', 'textAlign': 'center'}
1000
+ ),
1001
+
1002
+ html.Div(
1003
+ kpi_pilot,
1004
+ style={
1005
+ 'marginTop': '1%',
1006
+ 'marginBottom': '1%' # Pone los KPIs por delante del mapa
1007
+ }
1008
+ ),
1009
+ html.Div(interactive_table, style={'flex': 1})
1010
+ ]
1011
+ ),
1012
+
1013
+ # --- COLUMNA CENTRAL ---
1014
+ html.Div(
1015
+ id='middle-column',
1016
+ # --- MODIFICACIÓN: Añadimos position: 'relative' ---
1017
+ # Esto convierte a la columna en el contenedor de referencia para el posicionamiento absoluto.
1018
+ style={'width': '45%', 'padding': '1%', 'display': 'flex', 'flexDirection': 'column', 'position': 'relative'},
1019
+ children=[
1020
+ html.Div(
1021
+ style={'textAlign': 'center'},
1022
+ children=[
1023
+ html.H1("Top iRating", style={'fontSize': 48, 'color': 'white', 'margin': '-10px 0 10px 0'}),
1024
+ html.Div([
1025
+ # <-- AÑADIDO
1026
+ html.Button('Sports Car', id='btn-road', n_clicks=0, className='dashboard-type-button'),
1027
+ html.Button('Formula', id='btn-formula', n_clicks=0, className='dashboard-type-button'),
1028
+ html.Button('Oval', id='btn-oval', n_clicks=0, className='dashboard-type-button'),
1029
+ html.Button('Dirt Road', id='btn-dirt-road', n_clicks=0, className='dashboard-type-button'),
1030
+ html.Button('Dirt Oval', id='btn-dirt-oval', n_clicks=0, className='dashboard-type-button'),
1031
+ ], style={'display': 'flex', 'justifyContent': 'center', 'gap': '10px'})
1032
+ ]
1033
+ ),
1034
+
1035
+ html.Div(
1036
+ kpi_global,
1037
+ style={
1038
+
1039
+ 'width': '70%',
1040
+ 'margin': '0 auto',
1041
+ 'position': 'relative', # Necesario para que z-index funcione
1042
+ 'z-index': '10' # Pone los KPIs por delante del mapa
1043
+ }
1044
+ ),
1045
+ html.Div(
1046
+ continent_map,
1047
+ style={
1048
+ 'flex': 1,
1049
+ 'minHeight': 0,
1050
+ 'marginTop': '-5%'
1051
+ }
1052
+ ),
1053
+ # --- MODIFICACIÓN: Se elimina el posicionamiento absoluto ---
1054
+ # El histograma ahora está en el flujo normal de la página.
1055
+ html.Div(
1056
+ histogram_irating,
1057
+ style={
1058
+ 'width':'100%',
1059
+ 'height': '26vh', # Mantenemos una altura definida
1060
+ 'marginTop': '1%' # Añadimos un margen superior para separarlo del mapa
1061
+ }
1062
+ ),
1063
+
1064
+ ]
1065
+ ),
1066
+
1067
+ # --- COLUMNA DERECHA ---
1068
+ html.Div(
1069
+ id='right-column',
1070
+ style={'width': '25%', 'padding': '1%', 'display': 'flex', 'flexDirection': 'column'},
1071
+ children=[
1072
+
1073
+ # --- MODIFICACIÓN: Contenedor vacío para las tablas de competitividad ---
1074
+ html.Div(id='competitiveness-tables-container'),
1075
+
1076
+ # --- MODIFICACIÓN: Gráfico de burbujas vacío ---
1077
+ dcc.Graph(
1078
+ id='region-bubble-chart',
1079
+ style={'height': '32vh', 'marginTop': '3.5%','borderRadius': '10px','border': '1px solid #4A4A4A', 'overflow': 'hidden'}
1080
+ ),
1081
+
1082
+ # --- MODIFICACIÓN: Gráfico de líneas vacío ---
1083
+ dcc.Graph(
1084
+ id='irating-starts-scatter',
1085
+ style={'height': '32vh', 'marginTop': '7.4%', 'borderRadius': '10px', 'border': '1px solid #4A4A4A', 'overflow': 'hidden'},
1086
+ config={'displayModeBar': False}
1087
+ )
1088
+ ]
1089
+ )
1090
+ ]
1091
+ ),
1092
+
1093
+ # Componentes ocultos
1094
+ # --- ELIMINA EL dcc.Store ---
1095
+ dcc.Store(id='active-discipline-store', data='ROAD.csv'),
1096
+ dcc.Store(id='shared-data-store', data={}),
1097
+ dcc.Store(id='shared-data-store_1', data={}),
1098
+ html.Div(id='pilot-info-display', style={'display': 'none'})
1099
+ ]
1100
+ )
1101
+
1102
+ # --- 4. Callbacks ---
1103
+
1104
+
1105
+ # --- NUEVO CALLBACK PARA ACTUALIZAR GRÁFICOS DE LA COLUMNA DERECHA ---
1106
+ @app.callback(
1107
+ Output('competitiveness-tables-container', 'children'),
1108
+ Output('region-bubble-chart', 'figure'),
1109
+ Output('irating-starts-scatter', 'figure'),
1110
+ Input('active-discipline-store', 'data')
1111
+ )
1112
+ def update_right_column_graphs(filename):
1113
+ # 1. Cargar y procesar los datos de la disciplina seleccionada
1114
+ df_discipline = pd.read_csv(filename)
1115
+ df_discipline = df_discipline[df_discipline['IRATING'] > 1]
1116
+ df_discipline = df_discipline[df_discipline['STARTS'] > 1]
1117
+ df_discipline = df_discipline[df_discipline['CLASS'].str.contains('D|C|B|A|P|R', na=False)]
1118
+
1119
+ country_to_region_map = {country: region for region, countries in iracing_ragions.items() for country in countries}
1120
+ df_discipline['REGION'] = df_discipline['LOCATION'].map(country_to_region_map).fillna('International')
1121
+
1122
+ # 2. Calcular y crear las tablas de competitividad
1123
+ top_regions, top_countries = calculate_competitiveness(df_discipline)
1124
+ top_regions.insert(0, '#', range(1, 1 + len(top_regions)))
1125
+ top_countries.insert(0, '#', range(1, 1 + len(top_countries)))
1126
+
1127
+ # --- MODIFICACIÓN: Traducir códigos de país a nombres completos ---
1128
+ def get_country_name(code):
1129
+ try:
1130
+ return pycountry.countries.get(alpha_2=code).name
1131
+ except (LookupError, AttributeError):
1132
+ return code # Devuelve el código si no se encuentra
1133
+
1134
+ top_countries['LOCATION'] = top_countries['LOCATION'].apply(get_country_name)
1135
+ # --- FIN DE LA MODIFICACIÓN ---
1136
+
1137
+ table_style_base = {
1138
+ 'style_table': {'borderRadius': '10px', 'overflow': 'hidden', 'border': '1px solid #4A4A4A','backgroundColor': 'rgba(11,11,19,1)'},
1139
+ 'style_cell': {'textAlign': 'center', 'padding': '0px', 'backgroundColor': 'rgba(11,11,19,1)', 'color': 'rgb(255, 255, 255,.8)', 'border': 'none', 'font_size': '10px','textOverflow': 'ellipsis',
1140
+ 'whiteSpace': 'normal'},
1141
+ 'style_header': {'backgroundColor': 'rgba(30,30,38,1)', 'fontWeight': 'bold', 'color': 'white', 'border': 'none', 'textAlign': 'center'},
1142
+ 'style_cell_conditional': [
1143
+ {'if': {'column_id': '#'}, 'width': '10%', 'textAlign': 'center'},
1144
+ {'if': {'column_id': 'REGION'}, 'width': '50%', 'textAlign': 'center'},
1145
+ {'if': {'column_id': 'LOCATION'}, 'width': '50%', 'textAlign': 'center'},
1146
+ {'if': {'column_id': 'avg_irating'}, 'width': '40%', 'textAlign': 'center'},
1147
+ ]
1148
+ }
1149
+
1150
+ competitiveness_tables = html.Div(
1151
+ style={'display': 'flex', 'gap': '3%', 'marginTop': '1%'},
1152
+ children=[
1153
+ html.Div(dash_table.DataTable(
1154
+ columns=[{'name': '#', 'id': '#'}, {'name': 'Top Regions', 'id': 'REGION'}, {'name': 'AVG iRating', 'id': 'avg_irating'}],
1155
+ data=top_regions.to_dict('records'),
1156
+ # --- MODIFICACIÓN: Añadimos paginación nativa ---
1157
+ page_action='native', # Activa la paginación
1158
+ page_size=5, # Muestra 5 filas por página
1159
+ # --- FIN DE LA MODIFICACIÓN ---
1160
+ style_table={**table_style_base['style_table'], 'height': '20vh'},
1161
+ style_cell=table_style_base['style_cell'],
1162
+ style_header=table_style_base['style_header'],
1163
+ style_cell_conditional=table_style_base['style_cell_conditional']
1164
+ ), style={'width': '49%'}),
1165
+ html.Div(dash_table.DataTable(
1166
+ columns=[{'name': '#', 'id': '#'}, {'name': 'Top Countries', 'id': 'LOCATION'}, {'name': 'AVG iRating', 'id': 'avg_irating'}],
1167
+ data=top_countries.to_dict('records'),
1168
+ # --- MODIFICACIÓN: Añadimos paginación nativa ---
1169
+ page_action='native', # Activa la paginación
1170
+ page_size=5, # Muestra 5 filas por página
1171
+ # --- FIN DE LA MODIFICACIÓN ---
1172
+ style_table={**table_style_base['style_table'], 'height': '20vh'},
1173
+ style_cell=table_style_base['style_cell'],
1174
+ style_header=table_style_base['style_header'],
1175
+ style_cell_conditional=table_style_base['style_cell_conditional']
1176
+ ), style={'width': '49%'})
1177
+ ]
1178
+ )
1179
+
1180
+ # 3. Crear los otros gráficos
1181
+ bubble_chart_fig = create_region_bubble_chart(df_discipline)
1182
+ line_chart_fig = create_irating_trend_line_chart(df_discipline)
1183
+
1184
+ # 4. Devolver todos los componentes actualizados
1185
+ return competitiveness_tables, bubble_chart_fig, line_chart_fig
1186
+
1187
+
1188
+
1189
+
1190
+
1191
+ # --- ELIMINA EL CALLBACK update_data_source ---
1192
+
1193
+ @app.callback(
1194
+ Output('active-discipline-store', 'data'),
1195
+ Input('btn-road', 'n_clicks'),
1196
+ Input('btn-formula', 'n_clicks'),
1197
+ Input('btn-oval', 'n_clicks'),
1198
+ Input('btn-dirt-road', 'n_clicks'),
1199
+ Input('btn-dirt-oval', 'n_clicks'),
1200
+ prevent_initial_call=True
1201
+ )
1202
+ def update_active_discipline(road, formula, oval, dr, do):
1203
+ ctx = dash.callback_context
1204
+ button_id = ctx.triggered_id.split('.')[0]
1205
+
1206
+ file_map = {
1207
+ 'btn-road': 'ROAD.csv',
1208
+ 'btn-formula': 'FORMULA.csv',
1209
+ 'btn-oval': 'OVAL.csv',
1210
+ 'btn-dirt-road': 'DROAD.csv',
1211
+ 'btn-dirt-oval': 'DOVAL.csv'
1212
+ }
1213
+ return file_map.get(button_id, 'ROAD.csv')
1214
+
1215
+
1216
+ # NUEVO CALLBACK: Actualiza las opciones del filtro de país según la región seleccionada
1217
+ @app.callback(
1218
+ Output('country-filter', 'options'),
1219
+ Input('region-filter', 'value')
1220
+ )
1221
+ def update_country_options(selected_region):
1222
+ # --- MODIFICACIÓN: Traducir códigos de país a nombres completos ---
1223
+ if not selected_region or selected_region == 'ALL':
1224
+ # Si no hay región o es 'ALL', toma todos los códigos de país únicos del dataframe
1225
+ country_codes = df['LOCATION'].dropna().unique()
1226
+ else:
1227
+ # Si se selecciona una región, toma solo los países de esa región
1228
+ country_codes = iracing_ragions.get(selected_region, [])
1229
+
1230
+ options = [{'label': 'All', 'value': 'ALL'}]
1231
+
1232
+ # Crear una lista de tuplas (nombre_completo, codigo) para poder ordenarla por nombre
1233
+ country_list_for_sorting = []
1234
+ for code in country_codes:
1235
+ try:
1236
+ # Busca el país por su código de 2 letras y obtiene el nombre
1237
+ country_name = pycountry.countries.get(alpha_2=code).name
1238
+ country_list_for_sorting.append((country_name, code))
1239
+ except (LookupError, AttributeError):
1240
+ # Si no se encuentra (código inválido), usa el código como nombre para no romper la app
1241
+ country_list_for_sorting.append((code, code))
1242
+
1243
+ # Ordenar la lista alfabéticamente por el nombre completo del país
1244
+ sorted_countries = sorted(country_list_for_sorting)
1245
+
1246
+ # Crear las opciones para el dropdown a partir de la lista ordenada
1247
+ for country_name, country_code in sorted_countries:
1248
+ options.append({'label': country_name, 'value': country_code})
1249
+
1250
+ return options
1251
+ # --- FIN DE LA MODIFICACIÓN ---
1252
+
1253
+ # --- NUEVO CALLBACK: FILTRO POR PAÍS AL HACER CLIC EN EL MAPA ---
1254
+ @app.callback(
1255
+ Output('country-filter', 'value'),
1256
+ Input('continent-map', 'clickData'),
1257
+ prevent_initial_call=True
1258
+ )
1259
+ def update_country_filter_on_map_click(clickData):
1260
+ # Si no hay datos de clic (por ejemplo, al cargar la página), no hacemos nada.
1261
+ if not clickData:
1262
+ return dash.no_update
1263
+
1264
+ # Extraemos el código de país de 2 letras del 'customdata' que definimos en el gráfico.
1265
+ # clickData['points'][0] se refiere al primer país clickeado.
1266
+ # ['customdata'][0] se refiere al primer elemento de nuestra lista custom_data, que es 'LOCATION_2_LETTER'.
1267
+ country_code = clickData['points'][0]['customdata'][0]
1268
+
1269
+ # Devolvemos el código del país, que actualizará el valor del dropdown 'country-filter'.
1270
+ return country_code
1271
+
1272
+ @app.callback(
1273
+ Output('pilot-search-dropdown', 'options'),
1274
+ Input('pilot-search-dropdown', 'search_value'),
1275
+ State('pilot-search-dropdown', 'value'),
1276
+ State('region-filter', 'value'),
1277
+ State('country-filter', 'value'),
1278
+ # --- MODIFICACIÓN: Añadimos el State para saber la disciplina activa ---
1279
+ State('active-discipline-store', 'data'),
1280
+ prevent_initial_call=True,
1281
+ )
1282
+ def update_pilot_search_options(search_value, current_selected_pilot, region_filter, country_filter, active_discipline_filename):
1283
+ # --- MODIFICACIÓN: Cargamos el DataFrame correcto al inicio de la función ---
1284
+ # 1. Cargar los datos de la disciplina actual
1285
+ df_current_discipline = pd.read_csv(active_discipline_filename)
1286
+ # Aplicamos los mismos filtros iniciales que en el callback principal
1287
+ df_current_discipline = df_current_discipline[df_current_discipline['IRATING'] > 1]
1288
+ df_current_discipline = df_current_discipline[df_current_discipline['STARTS'] > 1]
1289
+ df_current_discipline = df_current_discipline[df_current_discipline['CLASS'].str.contains('D|C|B|A|P|R', na=False)]
1290
+
1291
+ # Asignamos la región para poder filtrar por ella
1292
+ country_to_region_map = {country: region for region, countries in iracing_ragions.items() for country in countries}
1293
+ df_current_discipline['REGION'] = df_current_discipline['LOCATION'].map(country_to_region_map).fillna('International')
1294
+ # --- FIN DE LA MODIFICACIÓN ---
1295
+
1296
+ # Si no hay texto de búsqueda, pero ya hay un piloto seleccionado,
1297
+ # nos aseguramos de que su opción esté disponible para que no desaparezca.
1298
+ if not search_value:
1299
+ if current_selected_pilot:
1300
+ return [{'label': current_selected_pilot, 'value': current_selected_pilot}]
1301
+ return []
1302
+
1303
+ # Mantenemos la optimización de no buscar con texto muy corto
1304
+ if len(search_value) < 2:
1305
+ return []
1306
+
1307
+ # 1. La lógica de filtrado ahora usa el DataFrame correcto
1308
+ if not region_filter: region_filter = 'ALL'
1309
+ if not country_filter: country_filter = 'ALL'
1310
+
1311
+ filtered_df = df_current_discipline
1312
+ if region_filter != 'ALL':
1313
+ filtered_df = filtered_df[filtered_df['REGION'] == region_filter]
1314
+ if country_filter != 'ALL':
1315
+ filtered_df = filtered_df[filtered_df['LOCATION'] == country_filter]
1316
+
1317
+ # 2. La búsqueda de coincidencias no cambia
1318
+ matches = filtered_df[filtered_df['DRIVER'].str.contains(search_value, case=False)]
1319
+ top_matches = matches.nlargest(20, 'IRATING')
1320
+
1321
+ # 3. Creamos las opciones a partir de las coincidencias
1322
+ options = [{'label': row['DRIVER'], 'value': row['DRIVER']}
1323
+ for _, row in top_matches.iterrows()]
1324
+
1325
+ # 4. LA CLAVE: Si el piloto ya seleccionado no está en la nueva lista de opciones
1326
+ # (porque borramos el texto, por ejemplo), lo añadimos para que no se borre de la vista.
1327
+ if current_selected_pilot and not any(opt['value'] == current_selected_pilot for opt in options):
1328
+ options.insert(0, {'label': current_selected_pilot, 'value': current_selected_pilot})
1329
+
1330
+ print(f"DEBUG: Búsqueda de '{search_value}' encontró {len(options)} coincidencias.")
1331
+
1332
+ return options
1333
+
1334
+ # --- CALLBACK para limpiar la búsqueda si cambian los filtros ---
1335
+ @app.callback(
1336
+ Output('pilot-search-dropdown', 'value'),
1337
+ Input('region-filter', 'value'),
1338
+ Input('country-filter', 'value'),
1339
+ Input('active-discipline-store', 'data') # <-- AÑADE ESTE INPUT
1340
+ )
1341
+ def clear_pilot_search_on_filter_change(region, country, discipline_data): # <-- AÑADE EL ARGUMENTO
1342
+ # Cuando un filtro principal o la disciplina cambia, reseteamos la selección del piloto
1343
+ return None
1344
+
1345
+ # --- CALLBACK PARA ESTILO DE FILTROS ACTIVOS (MODIFICADO) ---
1346
+ @app.callback(
1347
+ Output('region-filter', 'className'),
1348
+ Output('country-filter', 'className'),
1349
+ Output('pilot-search-dropdown', 'className'), # <-- AÑADIMOS LA SALIDA
1350
+ Input('region-filter', 'value'),
1351
+ Input('country-filter', 'value'),
1352
+ Input('pilot-search-dropdown', 'value') # <-- AÑADIMOS LA ENTRADA
1353
+ )
1354
+ def update_filter_styles(region_value, country_value, pilot_value):
1355
+ # Clases base para los dropdowns
1356
+ default_class = 'iracing-dropdown'
1357
+ active_class = 'iracing-dropdown active-filter'
1358
+
1359
+ # Asignar clases según el valor de cada filtro
1360
+ region_class = active_class if region_value and region_value != 'ALL' else default_class
1361
+ country_class = active_class if country_value and country_value != 'ALL' else default_class
1362
+ # NUEVA LÓGICA: El filtro de piloto está activo si tiene un valor
1363
+ pilot_class = active_class if pilot_value else default_class
1364
+
1365
+ return region_class, country_class, pilot_class
1366
+
1367
+ # --- CALLBACK PARA ESTILO DE BOTONES ACTIVOS ---
1368
+ @app.callback(
1369
+ Output('btn-formula', 'style'), # <-- AÑADIDO
1370
+ Output('btn-road', 'style'),
1371
+ Output('btn-oval', 'style'),
1372
+ Output('btn-dirt-road', 'style'),
1373
+ Output('btn-dirt-oval', 'style'),
1374
+ Input('btn-formula', 'n_clicks'), # <-- AÑADIDO
1375
+ Input('btn-road', 'n_clicks'),
1376
+ Input('btn-oval', 'n_clicks'),
1377
+
1378
+ Input('btn-dirt-road', 'n_clicks'),
1379
+ Input('btn-dirt-oval', 'n_clicks')
1380
+ )
1381
+ def update_button_styles(formula_clicks, road_clicks, oval_clicks, dirt_road_clicks, dirt_oval_clicks): # <-- AÑADIDO
1382
+ # Estilos base para los botones
1383
+ base_style = {'width': '120px'} # Asegura que todos tengan el mismo ancho
1384
+ active_style = {
1385
+ 'backgroundColor': 'rgba(0, 111, 255, 0.3)',
1386
+ 'border': '1px solid rgb(0, 111, 255)',
1387
+ 'width': '120px'
1388
+ }
1389
+
1390
+ # Determinar qué botón fue presionado
1391
+ ctx = dash.callback_context
1392
+ if not ctx.triggered_id:
1393
+ # Estado inicial: 'Road' activo por defecto
1394
+ return base_style, active_style, base_style, base_style, base_style # <-- MODIFICADO
1395
+
1396
+ button_id = ctx.triggered_id
1397
+
1398
+ # Devolver el estilo activo para el botón presionado y el base para los demás
1399
+ if button_id == 'btn-formula': # <-- AÑADIDO
1400
+ return active_style, base_style, base_style, base_style, base_style
1401
+ elif button_id == 'btn-road':
1402
+ return base_style, active_style, base_style, base_style, base_style # <-- MODIFICADO
1403
+ elif button_id == 'btn-oval':
1404
+ return base_style, base_style, active_style, base_style, base_style # <-- MODIFICADO
1405
+ elif button_id == 'btn-dirt-road':
1406
+ return base_style, base_style, base_style, active_style, base_style # <-- MODIFICADO
1407
+ elif button_id == 'btn-dirt-oval':
1408
+ return base_style, base_style, base_style, base_style, active_style # <-- MODIFICADO
1409
+
1410
+ # Fallback por si acaso
1411
+ return base_style, base_style, base_style, base_style, base_style # <-- MODIFICADO
1412
+
1413
+ # --- CALLBACK CONSOLIDADO: BÚSQUEDA Y TABLA (MODIFICADO PARA LEER DEL STORE) ---
1414
+ @app.callback(
1415
+ Output('datatable-interactiva', 'data'),
1416
+ Output('datatable-interactiva', 'page_count'),
1417
+ Output('datatable-interactiva', 'columns'),
1418
+ Output('datatable-interactiva', 'page_current'),
1419
+ Output('histogram-plot', 'figure'),
1420
+ Output('continent-map', 'figure'),
1421
+ Output('kpi-global', 'figure'),
1422
+ Output('kpi-pilot', 'figure'),
1423
+ Output('shared-data-store', 'data'),
1424
+ # --- ELIMINAMOS LOS BOTONES COMO INPUTS ---
1425
+ # Input('btn-road', 'n_clicks'),
1426
+ # Input('btn-formula', 'n_clicks'),
1427
+ # Input('btn-oval', 'n_clicks'),
1428
+ # Input('btn-dirt-road', 'n_clicks'),
1429
+ # Input('btn-dirt-oval', 'n_clicks'),
1430
+ # --- LOS INPUTS AHORA EMPIEZAN CON LOS FILTROS ---
1431
+ Input('region-filter', 'value'),
1432
+ Input('country-filter', 'value'),
1433
+ Input('pilot-search-dropdown', 'value'),
1434
+ Input('datatable-interactiva', 'page_current'),
1435
+ Input('datatable-interactiva', 'page_size'),
1436
+ Input('datatable-interactiva', 'sort_by'),
1437
+ Input('datatable-interactiva', 'active_cell'),
1438
+ # --- AÑADIMOS EL STORE COMO STATE ---
1439
+ State('active-discipline-store', 'data'),
1440
+ # --- AÑADIMOS UN INPUT DEL STORE PARA REACCIONAR AL CAMBIO ---
1441
+ Input('active-discipline-store', 'data')
1442
+ )
1443
+ def update_table_and_search(
1444
+ region_filter, country_filter, selected_pilot,
1445
+ page_current, page_size, sort_by, state_active_cell,
1446
+ active_discipline_filename, # <-- Nuevo argumento desde el State
1447
+ discipline_change_trigger # <-- Nuevo argumento desde el Input
1448
+ ):
1449
+
1450
+ ctx = dash.callback_context
1451
+ triggered_id = ctx.triggered[0]['prop_id'].split('.')[0] if ctx.triggered else 'region-filter'
1452
+
1453
+ # --- 1. LECTURA DEL ARCHIVO DESDE EL STORE ---
1454
+ # Ya no necesitamos el file_map aquí, simplemente usamos el nombre del archivo guardado.
1455
+ filename = active_discipline_filename
1456
+
1457
+ # --- 2. PROCESAMIENTO DE DATOS (se hace cada vez) ---
1458
+ # Leemos y procesamos el archivo seleccionado
1459
+ #df = pd.read_csv(filename)
1460
+ df = DISCIPLINE_DATAFRAMES[active_discipline_filename]
1461
+ '''df = df[df['IRATING'] > 1]
1462
+ df = df[df['STARTS'] > 1]
1463
+ df = df[df['CLASS'].str.contains('D|C|B|A|P|R', na=False)]
1464
+
1465
+ country_to_region_map = {country: region for region, countries in iracing_ragions.items() for country in countries}
1466
+ df['REGION'] = df['LOCATION'].map(country_to_region_map).fillna('International')
1467
+
1468
+ df['Rank World'] = df['IRATING'].rank(method='first', ascending=False).fillna(0).astype(int)
1469
+ df['Rank Region'] = df.groupby('REGION')['IRATING'].rank(method='first', ascending=False).fillna(0).astype(int)
1470
+ df['Rank Country'] = df.groupby('LOCATION')['IRATING'].rank(method='first', ascending=False).fillna(0).astype(int)
1471
+
1472
+ df['CLASS'] = df['CLASS'].str[0]'''
1473
+ df_for_graphs = df.copy() # Copia para gráficos que no deben ser filtrados
1474
+
1475
+ # --- 3. LÓGICA DE FILTRADO Y VISUALIZACIÓN (sin cambios) ---
1476
+ # El resto de la función sigue igual, pero ahora opera sobre el 'df' que acabamos de cargar.
1477
+
1478
+ # Lógica de columnas dinámicas
1479
+ base_cols = ['DRIVER', 'IRATING', 'LOCATION', 'REGION','CLASS', 'STARTS', 'WINS' ]
1480
+ if country_filter and country_filter != 'ALL':
1481
+ dynamic_cols = ['Rank Country'] + base_cols
1482
+ elif region_filter and region_filter != 'ALL':
1483
+ dynamic_cols = ['Rank Region'] + base_cols
1484
+ else:
1485
+ dynamic_cols = ['Rank World'] + base_cols
1486
+
1487
+ # Filtrado de datos
1488
+ if not region_filter: region_filter = 'ALL'
1489
+ if not country_filter: country_filter = 'ALL'
1490
+
1491
+ filtered_df = df[dynamic_cols].copy()
1492
+
1493
+ if region_filter != 'ALL':
1494
+ filtered_df = filtered_df[filtered_df['REGION'] == region_filter]
1495
+ if country_filter != 'ALL':
1496
+ filtered_df = filtered_df[filtered_df['LOCATION'] == country_filter]
1497
+
1498
+ # --- 3. ORDENAMIENTO ---
1499
+ if sort_by:
1500
+ filtered_df = filtered_df.sort_values(
1501
+ by=sort_by[0]['column_id'],
1502
+ ascending=sort_by[0]['direction'] == 'asc'
1503
+ )
1504
+
1505
+ # --- 4. LÓGICA DE BÚSQUEDA Y NAVEGACIÓN (CORREGIDA) ---
1506
+ target_page = page_current
1507
+ new_active_cell = state_active_cell
1508
+
1509
+ # Si el callback fue disparado por un cambio en los filtros,
1510
+ # reseteamos la celda activa y la página.
1511
+ if triggered_id in [
1512
+ 'region-filter', 'country-filter',
1513
+ 'active-discipline-store'
1514
+ ]:
1515
+ new_active_cell = None
1516
+ target_page = 0 # <-- Esto hace que siempre se muestre la primera página
1517
+
1518
+ # Si la búsqueda de piloto activó el callback, calculamos la nueva página y celda activa
1519
+ elif triggered_id == 'pilot-search-dropdown' and selected_pilot:
1520
+ match_index = filtered_df.index.get_loc(df[df['DRIVER'] == selected_pilot].index[0])
1521
+ if match_index is not None:
1522
+ target_page = match_index // page_size
1523
+ driver_column_index = list(filtered_df.columns).index('DRIVER')
1524
+ new_active_cell = {
1525
+ 'row': match_index % page_size,
1526
+ 'row_id': match_index % page_size,
1527
+ 'column': driver_column_index,
1528
+ 'column_id': 'DRIVER'
1529
+ }
1530
+
1531
+
1532
+
1533
+ # --- 5. GENERACIÓN DE COLUMNAS PARA LA TABLA ---
1534
+ columns_definition = []
1535
+ for col_name in filtered_df.columns:
1536
+ if col_name == "LOCATION":
1537
+ columns_definition.append({"name": "Country", "id": col_name, "presentation": "markdown", 'type': 'text'})
1538
+ elif col_name == "IRATING":
1539
+ columns_definition.append({"name": "iRating", "id": col_name})
1540
+ elif col_name.startswith("Rank "):
1541
+ columns_definition.append({"name": col_name.replace("Rank ", ""), "id": col_name})
1542
+ elif col_name == "CLASS":
1543
+ columns_definition.append({"name": "SR", "id": col_name})
1544
+ else:
1545
+ columns_definition.append({"name": col_name.title(), "id": col_name})
1546
+
1547
+ # --- 6. PAGINACIÓN ---
1548
+ start_idx = target_page * page_size
1549
+
1550
+ end_idx = start_idx + page_size
1551
+
1552
+ # Aplicamos el formato de bandera a los datos de la página actual
1553
+ page_df = filtered_df.iloc[start_idx:end_idx].copy()
1554
+ page_df['LOCATION'] = page_df['LOCATION'].map(lambda x: flag_img(x))
1555
+ page_data = page_df.to_dict('records')
1556
+
1557
+ total_pages = len(filtered_df) // page_size + (1 if len(filtered_df) % page_size > 0 else 0)
1558
+
1559
+ # --- 7. ACTUALIZACIÓN DE GRÁFICOS ---
1560
+ graph_indices = filtered_df.index
1561
+ highlight_irating = None
1562
+ highlight_name = None
1563
+ pilot_info_for_kpi = None # Variable para guardar los datos del piloto
1564
+
1565
+ pilot_to_highlight = selected_pilot
1566
+
1567
+ if triggered_id == 'datatable-interactiva' and new_active_cell:
1568
+ row_index_in_df = (target_page * page_size) + new_active_cell['row']
1569
+ if row_index_in_df < len(filtered_df):
1570
+ pilot_to_highlight = filtered_df.iloc[row_index_in_df]['DRIVER']
1571
+
1572
+ if pilot_to_highlight:
1573
+ pilot_data = df[df['DRIVER'] == pilot_to_highlight]
1574
+ if not pilot_data.empty:
1575
+ pilot_info_for_kpi = pilot_data.iloc[0] # <-- Guardamos toda la info del piloto
1576
+ highlight_irating = pilot_info_for_kpi['IRATING']
1577
+ highlight_name = pilot_info_for_kpi['DRIVER']
1578
+ elif not filtered_df.empty:
1579
+ top_pilot_in_view = filtered_df.nlargest(1, 'IRATING').iloc[0]
1580
+ highlight_irating = top_pilot_in_view['IRATING']
1581
+ highlight_name = top_pilot_in_view['DRIVER']
1582
+
1583
+ # --- NUEVO: Generamos el gráfico de KPIs ---
1584
+ filter_context = "World"
1585
+ if country_filter and country_filter != 'ALL':
1586
+ try:
1587
+ # Traducimos el código de país a nombre para el KPI
1588
+ filter_context = pycountry.countries.get(alpha_2=country_filter).name
1589
+ except (LookupError, AttributeError):
1590
+ filter_context = country_filter # Usamos el código si no se encuentra
1591
+ elif region_filter and region_filter != 'ALL':
1592
+ filter_context = region_filter
1593
+
1594
+ kpi_global_fig = create_kpi_global(filtered_df, filter_context)
1595
+ kpi_pilot_fig = create_kpi_pilot(filtered_df, pilot_info_for_kpi, filter_context)
1596
+
1597
+ updated_histogram_figure = create_histogram_with_percentiles(
1598
+ df.loc[graph_indices],
1599
+ 'IRATING',
1600
+ 100,
1601
+ highlight_irating=highlight_irating,
1602
+ highlight_name=highlight_name
1603
+ )
1604
+
1605
+ updated_map_figure = create_continent_map(df_for_graphs, region_filter, country_filter)
1606
+
1607
+ shared_data = {
1608
+ 'active_cell': new_active_cell,
1609
+ 'selected_pilot': selected_pilot or '',
1610
+ 'timestamp': str(pd.Timestamp.now())
1611
+ }
1612
+
1613
+ '''return (page_data, total_pages, columns_definition, target_page,
1614
+ new_active_cell, updated_histogram_figure, updated_map_figure)'''
1615
+
1616
+ return (page_data, total_pages, columns_definition, target_page,
1617
+ updated_histogram_figure, updated_map_figure,
1618
+ kpi_global_fig,
1619
+ kpi_pilot_fig,
1620
+ shared_data)
1621
+
1622
+ # --- NUEVO CALLBACK: IMPRIMIR DATOS DEL PILOTO SELECCIONADO ---
1623
+ @app.callback(
1624
+ Output('pilot-info-display', 'children'), # Necesitarás añadir este componente al layout
1625
+ Input('datatable-interactiva', 'active_cell'),
1626
+ State('datatable-interactiva', 'data'),
1627
+ State('region-filter', 'value'),
1628
+ State('country-filter', 'value'),
1629
+ prevent_initial_call=True
1630
+ )
1631
+ def print_selected_pilot_data(active_cell, table_data, region_filter, country_filter):
1632
+ if not active_cell or not table_data:
1633
+ return "No driver selected"
1634
+
1635
+ # Obtener el nombre del piloto de la fila seleccionada
1636
+ selected_row = active_cell['row']
1637
+ if selected_row >= len(table_data):
1638
+ return "Invalid row"
1639
+
1640
+ pilot_name = table_data[selected_row]['DRIVER']
1641
+
1642
+ # Buscar todos los datos del piloto en el DataFrame original
1643
+ pilot_data = df[df['DRIVER'] == pilot_name]
1644
+
1645
+ if pilot_data.empty:
1646
+ return f"No data found for {pilot_name}"
1647
+
1648
+ # Obtener la primera (y única) fila del piloto
1649
+ pilot_info = pilot_data.iloc[0]
1650
+
1651
+ # IMPRIMIR EN CONSOLA todos los datos del piloto
1652
+ print("\n" + "="*50)
1653
+ print(f"SELECTED DRIVER DATA: {pilot_name}")
1654
+ print("="*50)
1655
+ for column, value in pilot_info.items():
1656
+ print(f"{column}: {value}")
1657
+ print("="*50 + "\n")
1658
+
1659
+ # También retornar información para mostrar en la interfaz (opcional)
1660
+ return f"Selected driver: {pilot_name} (See console for full data)"
1661
+
1662
+ @app.callback(
1663
+ Output('datatable-interactiva', 'active_cell'),
1664
+ Output('shared-data-store_1', 'data'),
1665
+
1666
+
1667
+ Input('datatable-interactiva', 'active_cell'),
1668
+ State('shared-data-store', 'data'),
1669
+ State('shared-data-store_1', 'data'),
1670
+ Input('region-filter', 'value'),
1671
+ Input('country-filter', 'value'),
1672
+ prevent_initial_call=True
1673
+ )
1674
+ def update_active_cell_from_store(active_cell,ds,ds1,a,b):
1675
+ print(ds1)
1676
+ print(ds)
1677
+ print(active_cell)
1678
+
1679
+
1680
+ if not ds:
1681
+ return None
1682
+ if not ds1:
1683
+ ds1 = ds
1684
+ if ds.get('selected_pilot', '') == '':
1685
+
1686
+ return active_cell,ds1
1687
+ return ds.get('active_cell'),ds1
1688
+
1689
+ if ds.get('selected_pilot', '') == ds1.get('selected_pilot', ''):
1690
+ ds1 = ds
1691
+
1692
+ if active_cell == ds1.get('active_cell'):
1693
+
1694
+ return None,ds1
1695
+ ds1['active_cell'] = active_cell
1696
+
1697
+
1698
+ return active_cell,ds1
1699
+ else:
1700
+ ds1 = ds
1701
+ return ds.get('active_cell'),ds1
1702
+
1703
+
1704
+
1705
+
1706
+ '''active_cell = a
1707
+ selected_pilot = shared_data.get('selected_pilot', '')
1708
+ print('..............')
1709
+ print(shared_data)
1710
+
1711
+ print(f"DEBUG: Recuperando active_cell del store: {active_cell}")
1712
+ print(f"DEBUG: Piloto asociado: {selected_pilot}")
1713
+ shared_data['shared_data'] = ''
1714
+
1715
+
1716
+ return active_cell'''
1717
+
1718
+
1719
+ if __name__ == "__main__":
1720
+ app.run(debug=True)
app1.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ '''from iracingdataapi.client import irDataClient
2
+ import pandas as pd
3
+
4
+ # Reemplaza con tu email y contraseña de iRacing
5
+ username = 'danielsaed99@hotmail.com'
6
+ password = ''
7
+ idc = irDataClient(username=username, password=password)
8
+
9
+ # ------------------------------------
10
+ a = idc.driver_list(1)
11
+
12
+ df = pd.DataFrame(a)
13
+
14
+ print(df.head(10))'''
15
+
16
+ import pandas as pd
17
+
18
+ files = ['ROAD.csv', 'FORMULA.csv', 'OVAL.csv', 'DROAD.csv', 'DOVAL.csv']
19
+ for f in files:
20
+ df = pd.read_csv(f)
21
+ parquet_file = f.replace('.csv', '.parquet')
22
+ df.to_parquet(parquet_file)
23
+ print(f"Converted {f} to {parquet_file}")
assets/ADDCN___.TTF ADDED
Binary file (39.3 kB). View file
 
assets/custom.css ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* assets/custom.css */
2
+ .dash-table-container .dash-spreadsheet-container .dash-cell {
3
+ height: calc(70vh / 21.5); /* 70vh dividido por el número de filas por página */
4
+ min-height: 1px;
5
+ max-height: 5px;
6
+ }
7
+
8
+
9
+ @font-face {
10
+ font-family: 'DashboardFont'; /* Dale un nombre para usarla en tu app */
11
+ src: url('ADDCN___.ttf') format('truetype'); /* Apunta al archivo de tu fuente */
12
+ }
13
+ /* 2. Aplica la fuente a toda la aplicación */
14
+ /* body {
15
+ font-family: 'DashboardFont', sans-serif; Usa tu fuente. 'sans-serif' es un respaldo
16
+ } */
17
+
18
+
19
+ /* Opcional: Asegúrate de que los títulos también la usen */
20
+ h1, h2, h3, h4, h5, h6 {
21
+ font-family: 'DashboardFont', sans-serif;
22
+ }
23
+
24
+ .iracing-dropdown .Select-control {
25
+ background-color: rgba(11, 11, 19, 1) !important; /* Fondo gris oscuro */
26
+ border: 1px solid #4A4A4A !important; /* Borde sutil un poco más claro */
27
+ border-radius: 4px !important;
28
+ box-shadow: none !important; /* Sin sombras */
29
+ }
30
+
31
+ /* Texto de la opción seleccionada */
32
+ .iracing-dropdown .Select-value-label {
33
+ color: #E0E0E0 !important; /* Color de texto gris claro */
34
+ }
35
+
36
+ /* Flecha del dropdown */
37
+ .iracing-dropdown .Select-arrow {
38
+ border-color: #E0E0E0 transparent transparent !important; /* Hace la flecha del color del texto */
39
+ }
40
+
41
+ /* Menú que se despliega */
42
+ .iracing-dropdown .Select-menu-outer {
43
+ background-color: #323232 !important; /* Mismo fondo que el control */
44
+ border: 1px solid #4A4A4A !important; /* Mismo borde */
45
+ border-radius: 4px !important;
46
+ }
47
+
48
+ /* Estilo de cada opción en el menú */
49
+ .iracing-dropdown .Select-option {
50
+ background-color: #323232 !important; /* Fondo de la opción */
51
+ color: #E0E0E0 !important; /* Color del texto de la opción */
52
+ }
53
+
54
+ /* Estilo de la opción cuando el ratón está encima (hover) */
55
+ .iracing-dropdown .Select-option:hover {
56
+ background-color: #4A4A4A !important; /* Un gris un poco más claro para el hover */
57
+ }
58
+
59
+ /* Estilo de la opción que está seleccionada/enfocada */
60
+ .iracing-dropdown .Select-option.is-focused {
61
+ background-color: #4A4A4A !important;
62
+ }
63
+
assets/style.css ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Lato:wght@400;700;900&display=swap'); /* <-- MODIFICACIÓN: Añadimos ;900 */
2
+
3
+
4
+ body {
5
+ margin: 5px;
6
+ padding: 0;
7
+ background-color: rgba(5,5,15,255);
8
+ color: #ffffff;
9
+ font-family: 'Lato', sans-serif; /* <-- AÑADE ESTA LÍNEA */
10
+ font-weight: 500; /* <-- AÑADE ESTA LÍNEA para usar el grosor "Black" */
11
+ }
12
+
13
+ p {
14
+ margin-bottom: 0;
15
+ text-align : center;
16
+ }
17
+
18
+ #main-content-container {
19
+ flex-direction: row; /* Las columnas están una al lado de la otra */
20
+ }
21
+
22
+ #left-column, #middle-column, #right-column {
23
+ height: 100%; /* Ocupan toda la altura disponible */
24
+ }
25
+
26
+
27
+ /* Media Query para pantallas medianas y pequeñas (1200px o menos) */
28
+ @media (max-width: 1200px) {
29
+ /* Cambiamos la dirección del contenedor principal a vertical */
30
+ #main-content-container {
31
+ flex-direction: column; /* Las columnas se apilan una encima de la otra */
32
+ overflow-y: auto; /* Añadimos scroll vertical para toda la página */
33
+ }
34
+
35
+ /* Hacemos que cada columna ocupe todo el ancho disponible */
36
+ #left-column, #middle-column, #right-column {
37
+ width: 98% !important; /* Usamos !important para sobreescribir el estilo en línea de Python */
38
+ height: auto !important; /* La altura se ajusta al contenido */
39
+ min-height: 60vh; /* Damos una altura mínima para que los gráficos se vean bien */
40
+ margin: 1%;
41
+ }
42
+
43
+ /* Ajustamos la altura de la tabla para que no sea excesiva en vista móvil/vertical */
44
+ #datatable-interactiva .dash-spreadsheet-container {
45
+ height: 60vh !important;
46
+ }
47
+ }
48
+
49
+ /* --- ESTILOS PARA LA TABLA INTERACTIVA --- */
50
+
51
+ /* Seleccionamos las celdas de datos de la tabla */
52
+ #datatable-interactiva .dash-cell {
53
+ /*
54
+ * TIPOGRAFÍA FLUIDA (LA CLAVE DE LA SOLUCIÓN)
55
+ * clamp(MIN, IDEAL, MAX)
56
+ * - MIN (8px): El tamaño de fuente NUNCA será menor de 8px.
57
+ * - IDEAL (0.6vw): El tamaño ideal será el 0.6% del ANCHO de la ventana.
58
+ * Esto hace que la fuente se encoja al reducir la ventana.
59
+ * - MAX (12px): El tamaño de fuente NUNCA será mayor de 12px.
60
+ */
61
+ font-size: clamp(8px, 0.6vw, 12px) !important;
62
+
63
+ /* Ajustamos el padding para que las celdas no se sientan apretadas */
64
+ padding: 1px 1px !important;
65
+ }
66
+
67
+ /* Hacemos lo mismo para la cabecera de la tabla */
68
+ #datatable-interactiva .dash-header {
69
+ font-size: clamp(9px, 0.7vw, 10px) !important;
70
+ padding: 2px 1px !important;
71
+ }
72
+
73
+ /* --- ESTILOS PARA LOS DROPDOWNS Y LABELS (OPCIONAL PERO RECOMENDADO) --- */
74
+
75
+ /* Hacemos que los filtros también sean un poco responsivos */
76
+ .iracing-dropdown, .iracing-dropdown .Select-value-label, .iracing-dropdown .Select-placeholder {
77
+ font-size: clamp(10px, 0.8vw, 10px) !important;
78
+ }
79
+
80
+ label {
81
+ font-size: clamp(11px, 0.8vw, 12px) !important;
82
+ }
83
+
84
+ /* --- REGLA ESPECÍFICA PARA LA COLUMNA REGION --- */
85
+ /* Seleccionamos solo las celdas que pertenecen a la columna 'REGION' */
86
+ #datatable-interactiva .dash-cell[data-dash-column="REGION"] {
87
+ font-size: 10px !important; /* ¡Aquí sí funcionará! */
88
+ }
89
+
90
+ #datatable-interactiva .dash-cell[data-dash-column="DRIVER"] {
91
+ font-size: 10px !important; /* ¡Aquí sí funcionará! */
92
+ }
93
+
94
+ #datatable-interactiva .dash-cell[data-dash-column="CLASS"] {
95
+ font-size: 12px !important; /* ¡Aquí sí funcionará! */
96
+ }
97
+ #datatable-interactiva .dash-cell[data-dash-column="IRATING"] {
98
+ font-size: 10px !important; /* ¡Aquí sí funcionará! */
99
+ }
100
+ #datatable-interactiva .dash-cell[data-dash-column="STARTS"] {
101
+ font-size: 10px !important; /* ¡Aquí sí funcionará! */
102
+ }
103
+ #datatable-interactiva .dash-cell[data-dash-column="WINS"] {
104
+ font-size: 10px !important; /* ¡Aquí sí funcionará! */
105
+ }
106
+
107
+ /* --- ESTILOS PARA LOS BOTONES DEL TIPO DE TABLERO --- */
108
+ .dashboard-type-button {
109
+ background-color: rgba(18,18,26,.5);
110
+ color: white;
111
+ border: 1px solid #4A4A4A;
112
+ padding: 8px 8px;
113
+ border-radius: 5px;
114
+ cursor: pointer;
115
+ font-weight: bold;
116
+ /* --- AJUSTES PARA TAMAÑO Y TRANSICIÓN --- */
117
+ width: 80px; /* Ancho fijo para todos los botones */
118
+ text-align: center;
119
+ transition: background-color 0.2s, border-color 0.2s;
120
+ }
121
+
122
+ .dashboard-type-button:hover {
123
+ background-color: #4A4A4A;
124
+ }
125
+
126
+ /* Estilo para los dropdowns de filtros cuando están activos */
127
+ .iracing-dropdown.active-filter .Select-control {
128
+ background-color: rgba(0, 111, 255, 0.25) !important;
129
+ border: 1px solid rgba(0, 111, 255, 0.7) !important;
130
+ }
131
+
132
+ /* Opcional: Cambia el color del texto y la flecha para que se vea mejor */
133
+ .iracing-dropdown.active-filter .Select-placeholder,
134
+ .iracing-dropdown.active-filter .Select-value-label {
135
+ color: #FFFFFF;
136
+ }
137
+
138
+ .iracing-dropdown.active-filter .Select-arrow {
139
+ border-top-color: #FFFFFF;
140
+ }
141
+
142
+ @keyframes fadeIn {
143
+ from { opacity: 0; }
144
+ to { opacity: 1; }
145
+ }
146
+
147
+ /* Aplicamos la animación al cuerpo del HTML */
148
+ body {
149
+ animation: fadeIn 0.8s ease-in-out;
150
+ }
151
+
152
+ /* --- ESTILOS DEFINITIVOS PARA DROPDOWNS --- */
153
+
154
+ /* 1. Color del texto del placeholder (ej: "Buscar Piloto...") */
155
+ .iracing-dropdown .Select-placeholder {
156
+ color: rgba(255, 255, 255, 1) !important;
157
+ }
158
+
159
+ /* 2. Color del texto que se escribe en el buscador */
160
+ .iracing-dropdown .Select-input > input {
161
+ color: white !important;
162
+ }
163
+
164
+ /* 3. Color del texto del piloto una vez que ha sido seleccionado */
165
+ .iracing-dropdown .Select-value-label {
166
+ color: white !important;
167
+ }
168
+
169
+ #competitiveness-tables-container .dash-spreadsheet-inner::-webkit-scrollbar-thumb:hover {
170
+ background-color: #6c6c6c; /* Se aclara para dar feedback visual */
171
+ }
172
+
173
+ /* --- ESTILOS PARA LA PAGINACIÓN DARK MODE DE LAS TABLAS TOP --- */
174
+
175
+ /* Contenedor principal de la paginación */
176
+ #competitiveness-tables-container .pagination {
177
+ justify-content: center; /* Centra los botones de página */
178
+ margin-top: 5px;
179
+ margin-bottom: 0;
180
+ }
181
+
182
+ /* Estilo general para cada botón de paginación */
183
+ #competitiveness-tables-container .page-item .page-link {
184
+ background-color: rgba(11,11,19,1); /* Fondo oscuro como las celdas */
185
+ color: rgb(255, 255, 255, .8); /* Texto claro */
186
+ border: 1px solid #4A4A4A; /* Borde sutil */
187
+ font-size: 12px; /* Texto más pequeño */
188
+ padding: 4px 10px; /* Padding reducido para hacerlo más compacto */
189
+ margin: 0 2px; /* Espacio entre botones */
190
+ border-radius: 4px; /* Bordes ligeramente redondeados */
191
+ }
192
+
193
+ /* Estilo para los botones al pasar el ratón por encima (excepto el activo) */
194
+ #competitiveness-tables-container .page-item:not(.active) .page-link:hover {
195
+ background-color: #323232; /* Un gris un poco más claro */
196
+ color: white;
197
+ }
198
+
199
+ /* Estilo para el botón de la página activa */
200
+ #competitiveness-tables-container .page-item.active .page-link {
201
+ background-color: rgba(0, 111, 255, 0.5); /* Color de acento azul */
202
+ border-color: rgb(0, 111, 255);
203
+ color: white;
204
+ font-weight: bold;
205
+ }
206
+
207
+ /* Estilo para los botones deshabilitados (ej. "Previous" en la primera página) */
208
+ #competitiveness-tables-container .page-item.disabled .page-link {
209
+ background-color: rgba(11,11,19,1);
210
+ color: #4A4A4A; /* Color de texto muy atenuado */
211
+ border-color: #323232;
212
+ }
requirements.txt ADDED
Binary file (274 Bytes). View file