Spaces:
Sleeping
Sleeping
Upload 18 files
Browse files- .gitattributes +12 -35
- DOVAL.csv +3 -0
- DOVAL.parquet +3 -0
- DROAD.csv +3 -0
- DROAD.parquet +3 -0
- Dockerfile +24 -0
- FORMULA.csv +3 -0
- FORMULA.parquet +3 -0
- OVAL.csv +3 -0
- OVAL.parquet +3 -0
- ROAD.csv +3 -0
- ROAD.parquet +3 -0
- app.py +1720 -0
- app1.py +23 -0
- assets/ADDCN___.TTF +0 -0
- assets/custom.css +63 -0
- assets/style.css +212 -0
- requirements.txt +0 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,12 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 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''
|
| 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
|
|
|