2nzi commited on
Commit
ffe1030
·
verified ·
1 Parent(s): 88f685c

first commit

Browse files
.dockerignore ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Fichiers de développement
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ .Python
7
+ env/
8
+ venv/
9
+ .venv/
10
+ pip-log.txt
11
+ pip-delete-this-directory.txt
12
+
13
+ # Fichiers de données temporaires
14
+ *.tmp
15
+ *.temp
16
+
17
+ # Fichiers de logs
18
+ *.log
19
+
20
+ # Fichiers de configuration locaux
21
+ .env
22
+ .env.local
23
+
24
+ # Fichiers Git
25
+ .git/
26
+ .gitignore
27
+
28
+ # Fichiers de documentation
29
+ README.md
30
+ *.md
31
+
32
+ # Fichiers de tests
33
+ tests/
34
+ test_*.py
35
+ *_test.py
36
+
37
+ # Fichiers de développement
38
+ .vscode/
39
+ .idea/
40
+ *.swp
41
+ *.swo
42
+
43
+ # Fichiers système
44
+ .DS_Store
45
+ Thumbs.db
46
+
47
+ # Fichiers de cache
48
+ .cache/
49
+ .pytest_cache/
50
+
51
+ # Fichiers de données brutes (garder seulement les données transformées)
52
+ data/raw/
53
+ ETL/
54
+
55
+ # Fichiers Docker
56
+ Dockerfile
57
+ .dockerignore
58
+ docker-compose.yml
59
+
60
+ # Fichiers de déploiement
61
+ build-and-run.sh
62
+ README_DEPLOYMENT.md
.gitignore ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .env
6
+ .venv
7
+ env/
8
+ venv/
9
+ ENV/
10
+ .idea/
11
+ .vscode/
12
+ *.xlsx
13
+ !data/*.xlsx
14
+
15
+ /component-template/
Dockerfile CHANGED
@@ -1,21 +1,22 @@
1
- FROM python:3.9-slim
2
-
3
- WORKDIR /app
4
-
5
- RUN apt-get update && apt-get install -y \
6
- build-essential \
7
- curl \
8
- software-properties-common \
9
- git \
10
- && rm -rf /var/lib/apt/lists/*
11
-
12
- COPY requirements.txt ./
13
- COPY src/ ./src/
14
-
15
- RUN pip3 install -r requirements.txt
16
-
17
- EXPOSE 8501
18
-
19
- HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
20
-
21
- ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update && apt-get install -y \
6
+ build-essential \
7
+ curl \
8
+ software-properties-common \
9
+ git \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ COPY requirements.txt ./
13
+ COPY streamlit_app/ ./streamlit_app/
14
+ COPY data/ ./data/
15
+
16
+ RUN pip3 install -r requirements.txt
17
+
18
+ EXPOSE 8501
19
+
20
+ HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
21
+
22
+ ENTRYPOINT ["streamlit", "run", "streamlit_app/main.py", "--server.port=8501", "--server.address=0.0.0.0"]
README.md CHANGED
@@ -1,19 +1,85 @@
1
- ---
2
- title: Stade U18
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
- pinned: false
11
- short_description: Streamlit template space
12
- ---
13
-
14
- # Welcome to Streamlit!
15
-
16
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
17
-
18
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
19
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Rugby Analytics - Stade Toulousain U18
3
+ emoji: 🏉
4
+ colorFrom: red
5
+ colorTo: red
6
+ sdk: docker
7
+ app_port: 8501
8
+ tags:
9
+ - streamlit
10
+ - rugby
11
+ - analytics
12
+ - sports
13
+ pinned: false
14
+ short_description: Application d'analyse des performances rugby
15
+ ---
16
+
17
+ # 🏉 Rugby Analytics - Stade Toulousain U18
18
+
19
+ Application d'analyse des performances des joueuses U18 du Stade Toulousain Rugby, développée avec Streamlit.
20
+
21
+ ## 📊 Fonctionnalités
22
+
23
+ - **Analyse individuelle des joueuses** : Visualisation détaillée des performances par joueuse
24
+ - **Comparaison de matchs** : Analyse comparative entre différents matchs
25
+ - **Statistiques avancées** : Métriques de performance avec niveaux de qualité (0-3)
26
+ - **Graphiques interactifs** : Visualisations Plotly pour une meilleure compréhension
27
+
28
+ ## 🎯 Types d'actions analysées
29
+
30
+ - **DUEL** : Actions de contact et progression
31
+ - **PASSE** : Qualité et précision des passes
32
+ - **JAP** : Jeu au pied et stratégie
33
+ - **PLAQUAGE** : Efficacité défensive
34
+ - **RUCK** : Vitesse et qualité du ruck
35
+ - **RÉCEPTION JAP** : Réception des jeux au pied
36
+
37
+ ## 🚀 Déploiement
38
+
39
+ Cette application est déployée sur Hugging Face Spaces avec Docker.
40
+
41
+ ### Structure du projet
42
+
43
+ ```
44
+ Rugby/
45
+ ├── streamlit_app/
46
+ │ ├── main.py # Point d'entrée principal
47
+ │ ├── components/ # Composants Streamlit
48
+ │ ├── analytics/ # Logique d'analyse
49
+ │ ├── charts/ # Graphiques et visualisations
50
+ │ └── utils/ # Utilitaires
51
+ ├── data/ # Données transformées
52
+ ├── Dockerfile # Configuration Docker
53
+ └── requirements.txt # Dépendances Python
54
+ ```
55
+
56
+ ## 🛠️ Technologies utilisées
57
+
58
+ - **Streamlit** : Interface utilisateur
59
+ - **Plotly** : Graphiques interactifs
60
+ - **Pandas** : Manipulation des données
61
+ - **SQLite** : Base de données
62
+ - **Docker** : Conteneurisation
63
+
64
+ ## 📈 Métriques de performance
65
+
66
+ Chaque action est évaluée sur une échelle de 0 à 3 :
67
+ - **0** : Performance faible (erreur, perte de balle)
68
+ - **1** : Performance moyenne (neutre)
69
+ - **2** : Bonne performance (progression, gain)
70
+ - **3** : Excellente performance (break, domination)
71
+
72
+ ## 🔍 Utilisation
73
+
74
+ 1. Sélectionnez une joueuse dans le carousel
75
+ 2. Choisissez un match spécifique
76
+ 3. Explorez les statistiques détaillées
77
+ 4. Analysez les graphiques de performance
78
+
79
+ ## 📞 Support
80
+
81
+ Pour toute question ou suggestion, n'hésitez pas à ouvrir une issue sur le repository.
82
+
83
+ ---
84
+
85
+ *Développé pour l'analyse des performances du Stade Toulousain Rugby U18* 🏉
data/transformed/Rugby_Stats.csv ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt CHANGED
@@ -1,3 +1,14 @@
1
- altair
 
2
  pandas
3
- streamlit
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
  pandas
4
+ matplotlib
5
+ mplsoccer
6
+ python-dotenv
7
+ openpyxl
8
+ streamlit
9
+ plotly
10
+ seaborn
11
+ fuzzywuzzy
12
+ python-levenshtein
13
+ st_image_carousel
14
+ st_circular_kpi
streamlit_app/.streamlit/config.toml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [browser]
2
+ showErrorDetails = false
3
+
4
+ [client]
5
+ showErrorDetails = false
6
+
7
+ [server]
8
+ runOnSave = true
9
+
10
+ # Désactiver certains éléments
11
+ [theme]
12
+ primaryColor = "#1f4e79"
13
+ backgroundColor = "#ffffff"
14
+ secondaryBackgroundColor = "#f0f8ff"
15
+ textColor = "#000000"
16
+
17
+ [ui]
18
+ hideTopBar = true
streamlit_app/analytics/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Fichier vide pour faire du dossier un package Python
streamlit_app/analytics/scoring.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import streamlit as st
3
+
4
+ @st.cache_data
5
+ def calculate_player_scoring(df):
6
+ """
7
+ Calcule tous les niveaux de scoring des joueuses de rugby
8
+
9
+ Args:
10
+ df: DataFrame avec les données brutes
11
+
12
+ Returns:
13
+ dict: Contient tous les niveaux d'agrégation
14
+ - 'by_action': Scores détaillés par (match, joueuse, action)
15
+ - 'by_player_match': Scores par (match, joueuse)
16
+ - 'by_match': Scores moyens par match
17
+ - 'global_average': Moyenne générale
18
+ """
19
+
20
+ # Configuration
21
+ actions_interessees = [
22
+ "DUEL",
23
+ "PASSE",
24
+ "PLAQUAGE",
25
+ "RUCK",
26
+ "JAP",
27
+ "RECEPTION JAP"
28
+ ]
29
+
30
+ facteur_pond = 100/3
31
+
32
+ # 1. Calcul du score pondéré par (match, joueuse, action)
33
+ df_grouped = df.groupby(
34
+ ["Match", "Prenom", "Nom", "Action"]
35
+ ).apply(
36
+ lambda g: pd.Series({
37
+ "score_pondere": (g["Niveau"] * g["Nb_actions"]).sum() / g["Nb_actions"].sum(),
38
+ "nb_total_actions": g["Nb_actions"].sum()
39
+ })
40
+ ).reset_index()
41
+
42
+ # Filtrage sur les actions intéressées
43
+ score_actions = df_grouped[df_grouped["Action"].str.upper().isin(actions_interessees)]
44
+
45
+ # 2. Calcul de la note moyenne par (match, joueuse)
46
+ score_actions["note_match_joueuse"] = score_actions.groupby(
47
+ ["Match", "Prenom", "Nom"]
48
+ )["score_pondere"].transform("mean") * facteur_pond
49
+
50
+ score_match_joueuse = score_actions.drop_duplicates(
51
+ subset=["Match", "Prenom", "Nom"]
52
+ )[["Match", "Prenom", "Nom", "note_match_joueuse"]]
53
+
54
+ # 3. Calcul de la note moyenne par match
55
+ score_match = score_match_joueuse.groupby(["Match"]).apply(
56
+ lambda g: pd.Series({
57
+ "note_match_joueuse": g["note_match_joueuse"].mean()
58
+ })
59
+ ).reset_index()
60
+
61
+ # 4. Métrique globale
62
+ global_average = round(score_match["note_match_joueuse"].mean(), 2)
63
+
64
+ return {
65
+ 'by_action': score_actions,
66
+ 'by_player_match': score_match_joueuse,
67
+ 'by_match': score_match,
68
+ 'global_average': global_average
69
+ }
70
+
71
+ @st.cache_data
72
+ def get_global_score(df):
73
+ """
74
+ Retourne uniquement la métrique globale (pour usage immédiat)
75
+ """
76
+ scoring_data = calculate_player_scoring(df)
77
+ return scoring_data['global_average']
78
+
79
+ @st.cache_data
80
+ def get_top_players(df, n_players=10):
81
+ """
82
+ Retourne le top N des joueuses par note moyenne
83
+ """
84
+ scoring_data = calculate_player_scoring(df)
85
+
86
+ top_players = scoring_data['by_player_match'].groupby(['Prenom', 'Nom']).agg({
87
+ 'note_match_joueuse': 'mean'
88
+ }).reset_index().sort_values('note_match_joueuse', ascending=False).head(n_players)
89
+
90
+ return top_players
91
+
92
+ @st.cache_data
93
+ def get_match_scores(df):
94
+ """
95
+ Retourne les scores par match (pour charts futurs)
96
+ """
97
+ scoring_data = calculate_player_scoring(df)
98
+ return scoring_data['by_match']
99
+
100
+ @st.cache_data
101
+ def get_player_match_scores(df):
102
+ """
103
+ Retourne les scores par joueuse-match (pour charts futurs)
104
+ """
105
+ scoring_data = calculate_player_scoring(df)
106
+ return scoring_data['by_player_match']
streamlit_app/assets/Logo_Stade_Toulousain_Rugby.png ADDED
streamlit_app/assets/style.css ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ===============================
2
+ STREAMLIT UI CLEANUP
3
+ =============================== */
4
+
5
+ /* Supprimer la barre supérieure complète */
6
+ header[data-testid="stHeader"] {
7
+ display: none;
8
+ }
9
+
10
+ /* Supprimer le footer */
11
+ footer {
12
+ display: none;
13
+ }
14
+
15
+ /* Supprimer le menu hamburger */
16
+ #MainMenu {
17
+ display: none;
18
+ }
19
+
20
+ /* ===============================
21
+ PADDING ET SPACING PERSONNALISÉS
22
+ =============================== */
23
+
24
+ /* Votre padding spécifique - MODIFIABLE ICI */
25
+ .st-emotion-cache-1jicfl2 {
26
+ width: 100%;
27
+ padding: 2rem 1rem 2rem !important; /* Réduire de 6rem à 2rem */
28
+ min-width: auto;
29
+ max-width: initial;
30
+ }
31
+
32
+ /* Réduire l'espacement général en haut */
33
+ .stAppViewContainer .main .block-container {
34
+ padding-top: 1rem;
35
+ padding-bottom: 1rem;
36
+ }
37
+
38
+ /* Espacement pour le contenu principal */
39
+ .main .block-container {
40
+ padding: 1rem 2rem;
41
+ }
42
+
43
+ /* ===============================
44
+ STYLES POUR L'APPLICATION RUGBY
45
+ =============================== */
46
+
47
+ /* Titre principal */
48
+ .main-header {
49
+ font-size: 2.5rem;
50
+ color: #CC0C13;
51
+ text-align: center;
52
+ margin-bottom: 2rem;
53
+ /* border-bottom: 3px solid #000000; */
54
+ padding-bottom: 3rem;
55
+ font-weight: bold;
56
+ }
57
+
58
+ /* Logo et header combinés */
59
+ .rugby-header {
60
+ display: flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ gap: 1rem;
64
+ margin-bottom: 2rem;
65
+ }
66
+
67
+ .rugby-header img {
68
+ height: 60px;
69
+ width: auto;
70
+ }
71
+
72
+ /* Cartes métriques */
73
+ .metric-card {
74
+ background: linear-gradient(135deg, #f0f8ff 0%, #e6f3ff 100%);
75
+ padding: 1.5rem;
76
+ border-radius: 15px;
77
+ border-left: 5px solid #1f4e79;
78
+ margin: 0.5rem 0;
79
+ box-shadow: 0 2px 10px rgba(31, 78, 121, 0.1);
80
+ transition: transform 0.2s ease;
81
+ }
82
+
83
+ .metric-card:hover {
84
+ transform: translateY(-2px);
85
+ box-shadow: 0 4px 20px rgba(31, 78, 121, 0.15);
86
+ }
87
+
88
+ /* ===============================
89
+ SIDEBAR PERSONNALISÉE
90
+ =============================== */
91
+
92
+ /* Style pour la sidebar */
93
+ .css-1d391kg {
94
+ background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
95
+ }
96
+
97
+ /* Titres dans la sidebar */
98
+ .css-1d391kg h1, .css-1d391kg h2, .css-1d391kg h3 {
99
+ color: #1f4e79;
100
+ font-weight: bold;
101
+ }
102
+
103
+ /* ===============================
104
+ COMPOSANTS STREAMLIT
105
+ =============================== */
106
+
107
+ /* Style pour les selectbox */
108
+ .stSelectbox > div > div {
109
+ border: 2px solid #1f4e79;
110
+ border-radius: 10px;
111
+ }
112
+
113
+ /* Style pour les métriques Streamlit */
114
+ [data-testid="metric-container"] {
115
+ background: white;
116
+ border: 1px solid #e0e0e0;
117
+ padding: 1rem;
118
+ border-radius: 10px;
119
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
120
+ }
121
+
122
+ /* Tables et dataframes */
123
+ .stDataFrame {
124
+ border: none;
125
+ }
126
+
127
+ .stDataFrame > div {
128
+ border-radius: 10px;
129
+ overflow: hidden;
130
+ }
131
+
132
+ /* ===============================
133
+ BOUTONS PERSONNALISÉS
134
+ =============================== */
135
+
136
+ /* Style par défaut pour tous les boutons (transparent) */
137
+ .stButton > button {
138
+ background: transparent !important;
139
+ color: black !important;
140
+ border: none !important;
141
+ border-radius: 0 !important;
142
+ padding: 0.5rem 1rem !important;
143
+ font-weight: normal !important;
144
+ transition: all 0.3s ease !important;
145
+ box-shadow: none !important;
146
+ }
147
+
148
+ .stButton > button:hover {
149
+ background: rgba(40, 40, 40, 0.15) !important;
150
+ border-radius: 10px !important;
151
+ transform: translateX(5px) !important;
152
+ }
153
+
154
+ /* Style pour le bouton actif (page courante) - seulement quand il est focus/active */
155
+ .stButton > button:focus,
156
+ .stButton > button:active {
157
+ background: linear-gradient(135deg, #000000 0%, #252525 100%) !important;
158
+ color: white !important;
159
+ border: none !important;
160
+ border-radius: 10px !important;
161
+ padding: 0.5rem 1.5rem !important;
162
+ font-weight: bold !important;
163
+ box-shadow: 0 2px 8px rgba(204, 12, 19, 0.3) !important;
164
+ }
165
+
166
+ /* Supprimer le style rouge par défaut pour tous les boutons */
167
+ .stButton > button {
168
+ background: transparent !important;
169
+ color: black !important;
170
+ }
171
+
172
+ /* Style spécial pour le bouton de la page courante via session state */
173
+ .stButton > button[data-active="true"] {
174
+ background: linear-gradient(135deg, #000000 0%, #252525 100%) !important;
175
+ color: white !important;
176
+ border-radius: 10px !important;
177
+ font-weight: bold !important;
178
+ box-shadow: 0 2px 8px rgba(204, 12, 19, 0.3) !important;
179
+ }
180
+
181
+ /* ===============================
182
+ RESPONSIVE DESIGN
183
+ =============================== */
184
+
185
+ @media (max-width: 768px) {
186
+ .main-header {
187
+ font-size: 2rem;
188
+ }
189
+
190
+ .st-emotion-cache-1jicfl2 {
191
+ padding: 1rem 0.5rem 1rem !important;
192
+ }
193
+
194
+ .rugby-header {
195
+ flex-direction: column;
196
+ }
197
+ }
198
+
199
+ /* ===============================
200
+ CLASSES UTILITAIRES
201
+ =============================== */
202
+
203
+ .text-center {
204
+ text-align: center;
205
+ }
206
+
207
+ .text-rugby {
208
+ color: #1f4e79;
209
+ }
210
+
211
+ .bg-rugby {
212
+ background-color: #f0f8ff;
213
+ }
214
+
215
+ .border-rugby {
216
+ border: 2px solid #1f4e79;
217
+ }
218
+
219
+ /* Style personnalisé pour le titre avec logo */
220
+ .rugby-header-custom {
221
+ display: flex;
222
+ align-items: center;
223
+ justify-content: space-between;
224
+ margin-bottom: 2rem;
225
+ padding: 1rem 0;
226
+ position: relative;
227
+ }
228
+
229
+ .rugby-header-custom .title-left {
230
+ flex: 1;
231
+ text-align: right;
232
+ padding-right: 4rem;
233
+ }
234
+
235
+ .rugby-header-custom .logo-center {
236
+ position: absolute;
237
+ left: 50%;
238
+ transform: translateX(-50%);
239
+ z-index: 1;
240
+ }
241
+
242
+ .rugby-header-custom .title-right {
243
+ flex: 1;
244
+ text-align: left;
245
+ padding-left: 2rem;
246
+ }
247
+
248
+ .title-left, .title-right {
249
+ font-size: 2.5rem;
250
+ color: #000000;
251
+ font-weight: bold;
252
+ text-transform: uppercase;
253
+ }
254
+
255
+ .logo-center {
256
+ height: 80px;
257
+ width: auto;
258
+ filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
259
+ }
260
+
261
+ /* Responsive pour mobile */
262
+ @media (max-width: 768px) {
263
+ .rugby-header-custom {
264
+ flex-direction: column;
265
+ gap: 1rem;
266
+ position: static;
267
+ }
268
+
269
+ .rugby-header-custom .title-left,
270
+ .rugby-header-custom .title-right {
271
+ flex: none;
272
+ text-align: center;
273
+ padding: 0;
274
+ font-size: 1.8rem;
275
+ }
276
+
277
+ .rugby-header-custom .logo-center {
278
+ position: static;
279
+ transform: none;
280
+ height: 60px;
281
+ }
282
+ }
streamlit_app/charts/__init__.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Module de graphiques pour l'analyse rugby"""
2
+
3
+ from .player_charts import (
4
+ create_top_players_chart,
5
+ create_player_profile_chart,
6
+ create_player_evolution_chart,
7
+ create_player_comparison_radar,
8
+ create_player_actions_pie,
9
+ create_player_level_distribution,
10
+ create_performance_heatmap, # ← NOUVEAU
11
+ create_team_activity_heatmap, # ← NOUVEAU
12
+ create_performance_comparison_chart, # ← NOUVEAU
13
+ create_performance_violin_chart
14
+ )
15
+
16
+ from .match_charts import (
17
+ create_matches_ranking_chart,
18
+ create_match_comparison_chart,
19
+ create_matches_activity_chart,
20
+ create_actions_distribution_chart
21
+ )
22
+
23
+ __all__ = [
24
+ 'create_top_players_chart',
25
+ 'create_player_profile_chart',
26
+ 'create_player_evolution_chart',
27
+ 'create_player_comparison_radar',
28
+ 'create_player_actions_pie',
29
+ 'create_player_level_distribution',
30
+ 'create_performance_heatmap', # ← NOUVEAU
31
+ 'create_team_activity_heatmap', # ← NOUVEAU
32
+ 'create_matches_ranking_chart',
33
+ 'create_match_comparison_chart',
34
+ 'create_matches_activity_chart',
35
+ 'create_actions_distribution_chart'
36
+ ]
streamlit_app/charts/comparison_charts.py ADDED
File without changes
streamlit_app/charts/config.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Configuration et constantes pour les graphiques"""
2
+
3
+ # Couleurs Stade Toulousain
4
+ COLORS = {
5
+ 'primary': '#CC0C13', # Rouge principal
6
+ 'secondary': '#000000', # Noir
7
+ 'white': '#FFFFFF', # Blanc
8
+ 'light_red': 'rgba(204, 12, 19, 0.2)',
9
+ 'gray': '#666666'
10
+ }
11
+
12
+ # Palette pour dégradés STANDARD (noir -> rouge)
13
+ COLORSCALE = [[0, COLORS['secondary']], [1, COLORS['primary']]]
14
+
15
+ # Palette pour dégradés HEATMAP (blanc -> rouge)
16
+ COLORSCALE_HEATMAP = [[0, COLORS['white']], [1, COLORS['primary']]]
17
+
18
+ # Palette inversée (rouge -> blanc) pour certains cas
19
+ COLORSCALE_REVERSED = [[0, COLORS['primary']], [1, COLORS['white']]]
20
+
21
+ # Couleurs discrètes
22
+ DISCRETE_COLORS = [COLORS['primary'], COLORS['secondary'], COLORS['white'], COLORS['gray']]
23
+
24
+ # Style de base réutilisable
25
+ BASE_LAYOUT = {
26
+ 'plot_bgcolor': 'rgba(248, 249, 250, 0.5)',
27
+ 'paper_bgcolor': 'rgba(0,0,0,0)',
28
+ 'font': dict(family='Arial', color=COLORS['secondary'], size=11)
29
+ }
30
+
31
+ def remove_colorscale_legend(fig):
32
+ """Fonction utilitaire pour enlever la colorscale de n'importe quel graphique"""
33
+ fig.update_traces(showscale=False)
34
+ return fig
35
+
36
+ def apply_stade_style(fig, title=None, remove_colorbar=True):
37
+ """Applique le style Stade Toulousain avec option pour enlever la colorbar"""
38
+
39
+ if remove_colorbar:
40
+ fig.update_traces(showscale=False)
41
+
42
+ fig.update_layout(
43
+ **BASE_LAYOUT,
44
+ title={
45
+ 'text': title,
46
+ 'font': dict(color=COLORS['primary'], size=16, family='Arial Black'),
47
+ 'x': 0.5,
48
+ 'xanchor': 'center'
49
+ } if title else None
50
+ )
51
+
52
+ return fig
streamlit_app/charts/match_charts.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import plotly.express as px
3
+ from .config import COLORS, COLORSCALE, BASE_LAYOUT
4
+
5
+ def create_matches_activity_chart(df):
6
+ """Graphique d'activité par match"""
7
+
8
+ match_stats = df.groupby('Match').agg({
9
+ 'Nb_actions': 'sum',
10
+ 'Nom': 'nunique'
11
+ }).reset_index()
12
+
13
+ match_stats.columns = ['Match', 'Total_actions', 'Nb_joueuses']
14
+ match_stats = match_stats.sort_values('Total_actions', ascending=True)
15
+
16
+ fig = px.bar(
17
+ match_stats,
18
+ x='Total_actions',
19
+ y='Match',
20
+ orientation='h',
21
+ title="Activité totale par match",
22
+ color='Total_actions',
23
+ color_continuous_scale=COLORSCALE
24
+ )
25
+
26
+ fig.update_layout(
27
+ **BASE_LAYOUT,
28
+ height=500,
29
+ coloraxis_showscale=False,
30
+ yaxis={
31
+ 'categoryorder': 'total ascending',
32
+ 'title': '',
33
+ 'tickfont': dict(color=COLORS['secondary'], size=11)
34
+ },
35
+ xaxis={
36
+ 'title': 'Nombre total d\'actions',
37
+ 'tickfont': dict(color=COLORS['secondary'], size=11)
38
+ },
39
+ title={
40
+ 'font': dict(color=COLORS['secondary'], size=16, family='Arial Black'),
41
+ 'x': 0.5,
42
+ 'xanchor': 'center'
43
+ }
44
+ )
45
+
46
+ return fig
47
+
48
+ def create_actions_distribution_chart(df):
49
+ """Distribution des types d'actions avec dégradé basé sur l'importance"""
50
+
51
+ action_stats = df.groupby('Action')['Nb_actions'].sum().reset_index()
52
+
53
+ # Calculer le pourcentage pour chaque action
54
+ total_actions = action_stats['Nb_actions'].sum()
55
+ action_stats['Pourcentage'] = (action_stats['Nb_actions'] / total_actions) * 100
56
+
57
+ # Trier par importance (pourcentage décroissant)
58
+ action_stats = action_stats.sort_values('Pourcentage', ascending=False)
59
+
60
+ # Créer un dégradé de couleurs basé sur l'importance
61
+ n_actions = len(action_stats)
62
+ colors = []
63
+ for i in range(n_actions):
64
+ # Dégradé de noir (moins important) à rouge (plus important)
65
+ ratio = i / (n_actions - 1) if n_actions > 1 else 0
66
+ color = f"rgba({204 * ratio + 0 * (1-ratio):.0f}, {12 * ratio + 0 * (1-ratio):.0f}, {19 * ratio + 0 * (1-ratio):.0f}, 1)"
67
+ colors.append(color)
68
+
69
+ fig = px.pie(
70
+ action_stats,
71
+ values='Nb_actions',
72
+ names='Action',
73
+ title="Répartition des types d'actions",
74
+ color_discrete_sequence=colors
75
+ )
76
+
77
+ fig.update_layout(
78
+ **BASE_LAYOUT,
79
+ title={
80
+ 'font': dict(color=COLORS['secondary'], size=16, family='Arial Black'),
81
+ 'x': 0.4,
82
+ 'xanchor': 'center'
83
+ }
84
+ )
85
+
86
+ # Ajouter des bordures blanches entre chaque part
87
+ fig.update_traces(
88
+ marker=dict(line=dict(color='white', width=2))
89
+ )
90
+
91
+ return fig
92
+
93
+ def create_matches_ranking_chart(df):
94
+ """Même fonction que create_matches_activity_chart pour compatibilité"""
95
+ return create_matches_activity_chart(df)
96
+
97
+ def create_match_comparison_chart(df, match1, match2):
98
+ """Comparaison entre deux matchs - simple"""
99
+
100
+ comparison_data = []
101
+
102
+ for match in [match1, match2]:
103
+ match_data = df[df['Match'] == match]
104
+ stats = {
105
+ 'Match': match,
106
+ 'Total_actions': match_data['Nb_actions'].sum(),
107
+ 'Nb_joueuses': match_data['Nom'].nunique()
108
+ }
109
+ comparison_data.append(stats)
110
+
111
+ df_comparison = pd.DataFrame(comparison_data)
112
+
113
+ fig = px.bar(
114
+ df_comparison,
115
+ x='Match',
116
+ y='Total_actions',
117
+ title=f"Comparaison {match1} vs {match2}",
118
+ color='Match',
119
+ color_discrete_sequence=[COLORS['primary'], COLORS['secondary']]
120
+ )
121
+
122
+ fig.update_layout(**BASE_LAYOUT)
123
+
124
+ return fig
streamlit_app/charts/performance_charts.py ADDED
File without changes
streamlit_app/charts/player_charts.py ADDED
@@ -0,0 +1,519 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import plotly.express as px
3
+ import plotly.graph_objects as go
4
+ from .config import COLORS, COLORSCALE, COLORSCALE_HEATMAP, BASE_LAYOUT, apply_stade_style
5
+
6
+ def create_top_players_chart(df, n_players=10):
7
+ """Graphique complet du top des joueuses basé sur les notes moyennes"""
8
+
9
+ from analytics.scoring import calculate_player_scoring
10
+
11
+ # Calculer les notes moyennes par joueuse
12
+ scoring_data = calculate_player_scoring(df)['by_player_match']
13
+ top_players = scoring_data.groupby(['Prenom', 'Nom'])['note_match_joueuse'].mean().reset_index()
14
+ top_players['Nom_complet'] = top_players['Prenom'] + ' ' + top_players['Nom']
15
+ top_players = top_players.nlargest(n_players, 'note_match_joueuse')
16
+
17
+ # Créer le graphique AVEC son style
18
+ fig = px.bar(
19
+ top_players,
20
+ x='note_match_joueuse',
21
+ y='Nom_complet',
22
+ orientation='h',
23
+ title=f"Top {n_players} des joueuses par note moyenne",
24
+ color='note_match_joueuse',
25
+ color_continuous_scale=COLORSCALE
26
+ )
27
+
28
+ # Style intégré directement
29
+ fig.update_layout(
30
+ **BASE_LAYOUT,
31
+ height=400,
32
+ coloraxis_showscale=False,
33
+ yaxis={
34
+ 'categoryorder': 'total ascending',
35
+ 'title': '',
36
+ 'tickfont': dict(color=COLORS['secondary'], size=12)
37
+ },
38
+ xaxis={
39
+ 'title': 'Note moyenne',
40
+ 'tickfont': dict(color=COLORS['secondary'], size=11)
41
+ },
42
+ title={
43
+ 'font': dict(color=COLORS['secondary'], size=16, family='Arial Black'),
44
+ 'x': 0.5,
45
+ 'xanchor': 'center'
46
+ },
47
+ xaxis_range=[top_players['note_match_joueuse'].min()-1, top_players['note_match_joueuse'].max()+1]
48
+ )
49
+
50
+ # Axes
51
+ fig.update_xaxes(
52
+ showgrid=True,
53
+ gridcolor=COLORS['light_red'],
54
+ zeroline=True,
55
+ zerolinecolor=COLORS['primary']
56
+ )
57
+
58
+ fig.update_yaxes(showgrid=False)
59
+ fig.update_xaxes(showgrid=False)
60
+
61
+ return fig
62
+
63
+ def create_player_actions_pie(df, player_name):
64
+ """Graphique en camembert des actions d'une joueuse"""
65
+
66
+ player_data = df[df['Nom'].str.contains(player_name, case=False)]
67
+ if player_data.empty:
68
+ return None
69
+
70
+ action_breakdown = player_data.groupby('Action')['Nb_actions'].sum().reset_index()
71
+
72
+ fig = px.pie(
73
+ action_breakdown,
74
+ values='Nb_actions',
75
+ names='Action',
76
+ title=f"Actions de {player_name}",
77
+ color_discrete_sequence=[COLORS['primary'], COLORS['secondary'], COLORS['gray']]
78
+ )
79
+
80
+ # Style spécifique au pie chart
81
+ fig.update_layout(
82
+ **BASE_LAYOUT,
83
+ title={
84
+ 'font': dict(color=COLORS['secondary'], size=16, family='Arial Black'),
85
+ 'x': 0.5,
86
+ 'xanchor': 'center'
87
+ }
88
+ )
89
+
90
+ fig.update_traces(
91
+ textfont_size=12,
92
+ textfont_color='white',
93
+ marker=dict(line=dict(color='white', width=2))
94
+ )
95
+
96
+ return fig
97
+
98
+ def create_player_level_distribution(df, player_name):
99
+ """Distribution des niveaux pour une joueuse"""
100
+
101
+ player_data = df[df['Nom'].str.contains(player_name, case=False)]
102
+ if player_data.empty:
103
+ return None
104
+
105
+ level_data = player_data.groupby('Niveau')['Nb_actions'].sum().reset_index()
106
+ level_data['Niveau_label'] = level_data['Niveau'].map({
107
+ 0: '0', 1: '1', 2: '2', 3: '3'
108
+ })
109
+
110
+ fig = px.bar(
111
+ level_data,
112
+ x='Niveau_label',
113
+ y='Nb_actions',
114
+ title=f"Niveau de performance - {player_name}",
115
+ color='Niveau',
116
+ color_continuous_scale=COLORSCALE
117
+ )
118
+
119
+ fig.update_layout(
120
+ **BASE_LAYOUT,
121
+ xaxis={'title': 'Niveau de performance'},
122
+ yaxis={'title': 'Nombre d\'actions'},
123
+ title={
124
+ 'font': dict(color=COLORS['secondary'], size=16, family='Arial Black'),
125
+ 'x': 0.5,
126
+ 'xanchor': 'center'
127
+ }
128
+ )
129
+
130
+ return fig
131
+
132
+ def create_player_profile_chart(df, player_name):
133
+ """Graphique de profil d'une joueuse - alias pour create_player_actions_pie"""
134
+ return create_player_actions_pie(df, player_name)
135
+
136
+ def create_player_evolution_chart(df, player_name):
137
+ """Évolution d'une joueuse par match"""
138
+
139
+ player_data = df[df['Nom'].str.contains(player_name, case=False)]
140
+ if player_data.empty:
141
+ return None
142
+
143
+ evolution = player_data.groupby('Match')['Nb_actions'].sum().reset_index()
144
+
145
+ fig = px.line(
146
+ evolution,
147
+ x='Match',
148
+ y='Nb_actions',
149
+ title=f"Évolution de {player_name}",
150
+ markers=True
151
+ )
152
+
153
+ fig.update_traces(
154
+ line_color=COLORS['primary'],
155
+ marker_color=COLORS['primary'],
156
+ marker_size=8
157
+ )
158
+
159
+ fig.update_layout(
160
+ **BASE_LAYOUT,
161
+ title={
162
+ 'font': dict(color=COLORS['primary'], size=16, family='Arial Black'),
163
+ 'x': 0.5,
164
+ 'xanchor': 'center'
165
+ }
166
+ )
167
+
168
+ return fig
169
+
170
+ def create_player_comparison_radar(df, players_list):
171
+ """Graphique radar simple pour comparer des joueuses"""
172
+
173
+ # Version simplifiée pour éviter les erreurs
174
+ if not players_list or len(players_list) == 0:
175
+ return None
176
+
177
+ # Pour l'instant, retournons un graphique simple
178
+ player = players_list[0]
179
+ player_data = df[df['Nom'].str.contains(player, case=False)]
180
+
181
+ if player_data.empty:
182
+ return None
183
+
184
+ action_breakdown = player_data.groupby('Action')['Nb_actions'].sum().reset_index()
185
+
186
+ fig = px.bar(
187
+ action_breakdown,
188
+ x='Action',
189
+ y='Nb_actions',
190
+ title=f"Profil de {player}",
191
+ color='Nb_actions',
192
+ color_continuous_scale=COLORSCALE
193
+ )
194
+
195
+ fig.update_layout(**BASE_LAYOUT)
196
+
197
+ return fig
198
+
199
+ def create_performance_heatmap(df, n_players=15):
200
+ """Heatmap des performances - BLANC -> ROUGE uniquement"""
201
+
202
+ # Préparer les données pour la heatmap
203
+ heatmap_data = df.groupby(['Nom', 'Match'])['Nb_actions'].sum().reset_index()
204
+ heatmap_pivot = heatmap_data.pivot(index='Nom', columns='Match', values='Nb_actions').fillna(0)
205
+
206
+ # Limiter aux meilleures joueuses pour la lisibilité
207
+ top_players = df.groupby('Nom')['Nb_actions'].sum().nlargest(n_players).index
208
+ heatmap_pivot = heatmap_pivot.loc[top_players]
209
+
210
+ # Créer la heatmap avec BLANC -> ROUGE ← CHANGEMENT ICI
211
+ fig = px.imshow(
212
+ heatmap_pivot,
213
+ aspect='auto',
214
+ title=f"Intensité d'activité - Top {n_players}",
215
+ color_continuous_scale=COLORSCALE_HEATMAP, # ← BLANC -> ROUGE
216
+ labels={'color': 'Nb actions'}
217
+ )
218
+
219
+ # ENLEVER LA COLORSCALE LEGEND
220
+ fig.update_traces(showscale=False)
221
+
222
+ # Appliquer le style Stade Toulousain
223
+ fig.update_layout(
224
+ **BASE_LAYOUT,
225
+ height=600,
226
+ coloraxis_showscale=False,
227
+ title={
228
+ 'font': dict(color=COLORS['secondary'], size=16, family='Arial Black'),
229
+ 'x': 0.5,
230
+ 'xanchor': 'center'
231
+ },
232
+ # Style des axes
233
+ xaxis={
234
+ 'title': 'Matchs',
235
+ 'tickfont': dict(color=COLORS['secondary'], size=10),
236
+ 'tickangle': 45 # Incliner les noms de matchs pour la lisibilité
237
+ },
238
+ yaxis={
239
+ 'title': 'Joueuses',
240
+ 'tickfont': dict(color=COLORS['secondary'], size=10)
241
+ }
242
+ )
243
+
244
+ # Personnaliser le hover
245
+ fig.update_traces(
246
+ hoverlabel=dict(
247
+ bgcolor=COLORS['primary'],
248
+ font_color='white',
249
+ font_size=12
250
+ )
251
+ )
252
+
253
+ return fig
254
+
255
+ def create_team_activity_heatmap(df):
256
+ """Heatmap par action/niveau - BLANC -> ROUGE uniquement"""
257
+
258
+ # Préparer les données : Actions vs Niveaux
259
+ heatmap_data = df.groupby(['Action', 'Niveau'])['Nb_actions'].sum().reset_index()
260
+ heatmap_pivot = heatmap_data.pivot(index='Action', columns='Niveau', values='Nb_actions').fillna(0)
261
+
262
+ # Renommer les colonnes pour plus de clarté
263
+ level_labels = {0: '0', 1: '1', 2: '2', 3: '3'}
264
+ heatmap_pivot.columns = [level_labels.get(col, f'Niveau {col}') for col in heatmap_pivot.columns]
265
+
266
+ fig = px.imshow(
267
+ heatmap_pivot,
268
+ aspect='auto',
269
+ title="Intensité par action et niveau",
270
+ color_continuous_scale=COLORSCALE_HEATMAP
271
+ )
272
+
273
+ # Style Stade Toulousain
274
+ fig.update_traces(showscale=False)
275
+
276
+ fig.update_layout(
277
+ **BASE_LAYOUT,
278
+ height=400,
279
+ coloraxis_showscale=False,
280
+ title={
281
+ 'font': dict(color=COLORS['secondary'], size=16, family='Arial Black'),
282
+ 'x': 0.6,
283
+ 'xanchor': 'center'
284
+ },
285
+ xaxis={
286
+ 'title': '',
287
+ 'tickfont': dict(color=COLORS['secondary'], size=11)
288
+ },
289
+ yaxis={
290
+ 'title': '',
291
+ 'tickfont': dict(color=COLORS['secondary'], size=11)
292
+ }
293
+ )
294
+
295
+ fig.update_traces(
296
+ hoverlabel=dict(
297
+ bgcolor=COLORS['primary'],
298
+ font_color='white',
299
+ font_size=12
300
+ )
301
+ )
302
+
303
+ return fig
304
+
305
+ def create_performance_heatmap_advanced(df, n_players=15, match_filter=None, colorscale_type='stade'):
306
+ """Heatmap avancée avec options"""
307
+
308
+ # Filtrer par matchs si spécifié
309
+ if match_filter:
310
+ df_filtered = df[df['Match'].isin(match_filter)]
311
+ else:
312
+ df_filtered = df
313
+
314
+ # Préparer les données
315
+ heatmap_data = df_filtered.groupby(['Nom', 'Match'])['Nb_actions'].sum().reset_index()
316
+ heatmap_pivot = heatmap_data.pivot(index='Nom', columns='Match', values='Nb_actions').fillna(0)
317
+
318
+ # Top joueuses
319
+ top_players = df_filtered.groupby('Nom')['Nb_actions'].sum().nlargest(n_players).index
320
+ heatmap_pivot = heatmap_pivot.loc[top_players]
321
+
322
+ # Choisir la colorscale
323
+ if colorscale_type == 'stade':
324
+ colorscale = COLORSCALE
325
+ elif colorscale_type == 'reversed':
326
+ colorscale = [[0, COLORS['primary']], [1, COLORS['secondary']]]
327
+ else:
328
+ colorscale = COLORSCALE
329
+
330
+ fig = px.imshow(
331
+ heatmap_pivot,
332
+ aspect='auto',
333
+ title=f"Heatmap personnalisée - {len(heatmap_pivot.index)} joueuses",
334
+ color_continuous_scale=colorscale
335
+ )
336
+
337
+ # Style
338
+ fig.update_traces(showscale=False)
339
+
340
+ fig.update_layout(
341
+ **BASE_LAYOUT,
342
+ coloraxis_showscale=False,
343
+ height=500,
344
+ title={
345
+ 'font': dict(color=COLORS['secondary'], size=16, family='Arial Black'),
346
+ 'x': 0.5,
347
+ 'xanchor': 'center'
348
+ }
349
+ )
350
+
351
+ return fig
352
+
353
+ def create_performance_comparison_chart(df):
354
+ """Graphique combiné : barres (notes moyennes par action) + ligne (nombre d'actions) + points (meilleures/moins bonnes notes)"""
355
+
356
+ from analytics.scoring import calculate_player_scoring
357
+
358
+ # Calculer les données de scoring
359
+ scoring_data = calculate_player_scoring(df)['by_action']
360
+ avg_scores = scoring_data.groupby(['Match', 'Action'])['note_match_joueuse'].mean().reset_index()
361
+
362
+ # Calculer la plage dynamique : min note globale -10 à max note globale +10
363
+ min_note = scoring_data['note_match_joueuse'].min()
364
+ max_note = scoring_data['note_match_joueuse'].max()
365
+ y_range = [50, 77]
366
+
367
+ # Créer un graphique avec deux axes Y
368
+ fig = go.Figure()
369
+
370
+ # Ajouter le graphique en barres sur l'axe Y gauche avec dégradé de couleurs
371
+ actions = avg_scores['Action'].unique()
372
+ n_actions = len(actions)
373
+
374
+ for i, action in enumerate(actions):
375
+ action_data = avg_scores[avg_scores['Action'] == action]
376
+ # Calculer la couleur avec un dégradé de secondary (noir) à primary (rouge)
377
+ ratio = i / (n_actions - 1) if n_actions > 1 else 0
378
+ color = f"rgba({204 * ratio + 0 * (1-ratio):.0f}, {12 * ratio + 0 * (1-ratio):.0f}, {19 * ratio + 0 * (1-ratio):.0f}, 1)"
379
+
380
+ fig.add_trace(
381
+ go.Bar(
382
+ x=action_data['Match'],
383
+ y=action_data['note_match_joueuse'],
384
+ name=action,
385
+ yaxis='y',
386
+ marker_color=color
387
+ )
388
+ )
389
+
390
+ # Ajouter les meilleures notes par match sur l'axe Y droit
391
+ best_scores_with_names = scoring_data.loc[scoring_data.groupby(['Match'])['note_match_joueuse'].idxmax()][['Match', 'Prenom', 'Nom', 'note_match_joueuse']].reset_index(drop=True)
392
+ fig.add_trace(
393
+ go.Scatter(
394
+ x=best_scores_with_names['Match'],
395
+ y=best_scores_with_names['note_match_joueuse'],
396
+ name='Meilleure note par match',
397
+ yaxis='y2',
398
+ line=dict(width=0),
399
+ marker=dict(color='#2E8B57', size=12),
400
+ mode='markers+text',
401
+ text=best_scores_with_names['Prenom'] + ' ' + best_scores_with_names['Nom'],
402
+ textposition='top center',
403
+ textfont=dict(size=11, color='#2E8B57')
404
+ )
405
+ )
406
+
407
+ # Configuration des axes
408
+ fig.update_layout(
409
+ legend=dict(
410
+ orientation="h",
411
+ yanchor="top",
412
+ y=-0.2,
413
+ xanchor="center",
414
+ x=0.5
415
+ ),
416
+ yaxis=dict(
417
+ title="Note moyenne par action",
418
+ range=y_range,
419
+ side='left'
420
+ ),
421
+ yaxis2=dict(
422
+ title="Meilleure note par match",
423
+ range=[50, 110],
424
+ side='right',
425
+ overlaying='y'
426
+ ),
427
+ barmode='group',
428
+ **BASE_LAYOUT
429
+ )
430
+
431
+ return fig
432
+
433
+ def create_performance_violin_chart(df):
434
+ """Graphique violin plot pour la distribution des notes par match"""
435
+
436
+ from analytics.scoring import calculate_player_scoring
437
+
438
+ # Calculer les données de scoring
439
+ scoring_data = calculate_player_scoring(df)['by_action']
440
+
441
+ # Créer le violin plot
442
+ fig = px.violin(
443
+ scoring_data,
444
+ x="Match",
445
+ y="note_match_joueuse",
446
+ box=True,
447
+ color_discrete_sequence=[COLORS['primary'], COLORS['secondary'], COLORS['gray']]
448
+ )
449
+
450
+ # Appliquer le style Stade Toulousain
451
+ fig.update_layout(
452
+ **BASE_LAYOUT,
453
+ height=500,
454
+ xaxis={
455
+ 'title': 'Matchs',
456
+ 'tickfont': dict(color=COLORS['secondary'], size=11),
457
+ 'tickangle': 45
458
+ },
459
+ yaxis={
460
+ 'title': 'Notes',
461
+ 'tickfont': dict(color=COLORS['secondary'], size=11)
462
+ },
463
+ )
464
+
465
+ # Personnaliser le hover, la boîte et la largeur des violins
466
+ fig.update_traces(
467
+ hoverlabel=dict(
468
+ bgcolor=COLORS['primary'],
469
+ font_color='white',
470
+ font_size=12
471
+ ),
472
+ box=dict(
473
+ fillcolor=COLORS['primary'], # Couleur de remplissage de la boîte
474
+ # line=dict(color=COLORS['primary']) # Couleur de la bordure de la boîte
475
+ ),
476
+ line=dict(
477
+ color=COLORS['secondary']
478
+ ),
479
+ width=1 # Contrôler la largeur des violins (0.1 à 1.0)
480
+ )
481
+
482
+ # Ajouter les meilleures joueuses par match
483
+ best_scores_with_names = scoring_data.loc[scoring_data.groupby(['Match'])['note_match_joueuse'].idxmax()][['Match', 'Prenom', 'Nom', 'note_match_joueuse']].reset_index(drop=True)
484
+
485
+ # Ajouter des points pour les meilleures notes
486
+ fig.add_trace(
487
+ go.Scatter(
488
+ x=best_scores_with_names['Match'],
489
+ y=best_scores_with_names['note_match_joueuse'],
490
+ mode='markers',
491
+ marker=dict(
492
+ color='#ffffff', # Blanc pour les meilleures notes
493
+ size=10,
494
+ line=dict(color='#384454', width=2) # Bordure noire pour plus de contraste
495
+ ),
496
+ name='Meilleure joueuse par match',
497
+ showlegend=False
498
+ )
499
+ )
500
+
501
+ # Ajouter les noms des meilleures joueuses avec des annotations
502
+ for _, row in best_scores_with_names.iterrows():
503
+ fig.add_annotation(
504
+ x=row['Match'],
505
+ y=row['note_match_joueuse'],
506
+ text=row['Nom'],
507
+ showarrow=False,
508
+ yshift=30, # Déplacer le texte plus haut
509
+ font=dict(
510
+ size=12,
511
+ color='#000000',
512
+ family='Arial Black'
513
+ ),
514
+ bgcolor='rgba(255, 255, 255, 0.8)', # Fond blanc semi-transparent
515
+ # bordercolor='#000000',
516
+ borderwidth=1
517
+ )
518
+
519
+ return fig
streamlit_app/components/dashboard.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import plotly.express as px
4
+ import plotly.graph_objects as go
5
+ from plotly.subplots import make_subplots
6
+ from charts import (
7
+ create_top_players_chart,
8
+ create_actions_distribution_chart,
9
+ create_matches_activity_chart,
10
+ create_performance_heatmap, # ← NOUVEAU
11
+ create_team_activity_heatmap, # ← NOUVEAU
12
+ create_performance_comparison_chart, # ← NOUVEAU
13
+ create_performance_violin_chart
14
+ )
15
+ from analytics.scoring import *
16
+
17
+ def show_dashboard(df):
18
+ """Affiche le tableau de bord principal"""
19
+
20
+ st.divider()
21
+
22
+ # Métriques générales
23
+
24
+ metrics_config = [
25
+ {
26
+ "title": "Moy. note match",
27
+ "value": get_global_score(df),
28
+ "format": "{:,.2f}"
29
+ },
30
+ {
31
+ "title": "Total actions",
32
+ "value": df['Nb_actions'].sum(),
33
+ "format": "{:,.0f}"
34
+ },
35
+ {
36
+ "title": "Joueuses actives",
37
+ "value": df['Nom'].nunique(),
38
+ "format": "{}"
39
+ },
40
+ {
41
+ "title": "Matchs analysés",
42
+ "value": df['Match'].nunique(),
43
+ "format": "{}"
44
+ },
45
+ {
46
+ "title": "Moy. actions/joueuse",
47
+ "value": df.groupby(['Prenom', 'Nom'])['Nb_actions'].sum().mean(),
48
+ "format": "{:.0f}"
49
+ }
50
+ ]
51
+
52
+ # Création des colonnes dynamiquement
53
+ cols = st.columns(len(metrics_config))
54
+
55
+ # Affichage des métriques avec une boucle
56
+ for i, metric in enumerate(metrics_config):
57
+ with cols[i]:
58
+ formatted_value = metric["format"].format(metric["value"])
59
+ st.metric(metric["title"], formatted_value)
60
+
61
+
62
+ st.divider()
63
+
64
+
65
+
66
+ fig = create_performance_violin_chart(df)
67
+ # fig = create_performance_comparison_chart(df)
68
+ st.plotly_chart(fig, use_container_width=True)
69
+
70
+ # Graphiques principaux
71
+ col1, col2 = st.columns(2)
72
+
73
+ with col1:
74
+ fig = create_top_players_chart(df, n_players=15)
75
+ st.plotly_chart(fig, use_container_width=True)
76
+
77
+ with col2:
78
+ fig = create_actions_distribution_chart(df)
79
+ st.plotly_chart(fig, use_container_width=True)
80
+
81
+ # Graphiques pleine largeur
82
+ # col1, col2 = st.columns(2)
83
+
84
+ # with col1:
85
+ # fig = create_matches_activity_chart(df)
86
+ # st.plotly_chart(fig, use_container_width=True)
87
+
88
+ # with col2:
89
+ # fig = create_team_activity_heatmap(df)
90
+ # st.plotly_chart(fig, use_container_width=True)
91
+
92
+ # st.divider()
93
+
94
+ # fig = create_performance_heatmap(df, n_players=15)
95
+ # st.plotly_chart(fig, use_container_width=True)
96
+
streamlit_app/components/player_analysis.py ADDED
@@ -0,0 +1,507 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ from st_image_carousel import image_carousel
4
+ from st_circular_kpi import circular_kpi
5
+ import base64
6
+ from analytics.scoring import get_player_match_scores
7
+ import plotly.express as px
8
+
9
+
10
+ def calculate_stats(df, column, agg_func='sum', round_digits=1):
11
+ """Calcule min, max, mean pour une colonne groupée par joueur"""
12
+ stats = df.groupby('Nom').agg({column: agg_func})
13
+ return {
14
+ 'min': float(stats.min().round(round_digits).values[0]),
15
+ 'max': float(stats.max().round(round_digits).values[0]),
16
+ 'mean': float(stats.mean().round(round_digits).values[0])
17
+ }
18
+
19
+ def show_player_analysis(df):
20
+ """Composant simple pour l'analyse des joueuses"""
21
+
22
+ # Calcul des statistiques globales
23
+ actions_stats = calculate_stats(df, 'Nb_actions', 'sum')
24
+ matches_stats = calculate_stats(df, 'Match', 'nunique')
25
+
26
+ # Calcul des moyennes par match
27
+ player_avg_stats = df.groupby('Nom').agg({
28
+ 'Nb_actions': 'sum',
29
+ 'Match': 'nunique'
30
+ }).assign(
31
+ avg_per_match=lambda x: x['Nb_actions'] / x['Match']
32
+ )
33
+
34
+ avg_per_match_stats = {
35
+ 'min': float(player_avg_stats['avg_per_match'].min()),
36
+ 'max': float(player_avg_stats['avg_per_match'].max()),
37
+ 'mean': float(player_avg_stats['avg_per_match'].mean().round(1))
38
+ }
39
+
40
+ # Créer un dictionnaire avec les noms uniques des joueurs
41
+ joueurs_images = [
42
+ {
43
+ "name": f"{row['Prenom']} {row['Nom']}",
44
+ "url": ""
45
+ }
46
+ for _, row in sorted(df[['Nom', 'Prenom']].drop_duplicates().iterrows(),
47
+ key=lambda x: (x[1]['Nom'], x[1]['Prenom']))
48
+ ]
49
+
50
+ # Créer un dictionnaire avec les noms uniques des matchs
51
+ matchs_images = [
52
+ {
53
+ "name": match_name,
54
+ "url": ""
55
+ }
56
+ for match_name in sorted(df['Match'].unique())
57
+ ]
58
+
59
+ result = image_carousel(
60
+ images=joueurs_images,
61
+ selected_image=None,
62
+ background_color="#ffffff",
63
+ active_border_color="#000000",
64
+ active_glow_color="rgba(0, 0, 0, 0.7)",
65
+ fallback_background="#ffffff",
66
+ fallback_gradient_end="#ffffff",
67
+ text_color="#000000",
68
+ arrow_color="#31333f",
69
+ key="player_carousel"
70
+ )
71
+
72
+ # Liste des joueuses
73
+ if result and result.get('selected_image'):
74
+ selected_player = result['selected_image'].split(" ")[1]
75
+ else:
76
+ selected_player = None
77
+
78
+ # Tableau des données
79
+ if selected_player:
80
+ # Filtrer les données pour la joueuse sélectionnée
81
+ player_data = df[df['Nom'] == selected_player]
82
+
83
+ # Créer une liste de matchs filtrée pour cette joueuse (seulement les matchs où elle a participé)
84
+ player_matches = player_data.groupby('Match')['Nb_actions'].sum()
85
+ player_matches = player_matches[player_matches > 0].index.tolist() # Seulement les matchs avec des actions
86
+
87
+ matchs_images_filtered = [
88
+ {
89
+ "name": match_name,
90
+ "url": ""
91
+ }
92
+ for match_name in sorted(player_matches)
93
+ ]
94
+
95
+ # Statistiques du joueur
96
+ total_actions = player_data['Nb_actions'].sum()
97
+ nb_matchs = player_data['Match'].nunique()
98
+ avg_per_match = total_actions / nb_matchs if nb_matchs > 0 else 0
99
+
100
+ # Récupérer les scores de la joueuse sélectionnée
101
+ player_scores = get_player_match_scores(df)
102
+ player_scores_filtered = player_scores[
103
+ (player_scores['Prenom'] == player_data['Prenom'].iloc[0]) &
104
+ (player_scores['Nom'] == selected_player)
105
+ ].sort_values('Match')
106
+ # st.write(player_scores_filtered)
107
+
108
+ # Statistiques simples
109
+ col1, col2, col3, col4 = st.columns(4)
110
+
111
+ with col1:
112
+ circular_kpi(
113
+ value=total_actions,
114
+ label="Actions",
115
+ range=(-10, actions_stats['max']),
116
+ min_value=actions_stats['min'],
117
+ max_value=actions_stats['max'],
118
+ mean_value=actions_stats['mean'],
119
+ color_scheme="blue_purple",
120
+ background_color="transparent",
121
+ key="actions_kpi"
122
+ )
123
+
124
+ with col2:
125
+ circular_kpi(
126
+ value=nb_matchs,
127
+ label="Matchs",
128
+ range=(0, matches_stats['max']),
129
+ min_value=matches_stats['min'],
130
+ max_value=matches_stats['max'],
131
+ mean_value=matches_stats['mean'],
132
+ color_scheme="red",
133
+ background_color="transparent",
134
+ key="matches_kpi"
135
+ )
136
+
137
+ with col3:
138
+ circular_kpi(
139
+ value=avg_per_match.round(1),
140
+ label="Moyenne actions par match",
141
+ range=(0, avg_per_match_stats['max']),
142
+ min_value=avg_per_match_stats['min'],
143
+ max_value=avg_per_match_stats['max'],
144
+ mean_value=avg_per_match_stats['mean'],
145
+ color_scheme="green",
146
+ key="avg_per_match_kpi"
147
+ )
148
+ with col4:
149
+ # Calculer les statistiques globales sur les moyennes par joueuse
150
+ all_player_scores = get_player_match_scores(df)
151
+ # Calculer la moyenne par joueuse
152
+ player_averages = all_player_scores.groupby(['Prenom', 'Nom'])['note_match_joueuse'].mean().reset_index()
153
+
154
+ global_score_stats = {
155
+ 'min': float(player_averages['note_match_joueuse'].min().round(1)),
156
+ 'max': float(player_averages['note_match_joueuse'].max().round(1)),
157
+ 'mean': float(player_averages['note_match_joueuse'].mean().round(1))
158
+ }
159
+
160
+ circular_kpi(
161
+ value=player_scores_filtered['note_match_joueuse'].mean().round(1),
162
+ label="Note moyenne",
163
+ range=(0, 100),
164
+ min_value=global_score_stats['min'],
165
+ max_value=global_score_stats['max'],
166
+ mean_value=global_score_stats['mean'],
167
+ color_scheme="blue_purple",
168
+ key="note_moyenne_kpi"
169
+ )
170
+ # st.metric("Note moyenne", f"{player_scores_filtered['note_match_joueuse'].mean():.1f}") #note de la joueuse moyenne sur tous les matchs
171
+
172
+
173
+
174
+ if not player_scores_filtered.empty:
175
+ # Créer un graphique des scores par match
176
+ fig = px.line(
177
+ player_scores_filtered,
178
+ x='Match',
179
+ y='note_match_joueuse',
180
+ markers=True,
181
+ color_discrete_sequence=['black'] # Ligne noire
182
+ )
183
+
184
+ fig.update_layout(
185
+ height=400, # Moins haut
186
+ showlegend=False,
187
+ xaxis_title="", # Pas de titre axe X
188
+ yaxis_title="Note" # Pas de titre axe Y
189
+ )
190
+
191
+ # Ajouter une ligne de moyenne
192
+ avg_score = player_scores_filtered['note_match_joueuse'].mean()
193
+ fig.add_hline(
194
+ y=avg_score,
195
+ line_dash="dash",
196
+ line_color="red",
197
+ annotation_text=f"Moyenne: {avg_score:.1f}",
198
+ annotation_position="top right"
199
+ )
200
+
201
+ # Centrer le graphique
202
+ col1, col2, col3 = st.columns([1, 4, 2])
203
+ with col2:
204
+ st.plotly_chart(fig, use_container_width=False, key="scores_evolution_chart")
205
+ else:
206
+ st.info("Aucun score disponible pour cette joueuse.")
207
+
208
+ st.divider()
209
+
210
+ # Définir les colonnes
211
+ col_stats, col_match = st.columns([4, 1])
212
+
213
+ with col_match:
214
+ result_match = image_carousel(
215
+ images=matchs_images_filtered,
216
+ selected_image=None,
217
+ background_color="#ffffff",
218
+ active_border_color="#000000",
219
+ active_glow_color="rgba(0, 0, 0, 0.7)",
220
+ fallback_background="#ffffff",
221
+ fallback_gradient_end="#ffffff",
222
+ text_color="#000000",
223
+ arrow_color="#31333f",
224
+ orientation="vertical",
225
+ key="match_carousel"
226
+ )
227
+
228
+ # Récupérer le match sélectionné pour le filtrage
229
+ selected_match = None
230
+ if result_match and result_match.get('selected_image'):
231
+ selected_match = result_match['selected_image']
232
+
233
+
234
+ with col_stats:
235
+
236
+ # Filtrer les données selon le match sélectionné
237
+ display_data = player_data
238
+ if selected_match:
239
+ # display_data = player_data[player_data['Match'] == selected_match]
240
+ display_data = player_data[player_data['Match'] == selected_match]
241
+
242
+ # st.write(display_data)
243
+ # Recalculer les statistiques avec les données filtrées
244
+ filtered_total_actions = display_data['Nb_actions'].sum()
245
+ filtered_nb_matchs = display_data['Match'].nunique()
246
+ filtered_avg_per_match = filtered_total_actions / filtered_nb_matchs if filtered_nb_matchs > 0 else 0
247
+
248
+ # Calculer les statistiques POUR CETTE JOUEUSE SEULEMENT
249
+ player_match_stats = player_data.groupby('Match')['Nb_actions'].sum()
250
+ player_actions_stats = {
251
+ 'min': float(player_match_stats.min()),
252
+ 'max': float(player_match_stats.max()),
253
+ 'mean': float(player_match_stats.mean().round(1))
254
+ }
255
+
256
+
257
+ col_actions, col_matchs, col_avg_per_match = st.columns(3)
258
+
259
+ with col_actions:
260
+ circular_kpi(
261
+ value=filtered_total_actions,
262
+ label="Actions",
263
+ range=(0, player_actions_stats['max']),
264
+ min_value=player_actions_stats['min'],
265
+ max_value=player_actions_stats['max'],
266
+ mean_value=player_actions_stats['mean'],
267
+ color_scheme="blue_purple",
268
+ background_color="transparent",
269
+ key="actions_kpi_by_match"
270
+ )
271
+ with col_matchs:
272
+
273
+ # Calculer le nombre d'actions par niveau pour ce match
274
+ actions_by_level = display_data.groupby('Niveau')['Nb_actions'].sum().reset_index()
275
+
276
+ # Créer un DataFrame avec tous les niveaux (0, 1, 2, 3) même s'ils n'existent pas
277
+ all_levels = pd.DataFrame({'Niveau': [0, 1, 2, 3]})
278
+ actions_by_level = all_levels.merge(actions_by_level, on='Niveau', how='left').fillna(0)
279
+
280
+ fig = px.bar(
281
+ actions_by_level,
282
+ x='Niveau',
283
+ y='Nb_actions',
284
+ color='Niveau',
285
+ color_discrete_map={
286
+ 0: '#ff6b6b', # Rouge pour niveau 0
287
+ 1: '#4ecdc4', # Turquoise pour niveau 1
288
+ 2: '#45b7d1', # Bleu pour niveau 2
289
+ 3: '#96ceb4' # Vert pour niveau 3
290
+ }
291
+ )
292
+
293
+ # Supprimer complètement la barre de couleur
294
+ fig.update_layout(
295
+ showlegend=False,
296
+ coloraxis_showscale=False,
297
+ height=300 # Réduire la hauteur du graphique
298
+ )
299
+
300
+ st.plotly_chart(fig, use_container_width=True, key="niveau_actions_chart")
301
+ # st.metric((display_data['Niveau']*display_data['Nb_actions']).sum()/display_data['Nb_actions'].sum())
302
+
303
+
304
+ with col_avg_per_match:
305
+ match_note = player_scores_filtered[player_scores_filtered['Match'] == selected_match]['note_match_joueuse'].mean().round(1)
306
+ circular_kpi(
307
+ value=match_note,
308
+ label="Note du match",
309
+ range=(0, 100)
310
+ )
311
+
312
+
313
+ # Boucle sur les types d'actions avec niveau non-null uniquement
314
+ # Filtrer d'abord les données avec niveau non-null
315
+ actions_with_level = display_data[display_data['Niveau'].notna()]
316
+ filtered_total_actions_by_action = actions_with_level.groupby('Action')['Nb_actions'].sum()
317
+
318
+ # Calculer le score total du match (somme de tous les scores d'actions)
319
+ total_match_score = 0
320
+ for action, value in filtered_total_actions_by_action.items():
321
+ action_data = display_data[display_data['Action'] == action]
322
+ actions_by_level = action_data.groupby('Niveau')['Nb_actions'].sum().reset_index()
323
+
324
+ # Créer un DataFrame avec tous les niveaux (0, 1, 2, 3) même s'ils n'existent pas
325
+ all_levels = pd.DataFrame({'Niveau': [0, 1, 2, 3]})
326
+ actions_by_level = all_levels.merge(actions_by_level, on='Niveau', how='left').fillna(0)
327
+
328
+ # Calculer le score pour cette action
329
+ if actions_by_level['Nb_actions'].sum() > 0:
330
+ score_action = (actions_by_level['Niveau']*actions_by_level['Nb_actions']).sum()/actions_by_level['Nb_actions'].sum()
331
+ total_match_score += score_action
332
+
333
+ # Multiplier par 100 pour obtenir le pourcentage
334
+ total_match_score = total_match_score
335
+
336
+ # Dictionnaire des descriptions des niveaux par type d'action
337
+ level_descriptions = {
338
+ 'DUEL': {
339
+ 0: 'RECULE/PERTE',
340
+ 1: 'N\'AVANCE PAS',
341
+ 2: 'AVANCE/PASSE',
342
+ 3: 'PLAGE CASSÉ'
343
+ },
344
+ 'PASSE': {
345
+ 0: 'MANQUEE+PERTE',
346
+ 1: 'MANQUEE',
347
+ 2: 'PASSE COURSE/MAINS',
348
+ 3: 'PASSE COURSE+MAINS'
349
+ },
350
+ 'JAP': {
351
+ 0: 'CONTRE/GOBÉ',
352
+ 1: 'REBOND',
353
+ 2: 'GAIN TERRAIN',
354
+ 3: 'GAIN TERRAIN+POS'
355
+ },
356
+ 'PLAQUAGE': {
357
+ 0: 'MANQUÉ',
358
+ 1: 'SUBI',
359
+ 2: 'NEUTRE',
360
+ 3: 'DOMINANT'
361
+ },
362
+ 'RUCK': {
363
+ 0: 'INSPECTEUR',
364
+ 1: 'LENT 5+S',
365
+ 2: 'RAPIDE 3-4S',
366
+ 3: 'TRÈS RAPIDE 1-2S'
367
+ },
368
+ 'RECEPTION JAP': {
369
+ 0: 'REBOND/PERTE',
370
+ 1: 'REBOND',
371
+ 2: 'MAUVAIS GOBE',
372
+ 3: 'GOBE'
373
+ }
374
+ }
375
+
376
+ for action, value in filtered_total_actions_by_action.items():
377
+ # Calculer les statistiques POUR CETTE JOUEUSE ET CE TYPE D'ACTION sur tous les matchs
378
+ player_action_stats = player_data.groupby(['Match', 'Action'])['Nb_actions'].sum().reset_index()
379
+ player_action_stats = player_action_stats[player_action_stats['Action'] == action]
380
+
381
+ # Statistiques pour ce type d'action de cette joueuse
382
+ action_stats = {
383
+ 'min': float(player_action_stats['Nb_actions'].min()) if len(player_action_stats) > 0 else 0,
384
+ 'max': float(player_action_stats['Nb_actions'].max()) if len(player_action_stats) > 0 else 0,
385
+ 'mean': float(player_action_stats['Nb_actions'].mean().round(1)) if len(player_action_stats) > 0 else 0
386
+ }
387
+
388
+
389
+ # Créer 3 colonnes pour chaque type d'action avec niveau
390
+ col_action1, col_action2, col_action3 = st.columns(3)
391
+
392
+ with col_action1:
393
+ circular_kpi(
394
+ value=value,
395
+ label=f"{action}",
396
+ range=(0, action_stats['max']),
397
+ min_value=action_stats['min'],
398
+ max_value=action_stats['max'],
399
+ mean_value=action_stats['mean'],
400
+ color_scheme="blue_purple",
401
+ background_color="transparent",
402
+ key=f"actions_kpi_{action}"
403
+ )
404
+
405
+ with col_action2:
406
+ # Calculer le nombre d'actions par niveau pour cette action
407
+ action_data = display_data[display_data['Action'] == action]
408
+ actions_by_level = action_data.groupby('Niveau')['Nb_actions'].sum().reset_index()
409
+
410
+ # Créer un DataFrame avec tous les niveaux (0, 1, 2, 3) même s'ils n'existent pas
411
+ all_levels = pd.DataFrame({'Niveau': [0, 1, 2, 3]})
412
+ actions_by_level = all_levels.merge(actions_by_level, on='Niveau', how='left').fillna(0)
413
+
414
+ # Ajouter les descriptions des niveaux
415
+ if action in level_descriptions:
416
+ actions_by_level['Niveau_Desc'] = actions_by_level['Niveau'].map(level_descriptions[action])
417
+ else:
418
+ actions_by_level['Niveau_Desc'] = actions_by_level['Niveau'].astype(str)
419
+
420
+ fig = px.bar(
421
+ actions_by_level,
422
+ x='Niveau_Desc',
423
+ y='Nb_actions',
424
+ color='Niveau',
425
+ color_discrete_map={
426
+ 0: '#ff6b6b', # Rouge pour niveau 0
427
+ 1: '#4ecdc4', # Turquoise pour niveau 1
428
+ 2: '#45b7d1', # Bleu pour niveau 2
429
+ 3: '#96ceb4' # Vert pour niveau 3
430
+ }
431
+ )
432
+
433
+ # Supprimer complètement la barre de couleur
434
+ fig.update_layout(
435
+ showlegend=False,
436
+ coloraxis_showscale=False,
437
+ height=300, # Réduire la hauteur du graphique
438
+ xaxis_title="", # Pas de titre axe X
439
+ yaxis_title="" # Pas de titre axe Y
440
+ )
441
+
442
+ st.plotly_chart(fig, use_container_width=True, key=f"bar_chart_{action}")
443
+
444
+ with col_action3:
445
+ # st.write(actions_by_level)
446
+
447
+ # Calculer le score pour cette action spécifique
448
+ score_action = (actions_by_level['Niveau']*actions_by_level['Nb_actions']).sum()/actions_by_level['Nb_actions'].sum()
449
+ score_action_percent = score_action/total_match_score*100
450
+ # st.write(actions_by_level)
451
+ # st.write((actions_by_level['Niveau']*actions_by_level['Nb_actions']).sum())
452
+ # st.write(actions_by_level['Nb_actions'].sum())
453
+ # st.write((actions_by_level['Niveau']*actions_by_level['Nb_actions']).sum()/actions_by_level['Nb_actions'].sum())
454
+ # st.write((actions_by_level['Niveau']*actions_by_level['Nb_actions']).sum()/actions_by_level['Nb_actions'].sum()*100/33)
455
+
456
+ circular_kpi(
457
+ value=score_action_percent.round(1),
458
+ label=f"de la note",
459
+ range=(0, 100),
460
+ unit="%",
461
+ key=f"note_kpi_{action}"
462
+ )
463
+
464
+ # Maintenant afficher les actions avec niveau null (sur la même ligne)
465
+ actions_without_level = display_data[display_data['Niveau'].isna()]
466
+ filtered_total_actions_by_action_null = actions_without_level.groupby('Action')['Nb_actions'].sum()
467
+
468
+
469
+
470
+ st.divider()
471
+
472
+
473
+
474
+
475
+ if len(filtered_total_actions_by_action_null) > 0:
476
+
477
+ # Créer autant de colonnes que d'actions sans niveau
478
+ num_null_actions = len(filtered_total_actions_by_action_null)
479
+ if num_null_actions > 0:
480
+ # Créer les colonnes dynamiquement
481
+ cols = st.columns(num_null_actions)
482
+
483
+ for i, (action, value) in enumerate(filtered_total_actions_by_action_null.items()):
484
+ # Calculer les statistiques POUR CETTE JOUEUSE ET CE TYPE D'ACTION sur tous les matchs
485
+ player_action_stats = player_data.groupby(['Match', 'Action'])['Nb_actions'].sum().reset_index()
486
+ player_action_stats = player_action_stats[player_action_stats['Action'] == action]
487
+
488
+ # Statistiques pour ce type d'action de cette joueuse
489
+ action_stats = {
490
+ 'min': float(player_action_stats['Nb_actions'].min()) if len(player_action_stats) > 0 else 0,
491
+ 'max': float(player_action_stats['Nb_actions'].max()) if len(player_action_stats) > 0 else 0,
492
+ 'mean': float(player_action_stats['Nb_actions'].mean().round(1)) if len(player_action_stats) > 0 else 0
493
+ }
494
+
495
+ # Afficher le KPI dans la colonne correspondante
496
+ with cols[i]:
497
+ circular_kpi(
498
+ value=value,
499
+ label=f"{action}",
500
+ range=(0, action_stats['max']),
501
+ min_value=action_stats['min'],
502
+ max_value=action_stats['max'],
503
+ mean_value=action_stats['mean'],
504
+ color_scheme="green",
505
+ background_color="transparent",
506
+ key=f"actions_kpi_null_{action}"
507
+ )
streamlit_app/components/players_comparison.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+
4
+ def show_players_comparison(df):
5
+ """Composant simple pour comparer les matchs"""
6
+
7
+
8
+
9
+ st.header("In progress...")
10
+
streamlit_app/main.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import sqlite3
4
+ from pathlib import Path
5
+ import sys
6
+
7
+ # Ajouter le répertoire parent au path pour les imports
8
+ sys.path.append(str(Path(__file__).parent.parent))
9
+
10
+ # Configuration de la page
11
+ st.set_page_config(
12
+ page_title="U18 Féminine - Stade Toulousain",
13
+ page_icon="./assets/Logo_Stade_Toulousain_Rugby.png",
14
+ layout="wide",
15
+ initial_sidebar_state="expanded"
16
+ )
17
+
18
+ # Charger les styles personnalisés
19
+ from utils.styles import load_css, create_rugby_title
20
+
21
+ # CHARGER LES STYLES CSS
22
+ load_css()
23
+
24
+ # Imports des composants
25
+ from components.dashboard import show_dashboard
26
+ from components.player_analysis import show_player_analysis
27
+ from components.players_comparison import show_players_comparison
28
+ from utils.data_loader import load_data
29
+
30
+ def main():
31
+ # Titre principal
32
+ create_rugby_title("u18 féminine", "Stade Toulousain")
33
+
34
+ # Charger les données
35
+ try:
36
+ df = load_data()
37
+ except Exception as e:
38
+ st.error(f"Erreur lors du chargement des données : {e}")
39
+ st.stop()
40
+
41
+
42
+ from streamlit_option_menu import option_menu
43
+
44
+ with st.sidebar:
45
+ selected = option_menu( None,
46
+ ["Tableau de bord", 'Analyse individuelle', 'Comparaison des joueuses'],
47
+ icons=None, default_index=1,
48
+ menu_icon="cast",
49
+ styles={
50
+ "container": {"padding": "0!important", "background-color": "#fafafa"},
51
+ "icon": {"display": "None"},
52
+ "nav-link": {"font-size": "16px", "text-align": "left", "margin":"3px", "--hover-color": "#eee"},
53
+ "nav-link-selected": {"background-color": "#000000"},
54
+ }
55
+ )
56
+ # Affichage des pages
57
+ if selected == "Tableau de bord":
58
+ show_dashboard(df)
59
+ elif selected == "Analyse individuelle":
60
+ show_player_analysis(df)
61
+ elif selected == "Comparaison des joueuses":
62
+ show_players_comparison(df)
63
+
64
+ if __name__ == "__main__":
65
+ main()
streamlit_app/utils/chart_styles.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import plotly.express as px
2
+ import plotly.graph_objects as go
3
+
4
+ # Couleurs du Stade Toulousain
5
+ STADE_COLORS = {
6
+ 'primary': '#CC0C13', # Rouge principal
7
+ 'secondary': '#000000', # Noir
8
+ 'accent': '#FFFFFF', # Blanc
9
+ 'light_red': 'rgba(204, 12, 19, 0.2)',
10
+ 'dark_gray': '#333333'
11
+ }
12
+
13
+ def get_stade_toulousain_colorscale():
14
+ """Palette de couleurs Stade Toulousain"""
15
+ return [[0, STADE_COLORS['secondary']], [1, STADE_COLORS['primary']]]
16
+
17
+ def get_stade_toulousain_colors():
18
+ """Couleurs discrètes pour graphiques multiples"""
19
+ return [STADE_COLORS['primary'], STADE_COLORS['secondary'],
20
+ STADE_COLORS['dark_gray'], STADE_COLORS['light_red']]
21
+
22
+ def apply_stade_style(fig, title=None):
23
+ """Applique le style Stade Toulousain à n'importe quel graphique"""
24
+
25
+ fig.update_layout(
26
+ # Fond et papier
27
+ plot_bgcolor='rgba(248, 249, 250, 0.5)',
28
+ paper_bgcolor='rgba(0,0,0,0)',
29
+
30
+ # Police et couleurs
31
+ font=dict(family='Arial', color=STADE_COLORS['secondary'], size=11),
32
+
33
+ # Titre
34
+ title=dict(
35
+ text=title,
36
+ font=dict(color=STADE_COLORS['primary'], size=16, family='Arial Black'),
37
+ x=0.5,
38
+ xanchor='center'
39
+ ) if title else None,
40
+
41
+ # Barre de couleur
42
+ coloraxis_colorbar=dict(
43
+ tickfont=dict(color=STADE_COLORS['secondary'], size=10),
44
+ bgcolor='rgba(255,255,255,0.8)',
45
+ # bordercolor=STADE_COLORS['primary'],
46
+ )
47
+ )
48
+
49
+ # Axes
50
+ fig.update_xaxes(
51
+ showgrid=True,
52
+ gridcolor=STADE_COLORS['light_red'],
53
+ gridwidth=1,
54
+ zeroline=True,
55
+ zerolinecolor=STADE_COLORS['primary'],
56
+ zerolinewidth=2,
57
+ tickfont=dict(color=STADE_COLORS['secondary'], size=11)
58
+ )
59
+
60
+ fig.update_yaxes(
61
+ showgrid=False,
62
+ tickfont=dict(color=STADE_COLORS['secondary'], size=11)
63
+ )
64
+
65
+ # Hover personnalisé
66
+ fig.update_traces(
67
+ hoverlabel=dict(
68
+ bgcolor=STADE_COLORS['primary'],
69
+ font_color='white',
70
+ font_size=12
71
+ )
72
+ )
73
+
74
+ return fig
75
+
76
+ def create_custom_bar_chart(data, x, y, title="", orientation='h', remove_y_title=True):
77
+ """Crée un graphique en barres avec le style Stade Toulousain"""
78
+
79
+ fig = px.bar(
80
+ data,
81
+ x=x,
82
+ y=y,
83
+ orientation=orientation,
84
+ color=x if orientation=='h' else y,
85
+ color_continuous_scale=get_stade_toulousain_colorscale()
86
+ )
87
+
88
+ # Appliquer le style
89
+ fig = apply_stade_style(fig, title)
90
+
91
+ # Configuration spécifique pour les barres horizontales
92
+ if orientation == 'h':
93
+ fig.update_layout(
94
+ yaxis={
95
+ 'categoryorder': 'total ascending',
96
+ 'title': '' if remove_y_title else y
97
+ },
98
+ xaxis={'title': x.replace('_', ' ').title()}
99
+ )
100
+
101
+ return fig
streamlit_app/utils/data_loader.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import sqlite3
3
+ import streamlit as st
4
+ from pathlib import Path
5
+
6
+ @st.cache_data
7
+ def load_data():
8
+ """Charge les données depuis le fichier CSV"""
9
+ try:
10
+ data_path = Path(__file__).parent.parent.parent / "data/transformed/Rugby_Stats.csv"
11
+ df = pd.read_csv(data_path)
12
+ return df
13
+ except Exception as e:
14
+ st.error(f"Impossible de charger les données : {e}")
15
+ return pd.DataFrame()
16
+
17
+ @st.cache_data
18
+ def load_from_database():
19
+ """Charge les données depuis la base SQLite"""
20
+ try:
21
+ db_path = Path(__file__).parent.parent.parent / "data/transformed/Rugby_Stats.db"
22
+ conn = sqlite3.connect(db_path)
23
+
24
+ query = '''
25
+ SELECT j.prenom, j.nom, m.nom_match, s.numero, a.nom_action,
26
+ n.id_niveau, s.nb_actions
27
+ FROM Statistiques s
28
+ JOIN Joueuse j ON s.id_joueuse = j.id_joueuse
29
+ JOIN Match m ON s.id_match = m.id_match
30
+ JOIN Action a ON s.id_action = a.id_action
31
+ JOIN Niveau n ON s.id_niveau = n.id_niveau
32
+ '''
33
+
34
+ df = pd.read_sql_query(query, conn)
35
+ conn.close()
36
+ return df
37
+ except Exception as e:
38
+ st.error(f"Impossible de charger depuis la base : {e}")
39
+ return pd.DataFrame()
40
+
41
+ def get_database_stats():
42
+ """Retourne les statistiques générales de la base"""
43
+ try:
44
+ db_path = Path(__file__).parent.parent.parent / "data/transformed/Rugby_Stats.db"
45
+ conn = sqlite3.connect(db_path)
46
+
47
+ # Nombre de joueuses
48
+ nb_joueuses = pd.read_sql_query("SELECT COUNT(*) as count FROM Joueuse", conn).iloc[0]['count']
49
+
50
+ # Nombre de matchs
51
+ nb_matchs = pd.read_sql_query("SELECT COUNT(*) as count FROM Match", conn).iloc[0]['count']
52
+
53
+ # Nombre total de statistiques
54
+ nb_stats = pd.read_sql_query("SELECT COUNT(*) as count FROM Statistiques", conn).iloc[0]['count']
55
+
56
+ conn.close()
57
+
58
+ return {
59
+ 'nb_joueuses': nb_joueuses,
60
+ 'nb_matchs': nb_matchs,
61
+ 'nb_stats': nb_stats
62
+ }
63
+ except Exception as e:
64
+ return {'nb_joueuses': 0, 'nb_matchs': 0, 'nb_stats': 0}
65
+
66
+ def get_player_stats(df, player_name=None):
67
+ """Retourne les statistiques d'une joueuse spécifique"""
68
+ if player_name:
69
+ return df[df['Nom'].str.contains(player_name, case=False, na=False)]
70
+ return df
71
+
72
+ def get_match_stats(df, match_name=None):
73
+ """Retourne les statistiques d'un match spécifique"""
74
+ if match_name:
75
+ return df[df['Match'] == match_name]
76
+ return df
streamlit_app/utils/styles.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from pathlib import Path
3
+
4
+ def load_css():
5
+ """Charge le fichier CSS personnalisé"""
6
+ css_file = Path(__file__).parent.parent / "assets" / "style.css"
7
+
8
+ try:
9
+ with open(css_file, 'r', encoding='utf-8') as f:
10
+ css_content = f.read()
11
+
12
+ st.markdown(f"""
13
+ <style>
14
+ {css_content}
15
+ </style>
16
+ """, unsafe_allow_html=True)
17
+
18
+ except FileNotFoundError:
19
+ st.warning("Fichier CSS non trouvé. Styles par défaut utilisés.")
20
+ except Exception as e:
21
+ st.error(f"Erreur lors du chargement du CSS : {e}")
22
+
23
+ def load_logo():
24
+ """Charge le logo du Stade Toulousain"""
25
+ logo_path = Path(__file__).parent.parent / "assets" / "Logo_Stade_Toulousain_Rugby.png"
26
+
27
+ if logo_path.exists():
28
+ return str(logo_path)
29
+ return None
30
+
31
+ def create_rugby_title(left_text="u18 féminine", right_text="Stade Toulousain"):
32
+ """Crée un header titre avec logo centré"""
33
+
34
+ # Charger le logo
35
+ logo_path = load_logo()
36
+
37
+ if logo_path:
38
+ # Convertir le logo en base64 pour l'affichage
39
+ logo_base64 = get_base64_of_image(logo_path)
40
+
41
+ st.markdown(f"""
42
+ <div class="rugby-header-custom">
43
+ <span class="title-left">{left_text}</span>
44
+ <img src="data:image/png;base64,{logo_base64}" class="logo-center" alt="Logo Stade Toulousain"/>
45
+ <span class="title-right">{right_text}</span>
46
+ </div>
47
+ """, unsafe_allow_html=True)
48
+ else:
49
+ # Fallback si le logo n'est pas trouvé
50
+ st.markdown(f'<h1 class="main-header">{left_text} - {right_text}</h1>', unsafe_allow_html=True)
51
+
52
+ def get_base64_of_image(path):
53
+ """Convertit une image en base64 pour l'affichage"""
54
+ import base64
55
+ with open(path, "rb") as img_file:
56
+ return base64.b64encode(img_file.read()).decode()
57
+
58
+ def create_metric_card(title, value, description=""):
59
+ """Crée une carte métrique personnalisée"""
60
+ st.markdown(f"""
61
+ <div class="metric-card">
62
+ <h3 style="margin: 0; color: #1f4e79;">{title}</h3>
63
+ <h2 style="margin: 0.5rem 0; color: #1f4e79; font-size: 2rem;">{value}</h2>
64
+ <p style="margin: 0; color: #666; font-size: 0.9rem;">{description}</p>
65
+ </div>
66
+ """, unsafe_allow_html=True)
streamlit_app/utils/visualisations.py ADDED
File without changes