import streamlit as st import pandas as pd # ── Page Config ── st.set_page_config( page_title="Race Results Explorer", page_icon="🏃", layout="wide", initial_sidebar_state="expanded", ) # ── Custom CSS ── st.markdown(""" """, unsafe_allow_html=True) # ── Data Loading ── @st.cache_data def load_data(): df = pd.read_csv("new_data_01__2_.csv") # Drop the junk row at the bottom (non-numeric Place) df = df[pd.to_numeric(df["Place"], errors="coerce").notna()].copy() # Clean columns df["Place"] = df["Place"].astype(int) df["Bib"] = pd.to_numeric(df["Bib"], errors="coerce").astype("Int64") df["Gender"] = df["Gender"].str.strip().str.upper() df["City"] = df["City"].fillna("NOT SPECIFIED").str.strip().str.upper() df["State"] = df["State"].fillna("NOT SPECIFIED").str.strip().str.upper() df["Team"] = df["Team"].fillna("").str.strip() df["Time"] = df["Time"].astype(str).str.strip() df["Gun Time"] = df["Gun Time"].astype(str).str.strip() # Keep only valid genders df = df[df["Gender"].isin(["M", "F"])].copy() # Drop unnamed index column if present if "Unnamed: 0" in df.columns: df.drop(columns=["Unnamed: 0"], inplace=True) return df df = load_data() # ── Helper: parse time string to total seconds for filtering ── def time_to_seconds(t): try: parts = str(t).split(":") if len(parts) == 3: h, m, s = parts return int(h) * 3600 + int(m) * 60 + int(s) elif len(parts) == 2: m, s = parts return int(m) * 60 + int(s) except Exception: return None return None df["_time_sec"] = df["Time"].apply(time_to_seconds) df["_gun_sec"] = df["Gun Time"].apply(time_to_seconds) min_time = int(df["_time_sec"].min()) if df["_time_sec"].notna().any() else 0 max_time = int(df["_time_sec"].max()) if df["_time_sec"].notna().any() else 7200 min_gun = int(df["_gun_sec"].min()) if df["_gun_sec"].notna().any() else 0 max_gun = int(df["_gun_sec"].max()) if df["_gun_sec"].notna().any() else 7200 def seconds_to_label(s): h = s // 3600 m = (s % 3600) // 60 sec = s % 60 if h > 0: return f"{h}:{m:02d}:{sec:02d}" return f"{m}:{sec:02d}" # ══════════════════════════════════════ # SIDEBAR – FILTER CHAMBER # ══════════════════════════════════════ with st.sidebar: st.markdown('
', unsafe_allow_html=True) st.markdown("---") # ── 1. Gender ── st.markdown("##### 👤 Gender") gender_opts = ["All"] + sorted(df["Gender"].unique().tolist()) gender_sel = st.selectbox("Select Gender", gender_opts, index=0, label_visibility="collapsed") # ── 2. City ── st.markdown("##### 🏙️ City") city_list = sorted(df["City"].unique().tolist()) city_sel = st.multiselect("Select City", city_list, default=[], placeholder="All cities") # ── 3. State ── st.markdown("##### 📍 State") state_list = sorted(df["State"].unique().tolist()) state_sel = st.multiselect("Select State", state_list, default=[], placeholder="All states") # ── 4. Team ── st.markdown("##### 🏅 Team") team_mode = st.radio("Team filter mode", ["All Runners", "Team Members Only", "Independent Only", "Specific Team"], index=0, label_visibility="collapsed") specific_team = None if team_mode == "Specific Team": team_list = sorted([t for t in df["Team"].unique() if t]) specific_team = st.selectbox("Choose team", team_list, label_visibility="collapsed") st.markdown("---") # ── 5. Chip Time Range ── st.markdown("##### ⏱️ Chip Time Range") time_range = st.slider( "Chip Time", min_value=min_time, max_value=max_time, value=(min_time, max_time), step=60, format="%d sec", label_visibility="collapsed", ) st.caption(f"From **{seconds_to_label(time_range[0])}** to **{seconds_to_label(time_range[1])}**") # ── 6. Gun Time Range ── st.markdown("##### 🔫 Gun Time Range") gun_range = st.slider( "Gun Time", min_value=min_gun, max_value=max_gun, value=(min_gun, max_gun), step=60, format="%d sec", label_visibility="collapsed", ) st.caption(f"From **{seconds_to_label(gun_range[0])}** to **{seconds_to_label(gun_range[1])}**") st.markdown("---") # ── 7. Name Search ── st.markdown("##### 🔍 Search by Name") name_search = st.text_input("Name", placeholder="e.g. JARED WILSON", label_visibility="collapsed") # ── Reset ── if st.button("🔄 Reset All Filters", use_container_width=True): st.rerun() # ══════════════════════════════════════ # APPLY FILTERS # ══════════════════════════════════════ filtered = df.copy() # Gender if gender_sel != "All": filtered = filtered[filtered["Gender"] == gender_sel] # City if city_sel: filtered = filtered[filtered["City"].isin(city_sel)] # State if state_sel: filtered = filtered[filtered["State"].isin(state_sel)] # Team if team_mode == "Team Members Only": filtered = filtered[filtered["Team"] != ""] elif team_mode == "Independent Only": filtered = filtered[filtered["Team"] == ""] elif team_mode == "Specific Team" and specific_team: filtered = filtered[filtered["Team"] == specific_team] # Chip Time filtered = filtered[ (filtered["_time_sec"] >= time_range[0]) & (filtered["_time_sec"] <= time_range[1]) | filtered["_time_sec"].isna() ] # Gun Time filtered = filtered[ (filtered["_gun_sec"] >= gun_range[0]) & (filtered["_gun_sec"] <= gun_range[1]) | filtered["_gun_sec"].isna() ] # Name if name_search: filtered = filtered[filtered["Name"].str.contains(name_search.upper(), case=False, na=False)] # ══════════════════════════════════════ # MAIN CONTENT # ══════════════════════════════════════ st.markdown('Showing {len(filtered)} of {len(df)} runners
', unsafe_allow_html=True) # ── Metric Cards ── c1, c2, c3, c4, c5 = st.columns(5) with c1: st.markdown(f"""