Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import plotly.graph_objects as go | |
| import os | |
| # Configuration de la page | |
| st.set_page_config(page_title="Brake Performance Lab", layout="wide", page_icon="🚲") | |
| # --- LE "SUPER CSS" POUR FORCER LE BLANC ET LE NOIR --- | |
| st.markdown(""" | |
| <style> | |
| /* 1. On force le fond de toute la page en BLANC */ | |
| .stApp { | |
| background-color: #FFFFFF !important; | |
| } | |
| /* 2. On force TOUT le texte en NOIR ABSOLU */ | |
| * { | |
| color: #000000 !important; | |
| } | |
| /* 3. On force les étiquettes des menus (Selectbox, Slider, Multiselect) en NOIR */ | |
| label, .stMultiSelect div, .stSelectbox div, .stSlider div { | |
| color: #000000 !important; | |
| font-weight: bold !important; | |
| } | |
| /* 4. On s'assure que les entrées de texte et menus ont un fond clair pour voir le texte noir dedans */ | |
| .stMultiSelect span, .stSelectbox div[data-baseweb="select"] { | |
| background-color: #F0F2F6 !important; | |
| } | |
| /* 5. Metrics et boîtes d'analyse */ | |
| [data-testid="stMetricValue"] { color: #000000 !important; font-weight: 800 !important; font-size: 22px !important; } | |
| [data-testid="stMetricLabel"] { color: #000000 !important; font-weight: bold !important; } | |
| [data-testid="column"] { | |
| padding: 10px !important; | |
| border: 2px solid #000000 !important; | |
| border-radius: 8px !important; | |
| background-color: #FFFFFF !important; | |
| } | |
| /* 6. Alertes (on garde les couleurs mais en version foncée pour la lisibilité) */ | |
| .alert-red { color: #B71C1C !important; font-weight: 900 !important; } | |
| .check-green { color: #1B5E20 !important; font-weight: 900 !important; } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| def load_data(): | |
| current_dir = os.path.dirname(__file__) | |
| file_path = os.path.join(current_dir, "Brake_Lab_Test_Data.xlsx") | |
| data = pd.read_excel(file_path, sheet_name='Data') | |
| data.columns = data.columns.str.strip() | |
| return data | |
| try: | |
| df = load_data() | |
| all_models = df['model name'].unique().tolist() | |
| with st.sidebar: | |
| st.image("https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Decathlon_Logo.svg/1280px-Decathlon_Logo.svg.png", width=200) | |
| st.title("⚙️ SETTINGS") | |
| x_input = st.slider("🫱 Lever Effort [N]", 40, 200, 100) | |
| st.markdown("---") | |
| selected_models = st.multiselect("Select Models to Display", options=all_models, default=all_models[:2]) | |
| st.markdown("---") | |
| st.subheader("📋 Standard Compliance") | |
| norm_type = st.selectbox("Category", ["None", "City/Trekking", "Kids", "MTB", "Racing"]) | |
| n_dry, n_wet = 0, 0 | |
| if norm_type == "City/Trekking": n_dry, n_wet = 340, 220 | |
| elif norm_type == "Kids": n_dry, n_wet = 204, 132 | |
| elif norm_type == "MTB": n_dry, n_wet = 425, 280 | |
| elif norm_type == "Racing": n_dry, n_wet = 425, 260 | |
| with st.expander("🔍 Display Options"): | |
| show_loss = st.checkbox("Show Wet Loss Analysis", value=True) | |
| enable_comparison = st.checkbox("Enable Reference Comparison", value=True) | |
| ref_model = st.selectbox("Reference Model (Benchmark)", options=all_models) | |
| condition_view = st.radio("Conditions to display", ["Both", "Dry only", "Wet only"], index=0) | |
| # --- DIAGNOSTIC HEADER --- | |
| if x_input < 70: label, color_alert = "❄️ LIGHT BRAKING", "#a1c4fd" | |
| elif 70 <= x_input <= 110: label, color_alert = "⚖️ MODERATE BRAKING", "#ffdb58" | |
| else: label, color_alert = "🔥 POWERFUL BRAKING", "#ff4b4b" | |
| st.markdown(f""" | |
| <div style="background-color:{color_alert}; padding:10px; border-radius:8px; text-align:center; border: 3px solid #000000; margin-bottom: 15px;"> | |
| <span style="color:#000000 !important; font-weight:900; font-size:16px;">{label} | Effort: {round(float(x_input), 1)} N</span> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # --- GRAPHIC AREA --- | |
| filtered_df = df[df['model name'].isin(selected_models)] | |
| fig = go.Figure() | |
| x_range = np.linspace(40, 200, 150) | |
| # Couleurs courbes : Bleu Decathlon, Rouge, Noir, Vert, Jaune | |
| colors = ['#0082C3', '#E63312', '#000000', '#00A14B', '#FFD200'] | |
| row_ref = df[df['model name'] == ref_model].iloc[0] | |
| ref_dry_val = row_ref['dry a'] * x_input + row_ref['dry b'] | |
| ref_wet_val = row_ref['wet a'] * x_input + row_ref['wet b'] | |
| comparison_results = [] | |
| for i, (index, row) in enumerate(filtered_df.iterrows()): | |
| color = colors[i % len(colors)] | |
| y_d = row['dry a'] * x_input + row['dry b'] | |
| y_w = row['wet a'] * x_input + row['wet b'] | |
| comparison_results.append({"name": row['model name'], "dry": y_d, "wet": y_w, "row": row}) | |
| if condition_view in ["Both", "Dry only"]: | |
| fig.add_trace(go.Scatter(x=x_range, y=row['dry a']*x_range+row['dry b'], mode='lines', name=f"{row['model name']} (Dry)", line=dict(color=color, width=4))) | |
| if n_dry > 0: | |
| xt = (n_dry - row['dry b']) / row['dry a'] | |
| if xt <= 200: fig.add_trace(go.Scatter(x=[xt], y=[n_dry], mode='markers+text', text=[f"{round(xt,1)}N"], textfont=dict(color="#000000", size=12, weight=700), textposition="top center", marker=dict(color=color, size=10, symbol='x'), showlegend=False)) | |
| if condition_view in ["Both", "Wet only"]: | |
| fig.add_trace(go.Scatter(x=x_range, y=row['wet a']*x_range+row['wet b'], mode='lines', name=f"{row['model name']} (Wet)", line=dict(color=color, width=3, dash='dot'))) | |
| if n_wet > 0: | |
| xtw = (n_wet - row['wet b']) / row['wet a'] | |
| if xtw <= 200: fig.add_trace(go.Scatter(x=[xtw], y=[n_wet], mode='markers+text', text=[f"{round(xtw,1)}N"], textfont=dict(color="#000000", size=12, weight=700), textposition="bottom center", marker=dict(color=color, size=10, symbol='circle-open'), showlegend=False)) | |
| # Lignes de normes | |
| if n_dry > 0 and (condition_view in ["Both", "Dry only"]): | |
| fig.add_hline(y=n_dry, line_width=3, line_color="#000000", annotation_text=f"Norm Dry: {n_dry}N", annotation_font=dict(color="black", size=12, weight=700)) | |
| if n_wet > 0 and (condition_view in ["Both", "Wet only"]): | |
| fig.add_hline(y=n_wet, line_width=3, line_dash="dot", line_color="#000000", annotation_text=f"Norm Wet: {n_wet}N", annotation_font=dict(color="black", size=12, weight=700)) | |
| fig.add_vline(x=x_input, line_width=2, line_dash="dash", line_color="#000000") | |
| fig.update_layout( | |
| height=480, | |
| xaxis=dict(title="Lever Effort [N]", color="#000000", linecolor="#000000", linewidth=3, tickfont=dict(color="#000000", size=13, weight=700), gridcolor="#E0E0E0"), | |
| yaxis=dict(title="Performance [N]", color="#000000", linecolor="#000000", linewidth=3, tickfont=dict(color="#000000", size=13, weight=700), gridcolor="#E0E0E0"), | |
| font=dict(color="#000000", size=12), | |
| plot_bgcolor='#FFFFFF', paper_bgcolor='#FFFFFF', | |
| hovermode="x unified", | |
| legend=dict(font=dict(color="#000000", size=12, weight=700), bordercolor="#000000", borderwidth=2, bgcolor="#FFFFFF") | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| # --- ANALYSIS DASHBOARD --- | |
| st.markdown(f"<p style='color:#000000; font-weight:900; font-size:16px;'>📊 Performance Analysis [N] | Ref: {ref_model}</p>", unsafe_allow_html=True) | |
| if not filtered_df.empty: | |
| cols = st.columns(len(comparison_results)) | |
| for i, res in enumerate(comparison_results): | |
| with cols[i]: | |
| st.markdown(f"<p style='font-size:14px; font-weight:900; color:#000000; margin-bottom:5px; text-decoration: underline;'>{res['name']} {'⭐' if (res['name'] == ref_model) else ''}</p>", unsafe_allow_html=True) | |
| if condition_view in ["Both", "Dry only"]: | |
| dv = round(res['dry'], 1) | |
| if enable_comparison and not (res['name'] == ref_model): | |
| diff = dv - round(ref_dry_val, 1) | |
| st.metric("Dry Perf.", f"{dv} N", f"{diff:+.1f} N Vs Ref.") | |
| else: st.metric("Dry Perf.", f"{dv} N") | |
| if n_dry > 0: | |
| xt = (n_dry - res['row']['dry b']) / res['row']['dry a'] | |
| if xt > 180: st.markdown(f"<div class='alert-red'>❌ NON CONFORME SEC ({norm_type})<br>Target: {round(xt,1)}N > 180N</div>", unsafe_allow_html=True) | |
| else: st.markdown(f"<div class='check-green'>✅ Conforme Sec ({round(xt,1)}N)</div>", unsafe_allow_html=True) | |
| if condition_view in ["Both", "Wet only"]: | |
| wv = round(res['wet'], 1) | |
| if enable_comparison and not (res['name'] == ref_model): | |
| diffw = wv - round(ref_wet_val, 1) | |
| st.metric("Wet Perf.", f"{wv} N", f"{diffw:+.1f} N Vs Ref.") | |
| else: st.metric("Wet Perf.", f"{wv} N") | |
| if n_wet > 0: | |
| xtw = (n_wet - res['row']['wet b']) / res['row']['wet a'] | |
| if xtw > 180: st.markdown(f"<div class='alert-red'>❌ NON CONFORME HUMIDE ({norm_type})<br>Target: {round(xtw,1)}N > 180N</div>", unsafe_allow_html=True) | |
| else: st.markdown(f"<div class='check-green'>✅ Conforme Humide ({round(xtw,1)}N)</div>", unsafe_allow_html=True) | |
| if show_loss and condition_view == "Both": | |
| loss_pct = ((res['dry'] - res['wet']) / res['dry'] * 100) if res['dry'] != 0 else 0 | |
| st.metric("Efficiency Loss", f"-{round(loss_pct, 1)}%", delta_color="inverse") | |
| except Exception as e: | |
| st.error(f"Error: {e}") |