Figure Friday 2024 - week 29

Update : Figure Friday 2024 - week 30 is the newer dataset.

Hey Everyone,
We are happy to announce the next data set of the Figure Friday initiative . This week we will be highlighting the English Women’s Football league. You can choose to work on any or all of the three data sets provided.

Here is the sample figure built on top of the ewf_standings.csv data set:

Code for sample figure:
import pandas as pd
import plotly.express as px

# Load the data
df = pd.read_csv('https://raw.githubusercontent.com/plotly/Figure-Friday/main/2024/week-29/ewf_standings.csv')

# Filter the data for Manchester City Women
man_city_women = df[df['team_name'] == 'Manchester City Women']

# Create a bar chart
fig = px.bar(man_city_women, x='season', y=['wins', 'losses', 'draws'], barmode='group',
             title='Manchester City Women: Wins, Losses, and Draws by Season')

# Show the chart
fig.show()

Participation Instructions:

  • Create - use the weekly data set to build your own Plotly visualization or Dash app. Or, enhance the sample figure provided in this post, using Plotly or Dash.
  • Submit - post your creation to LinkedIn or Twitter with the hashtags #FigureFriday and #plotly by midnight Thursday, your time zone. Please also submit your visualization as a new post in this thread.
  • Celebrate - join the Figure Friday sessions to showcase your creation, receive feedback from the community, and vote on your favorite visualization.

Per the author’s request, please cite the data as:
The English Women’s Football (EWF) Database, May 2024, https://github.com/probjects/ewf-database.

:point_right: If you prefer to collaborate with others on Discord, join the Plotly Discord channel .

Thank you to Rob Clapp for creating and sharing these data sets.

5 Likes

Here is a first and simple exploration into English Woman’s Football’s league-wide attendance patterns.

8 Likes

I have created an animation using Plotly using The English Women’s Football (EWF) Database, May 2024, https://github.com/probjects/ewf-database .
Screen Recording 2024-07-21 at 8.26.33 PM

I have chosen to associate to each team a fixed color in order to be able to see how each team changed its relative standing position with the number of games played.

The code I used to generate the animation above is here (Quarto format):

---
title: "English Women's Football League"
format: 
  html:
    page-layout: full
---

After looking at the three files, I decided to use the `ewf_appearances.csv` file to create a visualization (animation) of the evolution of the standing in a season. The reason for using this file is that it can be used to generate the other two, so by using it, there is no loss of information. I concentrated on the **WSL** division.

```{python}
import numpy as np
import pandas as pd

import plotly.express as px

I loaded the data and filtered it to get only the WSL division.

appearances = pd.read_csv('data/ewf_appearances.csv', parse_dates=['date'])
appearances = appearances[appearances['division'].str.contains("Women's Super League")]
appearances = appearances.sort_values(['date']).reset_index(drop=True)

I then created a new column for the total points and the total games played by each team in each season.

dfg = []
for g, df in appearances.groupby(['team_id', 'season']):
    df['tgames'] = 1
    df['tpoints'] = df['points'].cumsum()
    df['tgames'] = df['tgames'].cumsum()
    dfg.append(df)
appearances = pd.concat(dfg, ignore_index=True).sort_values(['date', 'tgames'], ascending=[True, False]).reset_index(drop=True)

color_map = {tid: px.colors.qualitative.Dark24[i] for i, tid in enumerate(sorted(appearances['team_id'].unique()))}

I then created the animation of the standings in the WSL division for the 2022-2023 season. Certainly the code can be modified to create a Dash application in which the variable SEASON can be selected from a dropdown menu.

SEASON = '2022-2023'
df = appearances[(appearances['season'] == SEASON)]
XMAX = df['tpoints'].max()+1
fig1 = px.bar(df, 
    y='team_name', 
    x='tpoints', 
    color='team_id',
    color_discrete_map=color_map, 
    orientation='h',
    animation_frame='tgames',
    range_x=[0, XMAX],
    width=1000,
    # height=600,
    title=f'Women\'s Super League standings in season {SEASON}',
)
for k, frm in enumerate(fig1.frames):
    frm['layout'].update(title_text=f'Women\'s Super League standings in season {SEASON}<br><sup>after {k+1:2d} games</sup>')
sliders = [dict(
    currentvalue={"prefix": "Games played: "},
)]
fig1.update_layout(
    yaxis_title=None,
    xaxis_title='Total Points',
    yaxis={'categoryorder':'total ascending'}, 
    showlegend=False,
    sliders=sliders,
)
fig1.show()
10 Likes

df = pd.read_csv("../ewf_appearances.csv")
df.dropna(subset=['attendance'],inplace=True)
df['attendance'] = [i.replace(',','') for i in df['attendance']]
df['attendance'] = [int(i) for i in df['attendance']]

fig = px.scatter(df,x='team_name',y='opponent_name',size='attendance')
fig.update_layout(width=1000,height=1000,template='simple_white')
fig.update_xaxes(categoryorder='category ascending')
fig.update_yaxes(categoryorder='category ascending')
fig.update_traces(marker_color='gray')
fig.show()
4 Likes

@Mike_Purtell, thank you for sharing the graph, would you be so kind as to share the code? I want to learn how to do the annotations. Do you know if the annotations can be part of a data frame, that is a column that contains the annotations and used as a graph on itself?

@Mike_Purtell that’s an impressive graph.
I like the idea of the two shades, but they have two titles each, so I wasn’t sure what they are referring to: summer vs. fall season or low attendance vs big attendance.

Do you think the boost in attendance is due to the fact that after covid everyone was itching to get out of the house and interact with others or is it due to the fact that the season shifted from summer to fall?

The animated bar chart is always a winner, beautiful, delicious eye candy. Well done, @hebertodelrio and thank you for sharing the code as well. I can’t stop looking at the gif :smile:

Is there a reason you chose the 2022-2023 season?

Welcome to the community @nooraaz

Cool graph. What does it mean when you see solid markers inside of empty scatter markers? For example, if you look at the two dots in the bottom left corner, they look like two dots wrapped by a ring. While in other cases, the markers are completely solid.

@adamschroeder there was no reason for choosing that season, I am working on creating a dashboard with this, so the dashboard will have one pulldown menu from where you can choose the season (in my case the SEASON variable) and once I have that I can display the animation for that particular season.

1 Like

Thank you @hebertodelrio . I will indeed post the code very soon.

Hello @adamschroeder, EWF was a summer-only sport until after 2016, when it was changed it to a fall-to-spring schedule similar to English Premier League. This was described on the GIT page where these datasets live. I considered that summer leagues might be unpopular while the men’s Premier League is on recess. We see a pattern like this with US football, our most popular sport when it is in season, but very unpopular at other times of the year as evidenced by various upstart leagues with spring or summer schedules that have pretty much all failed. But my assumption is not backed by the data. The attendance did not rise significantly after the schedule change. There are modest attendance boosts in the woman’s world cup years, and the giant 100% boost after Covid for the 2022 season. Thank you.

1 Like

## This code produced the heavily annotated EWF attendance trends plot
import polars as pl
import plotly.express as px
import plotly.graph_objects as go

my_arrow_size = 1
my_arrow_style = 1
my_arrow_width = 1.5


def custom_annotation(fig, text, showarrow, x, y,  xanchor, yanchor, xshift, yshift, align, ax=0, ay=0):
    fig.add_annotation(
        text = text,
        showarrow=showarrow,
        arrowwidth=my_arrow_width,
        arrowsize= my_arrow_size,
        arrowhead = my_arrow_style,
        x = x,
        y = y,
        xanchor=xanchor,
        yanchor=yanchor,
        xshift=xshift,
        yshift=yshift,
        font=dict(size=10, color="grey"),
        align=align,
        ax=ax,
        ay=ay
    )
    return fig

# read and tweak appearances data
df_appearances = (
    pl.read_csv('ewf_appearances.csv')
    .filter(
        pl.col('attendance') != 'NA',
        pl.col('tier') == 1
    )
    .with_columns(
        pl.col('attendance').str.replace(',', '').cast(pl.Int32),
        SEASON = pl.col('season').str.slice(0,4).cast(pl.Int32)
    )
    .with_columns(
         Q1 = pl.col('attendance').quantile(0.25).over('season').cast(pl.Int32),
         Q3 = pl.col('attendance').quantile(0.75).over('season').cast(pl.Int32),
    )
    .with_columns(
         IQR = (pl.col('Q3') -  pl.col('Q1')).cast(pl.Int32)
    )
    .with_columns(
         OUTLIER_L = (pl.col('Q1') - 1.5 * pl.col('IQR')).cast(pl.Int32),
         OUTLIER_H = (pl.col('Q3') + 1.5 * pl.col('IQR')).cast(pl.Int32),
    )
    .with_columns(
         IS_OUTLIER = (
             pl.when(
                 (pl.col('attendance') > pl.col('OUTLIER_H')) 
                 | 
                 (pl.col('attendance') < pl.col('OUTLIER_L'))
             )
         .then(pl.lit(True))
         .otherwise(pl.lit(False))
        )
    )
    .with_columns(MEDIAN = pl.col('attendance').median().round(0).over('season'))       
    .select(pl.col('SEASON', 'attendance', 'MEDIAN', 'Q1', 'Q3', 'IQR','OUTLIER_L','OUTLIER_H','IS_OUTLIER'))
    .unique(subset='SEASON')
    .sort('SEASON')
)

# Excluding outliers
df_no_fliers = df_appearances.filter(pl.col('IS_OUTLIER') == False)

fig  = px.line(df_no_fliers, 'SEASON', 'MEDIAN')
fig.layout.template = 'plotly_white' 

fig.add_trace(
    go.Scatter(
        # connectgaps=False,
        x=df_no_fliers['SEASON'],
        y=df_no_fliers['MEDIAN'],
        marker=dict(color='blue',  size=10), 
    ),
)

fig.update_layout(
    title=go.layout.Title(
        text="English Woman's Football, Tier 1",
        xref="paper",
        x=0
    ),
    xaxis=go.layout.XAxis(
        title=go.layout.xaxis.Title(
            text='' # "Year refers to start of the season<br>SOURCE: The English Women's Football (EWF) Database, May 2024, https://github.com/probjects/ewf-database.",
            )
    ),
    yaxis=go.layout.YAxis(
        title=go.layout.yaxis.Title(
            text='League-wide median attendance'
            ),
    )
)

fig.update_layout(yaxis_range=[0, 3500])

text = "Year refers to start of the season<br><br>"
text += "SOURCE: The English Women's Football (EWF) Database, May 2024  "
text += '<a href="https://github.com/probjects/ewf-database">https://github.com/probjects/ewf-database</a>'
fig.add_annotation(
    text = text, showarrow=False, x = 0, y = -0.05, xref='paper', yref='paper',
    xanchor='left', yanchor='top', xshift=0, yshift=0,
    font=dict(size=10, color="grey"), align="left",
)

# Annotate 2015
text = '2015 World Cup, Canada.<br>EWF Attendance up 50%<br>54,000 people attended match<br>when 3rd place England beat Canada<br>'
fig = custom_annotation(fig, text, showarrow=True,  x = 2015, y = 951,  xanchor='center', yanchor='bottom', xshift=-1, yshift=10,  align="left", ax=-50, ay=-20)

# Annotate 2019
text = '2019 World Cup, France.<br>EWF Attendance up 97% after <br>England finished 4th<br>Season shortened by Covid'
fig = custom_annotation(fig, text, showarrow=True,  x = 2019, y = 1306,  xanchor='center', yanchor='bottom', xshift=0, yshift=10,  align="left", ax=0,ay=-20)

# Annotate 2021
text = '2020 cancelled,<br>league play resumed in 2021'
fig = custom_annotation(fig, text, showarrow=True,  x = 2021, y = 1439,  xanchor='center', yanchor='top', xshift=0, yshift=-20,  align="left", ay=30)

# Annotate 2022
text = '2022: attendance up 100%'
fig = custom_annotation(fig, text, showarrow=True,  x = 2022, y = 2966,  xanchor='right', yanchor='middle', xshift=-10, yshift=0,  align="left", ax=-50, ay=25)

# Annotate Summer League
text = '<b>Summer schedule</b><br>'
fig = custom_annotation(fig, text, showarrow=False,  x = 2012.5, y = 3200,  xanchor='left', yanchor='top', xshift=0, yshift=10,  align="left", ax=0,ay=-20)

# Annotate Summer League
text = '<b>Fall to Spring schedule</b> <br>Traditional English football season'
fig = custom_annotation(fig, text, showarrow=False,  x = 2017, y = 3200,  xanchor='left', yanchor='top', xshift=0, yshift=10,  align="left", ax=0,ay=-20)

# Annotate Takeaway
text = '<b>Early years, low attendance</b>'
fig = custom_annotation(fig, text, showarrow=False,  x = 2012, y = 3500,  xanchor='left', yanchor='top', xshift=0, yshift=10,  align="left", ax=0,ay=-20)

# Annotate Takeaway
text = '<b>Big attendance boost in 2022</b>'
fig = custom_annotation(fig, text, showarrow=False,  x = 2017, y = 3500,  xanchor='left', yanchor='top', xshift=0, yshift=10,  align="left", ax=0,ay=-20)


fig.update_layout(
    autosize=False,
    width=800,
    height=600,
    showlegend=False,
)

fig.add_vrect(
    x0=2012, x1=2016.5,
    y0=0, y1=0.95,
    fillcolor="LightSalmon", opacity=0.25,
    layer="below", line_width=0,
)
fig.add_vrect(
    x0=2016.5, x1=2022.5,
    y0=0, y1=0.95,
    fillcolor="wheat", opacity=0.25,
    layer="below", line_width=0,
)

fig.update_xaxes(showgrid=False)
fig.update_yaxes(showgrid=False)
fig.update_layout(margin=dict(t=100))
fig.show()
fig.write_html('EWF-League-Wide-Attendance.html')
print('done')

2 Likes

Here is my app:

zoomed in on charts:

Obviously, I prefer dark mode, but there is light mode available.

Here is the code:

app.py

### import libraries
import traceback

import dash
from dash import *
import pandas as pd
import dash_ag_grid as dag
import dash_mantine_components as dmc
import dash_bootstrap_components as dbc
from dash_iconify import DashIconify
import plotly.express as px
import plotly.graph_objects as go
import plotly.io as pio

### import data
owner = "BSd3v"
data = {}
week = "29"
year = "2024"
base_url = (
    f"https://raw.githubusercontent.com/plotly/Figure-Friday/main/{year}/week-{week}/"
)
attribution = """The English Women's Football (EWF) Database, May 2024, https://github.com/probjects/ewf-database."""

files = ["ewf_appearances.csv", "ewf_matches.csv", "ewf_standings.csv"]
for f in files:
    if ".csv" in f.lower():
        data[f] = pd.read_csv(f"{base_url}/{files[0]}")
    else:
        data[f] = pd.read_excel(f"{base_url}/{files[0]}")


### dash app
stylesheets = [
    "https://unpkg.com/@mantine/dates@7/styles.css",
    "https://unpkg.com/@mantine/code-highlight@7/styles.css",
    "https://unpkg.com/@mantine/charts@7/styles.css",
    "https://unpkg.com/@mantine/carousel@7/styles.css",
    "https://unpkg.com/@mantine/notifications@7/styles.css",
    "https://unpkg.com/@mantine/nprogress@7/styles.css",
]

dash._dash_renderer._set_react_version("18.2.0")

app = Dash(__name__, use_pages=True, pages_folder="", external_stylesheets=stylesheets)


### visualizations

for f in files:
    data[f]["attendance"] = data[f]["attendance"].map(
        lambda x: int(str(x).replace(",", "")) if x and str(x) != "nan" else None
    )

default_layout = {"margin": {"r": 0, "l": 0, "b": 0, "t": 35}}

home_team = data[files[1]][data[files[1]]["home_team"] == 1].copy()
away_team = data[files[1]][data[files[1]]["away_team"] == 1].copy()
home_team["date"] = pd.to_datetime(home_team["date"])

## fix names
for i, row in home_team.iterrows():
    home_team.loc[i, "team_name"] = home_team[
        home_team["team_id"] == row["team_id"]
    ].iloc[-1]["team_name"]
    home_team.loc[i, "opponent_name"] = home_team[
        home_team["opponent_id"] == row["opponent_id"]
    ].iloc[-1]["opponent_name"]

figures = dmc.Grid(
    [
        dmc.GridCol(
            [
                dcc.Graph(figure=go.Figure(), id="home_attendance_treemap"),
            ]
        ),
        dmc.GridCol(
            [
                "Match Attendance Range",
                dmc.RangeSlider(
                    value=[
                        home_team["attendance"].min(),
                        home_team["attendance"].max(),
                    ],
                    min=home_team["attendance"].min(),
                    max=home_team["attendance"].max(),
                    id="attendance_range",
                ),
                dmc.DatePicker(
                    label="Match Date Range",
                    value=[home_team["date"].min(), home_team["date"].max()],
                    minDate=home_team["date"].min(),
                    maxDate=home_team["date"].max(),
                    id="date_range",
                    type="range",
                    numberOfColumns=2,
                ),
                dmc.RadioGroup(
                    children=dmc.Group(
                        [dmc.Radio(k, value=k) for k in ["All", "1", "2"]], my=10
                    ),
                    id="match_tier",
                    value="All",
                    label="Match Tier",
                    size="sm",
                    mb=10,
                ),
                dmc.MultiSelect(
                    id="home_teams",
                    data=sorted(home_team["team_name"].unique().tolist()),
                    clearable=True,
                    label="Home Teams",
                ),
                dmc.MultiSelect(
                    id="away_teams",
                    data=sorted(home_team["opponent_name"].unique().tolist()),
                    clearable=True,
                    label="Away Teams",
                ),
            ],
            span=2,
            className="filter-card",
            style={"padding": "15px"},
        ),
        dmc.GridCol(
            [
                dmc.Group(
                    [
                        dcc.Graph(
                            figure=go.Figure(),
                            id="attendance_time",
                            style={"height": "100%"},
                        ),
                        html.Div(
                            [
                                dcc.Graph(
                                    figure=go.Figure(),
                                    id="most_attendance",
                                    style={"height": "100%"},
                                )
                            ],
                            style={
                                "display": "flex",
                                "flexDirection": "column",
                                "height": "100%",
                            },
                        ),
                    ],
                    style={
                        "height": "300px",
                        "maxHeight": "300px",
                        "overflow": "hidden",
                        "padding": "15px",
                    },
                )
            ],
            span=10,
        ),
    ]
)


@callback(
    Output("home_attendance_treemap", "figure"),
    Output("attendance_time", "figure"),
    Output("most_attendance", "figure"),
    Input("attendance_range", "value"),
    Input("date_range", "value"),
    Input("match_tier", "value"),
    Input("home_teams", "value"),
    Input("away_teams", "value"),
    Input("mode", "checked"),
)
def updateTreemap(v, v2, v3, v4, v5, c):
    if ctx.triggered_id == "mode":
        newTemplate = pio.templates[
            "plotly_white" if not c else "plotly_dark"
        ].to_plotly_json()
        fig = Patch()
        fig["layout"]["template"] = newTemplate
        return [fig] * len(ctx.outputs_list)
    if len(v) == 2 and len(v2) == 2:
        try:
            team_ids = home_team["team_name"].drop_duplicates().tolist()
            opponent_ids = home_team["opponent_name"].unique().tolist()
            mask = home_team[
                (
                    home_team["team_name"].isin(v4 if len((v4 or [])) > 0 else team_ids)
                    & home_team["opponent_name"].isin(
                        v5 if len((v5 or [])) > 0 else opponent_ids
                    )
                    & home_team["attendance"].between(v[0], v[1], inclusive="both")
                    & home_team["date"].between(v2[0], v2[1], inclusive="both")
                    & home_team["tier"].isin([int(v3)] if not v3 == "All" else [1, 2])
                )
            ].dropna(subset=["attendance"])
            fig = Patch()
            newTree = px.treemap(
                mask,
                path=["team_name", "opponent_name", "date"],
                values="attendance",
                color="attendance",
                template="plotly_white" if not c else "plotly_dark",
                title="Home Team Attendance Distribution",
                range_color=[
                    home_team["attendance"].min(),
                    home_team["attendance"].max(),
                ],
            ).update_layout(default_layout)
            fig["data"] = newTree.data
            fig2 = Patch()
            newScatter = px.scatter(
                mask,
                x="date",
                y="attendance",
                title="Attendance Over Time",
                template="plotly_white" if not c else "plotly_dark",
            ).update_layout(default_layout)
            fig2["data"] = newScatter.data
            fig3 = Patch()
            sorted_df = mask.sort_values("attendance", ascending=False)
            newMax = go.Figure(
                go.Indicator(
                    value=mask["attendance"].max(),
                    title=f"{sorted_df.iloc[0].loc['match_name']}<br>"
                    + f"({str(sorted_df.iloc[0].loc['date']).split(' ')[0]})",
                )
            ).update_layout(
                {
                    **default_layout,
                    "template": "plotly_white" if not c else "plotly_dark",
                }
            )
            fig3["data"] = newMax.data
            fig2["layout"] = newScatter.layout
            fig["layout"] = newTree.layout
            fig3["layout"] = newMax.layout
            return [fig, fig2, fig3]
        except:
            print(traceback.format_exc())
            pass
    return [no_update] * len(ctx.outputs_list)


register_page("Visualizations", path="/visualizations", layout=figures)

### defaults


raw_data = [
    html.H2("Raw Data"),
    dcc.Markdown(attribution),
    dmc.TextInput(
        label="Quick Filter Text",
        id="filter_raw_data",
        placeholder="Type to filter all data sets",
    ),
    html.Div(
        [
            html.Div(
                [
                    html.H4(f),
                    dag.AgGrid(
                        id={"index": f, "type": "information"},
                        rowData=data[f].to_dict("records"),
                        columnDefs=[{"field": x} for x in data[f].columns],
                        dashGridOptions={"quickFilterText": ""},
                    ),
                ]
            )
            for f in files
        ]
    ),
]


@callback(
    Output(
        {"index": ALL, "type": "information"}, "dashGridOptions", allow_duplicate=True
    ),
    Input("filter_raw_data", "value"),
    prevent_initial_call=True,
)
def filter_raw_data(v):
    options = Patch()
    options["quickFilterText"] = v
    return [options] * len(files)


register_page("Data", path="/data", layout=raw_data)

app.layout = dmc.MantineProvider(
    [
        dmc.AppShell(
            [
                dmc.AppShellHeader(
                    html.Div(
                        [
                            html.H2(f"{owner}"),
                            html.H1(
                                f"Figure Friday - Year {year} - Week {week}",
                                className="mantine-visible-from-md",
                            ),
                            html.H3(f"FF{year}{week}", className="mantine-hidden-from-md"),
                            dmc.Group(
                                [
                                    dmc.Anchor(
                                        DashIconify(icon="ion:logo-github", width=35),
                                        href=f"https://github.com/{owner}",
                                        style={
                                            "height": "100%",
                                            "display": "flex",
                                            "alignItems": "center",
                                        },
                                        target="_blank",
                                        className="mantine-visible-from-sm",
                                    ),
                                    dmc.Anchor(
                                        DashIconify(
                                            icon="skill-icons:discord", width=35
                                        ),
                                        href="https://discord.com/channels/1247975306472591470",
                                        style={
                                            "height": "100%",
                                            "display": "flex",
                                            "alignItems": "center",
                                        },
                                        target="_blank",
                                        className="mantine-visible-from-sm",
                                    ),
                                    dmc.Anchor(
                                        html.Img(
                                            src="https://dash.plotly.com/assets/images/plotly_logo_light.png",
                                            style={"width": "150px"},
                                            id="plotly_logo",
                                        ),
                                        href="https://dash.plotly.com/",
                                        style={
                                            "display": "flex",
                                            "alignItems": "center",
                                            "margin": "-15px",
                                        },
                                        target="_blank",
                                        className="mantine-visible-from-sm",
                                    ),
                                    dmc.Switch(
                                        offLabel=DashIconify(
                                            icon="radix-icons:moon", width=20
                                        ),
                                        onLabel=DashIconify(
                                            icon="radix-icons:sun", width=20
                                        ),
                                        size="xl",
                                        id="mode",
                                        style={"cursor": "pointer"},
                                    ),
                                    dmc.Burger(className="mantine-hidden-from-sm", id="display-nav"),
                                    dcc.Store(id="theme-switch", storage_type="local"),
                                ],
                                gap=5,
                                style={"height": "100%"},
                            ),
                        ],
                        style={
                            "display": "flex",
                            "justifyContent": "space-between",
                            "alignItems": "center",
                            "paddingLeft": "25px",
                            "paddingRight": "25px",
                            "height": "100%",
                        },
                    )
                ),
                dmc.AppShellNavbar(
                    dmc.Stack(
                        [
                            dmc.Anchor(
                                pg["title"],
                                href=pg["path"],
                                style={"paddingLeft": "30px", "width": "100%"},
                            )
                            for pg in page_registry.values()
                        ],
                        align="center",
                    )
                ),
                dmc.AppShellMain(page_container),
            ],
            header={"height": 70},
            padding="xl",
            zIndex=1400,
            navbar={
                "width": 300,
                "breakpoint": "sm",
                "collapsed": {"mobile": True},
            },
            styles={
                "main": {
                    "paddingTop": "var(--app-shell-header-height)",
                    "paddingBottom": "25px",
                }
            },
        ),
        dmc.Drawer(
            dmc.Stack(
                [
                    html.Div(
                        dmc.Anchor(
                            pg["title"],
                            href=pg["path"],
                            style={"paddingLeft": "30px", "width": "100%", "display": "block"},
                        ),
                        id={"index": pg["title"], "type": "mobile-nav"},
                        style={"width": "100%"},
                    )
                    for pg in page_registry.values()
                ],
                style={"top": "80px", "position": "absolute", "width": "100%"},
            ),
            id="nav-drawer",
        ),
    ],
    defaultColorScheme="auto",
    id="mantine-provider",
)

clientside_callback(
    """(n) => {
        return n
    }""",
    Output("nav-drawer", "opened"),
    Input("display-nav", "opened"),
)

clientside_callback(
    """(_) => {
        return false
    }""",
    Output("display-nav", "opened"),
    Input({"index": ALL, "type": "mobile-nav"}, "n_clicks"),
    prevent_initial_call=True,
)


@callback(
    Output({"index": ALL, "type": "information"}, "className"), Input("mode", "checked")
)
def updateClassNames(c):
    return ["ag-theme-alpine-dark" if c else "ag-theme-alpine"] * len(files)


clientside_callback(
    """(c) => {
        trg = c ? 'dark' : 'light'
        document.body.classList = [trg]
        return [trg, `https://dash.plotly.com/assets/images/plotly_logo_${trg}.png`, c]
    }""",
    Output("mantine-provider", "forceColorScheme"),
    Output("plotly_logo", "src"),
    Output("theme-switch", "data", allow_duplicate=True),
    Input("mode", "checked"),
    prevent_initial_call=True,
)

clientside_callback(
    """
        (_, data) => {
            if (data !== null) {
                return [data, data]
            }
           return [
            window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light',
            window.dash_clientside.no_update
            ]
        }
    """,
    Output("mode", "checked"),
    Output("theme-switch", "data"),
    Input("theme-switch", "id"),
    State("theme-switch", "data"),
)

app.run(debug=True)

style.css

body.light {
    .dash-graph, .filter-card {
        border-radius: 15px;
        -webkit-box-shadow: 0px 0px 15px -4px rgba(0,0,0,0.5);
        -moz-box-shadow: 0px 0px 15px -4px rgba(0,0,0,0.5);
        box-shadow: 0px 0px 15px -4px rgba(0,0,0,0.5);
        padding: 5px;
        background: white;
    }
}

body.dark {
    .dash-graph, .filter-card {
        border-radius: 15px;
        -webkit-box-shadow: 0px 0px 15px -4px rgba(0,0,0,0.5);
        -moz-box-shadow: 0px 0px 15px -4px rgba(0,0,0,0.5);
        box-shadow: 0px 0px 15px -4px rgba(0,0,0,0.5);
        padding: 5px;
        background: #111111;
    }
}

.dash-graph[data-dash-is-loading="true"] {
    .js-plotly-plot {
        opacity: 50%
    }
}

I found a large correlation to the home team that was playing which had the largest distribution of the attendance. There were other factors that could be explored, as standing of both teams in the match, weekend vs weekday game, whether the game was part of a tournament, etc.


I tried to write this app in a way to be of beneficial use to others. :smiley:


requires:

dash-mantine-components==0.14.3
dash-ag-grid==31.2.0
dash>=2.17.0
dash-bootstrap-components
dash-iconify
pandas

Bootstraps components were unused in this specific app.

5 Likes

Those are overlapped markers. Some matches have occurred more than once, with different attendance, and if the latest one is not the largest one, you would see multiple circles.

You used polars @Mike_Purtell :heart:
Nice to see that with Plotly. Most of the Plotly graphs made are using pandas data frames. But polars can be much faster.

Thank you @adamschroeder. I used pandas for about 5 years, switched to polars 1 year ago. With plotly, the transition to polars has been seamless.

I’ve built a compact (around 200 lines of code) Dash app that shows the league table for any of the English women’s leagues at any date, similar to the Premier league interactive tables at Premier League Table, Form Guide & Season Archives

Uses Dash Mantine Components (including its nice date picker) and Dash AG Grid, with very little added styling
Comments / suggestions for improvement very welcome!

Code at GitHub - dh3968mlq/figure-friday-week29
Deployed (for now) at https://dh3968figfriday2024-29-d85d48264713.herokuapp.com/

7 Likes

Nice app, @davidharris

It even better than the Premier league’s site because you have the one week back and one week forward option :slight_smile:

Chelsea and Manchester both finished with the same points this last season. I wonder why Chelsea finished in first place… Maybe it’s the goal difference.

1 Like

Hello!

Here is my entry for the week.

Created a dashboard, in which I converted a heatmap into the percentage of win - draw - loss and a bar chart with all the games played by the team selected.

The bar chart display the goals scored and goals taken, the dots on top is showing if the game resulted in a win, draw or loss for the selected team.

It was fun designing it, my Dash capabilities are very limited, specially for designing.
I posted in Py.Cafe, which I learned from the previous week.

Demo
FFW29

Dash App Py.Cafe
Code

# Import packages
from dash import Dash, html, dcc, callback, Output, Input, State
import pandas as pd
import numpy as np
import plotly.graph_objects as go
import dash_bootstrap_components as dbc

def generate_list(number1_percentage, number2_percentage, total_size=10000):
    number1_count = int(total_size * (number1_percentage))
    number2_count = int(total_size * (number2_percentage))
    number0_count = total_size - number1_count - number2_count
    
    return [2] * number1_count + [1] * number2_count + [0] * number0_count

def win_loss_chart(win,draw):
    # Sample list of 100 numbers
    numbers = generate_list(win,draw)

    # Create a 10x10 array initialized with zeros
    array = np.zeros((100, 100), dtype=int)

    # Fill the array in diagonal order
    k = 0
    for i in range(100):
        for j in range(i + 1):
            if k < 10000:
                array[i - j, j] = numbers[k]
                k += 1

    for i in range(1, 100):
        for j in range(100 - i):
            if k < 10000:
                array[99 - j, i + j] = numbers[k]
                k += 1

    color_scale = [[0,"#d90429"],[0.5,"#edf2f4"],[1,"#2b2d42"]]


    fig = go.Figure()

    fig.add_trace(go.Heatmap(
        z=array[::-1],
        colorscale=color_scale
    ))

    fig.update_xaxes(showticklabels=False,linewidth=1, linecolor='black', mirror=True, showline=True)
    fig.update_yaxes(showticklabels=False,linewidth=1, linecolor='black', mirror=True, showline=True)
    fig.update_layout(width=250,height=250,margin=dict(l=5, r=5, b=5, t=5))
    fig.update_traces(showscale=False)

    return fig

df = pd.read_csv('ewf_standings.csv')
df_seasons = df.groupby('team_name')[['played','wins','draws','losses']].sum()
df_seasons['wins_percentage'] = df_seasons['wins'] / df_seasons['played']
df_seasons['draws_percentage'] = df_seasons['draws'] / df_seasons['played']

df_matches = pd.read_csv('ewf_matches.csv')

def games_played(df, team):

    df_filtered = df[df['match_name'].str.contains(team)]
    df_filtered['Goals Scored'] = np.where(df_filtered['home_team_name'] == "Arsenal Ladies", df_filtered['home_team_score'], df_filtered['away_team_score'])
    df_filtered['Goals Taken'] = np.where(df_filtered['home_team_name'] == "Arsenal Ladies", df_filtered['away_team_score'], df_filtered['home_team_score'])
    df_filtered['Result'] = np.where(df_filtered['Goals Scored'] > df_filtered['Goals Taken'], 1, np.where(df_filtered['Goals Scored'] < df_filtered['Goals Taken'], -1, 0))

    fig = go.Figure()

    fig.add_trace(go.Bar(
        x=df_filtered['date'],
        y=df_filtered['Goals Scored'],
        marker_color=['#2b2d42' if value == 1 else '#8d99ae' for value in df_filtered['Result']]
    ))

    fig.add_trace(go.Bar(
        x=df_filtered['date'],
        y=df_filtered['Goals Taken']*-1,
        marker_color=['#8d99ae' if value == 1 else '#d90429' for value in df_filtered['Result']]
    ))

    fig.add_trace(go.Scatter(
        x=df_filtered['date'],
        y=[df_filtered['Goals Scored'].max() + 2]*len(df_filtered),
        mode='markers',
        marker_size=7,
        marker_symbol='square',
        marker_color = ['#2b2d42' if value == 1 else '#d90429' if value == -1  else '#8d99ae' for value in df_filtered['Result']]
    ))

    fig.update_xaxes(type='category')
    fig.update_yaxes(showticklabels=False)
    fig.update_layout(barmode='relative',showlegend=False,plot_bgcolor='white',margin=dict(l=5, r=5, b=5, t=5))

    fig.add_annotation(
        x=df_filtered['date'].iloc[2],
        y=df_filtered['Goals Scored'].max() + 3,
        text="Win-Loss Ratio",
        showarrow=False,
        xref="x",
        yref="y",
    )

    fig.add_annotation(
        x=df_filtered['date'].iloc[2],
        y=df_filtered['Goals Scored'].max() - 1,
        text="Goals Scored",
        showarrow=False,
        xref="x",
        yref="y",
    )

    fig.add_annotation(
        x=df_filtered['date'].iloc[2],
        y=df_filtered['Goals Taken'].max()*-1,
        text="Goals Taken",
        showarrow=False,
        xref="x",
        yref="y",
    )

    return fig

# Initialize the app
app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
server = app.server

# App layout
app.layout = dbc.Container([
    dbc.Row([
        dbc.Col(html.Div(children='Figure Friday W29', className='text-center h2')),
    ]),
    html.Hr(),
    dbc.Row([
        dbc.Col([
            dcc.Dropdown(id='select_team',options=[{'label':i,'value':i} for i in df_seasons.index.unique()],value=df_seasons.first_valid_index(),multi=False,style={'width': '100%'}),
        ],width=3),
    ]),
    html.Hr(),
    dbc.Row([
        dbc.Col([
            dbc.Row([
                html.B(children='TOTAL STANDINGS',style={'font-size':'40px'}),
                html.Div(id='time',children=''),
                ]),
            html.Br(),
            dbc.Row([
                dbc.Col(html.Div(id='win-loss-text',children='',style={'font-size':'25px'}),width=4,align='center'),
                dbc.Col(dcc.Graph(id='win-loss-chart',figure={}),width=8)
            ])
        ],width=3),
        dbc.Col([
            dbc.Row([
                html.B(children='GAMES PLAYED ALL SEASONS',style={'font-size':'32px'}),
            ]),
            html.Hr(),
            dbc.Row([
                dcc.Graph(id='games-played',figure={})
            ]),
        ])
            
        ])
], fluid=True)

@app.callback(
    Output('time', 'children'),
    Output('win-loss-text', 'children'),
    Output('win-loss-chart', 'figure'),
    Output('games-played', 'figure'),
    Input('select_team', 'value')
    )
def display_click_data(select_team):

    stats = df_seasons.loc[[select_team]]
    win_percentage = stats['wins'] / ( stats['wins'] + stats['draws'] + stats['losses'] )
    draw_percentage = stats['draws'] / ( stats['wins'] + stats['draws'] + stats['losses'] )

    team_name = html.B(stats.index[0].upper(),style={'font-size':'30px'})

    win_loss_text = [
        html.Div(children=f'Wins | {stats["wins"][0]}'),
        html.Br(),
        html.Div(children=f'Draws  | {stats["draws"][0]}'),
        html.Br(),
        html.Div(children=f'Losses | {stats["losses"][0]}'),
    ]

    fig_wlc = win_loss_chart(win_percentage,draw_percentage)

    fig_games = games_played(df_matches,select_team)

    return team_name, win_loss_text, fig_wlc, fig_games
    

# Run the app
if __name__ == '__main__':
    app.run_server(debug=True)
6 Likes