Team Ai
Apppublic

LRWise/Data_Visualization_TdF_Winner

sourceHugging Facemitupdated 1y agoView on Hugging Face
0likes
app.py1824 linesDownload Raw Back to root
1import marimo2 3__generated_with = "0.11.26"4app = marimo.App(width="medium")5 6 7@app.cell8def _():9    import marimo as mo10    import pandas as pd11    import svg12    import plotly.express as px13    import plotly.graph_objects as go14    import math15    return go, math, mo, pd, px, svg16 17 18@app.cell19def _(pd):20    winners = pd.read_csv("Data/tdf_winners.csv")21    df_stage = pd.read_csv("Data/stage_data.csv")22    df_stage_winner = pd.read_csv("Data/tdf_stages.csv")23    # This is needed to find geolocations.24    # unique_locations25    #client_ors = ors.Client(key='5b3ce3597851110001cf624802e069d6633748a5ae4e9842334f1dc2')26    #df_final = get_coordinates_for_locations(client_ors, unique_locations)27    #import time28 29    #def get_coordinates_for_locations(client, unique_locations, sleep = 0.5):30    #    results = []31 32    #    for location in unique_locations:33    #        response = client.pelias_search(text=location)34 35            # Get the first result if available36    #        features = response.get("features", [])37    #        if features:38    #            coords = features[0]["geometry"]["coordinates"]  # [lon, lat]39    #            results.append({40    #                "location": location,41    #                "longitude": coords[0],42    #                "latitude": coords[1],43    #                "coordinates": coords44    #            })45    #        else:46    #            results.append({47    #                "location": location,48    #                "longitude": None,49    #                "latitude": None,50    #                "coordinates": None51    #            })52    #        time.sleep(sleep)53 54    #    return pd.DataFrame(results)55 56    df_stage_final = pd.read_excel("Data/stage_information.xlsx")57    return df_stage, df_stage_final, df_stage_winner, winners58 59 60@app.cell61def _(pd, winners):62    nationality_info = {63        "\xa0Luxembourg": (["#EA141D", "#FFFFFF", "#51ADDA"], "up_down"),64        "\xa0France": (["#002395", "#FFFFFF", "#ED2939"], "left_right"),65        "\xa0Belgium": (["#000000", "#FAE042", "#ED2939"], "left_right"),66        "\xa0Italy":(["#009246", "#ffffff", "#ce2b37"], "left_right"),67        "\xa0\xa0Switzerland":(["#ff0000", "#ffffff", "#ff0000"], "center"),68        "\xa0Spain": (["#C60B1E", "#FFC400", "#C60B1E"], "up_down"),69        "\xa0Netherlands": (["#AE1C28", "#FFFFFF", "#21468B"], "up_down"),70        "\xa0United States": (["#3C3B6E", "#FFFFFF", "#B22234"], "left_right"),71        "\xa0Ireland": (["#169B62", "#FFFFFF", "#FF883E"], "left_right"),72        "\xa0Denmark": (["#C60C30", "#FFFFFF", "#C60C30"], "up_down"),73        "\xa0Germany": (["#000000", "#DD0000", "#FFCE00"], "up_down"),74        "\xa0Australia": (["#012169", "#FFFFFF", "#E4002B"], "UK"),75        "\xa0Great Britain": (["#00247D", "#FFFFFF", "#CF142B"], "UK"),76        "\xa0Colombia": (["#FCD116", "#003893", "#CE1126"], "up_down"), 77    }78    # Map in a single step using .apply()79    winners[["colors", "flag_type"]] = winners["nationality"].apply(80        lambda x: pd.Series(nationality_info.get(x, (["#FFFFFF", "#FFFFFF", "#FFFFFF"], "unknown")))81    )82 83    new_winners = winners["start_date"].str.split("-", n=2, expand=True)84    new_winners = new_winners.apply(pd.to_numeric, errors='coerce') 85    new_winners['TIME'] = new_winners[0]86    winners['Year'] = new_winners['TIME']87 88    winners.dropna(subset=["colors"], inplace=True)89    return nationality_info, new_winners90 91 92@app.cell93def _(df_stage_final, df_stage_winner, pd):94    new_stage_winner = df_stage_winner["Date"].str.split("-", n=2, expand=True)95    new_stage_winner = new_stage_winner.apply(pd.to_numeric, errors='coerce') 96    new_stage_winner['TIME'] = new_stage_winner[0]97    df_stage_winner['Year'] = new_stage_winner['TIME']98 99    result_tdf = df_stage_winner.merge(df_stage_final, how='left', left_on='Origin', right_on='location').drop('location', axis=1)100    result_tdf = result_tdf.merge(df_stage_final, how='left', left_on='Destination', right_on='location', suffixes=('_Origin', '_Destination')).drop('location', axis=1)101    return new_stage_winner, result_tdf102 103 104 105 106@app.cell107def _(go, pd, px):108    def plot_route_type(year, result_tdf, winners_df):109        df = result_tdf[result_tdf["Year"] == year].copy()110        df = df.sort_values("Stage")111        overall_winner = pd.unique(winners_df["Overall_Winner"][winners_df["Year"]==year])[0]112 113        fig = go.Figure()114 115        type_colors = {116            stage_type: color117            for stage_type, color in zip(118                df["Type"].unique(), px.colors.qualitative.Set1119            )120        }121 122        plotted_types = set()123 124        for stage_type in df["Type"].unique():125            fig.add_trace(go.Scattergeo(126            lon=[None],  # Invisible trace127            lat=[None],128            mode="lines",129            line=dict(width=3, color=type_colors[stage_type], dash="solid"),130            name=stage_type,131            showlegend=True132            ))133 134        for idx, row in df.iterrows():135            stage_type = row["Type"]136            showlegend = False  # We handle legend with dummy traces above137    138            139        140            is_overall_winner = row["Winner"] == overall_winner141            line_style = dict(142                width=3 if is_overall_winner else 2,143                color=type_colors[stage_type],144                dash="solid" if is_overall_winner else "dot"145            )146        147            fig.add_trace(go.Scattergeo(148                lon=[row["longitude_Origin"], row["longitude_Destination"]],149                lat=[row["latitude_Origin"], row["latitude_Destination"]],150                mode="lines",151                line=line_style,152                hoverinfo="text",153                text=(154                    f"Stage {row['Stage']}: {row['Origin']} → {row['Destination']}<br>"155                    f"Type: {row['Type']}<br>Winner: {row['Winner']}<br>Distance: {row['Distance']} km"156                ),157                name=stage_type,158                showlegend=showlegend159            ))160 161            # Origin and Destination markers (unchanged)162            for lon, lat, color in [163                (row["longitude_Origin"], row["latitude_Origin"], "gold"),164                (row["longitude_Destination"], row["latitude_Destination"], "darkorange")165            ]:166                fig.add_trace(go.Scattergeo(167                    lon=[lon],168                    lat=[lat],169                    mode="markers",170                    marker=dict(size=10, color=color),171                    text=(172                        f"Stage {row['Stage']}: {row['Origin']} → {row['Destination']}<br>"173                        f"Type: {row['Type']}<br>Winner: {row['Winner']}<br>Distance: {row['Distance']} km"174                    ),175                    hoverinfo="text",176                    showlegend=False177                ))178 179            plotted_types.add(stage_type)180 181        # Layout styling182        fig.update_layout(183            title=f"Tour de France Route - {year}",184            title_x=0.5,185            width=570,186            height=500,187            margin=dict(t=50, b=50, l=10, r=10),  # slightly larger bottom margin188            geo=dict(189                scope="europe",190                projection_type="mollweide",191                projection_scale=5,192                center=dict(lat=46.2, lon=2.87),193                landcolor="white",194                oceancolor="lightblue",195                showocean=True,196                bgcolor="#F0F0F0"197            ),198            paper_bgcolor="rgba(0,0,0,0)",199            plot_bgcolor="rgba(0,0,0,0)",200            legend=dict(201                orientation="h",202                yanchor="top",203                y=-0.2,204                xanchor="center",205                x=0.5206            )207        )208 209        return fig210    return (plot_route_type,)211 212 213@app.cell214def _(go, pd):215    def winner_data(df_stage, df_stage_winner, winners):216        # Load your datasets (update file paths accordingly)217        stages_df = df_stage  # Contains rider, stage ID, time diff218        stages_df["rank"] = pd.to_numeric(stages_df["rank"], errors='coerce')219        stages_df["Year"] = stages_df["year"]220        winners_df = df_stage_winner  # Contains stage ID, Type, Distance, Winner221        overall_df = winners # Contains overall winners per year222 223        winners_df = winners_df.merge(overall_df, on="Year", how="left")224        winners_df = winners_df.merge(overall_df, on="Year", how="left")225 226 227        # Rename relevant columns for clarity228        winners_df.rename(columns={"winner_name_y": "Overall_Winner"}, inplace=True)229 230        # Keep only the relevant columns231        winners_df = winners_df[["Year", "Stage", "Type", "Winner", "Distance", "Overall_Winner", "Winner_Country"]]232 233        return winners_df234 235    def type_winner(df_stage, winners_df):236 237        stages_df = df_stage238        # Keep top 10 riders per stage239        stages_top = stages_df.loc[stages_df["rank"] <= 10].copy()240        stages_top = stages_top[stages_top["Year"]<2018]241 242        def normalize_name(name):243           return " ".join(sorted(name.strip().lower().split()))244 245        stages_top["rider_clean"] = stages_top["rider"].apply(normalize_name)246        stages_top["Year"] = stages_top["year"]247        winners_df_top = winners_df.copy()248        winners_df_top["rider_clean"] = winners_df_top["Overall_Winner"].apply(normalize_name)249        winners_df_top["winner_clean"] = winners_df_top["Overall_Winner"].apply(normalize_name)250        # Merge with winners_df (to get overall winner)251 252        # Merge on both rider name and edition253        stages_winners_merge = stages_top.merge(254            winners_df_top,255            on=["rider_clean", "Year"],256            how="inner"257        )258 259        new_stage_winner_merge = stages_winners_merge["stage_results_id"].str.split("-", n=2, expand=True)260        new_stage_winner_merge = new_stage_winner_merge.apply(pd.to_numeric, errors='coerce') 261        new_stage_winner_merge['STAGE'] = new_stage_winner_merge[1]262        stages_winners_merge['stage'] = new_stage_winner_merge['STAGE']263        stages_winners_merge["Stage"] = pd.to_numeric(stages_winners_merge["Stage"], errors='coerce')264 265        filtered_stage_wins_df = stages_winners_merge[266            stages_winners_merge["stage"] == stages_winners_merge["Stage"]267        ]268 269 270        filtered_stage_wins_df = filtered_stage_wins_df[271            filtered_stage_wins_df["rider_clean"] == filtered_stage_wins_df["winner_clean"]272        ]273 274        # Drop duplicates per edition-stage to avoid multiple rows if the winner placed top 3 more than once275        filtered_stage_wins_df = filtered_stage_wins_df.drop_duplicates(subset=["edition", "Stage"])276        filtered_stage_wins_df = filtered_stage_wins_df.drop_duplicates(subset=["edition", "Stage"])277 278        type_counts_per_year = filtered_stage_wins_df.groupby("Year")["Type"].value_counts().unstack(fill_value=0)279 280        simplified_counts_df = type_counts_per_year.copy()281 282        # Combine relevant columns into broader categories283        simplified_counts_df["Time trial"] = (284            simplified_counts_df.get("Individual time trial", 0) +285            simplified_counts_df.get("Team time trial", 0) +286            simplified_counts_df.get("Mountain time trial", 0)287        )288 289        simplified_counts_df["Flat stage"] = (290            simplified_counts_df.get("Flat Stage", 0) +291            simplified_counts_df.get("Flat stage", 0) +292            simplified_counts_df.get("Flat cobblestone stage", 0) +293            simplified_counts_df.get("Plain stage", 0) +294            simplified_counts_df.get("Plain stage with cobblestones", 0)295        )296 297        simplified_counts_df["Intermediate stage"] = (298            simplified_counts_df.get("Transition stage", 0) +299            simplified_counts_df.get("Half Stage", 0)  # Optional, if it exists300        )301 302        simplified_counts_df["Mountain stage"] = (303            simplified_counts_df.get("Mountain stage", 0) +304            simplified_counts_df.get("Mountain Stage", 0) +305            simplified_counts_df["Hilly stage"] +306            simplified_counts_df.get("High mountain stage", 0) +307            simplified_counts_df.get("Medium mountain stage", 0) +308            simplified_counts_df.get("Stage with mountain", 0) +309            simplified_counts_df.get("Stage with mountain(s)", 0)310        )311 312        # Keep only the new simplified columns313        simplified_counts_df = simplified_counts_df[["Time trial", "Flat stage", "Intermediate stage", "Mountain stage"]]314 315        # Optional: Reset index if needed316        simplified_counts_df = simplified_counts_df.reset_index()317        return simplified_counts_df318 319    def simplified_stage_types_df(winners_df):320        types_per_year = winners_df.groupby("Year")["Type"].value_counts().unstack(fill_value=0)321        simplified_stage_types_df = types_per_year.copy()322 323        # === Simplified categories ===324 325        # 1. Time Trials (individual, team, mountain)326        simplified_stage_types_df["Time trial"] = (327            simplified_stage_types_df["Individual time trial"] +328            simplified_stage_types_df["Team time trial"] +329            simplified_stage_types_df.get("Mountain time trial", 0)330        )331        # 2. Flat stages (various naming variants + cobblestones)332        simplified_stage_types_df["Flat stage"] = (333            simplified_stage_types_df["Flat Stage"] +334            simplified_stage_types_df["Flat stage"] +335            simplified_stage_types_df["Flat cobblestone stage"] +336            simplified_stage_types_df["Plain stage"] +337            simplified_stage_types_df["Plain stage with cobblestones"]338        )339 340        simplified_stage_types_df["Intermediate stage"] = (341            simplified_stage_types_df["Transition stage"] +342            simplified_stage_types_df.get("Half Stage", 0)343        )344 345        simplified_stage_types_df["Mountain stage"] = (346            simplified_stage_types_df["Mountain stage"] +347            simplified_stage_types_df["High mountain stage"] +348            simplified_stage_types_df["Medium mountain stage"] +349            simplified_stage_types_df["Mountain Stage"] +350            simplified_stage_types_df["Hilly stage"] +351            simplified_stage_types_df.get("Stage with mountain", 0) +352            simplified_stage_types_df.get("Stage with mountain(s)", 0)353        )354 355        # Keep only the new simplified columns356        simplified_stage_types_df = simplified_stage_types_df[["Time trial", "Flat stage", "Intermediate stage", "Mountain stage"]]357        simplified_stage_types_df = simplified_stage_types_df.reset_index()358        simplified_stage_types_df["year"] = simplified_stage_types_df["Year"]359 360        # Optional: Reset index if needed361        simplified_stage_types_df = simplified_stage_types_df.reset_index()362 363        return simplified_stage_types_df364 365 366    def percentage_type_winner(df_stage, winners_df):367        simplified_counts_df = type_winner(df_stage, winners_df)368        types_per_year = winners_df.groupby("Year")["Type"].value_counts().unstack(fill_value=0)369 370        simplified_stage_types_df = types_per_year.copy()371 372        # === Simplified categories ===373 374        # 1. Time Trials (individual, team, mountain)375        simplified_stage_types_df["Time trial"] = (376            simplified_stage_types_df["Individual time trial"] +377            simplified_stage_types_df["Team time trial"] +378            simplified_stage_types_df.get("Mountain time trial", 0)379        )380 381        # 2. Flat stages (various naming variants + cobblestones)382        simplified_stage_types_df["Flat stage"] = (383            simplified_stage_types_df["Flat Stage"] +384            simplified_stage_types_df["Flat stage"] +385            simplified_stage_types_df["Flat cobblestone stage"] +386            simplified_stage_types_df["Plain stage"] +387            simplified_stage_types_df["Plain stage with cobblestones"]388        )389 390        simplified_stage_types_df["Intermediate stage"] = (391            simplified_stage_types_df["Transition stage"] +392            simplified_stage_types_df.get("Half Stage", 0)393        )394 395        simplified_stage_types_df["Mountain stage"] = (396            simplified_stage_types_df["Mountain stage"] +397            simplified_stage_types_df["High mountain stage"] +398            simplified_stage_types_df["Medium mountain stage"] +399            simplified_stage_types_df["Mountain Stage"] +400            simplified_stage_types_df["Hilly stage"] +401            simplified_stage_types_df.get("Stage with mountain", 0) +402            simplified_stage_types_df.get("Stage with mountain(s)", 0)403        )404 405 406 407 408        # Keep only the new simplified columns409        simplified_stage_types_df = simplified_stage_types_df[["Time trial", "Flat stage", "Intermediate stage", "Mountain stage"]]410        simplified_stage_types_df = simplified_stage_types_df.reset_index()411        simplified_stage_types_df["year"] = simplified_stage_types_df["Year"]412 413        # Optional: Reset index if needed414        simplified_stage_types_df = simplified_stage_types_df.reset_index()415 416        merged_counts = simplified_counts_df.merge(simplified_stage_types_df, on="Year", how="left")417 418        percentage_win_df = pd.DataFrame()419        percentage_win_df["year"] = merged_counts["year"]420 421        percentage_win_df["Time trial"] = (merged_counts["Time trial_x"]/merged_counts["Time trial_y"])*100422        percentage_win_df["Flat stage"] = (merged_counts["Flat stage_x"]/merged_counts["Flat stage_y"])*100423        percentage_win_df["Intermediate stage"] = (merged_counts["Intermediate stage_x"]/merged_counts["Intermediate stage_y"])*100424        percentage_win_df["Mountain stage"] = (merged_counts["Mountain stage_x"]/merged_counts["Mountain stage_y"])*100425 426        return percentage_win_df427 428    def visualize(year_selected, winners_df):429        # Filter dataset for the selected year430        filtered_stages = winners_df[winners_df["Year"] == year_selected]431 432        # Create unique labels for nodes433        stage_types = filtered_stages["Type"].unique().tolist()434        stage_winners = filtered_stages["Winner"].unique().tolist()435        overall_winner = filtered_stages["Overall_Winner"].unique().tolist()436 437        all_labels = stage_types + stage_winners438 439        # Map labels to indices440        node_indices = {label: i for i, label in enumerate(all_labels)}441 442        source_nodes = []443        target_nodes = []444        values = []445        colors = []446 447        # National colors448        country_colors = {449            "GBR": "#00247D", "GER": "#000000", "SVK": "#003897", "FRA": "#0055A4", "ITA": "#008C45",450            "COL": "#FCD116", "AUS": "#00008B", "NED": "#21468B", "SLO": "#005DA4", "NOR": "#BA0C2F",451            "POL": "#DC143C", "BEL": "#FFD700", "RUS": "#0033A0", "ESP": "#AA151B", "CZE": "#11457E",452            "LTU": "#FDB913", "IRL": "#169B62", "POR": "#006600", "SUI": "#D52B1E", "USA": "#B22234",453            "LUX": "#00A1DE", "KAZ": "#00BFFF", "DEN": "#C60C30", "RSA": "#007847", "UKR": "#0057B7",454            "AUT": "#ED2939", "EST": "#0072CE", "SWE": "#005EB8", "UZB": "#1EB53A", "LAT": "#A4343A",455            "IRE": "#169B62", "URS": "#FF0000", "BRA": "#009C3B", "MEX": "#006847", "GDR": "#FFCE00",456            "CAN": "#FF0000", "FRG": "#000000",457            'c("FRA", "FRA")': "#0055A4", 'c("BEL", "GER")': "#FFD700", 'c("BEL", "BEL")': "#FFD700",458            float("nan"): "rgba(160,160,160,0.6)",459        }460 461        winner_nationality_map = winners_df.drop_duplicates("Winner").set_index("Winner")["Winner_Country"].to_dict()462        node_colors = ["rgba(180,180,180,0.8)"] * len(all_labels)463 464        # Assign colors to winner nodes465        for winner in stage_winners:466            if winner in winner_nationality_map:467                nationality = winner_nationality_map[winner]468                color = country_colors.get(nationality, "rgba(180,180,180,0.8)")469                node_colors[node_indices[winner]] = color470 471        # Create Sankey links472        for _, row2 in filtered_stages.iterrows():473            source_nodes.append(node_indices[row2["Type"]])474            target_nodes.append(node_indices[row2["Winner"]])475            values.append(row2["Distance"])476            colors.append("rgba(255, 223, 0, 0.8)" if row2["Winner"] == row2["Overall_Winner"] else "rgba(128, 128, 128, 0.6)")477 478        # Sankey diagram479        fig = go.Figure(go.Sankey(480            node=dict(481                pad=15,482                thickness=20,483                line=dict(color="black", width=0.5),484                label=all_labels,485                color=node_colors486            ),487            link=dict(488                source=source_nodes,489                target=target_nodes,490                value=values,491                color=colors492            )493        ))494 495        # --- ADD LEGEND BELOW ---496        used_countries = {497            winner_nationality_map[w]: country_colors.get(winner_nationality_map[w], "#aaa")498            for w in stage_winners499            if w in winner_nationality_map and pd.notna(winner_nationality_map[w])500        }501 502        # Now safe to sort503        used_countries = dict(sorted(used_countries.items()))504 505 506        # Adjust layout to make room for legend507        fig.update_layout(508            autosize=True,509            margin=dict(t=50, b=20, l=10, r=10),510            width = 430,511            height=500,512            font_size=10,513            title_text="Stage types and the stage winners",514            title_x=0.5,515        )516 517        return fig, used_countries518    return (519        percentage_type_winner,520        simplified_stage_types_df,521        type_winner,522        visualize,523        winner_data,524    )525 526 527@app.cell528def _(math, pd, svg):529    # Class that creates a bike svg changing color depending on the riders nationality530    class Bike():531        def __init__(self, row, win_type, type_winner_abs, simplified_stage_types_df):532            if row is not None:# and not row.empty:533                self.flag_type = row["flag_type"]534                colors = row["colors"]535                self.color1 = colors[0]536                self.color2 = colors[1]537                self.color3 = colors[2]538                self.row = row539                win_type = win_type540                win_type["Year"] = pd.to_numeric(win_type["Year"])541                self.perc_flat = win_type[win_type["Year"]==row["Year"]]["Flat stage"]542                self.perc_mountain = win_type[win_type["Year"]==row["Year"]]["Mountain stage"]543                self.type_winner_abs = type_winner_abs544                self.simplified_stage_types_df = simplified_stage_types_df545 546                if pd.isna(row["height"]):547                    self.size = 1.8548                else:549                    self.size = row["height"]550 551 552        def _title(self):553            return svg.Title(elements=[f'Winner {self.row["Year"]}: {self.row["winner_name"]}, stage wins: {self.row["stage_wins"]}, team: {self.row["winner_team"]}, nationality: {self.row["nationality"]}'])554        555        def pie_chart_slices(self, cx, cy, r, data, colors, rotate_deg=0):556            paths = []557            start_angle = rotate_deg558            for label, value in data.items():559                if value == 0:560                    continue561 562                if value == 1.0:563                    # Full circle edge case564                    paths.append(svg.Circle(565                        cx=cx,566                        cy=cy,567                        r=r,568                        fill=colors[label]569                    ))570                    continue571 572                end_angle = start_angle + 360 * value573                x1 = cx + r * math.cos(math.radians(start_angle))574                y1 = cy + r * math.sin(math.radians(start_angle))575                x2 = cx + r * math.cos(math.radians(end_angle))576                y2 = cy + r * math.sin(math.radians(end_angle))577                large_arc = 1 if (end_angle - start_angle) > 180 else 0578                path_d = f"M{cx},{cy} L{x1},{y1} A{r},{r} 0 {large_arc},1 {x2},{y2} Z"579                paths.append(svg.Path(d=path_d, fill=colors[label]))580                start_angle = end_angle581            return paths582 583        def front_wheel_pie(self):584            S = self.size585            if self.flag_type == "center" or self.flag_type=="UK":586                fill_color = self.color1587                stroke_color = self.color1588            elif self.flag_type == "left_right" or self.flag_type == "up_down":589                fill_color = self.color3590                stroke_color = self.color3591 592            if fill_color.lower() in ["#ffffff", "white"]:593                stroke_color = "#000000"594 595 596            cx=83.86574 * S597            cy=201.976 * S598            r = 23.692965 * S599 600            year = self.row["Year"]601 602            # Filter for the year and sum stage type columns603            winner_row = self.type_winner_abs[self.type_winner_abs["Year"] == year]604            stage_totals = self.simplified_stage_types_df[self.simplified_stage_types_df["Year"] == year]605 606            relevant_cols = ["Time trial", "Mountain stage", "Intermediate stage", "Flat stage"]607 608            if not winner_row.empty and not stage_totals.empty:609                winner_count = winner_row[relevant_cols].sum().sum()610                total_count = stage_totals[relevant_cols].sum().sum()611                if total_count > 0:612                    data = {613                        "Top 10": winner_count / total_count614                        #"Other stages": 1 - (winner_count / total_count)615                    }616                else:617                    data = {"Unknown": 1}618            else:619                data = {"Unknown": 1}620 621            colors = {622                "Top 10": fill_color,623                #"Other stages": "#ffffff",624                "Unknown": "#e0e0e0"625            }626                # Draw pie slices627            # Pie chart slices628            pie_slices = self.pie_chart_slices(cx, cy, r, data, colors)629 630            # Pie + outline rotated group631            wheel_group = svg.G(632                elements=pie_slices + [633                    svg.Circle(634                        cx=cx,635                        cy=cy,636                        r=r,637                        fill="none",638                        stroke=fill_color,639                        stroke_width=2.61407 * S640                    )641                ],642                transform="rotate(-20.154323)"643            )644 645            # Legend646            legend_x = cx + 85 * S647            legend_y = cy - 65 * S648            box_size = 7 * S649            spacing = 10 * S650 651            if "Top 10" in data and data["Top 10"] > 0:652                percent = round(data["Top 10"] * 100)653                tooltip_text = f'{self.row["winner_name"]} won {percent}% of all stages'654            else:655                tooltip_text = "No stage win data available"656    657            return svg.G(elements=[658                wheel_group,659                svg.Title(elements=[tooltip_text])660            ])661 662        def head(self):663            # up_down = 0,0; left_right=2,2 ;UK=0,0 ; center=0,0664            S = self.size665            if (self.flag_type == "center" or self.flag_type == "up_down" or self.flag_type=="UK"):666                fill_color = self.color1667                stroke_color = self.color1668            elif (self.flag_type == "left_right"):669                fill_color = self.color3670                stroke_color = self.color3671 672            if fill_color.lower() in ["#ffffff", "white"]:673                stroke_color = "#000000"674 675            return svg.Circle(676                                cx=152.73752 * S, cy=114.65292 * S, r=8.827405 * S,677                                stroke=stroke_color,678                                fill=fill_color,  # Corrected color format679                                stroke_width=2.61407 * S,680                                elements = [self._title()]681                            )682 683        def body(self):684            # center color = 0,0; UK=2,1 ;left_right=1,1 ; up_down=0,0685            S = self.size686            if self.flag_type == "center" or self.flag_type == "up_down":687                fill_color = self.color1688                stroke_color = self.color1689            elif self.flag_type == "UK":690                fill_color = self.color3691                stroke_color = self.color2692            elif self.flag_type == "left_right":693                fill_color = self.color2694                stroke_color = self.color2695 696            if fill_color.lower() in ["#ffffff", "white"]:697                stroke_color = "#000000"698 699            return svg.Path(d=[svg.M(95.986494 * S, 110.4811 * S), svg.L(140.43661 * S, 111.44741 * S), 700                        svg.V(124.3315 * S), svg.H(108.87058 * S),],701                        fill=fill_color,702                        stroke=stroke_color,703                        stroke_width=1 * S,704                        elements = [self._title()]705                            )706 707        def arm(self):708            # up_down =1,1; left_right=1,1 ;UK=2,1 ; center=1,0709            S = self.size710 711            if self.flag_type == "up_down" or self.flag_type == "left_right":712                fill_color = self.color2713                stroke_color = self.color2714            elif self.flag_type == "UK":715                fill_color = self.color3716                stroke_color = self.color2717            elif self.flag_type == "center":718                fill_color = self.color2719                stroke_color = self.color1720 721            if fill_color.lower() in ["#ffffff", "white"]:722                stroke_color = "#000000"723 724            return svg.Path(d=[svg.M(140.27555 * S, 124.29123 * S), svg.L(130.77354 * S, 133.02826 * S), 725                               svg.L(144.94604 * S, 144.94604 * S), svg.L(139.7924 * S, 148.81127 * S),726                               svg.L(121.43257 * S, 133.67246 * S),  svg.L(130.12934 * S, 124.08991 * S),],727                                fill=fill_color,728                                stroke=stroke_color,729                                stroke_width=1 * S,730                                elements = [self._title()]731                            )732 733 734        def left_leg(self):735            S = self.size736            if self.flag_type == "up_down" or self.flag_type == "left_right":737                fill_color = self.color2738                stroke_color = self.color2739            elif self.flag_type == "UK":740                fill_color = self.color3741                stroke_color = self.color2742            elif self.flag_type == "center":743                fill_color = self.color1744                stroke_color = self.color1745 746            if fill_color.lower() in ["#ffffff", "white"]:747                stroke_color = "#000000"748 749            return svg.Path(750                d=[751                    svg.M(94.532861 * S, 112.61697 * S),752                    svg.L(94.524268 * S, 172.02704 * S),753                    svg.L(101.4425 * S, 172.02704 * S),754                    svg.L(101.4283 * S, 126.87143 * S)755                ],756                fill=fill_color,757                stroke=stroke_color,758                stroke_width=1 * S,759                elements = [self._title()]760                )761 762 763        def right_leg(self):764            S = self.size765 766            if self.flag_type == "center":767                fill_color, stroke_color = self.color2, self.color1768            elif self.flag_type == "UK":769                fill_color, stroke_color = self.color3, self.color2770            else:771                fill_color = stroke_color = self.color2772 773            if fill_color.lower() in ["#ffffff", "white"]:774                stroke_color = "#000000"775 776            return svg.Path(777                d=[778                    svg.M(94.532861 * S, 112.61697 * S),779                    svg.L(121.51828 * S, 142.12337 * S),780                    svg.L(101.12085 * S, 158.57267 * S),781                    svg.L(101.06795 * S, 150.68512 * S),782                    svg.L(111.04127 * S, 142.17389 * S),783                    svg.L(94.54537 * S, 124.52411 * S)784                ],785                fill=fill_color,786                stroke=stroke_color,787                stroke_width=1 * S,788                elements = [self._title()]789                )790 791        792        def wheel1(self):793            # up_down = 0,0; left_right=2,2 ;UK=0,0 ; center=0,0794            S = self.size795            if self.flag_type == "center" or self.flag_type=="UK":796                fill_color = self.color1797                stroke_color = self.color1798            elif self.flag_type == "left_right" or self.flag_type == "up_down":799                fill_color = self.color3800                stroke_color = self.color3801 802            if fill_color.lower() in ["#ffffff", "white"]:803                stroke_color = "#000000"804 805            return svg.Circle(806                                cx=148.86574 * S, cy=158.976 * S, r=23.692965 * S,807                                fill=fill_color,808                                stroke=stroke_color,809                                stroke_width=2.61407 * S,810                                elements = [self._title()]811                            )812 813 814        def wheel2(self):815            # up_down = 0,0; left_right=2,2 ;UK=0,0 ; center=0,0816            S = self.size817            if (self.flag_type == "center" or self.flag_type == "left_right" or self.flag_type=="UK"):818                fill_color = self.color1819                stroke_color = self.color1820            elif (self.flag_type == "up_down"):821                fill_color = self.color3822                stroke_color = self.color3823 824            if fill_color.lower() in ["#ffffff", "white"]:825                stroke_color = "#000000"826 827            return svg.Circle(cx=67.373695 * S, cy=158.976 * S, r=23.692965 * S,828                                fill=fill_color,829                                stroke=stroke_color,830                                stroke_width=2.61407 * S,831                                elements = [self._title()]832                            )833 834 835 836        def draw(self):837            S = self.size838            return svg.SVG(839                width=290 * S,840                height=250 * S,841                elements=[842                self.head(),843                self.body(),844                self.left_leg(),845                self.right_leg(),846                self.front_wheel_pie(),847                self.arm(),848                self.wheel2()849            ])850    return (Bike,)851 852 853@app.cell854def _(math, pd, svg):855    # Class that creates a bike svg changing color depending on the riders nationality856    # It also adds extra for being primarily a flat stage rider857    class BikeFast():858        def __init__(self, row, win_type, type_winner_abs, simplified_stage_types_df):859            if row is not None:# and not row.empty:860                self.flag_type = row["flag_type"]861                colors = row["colors"]862                self.color1 = colors[0]863                self.color2 = colors[1]864                self.color3 = colors[2]865                self.row = row866                win_type = win_type867                win_type["Year"] = pd.to_numeric(win_type["Year"])868                self.perc_flat = win_type[win_type["Year"]==row["Year"]]["Flat stage"]869                self.perc_mountain = win_type[win_type["Year"]==row["Year"]]["Mountain stage"]870                self.type_winner_abs = type_winner_abs871                self.simplified_stage_types_df = simplified_stage_types_df872 873                if pd.isna(row["height"]):874                    self.size = 1.8875                else:876                    self.size = row["height"]877 878        def _title(self):879            return svg.Title(elements=[f'Winner {self.row["Year"]}: {self.row["winner_name"]}, stage wins: {self.row["stage_wins"]}, team: {self.row["winner_team"]}, nationality: {self.row["nationality"]}'])880        881        def pie_chart_slices(self, cx, cy, r, data, colors, rotate_deg=0):882            paths = []883            start_angle = rotate_deg884            for label, value in data.items():885                if value == 0:886                    continue887 888                if value == 1.0:889                    # Full circle edge case890                    paths.append(svg.Circle(891                        cx=cx,892                        cy=cy,893                        r=r,894                        fill=colors[label]895                    ))896                    continue897 898                end_angle = start_angle + 360 * value899                x1 = cx + r * math.cos(math.radians(start_angle))900                y1 = cy + r * math.sin(math.radians(start_angle))901                x2 = cx + r * math.cos(math.radians(end_angle))902                y2 = cy + r * math.sin(math.radians(end_angle))903                large_arc = 1 if (end_angle - start_angle) > 180 else 0904                path_d = f"M{cx},{cy} L{x1},{y1} A{r},{r} 0 {large_arc},1 {x2},{y2} Z"905                paths.append(svg.Path(d=path_d, fill=colors[label]))906                start_angle = end_angle907            return paths908 909        def front_wheel_pie(self):910            S = self.size911            if self.flag_type == "center" or self.flag_type=="UK":912                fill_color = self.color1913                stroke_color = self.color1914            elif self.flag_type == "left_right" or self.flag_type == "up_down":915                fill_color = self.color3916                stroke_color = self.color3917 918            if fill_color.lower() in ["#ffffff", "white"]:919                stroke_color = "#000000"920 921 922            cx=148.86574 * S923            cy=158.976 * S924            r = 23.692965 * S925 926            year = self.row["Year"]927 928            # Filter for the year and sum stage type columns929            winner_row = self.type_winner_abs[self.type_winner_abs["Year"] == year]930            stage_totals = self.simplified_stage_types_df[self.simplified_stage_types_df["Year"] == year]931 932            relevant_cols = ["Time trial", "Mountain stage", "Intermediate stage", "Flat stage"]933 934            if not winner_row.empty and not stage_totals.empty:935                winner_count = winner_row[relevant_cols].sum().sum()936                total_count = stage_totals[relevant_cols].sum().sum()937 938                if total_count > 0:939                    data = {940                        "Top 10": winner_count / total_count941                        #"Other stages": 1 - (winner_count / total_count)942                    }943                else:944                    data = {"Unknown": 1}945            else:946                data = {"Unknown": 1}947 948            colors = {949                "Top 10": fill_color,950                #"Other stages": "#ffffff",951                "Unknown": "#e0e0e0"952            }953                # Draw pie slices954            # Pie chart slices955            pie_slices = self.pie_chart_slices(cx, cy, r, data, colors)956 957            # Pie + outline rotated group958            wheel_group = svg.G(959                elements=pie_slices + [960                    svg.Circle(961                        cx=cx,962                        cy=cy,963                        r=r,964                        fill="none",965                        stroke=fill_color,966                        stroke_width=2.61407 * S967                    )968                ]969            )970 971            # Legend972            legend_x = cx + 85 * S973            legend_y = cy - 65 * S974            box_size = 7 * S975            spacing = 10 * S976 977            if "Top 10" in data and data["Top 10"] > 0:978                percent = round(data["Top 10"] * 100)979                tooltip_text = f'{self.row["winner_name"]} won {percent}% of all stages'980            else:981                tooltip_text = "No stage win data available"982    983            return svg.G(elements=[984                wheel_group,985                svg.Title(elements=[tooltip_text])986            ])987 988        def head(self):989            # up_down = 0,0; left_right=2,2 ;UK=0,0 ; center=0,0990            S = self.size991            if (self.flag_type == "center" or self.flag_type == "up_down" or self.flag_type=="UK"):992                fill_color = self.color1993                stroke_color = self.color1994            elif (self.flag_type == "left_right"):995                fill_color = self.color3996                stroke_color = self.color3997 998            if fill_color.lower() in ["#ffffff", "white"]:999                stroke_color = "#000000"1000 1001            return svg.Circle(1002                                cx=152.73752 * S, cy=114.65292 * S, r=8.827405 * S,1003                                stroke=stroke_color,1004                                fill=fill_color,  # Corrected color format1005                                stroke_width=2.61407 * S,1006                                elements = [self._title()]1007                            )1008 1009        def body(self):1010            # center color = 0,0; UK=2,1 ;left_right=1,1 ; up_down=0,01011            S = self.size1012            if self.flag_type == "center" or self.flag_type == "up_down":1013                fill_color = self.color11014                stroke_color = self.color11015            elif self.flag_type == "UK":1016                fill_color = self.color31017                stroke_color = self.color21018            elif self.flag_type == "left_right":1019                fill_color = self.color21020                stroke_color = self.color21021 1022            if fill_color.lower() in ["#ffffff", "white"]:1023                stroke_color = "#000000"1024 1025            return svg.Path(d=[svg.M(95.986494 * S, 110.4811 * S), svg.L(140.43661 * S, 111.44741 * S), 1026                        svg.V(124.3315 * S), svg.H(108.87058 * S),],1027                        fill=fill_color,1028                        stroke=stroke_color,1029                        stroke_width=1 * S,1030                        elements = [self._title()])1031 1032        def arm(self):1033            # up_down =1,1; left_right=1,1 ;UK=2,1 ; center=1,01034            S = self.size1035 1036            if self.flag_type == "up_down" or self.flag_type == "left_right":1037                fill_color = self.color21038                stroke_color = self.color21039            elif self.flag_type == "UK":1040                fill_color = self.color31041                stroke_color = self.color21042            elif self.flag_type == "center":1043                fill_color = self.color21044                stroke_color = self.color11045 1046            if fill_color.lower() in ["#ffffff", "white"]:1047                stroke_color = "#000000"1048 1049            return svg.Path(d=[svg.M(140.27555 * S, 124.29123 * S), svg.L(130.77354 * S, 133.02826 * S), 1050                               svg.L(144.94604 * S, 144.94604 * S), svg.L(139.7924 * S, 148.81127 * S),1051                               svg.L(121.43257 * S, 133.67246 * S),  svg.L(130.12934 * S, 124.08991 * S),],1052                                fill=fill_color,1053                                stroke=stroke_color,1054                                stroke_width=1 * S,1055                                elements = [self._title()]1056                            )1057 1058        def left_leg(self):1059            S = self.size1060            if self.flag_type == "up_down" or self.flag_type == "left_right":1061                fill_color = self.color21062                stroke_color = self.color21063            elif self.flag_type == "UK":1064                fill_color = self.color31065                stroke_color = self.color21066            elif self.flag_type == "center":1067                fill_color = self.color11068                stroke_color = self.color11069 1070            if fill_color.lower() in ["#ffffff", "white"]:1071                stroke_color = "#000000"1072 1073            return svg.Path(1074                d=[1075                    svg.M(94.532861 * S, 112.61697 * S),1076                    svg.L(94.524268 * S, 172.02704 * S),1077                    svg.L(101.4425 * S, 172.02704 * S),1078                    svg.L(101.4283 * S, 126.87143 * S)1079                ],1080                fill=fill_color,1081                stroke=stroke_color,1082                stroke_width=1 * S,1083                elements=[self._title()],1084            )1085 1086        def right_leg(self):1087            S = self.size1088 1089            if self.flag_type == "center":1090                fill_color, stroke_color = self.color2, self.color11091            elif self.flag_type == "UK":1092                fill_color, stroke_color = self.color3, self.color21093            else:1094                fill_color = stroke_color = self.color21095 1096            if fill_color.lower() in ["#ffffff", "white"]:1097                stroke_color = "#000000"1098 1099            return svg.Path(1100                d=[1101                    svg.M(94.532861 * S, 112.61697 * S),1102                    svg.L(121.51828 * S, 142.12337 * S),1103                    svg.L(101.12085 * S, 158.57267 * S),1104                    svg.L(101.06795 * S, 150.68512 * S),1105                    svg.L(111.04127 * S, 142.17389 * S),1106                    svg.L(94.54537 * S, 124.52411 * S)1107                ],1108                fill=fill_color,1109                stroke=stroke_color,1110                stroke_width=1 * S,1111                elements=[self._title()],1112            )1113            1114        def wheel1(self):1115            # up_down = 0,0; left_right=2,2 ;UK=0,0 ; center=0,01116            S = self.size1117            if self.flag_type == "center" or self.flag_type=="UK":1118                fill_color = self.color11119                stroke_color = self.color11120            elif self.flag_type == "left_right" or self.flag_type == "up_down":1121                fill_color = self.color31122                stroke_color = self.color31123 1124            if fill_color.lower() in ["#ffffff", "white"]:1125                stroke_color = "#000000"1126 1127            return svg.Circle(1128                                cx=148.86574 * S, cy=158.976 * S, r=23.692965 * S,1129                                fill=fill_color,1130                                stroke=stroke_color,1131                                stroke_width=2.61407 * S,1132                                elements = [self._title()]1133                            )1134 1135        def wheel2(self):1136            # up_down = 0,0; left_right=2,2 ;UK=0,0 ; center=0,01137            S = self.size1138            if (self.flag_type == "center" or self.flag_type == "left_right" or self.flag_type=="UK"):1139                fill_color = self.color11140                stroke_color = self.color11141            elif (self.flag_type == "up_down"):1142                fill_color = self.color31143                stroke_color = self.color31144 1145            if fill_color.lower() in ["#ffffff", "white"]:1146                stroke_color = "#000000"1147 1148            return svg.Circle(cx=67.373695 * S, cy=158.976 * S, r=23.692965 * S,1149                                fill=fill_color,1150                                stroke=stroke_color,1151                                stroke_width=2.61407 * S,1152                                elements = [self._title()])1153 1154        def fast_stripe1(self):1155            S = self.size1156            return svg.Rect(1157                                width=41 * S, height=3.6441717 * S, 1158                                x=35.530674 * S, y=114.33589 * S, ry=1.776534 * S,1159                                fill="black",1160                                stroke="black", 1161                                stroke_width=1 * S,1162                                elements = [self._title()])1163 1164        def fast_stripe2(self):1165            S = self.size1166            return svg.Rect(1167                                width=41 * S, height=3.6441717 * S, 1168                                x=35.530674 * S, y=123.36678 * S, ry=1.776534 * S,1169                                fill="black",1170                                stroke="black", 1171                                stroke_width=1 * S,1172                                elements = [self._title()])1173 1174        def fast_stripe3(self):1175            S = self.size1176            return svg.Rect(1177                                width=20 * S, height=3.6441717 * S, 1178                                x=35.530674 * S, y=133.12637 * S, ry=1.776534 * S,1179                                fill="black",1180                                stroke="black", 1181                                stroke_width=1 * S,1182                                elements = [self._title()])1183 1184        def draw(self):1185            S = self.size1186            return svg.SVG(1187                width=290 * S,1188                height=250 * S,1189                elements=[1190                self.head(),1191                self.body(),1192                self.left_leg(),1193                self.right_leg(),1194                self.front_wheel_pie(),1195                self.arm(),1196                self.wheel2(),1197                self.fast_stripe1(),1198                self.fast_stripe2(),1199                self.fast_stripe3()1200            ])

Showing the first 1,200 of 1824 lines. Download the file for the rest.