MaxBDKT commited on
Commit
49f7133
·
verified ·
1 Parent(s): c876341

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +158 -111
src/streamlit_app.py CHANGED
@@ -5,103 +5,110 @@ import plotly.graph_objects as go
5
  import os
6
 
7
  # ==============================================================================
8
- # 1. CONFIGURATION ET STYLE CSS (FORÇAGE CONTRASTE TOTAL & ANTI-DARK MODE)
9
  # ==============================================================================
10
- st.set_page_config(page_title="Brake Performance Lab", layout="wide", page_icon="🚲")
 
 
 
 
11
 
12
  st.markdown("""
13
  <style>
14
- /* 1.1 Thème de base blanc pur */
15
- .stApp, [data-testid="stSidebar"] {
16
  background-color: #FFFFFF !important;
17
  }
18
 
19
- /* 1.2 Force le texte en NOIR PUR partout (Légendes, Labels, Titres) */
20
- * {
21
  color: #000000 !important;
22
- font-family: 'Arial', sans-serif;
23
  }
24
-
25
- /* 1.3 Compactage Sidebar */
26
  [data-testid="stSidebar"] [data-testid="stVerticalBlock"] {
27
  gap: 0.1rem !important;
28
  padding-top: 0rem !important;
29
  }
30
- hr { border-top: 1px solid #000 !important; margin: 0.5rem 0 !important; }
 
 
 
31
 
32
- /* 1.4 DROPDOWNS : Correction Noir sur Noir pour Hugging Face */
33
- div[data-baseweb="select"], div[role="combobox"] {
34
  border: 2px solid #000000 !important;
35
  background-color: #FFFFFF !important;
36
  }
37
- div[data-baseweb="select"] * { color: #000000 !important; }
38
- ul[role="listbox"] {
39
- background-color: #FFFFFF !important;
40
- border: 2px solid #000000 !important;
41
- }
42
- li[role="option"] {
43
  color: #000000 !important;
44
- font-weight: bold !important;
 
 
 
45
  background-color: #FFFFFF !important;
 
46
  }
47
- li[role="option"]:hover {
48
  background-color: #0082C3 !important;
49
  color: #FFFFFF !important;
50
  }
51
 
52
- /* 1.5 INPUTS NUMÉRIQUES (TA DEMANDE : Texte BLANC sur fond sombre) */
53
- input[type="number"], div[data-baseweb="input"] input {
54
  color: #FFFFFF !important;
55
- background-color: #333333 !important;
56
  border-radius: 4px !important;
57
  font-weight: bold !important;
 
58
  }
59
 
60
- /* 1.6 Correction des étiquettes Streamlit (Radios, Expanders, Labels) */
61
- .st-emotion-cache-p4mowd, .st-emotion-cache-16idsys p, label, .st-ae {
62
- color: #000000 !important;
63
- font-weight: bold !important;
64
- }
65
-
66
- /* 1.7 Tags Multiselect (Bleu Decathlon) */
67
- [data-testid="stMultiSelect"] span {
68
- background-color: #0082C3 !important;
69
- color: #FFFFFF !important;
70
  }
71
 
72
- /* 1.8 Boites du Dashboard Analyse */
73
  [data-testid="column"] {
74
  padding: 15px !important;
75
  border: 2px solid #000000 !important;
76
  border-radius: 10px !important;
77
  background-color: #FFFFFF !important;
 
 
 
 
 
78
  }
79
- [data-testid="stMetricValue"] { font-weight: 900 !important; font-size: 24px !important; }
80
 
81
- /* 1.9 BADGES NORMES (TA DEMANDE : Texte BLANC sur fond couleur) */
82
  .alert-red {
83
  color: #FFFFFF !important;
84
  background-color: #B71C1C !important;
85
  font-weight: 900 !important;
86
- padding: 8px;
87
  border-radius: 5px;
88
  text-align: center;
89
- margin-top: 5px;
 
90
  }
91
  .check-green {
92
  color: #FFFFFF !important;
93
  background-color: #1B5E20 !important;
94
  font-weight: 900 !important;
95
- padding: 8px;
96
  border-radius: 5px;
97
  text-align: center;
98
- margin-top: 5px;
 
99
  }
100
  </style>
101
  """, unsafe_allow_html=True)
102
 
103
  # ==============================================================================
104
- # 2. ACCÈS AUX DONNÉES
105
  # ==============================================================================
106
  @st.cache_data
107
  def load_data():
@@ -114,147 +121,187 @@ def load_data():
114
  return df
115
 
116
  df = load_data()
 
117
  if df.empty:
118
- st.error("Fichier Excel introuvable.")
119
  st.stop()
120
 
 
 
 
121
  try:
122
  all_models = df['model name'].unique().tolist()
123
 
124
- # ==============================================================================
125
- # 3. SIDEBAR (CONTRÔLE TOTAL)
126
- # ==============================================================================
127
  with st.sidebar:
128
  st.image("https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Decathlon_Logo.svg/1280px-Decathlon_Logo.svg.png", width=150)
129
  st.title("⚙️ CONFIGURATION")
130
 
131
- # 3.1 Entrée d'effort au levier
132
- effort_x = st.slider("🫱 Effort Levier [N]", 40, 200, 100)
133
- selected_models = st.multiselect("Modèles", options=all_models, default=all_models[:2])
 
 
134
 
135
- # 3.2 Normes
136
- st.subheader("📋 Normes de conformité")
137
- norm_cat = st.selectbox("Catégorie", ["Aucune", "City/Trekking", "Kids", "MTB", "Racing"])
138
 
139
  st.markdown("---")
140
 
141
- # 3.3 Simulation (TA DEMANDE : En déroulant)
142
  with st.expander("📏 Braking Simulation", expanded=False):
143
  v_kmh = st.number_input("Vitesse (km/h)", value=25)
144
  m_total = st.number_input("Masse Totale (kg)", value=100)
145
  rim_inch = st.selectbox("Taille de roue (inch)", [20, 24, 26, 27.5, 28, 29], index=4)
146
- sharing_av = st.slider("Sharing Front (%)", 0, 100, 70)
147
- st.caption(f"Sharing Rear: {100 - sharing_av}%")
148
 
149
- # 3.4 Options d'affichage (TA DEMANDE : En déroulant)
150
  with st.expander("🔍 Options d'affichage", expanded=False):
151
- ref_model_name = st.selectbox("Modèle de Benchmark", options=all_models)
152
  view_mode = st.radio("Mode de vue", ["Sec & Humide", "Sec uniquement", "Humide uniquement"], index=0)
153
- show_loss = st.checkbox("Calculer Perte Wet (%)", value=True)
154
 
155
- # ==============================================================================
156
- # 4. MOTEUR PHYSIQUE (CONVERSIONS S.I. & INERTIE)
157
- # ==============================================================================
158
- # 4.1 Unités S.I.
159
- v_ms = v_kmh / 3.6 # km/h vers m/s
160
- r_m = (rim_inch * 0.0254) / 2 # inch vers mètres
161
 
162
- # 4.2 Énergie Totale (E_trans + E_rot)
163
- # Estimation masse rotative (jante+pneu) = 1.5kg par roue
 
 
 
 
 
 
164
  j_wheel = 1.5 * (r_m ** 2)
165
  omega = v_ms / r_m
166
- e_total_j = (0.5 * m_total * (v_ms**2)) + (2 * (0.5 * j_wheel * (omega**2)))
167
 
168
- # 4.3 Seuils Normes
169
- n_s, n_w = 0, 0
170
- if norm_cat == "City/Trekking": n_s, n_w = 340, 220
171
- elif norm_cat == "Kids": n_s, n_w = 204, 132
172
- elif norm_cat == "MTB": n_s, n_w = 425, 280
173
- elif norm_cat == "Racing": n_s, n_w = 425, 260
 
 
 
 
 
 
 
 
174
 
175
- # 4.4 Référence Benchmark
176
  row_bench = df[df['model name'] == ref_model_name].iloc[0]
177
  ref_f_sec = row_bench['dry a'] * effort_x + row_bench['dry b']
178
  ref_f_wet = row_bench['wet a'] * effort_x + row_bench['wet b']
179
 
180
- # 5. HEADER DIAGNOSTIC
181
- label, color = ("❄️ LIGHT", "#a1c4fd") if effort_x < 70 else (("⚖️ MODERATE", "#ffdb58") if effort_x <= 110 else ("🔥 POWERFUL", "#ff4b4b"))
182
- st.markdown(f"<div style='background-color:{color}; padding:8px; border-radius:8px; text-align:center; border: 3px solid #000; margin-bottom: 10px;'><span style='font-weight:900;'>{label} BRAKING | Effort: {effort_x} N</span></div>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
- # ==============================================================================
185
- # 6. GRAPHIQUE (FIX COULEURS TEXTE NOIR)
186
- # ==============================================================================
187
  filtered_df = df[df['model name'].isin(selected_models)]
188
  fig = go.Figure()
189
  x_grid = np.linspace(40, 200, 150)
190
- color_palette = ['#0082C3', '#E63312', '#333333', '#00A14B', '#FFD200']
191
 
192
  results_summary = []
 
193
  for i, (idx, row) in enumerate(filtered_df.iterrows()):
194
- clr = color_palette[i % len(color_palette)]
195
  f_sec = row['dry a'] * effort_x + row['dry b']
196
  f_wet = row['wet a'] * effort_x + row['wet b']
197
  results_summary.append({"name": row['model name'], "dry": f_sec, "wet": f_wet, "row": row})
198
 
 
199
  if view_mode != "Humide uniquement":
200
- fig.add_trace(go.Scatter(x=x_grid, y=row['dry a']*x_grid + row['dry b'], name=f"{row['name']} (D)", line=dict(color=clr, width=4)))
201
  if n_s > 0:
202
  xt = (n_s - row['dry b']) / row['dry a']
203
  if 40 <= xt <= 200:
204
  fig.add_trace(go.Scatter(x=[xt], y=[n_s], mode='markers+text', text=[f"{round(xt,1)}N"], textfont=dict(color="black", weight=800), textposition="top center", marker=dict(color=clr, size=12, symbol='x'), showlegend=False))
205
 
 
206
  if view_mode != "Sec uniquement":
207
- fig.add_trace(go.Scatter(x=x_grid, y=row['wet a']*x_grid + row['wet b'], name=f"{row['name']} (W)", line=dict(color=clr, width=2, dash='dot')))
208
 
209
- # TA DEMANDE : VISUALISATION DES LIGNES DE NORMES EN NOIR
210
  if n_s > 0 and view_mode != "Humide uniquement":
211
- fig.add_hline(y=n_s, line_width=2, line_color="#000", annotation_text="NORME SEC", annotation_font=dict(color="black", weight=700))
212
  if n_w > 0 and view_mode != "Sec uniquement":
213
- fig.add_hline(y=n_w, line_width=2, line_dash="dot", line_color="#000", annotation_text="NORME HUMIDE", annotation_font=dict(color="black", weight=700))
214
 
 
215
  fig.add_vline(x=effort_x, line_width=2, line_dash="dash", line_color="#555")
216
 
217
- # FORÇAGE NOIR SUR TOUT LE GRAPHIQUE
218
  fig.update_layout(
219
- height=480, plot_bgcolor='white', paper_bgcolor='white', font=dict(color="black"),
220
- xaxis=dict(title=dict(text="Effort Levier [N]", font=dict(color="black", weight=700)), tickfont=dict(color="black", weight=700), linecolor="black", gridcolor="#EEE"),
221
- yaxis=dict(title=dict(text="Performance [N]", font=dict(color="black", weight=700)), tickfont=dict(color="black", weight=700), linecolor="black", gridcolor="#EEE"),
222
  legend=dict(font=dict(color="black", weight=700), bordercolor="black", borderwidth=1, bgcolor="white")
223
  )
224
  st.plotly_chart(fig, use_container_width=True)
225
 
226
- # ==============================================================================
227
- # 7. DASHBOARD ANALYSE (ALIGNE SUR TON EXEMPLE)
228
- # ==============================================================================
229
- st.markdown(f"**📊 Dynamics Analysis | {v_kmh}km/h | Energy: {int(e_total_j)} J**")
 
230
  cols = st.columns(len(results_summary))
231
 
232
  for i, res in enumerate(results_summary):
233
  with cols[i]:
234
- is_bench = (res['name'] == ref_model_name)
235
- st.markdown(f"<u>**{res['name']}**</u> {'⭐' if is_bench else ''}", unsafe_allow_html=True)
236
 
 
237
  if view_mode != "Humide uniquement":
238
- st.metric("Force Sec", f"{round(res['dry'], 1)} N", f"{round(res['dry'] - ref_f_sec, 1)} N vs Ref" if not is_bench else None)
 
 
 
239
  d_sec = e_total_j / res['dry'] if res['dry'] > 0 else 0
240
- st.write(f"🛑 **Arrêt (Sec): {round(d_sec, 2)} m**")
 
241
  if n_s > 0:
242
- xt = (n_s - res['row']['dry b']) / res['row']['dry a']
243
- if xt > 180: st.markdown(f"<div class='alert-red'>HORS NORME SEC ({round(xt,1)}N)</div>", unsafe_allow_html=True)
244
- else: st.markdown(f"<div class='check-green'>CONFORME SEC</div>", unsafe_allow_html=True)
 
 
245
 
 
246
  if view_mode != "Sec uniquement":
247
- st.metric("Force Wet", f"{round(res['wet'], 1)} N", f"{round(res['wet'] - ref_f_wet, 1)} N vs Ref" if not is_bench else None)
 
 
248
  d_wet = e_total_j / res['wet'] if res['wet'] > 0 else 0
249
- st.write(f"🌧️ **Arrêt (Wet): {round(d_wet, 2)} m**")
 
250
  if n_w > 0:
251
- xtw = (n_w - res['row']['wet b']) / res['row']['wet a']
252
- if xtw > 180: st.markdown(f"<div class='alert-red'>HORS NORME HUMIDE</div>", unsafe_allow_html=True)
253
- else: st.markdown(f"<div class='check-green'>CONFORME HUMIDE</div>", unsafe_allow_html=True)
254
-
255
- if show_loss and view_mode == "Sec & Humide":
256
- loss = ((res['dry'] - res['wet']) / res['dry'] * 100) if res['dry'] != 0 else 0
257
- st.metric("Wet Loss", f"-{round(loss, 1)}%", delta_color="inverse")
258
 
259
  except Exception as e:
260
- st.error(f"Erreur Système : {e}")
 
5
  import os
6
 
7
  # ==============================================================================
8
+ # 1. CONFIGURATION ET VISIBILITÉ TOTALE (CSS BRUTE FORCE)
9
  # ==============================================================================
10
+ st.set_page_config(
11
+ page_title="Brake Performance Lab",
12
+ layout="wide",
13
+ page_icon="🚲"
14
+ )
15
 
16
  st.markdown("""
17
  <style>
18
+ /* 1.1 FOND BLANC ET TEXTE NOIR GLOBAL */
19
+ .stApp, [data-testid="stSidebar"], .main {
20
  background-color: #FFFFFF !important;
21
  }
22
 
23
+ /* 1.2 TYPOGRAPHIE : FORCE LE NOIR SUR TOUT */
24
+ h1, h2, h3, h4, h5, h6, p, li, label, span, div, .stMarkdown {
25
  color: #000000 !important;
26
+ font-family: 'Arial', sans-serif !important;
27
  }
28
+
29
+ /* 1.3 SIDEBAR : CONTRASTE ET ESPACEMENT */
30
  [data-testid="stSidebar"] [data-testid="stVerticalBlock"] {
31
  gap: 0.1rem !important;
32
  padding-top: 0rem !important;
33
  }
34
+ hr {
35
+ border-top: 1px solid #000 !important;
36
+ margin: 0.5rem 0 !important;
37
+ }
38
 
39
+ /* 1.4 WIDGETS : SELECTBOX (FIX NOIR SUR NOIR) */
40
+ div[data-baseweb="select"], div[role="combobox"], .stSelectbox {
41
  border: 2px solid #000000 !important;
42
  background-color: #FFFFFF !important;
43
  }
44
+ div[data-baseweb="select"] * {
 
 
 
 
 
45
  color: #000000 !important;
46
+ }
47
+
48
+ /* MENU DÉROULANT (OPTIONS) */
49
+ div[role="listbox"] ul li {
50
  background-color: #FFFFFF !important;
51
+ color: #000000 !important;
52
  }
53
+ div[role="listbox"] ul li:hover {
54
  background-color: #0082C3 !important;
55
  color: #FFFFFF !important;
56
  }
57
 
58
+ /* 1.5 INPUTS : TEXTE BLANC SUR FOND NOIR (TA DEMANDE) */
59
+ input[type="number"], .stNumberInput div[data-baseweb="input"] input {
60
  color: #FFFFFF !important;
61
+ background-color: #1E1E1E !important;
62
  border-radius: 4px !important;
63
  font-weight: bold !important;
64
+ border: 1px solid #000 !important;
65
  }
66
 
67
+ /* 1.6 EXPANDERS ET SLIDERS */
68
+ .streamlit-expanderHeader, .st-ae, .st-af, .st-ag, .st-ai {
69
+ color: #000000 !important;
70
+ font-weight: bold !important;
 
 
 
 
 
 
71
  }
72
 
73
+ /* 1.7 DASHBOARD : COLONNES ET MÉTRIQUES */
74
  [data-testid="column"] {
75
  padding: 15px !important;
76
  border: 2px solid #000000 !important;
77
  border-radius: 10px !important;
78
  background-color: #FFFFFF !important;
79
+ margin-bottom: 10px;
80
+ }
81
+ [data-testid="stMetricValue"] {
82
+ font-weight: 900 !important;
83
+ color: #000000 !important;
84
  }
 
85
 
86
+ /* 1.8 NORMES : BADGES TEXTE BLANC / FOND COULEUR */
87
  .alert-red {
88
  color: #FFFFFF !important;
89
  background-color: #B71C1C !important;
90
  font-weight: 900 !important;
91
+ padding: 10px;
92
  border-radius: 5px;
93
  text-align: center;
94
+ margin-top: 5px;
95
+ border: 1px solid #000;
96
  }
97
  .check-green {
98
  color: #FFFFFF !important;
99
  background-color: #1B5E20 !important;
100
  font-weight: 900 !important;
101
+ padding: 10px;
102
  border-radius: 5px;
103
  text-align: center;
104
+ margin-top: 5px;
105
+ border: 1px solid #000;
106
  }
107
  </style>
108
  """, unsafe_allow_html=True)
109
 
110
  # ==============================================================================
111
+ # 2. CHARGEMENT DES DONNÉES
112
  # ==============================================================================
113
  @st.cache_data
114
  def load_data():
 
121
  return df
122
 
123
  df = load_data()
124
+
125
  if df.empty:
126
+ st.error("ERREUR : Fichier Excel manquant.")
127
  st.stop()
128
 
129
+ # ==============================================================================
130
+ # 3. BARRE LATÉRALE (SIDEBAR)
131
+ # ==============================================================================
132
  try:
133
  all_models = df['model name'].unique().tolist()
134
 
 
 
 
135
  with st.sidebar:
136
  st.image("https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Decathlon_Logo.svg/1280px-Decathlon_Logo.svg.png", width=150)
137
  st.title("⚙️ CONFIGURATION")
138
 
139
+ # Saisie Effort Principal
140
+ effort_x = st.slider("🫱 Effort au Levier [N]", 40, 200, 100)
141
+
142
+ # Sélection Modèles
143
+ selected_models = st.multiselect("Modèles à comparer", options=all_models, default=all_models[:2])
144
 
145
+ # Sélection Norme
146
+ norm_cat = st.selectbox("Catégorie Norme", ["Aucune", "City/Trekking", "Kids", "MTB", "Racing"])
 
147
 
148
  st.markdown("---")
149
 
150
+ # SECTION SIMULATION DÉROULANTE
151
  with st.expander("📏 Braking Simulation", expanded=False):
152
  v_kmh = st.number_input("Vitesse (km/h)", value=25)
153
  m_total = st.number_input("Masse Totale (kg)", value=100)
154
  rim_inch = st.selectbox("Taille de roue (inch)", [20, 24, 26, 27.5, 28, 29], index=4)
155
+ sharing_av = st.slider("Répartition AV (%)", 0, 100, 70)
 
156
 
157
+ # SECTION OPTIONS D'AFFICHAGE DÉROULANTE
158
  with st.expander("🔍 Options d'affichage", expanded=False):
159
+ ref_model_name = st.selectbox("Benchmark de référence", options=all_models)
160
  view_mode = st.radio("Mode de vue", ["Sec & Humide", "Sec uniquement", "Humide uniquement"], index=0)
 
161
 
162
+ # ==========================================================================
163
+ # 4. MOTEUR PHYSIQUE (CONVERSIONS ET DYNAMIQUE)
164
+ # ==========================================================================
165
+ # 4.1 Conversion km/h vers m/s
166
+ v_ms = v_kmh / 3.6
 
167
 
168
+ # 4.2 Conversion inch vers mètres
169
+ r_m = (rim_inch * 0.0254) / 2
170
+
171
+ # 4.3 Énergie Cinétique de Translation
172
+ e_trans = 0.5 * m_total * (v_ms**2)
173
+
174
+ # 4.4 Énergie Cinétique de Rotation (2 roues)
175
+ # Masse estimée jante+pneu = 1.5kg
176
  j_wheel = 1.5 * (r_m ** 2)
177
  omega = v_ms / r_m
178
+ e_rot = 2 * (0.5 * j_wheel * (omega**2))
179
 
180
+ # 4.5 Énergie Totale du Système
181
+ e_total_j = e_trans + e_rot
182
+
183
+ # 4.6 Définition des Seuils de Normes
184
+ n_s = 0
185
+ n_w = 0
186
+ if norm_cat == "City/Trekking":
187
+ n_s, n_w = 340, 220
188
+ elif norm_cat == "Kids":
189
+ n_s, n_w = 204, 132
190
+ elif norm_cat == "MTB":
191
+ n_s, n_w = 425, 280
192
+ elif norm_cat == "Racing":
193
+ n_s, n_w = 425, 260
194
 
195
+ # 4.7 Données du Benchmark
196
  row_bench = df[df['model name'] == ref_model_name].iloc[0]
197
  ref_f_sec = row_bench['dry a'] * effort_x + row_bench['dry b']
198
  ref_f_wet = row_bench['wet a'] * effort_x + row_bench['wet b']
199
 
200
+ # ==========================================================================
201
+ # 5. HEADER : DIAGNOSTIC VISUEL
202
+ # ==========================================================================
203
+ label_diag = "❄️ FREINAGE LÉGER"
204
+ color_diag = "#a1c4fd"
205
+ if 70 <= effort_x <= 110:
206
+ label_diag = "⚖️ FREINAGE MODÉRÉ"
207
+ color_diag = "#ffdb58"
208
+ elif effort_x > 110:
209
+ label_diag = "🔥 FREINAGE PUISSANT"
210
+ color_diag = "#ff4b4b"
211
+
212
+ st.markdown(f"""
213
+ <div style='background-color:{color_diag}; padding:10px; border-radius:8px; text-align:center; border: 3px solid #000; margin-bottom: 10px;'>
214
+ <span style='font-weight:900; font-size:18px;'>{label_diag} | Effort: {effort_x} N</span>
215
+ </div>
216
+ """, unsafe_allow_html=True)
217
 
218
+ # ==========================================================================
219
+ # 6. GRAPHIQUE INTERACTIF (PLOTLY NOIR SUR BLANC)
220
+ # ==========================================================================
221
  filtered_df = df[df['model name'].isin(selected_models)]
222
  fig = go.Figure()
223
  x_grid = np.linspace(40, 200, 150)
224
+ color_p = ['#0082C3', '#E63312', '#333333', '#00A14B', '#FFD200']
225
 
226
  results_summary = []
227
+
228
  for i, (idx, row) in enumerate(filtered_df.iterrows()):
229
+ clr = color_p[i % len(color_p)]
230
  f_sec = row['dry a'] * effort_x + row['dry b']
231
  f_wet = row['wet a'] * effort_x + row['wet b']
232
  results_summary.append({"name": row['model name'], "dry": f_sec, "wet": f_wet, "row": row})
233
 
234
+ # Courbe Sec
235
  if view_mode != "Humide uniquement":
236
+ fig.add_trace(go.Scatter(x=x_grid, y=row['dry a']*x_grid + row['dry b'], name=f"{row['model name']} (Sec)", line=dict(color=clr, width=4)))
237
  if n_s > 0:
238
  xt = (n_s - row['dry b']) / row['dry a']
239
  if 40 <= xt <= 200:
240
  fig.add_trace(go.Scatter(x=[xt], y=[n_s], mode='markers+text', text=[f"{round(xt,1)}N"], textfont=dict(color="black", weight=800), textposition="top center", marker=dict(color=clr, size=12, symbol='x'), showlegend=False))
241
 
242
+ # Courbe Humide
243
  if view_mode != "Sec uniquement":
244
+ fig.add_trace(go.Scatter(x=x_grid, y=row['wet a']*x_grid + row['wet b'], name=f"{row['model name']} (Wet)", line=dict(color=clr, width=2, dash='dot')))
245
 
246
+ # Lignes de normes horizontales noires
247
  if n_s > 0 and view_mode != "Humide uniquement":
248
+ fig.add_hline(y=n_s, line_width=3, line_color="#000", annotation_text="NORME SEC", annotation_font=dict(color="black", size=14, weight=800))
249
  if n_w > 0 and view_mode != "Sec uniquement":
250
+ fig.add_hline(y=n_w, line_width=3, line_dash="dot", line_color="#000", annotation_text="NORME HUMIDE", annotation_font=dict(color="black", size=14, weight=800))
251
 
252
+ # Ligne d'effort vertical
253
  fig.add_vline(x=effort_x, line_width=2, line_dash="dash", line_color="#555")
254
 
255
+ # Mise en forme graphique (Forçage Noir)
256
  fig.update_layout(
257
+ height=500, plot_bgcolor='white', paper_bgcolor='white',
258
+ xaxis=dict(title=dict(text="Effort Levier [N]", font=dict(color="black", size=14, weight=700)), tickfont=dict(color="black", weight=700), linecolor="black", gridcolor="#EEE"),
259
+ yaxis=dict(title=dict(text="Performance [N]", font=dict(color="black", size=14, weight=700)), tickfont=dict(color="black", weight=700), linecolor="black", gridcolor="#EEE"),
260
  legend=dict(font=dict(color="black", weight=700), bordercolor="black", borderwidth=1, bgcolor="white")
261
  )
262
  st.plotly_chart(fig, use_container_width=True)
263
 
264
+ # ==========================================================================
265
+ # 7. DASHBOARD D'ANALYSE (COMPARAISON ET DISTANCE)
266
+ # ==========================================================================
267
+ st.markdown(f"**📊 Dynamics Analysis | Energie du système : {int(e_total_j)} Joules**")
268
+
269
  cols = st.columns(len(results_summary))
270
 
271
  for i, res in enumerate(results_summary):
272
  with cols[i]:
273
+ st.markdown(f"### {res['name']}")
 
274
 
275
+ # --- ANALYSE CONDITION SEC ---
276
  if view_mode != "Humide uniquement":
277
+ delta_s = round(res['dry'] - ref_f_sec, 1) if res['name'] != ref_model_name else None
278
+ st.metric("Force Sec", f"{round(res['dry'], 1)} N", delta_s)
279
+
280
+ # Calcul de la distance d'arrêt (D = E/F)
281
  d_sec = e_total_j / res['dry'] if res['dry'] > 0 else 0
282
+ st.write(f"🛑 **Stop (Sec): {round(d_sec, 2)} m**")
283
+
284
  if n_s > 0:
285
+ xt_req = (n_s - res['row']['dry b']) / res['row']['dry a']
286
+ if xt_req > 180:
287
+ st.markdown(f"<div class='alert-red'>NON CONFORME SEC</div>", unsafe_allow_html=True)
288
+ else:
289
+ st.markdown(f"<div class='check-green'>CONFORME SEC</div>", unsafe_allow_html=True)
290
 
291
+ # --- ANALYSE CONDITION HUMIDE ---
292
  if view_mode != "Sec uniquement":
293
+ delta_w = round(res['wet'] - ref_f_wet, 1) if res['name'] != ref_model_name else None
294
+ st.metric("Force Wet", f"{round(res['wet'], 1)} N", delta_w)
295
+
296
  d_wet = e_total_j / res['wet'] if res['wet'] > 0 else 0
297
+ st.write(f"🌧️ **Stop (Wet): {round(d_wet, 2)} m**")
298
+
299
  if n_w > 0:
300
+ xtw_req = (n_w - res['row']['wet b']) / res['row']['wet a']
301
+ if xtw_req > 180:
302
+ st.markdown(f"<div class='alert-red'>NON CONFORME HUMIDE</div>", unsafe_allow_html=True)
303
+ else:
304
+ st.markdown(f"<div class='check-green'>CONFORME HUMIDE</div>", unsafe_allow_html=True)
 
 
305
 
306
  except Exception as e:
307
+ st.error(f"Erreur Détectée : {e}")