diff --git a/CHANGELOG.md b/CHANGELOG.md index 19373e0..3a4c120 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # Update History -## 2.14.1 | 2024-01-25 +## 2.15.0 | 2024-01-28 + +### Added +* [#165](https://github.com/digitalghost-dev/premier-league/issues/165) - Added each team's club icon to the **Squads** tab when a team is selected from the dropdown menu. +* [#172](https://github.com/digitalghost-dev/premier-league/issues/172) - Added a new **Players Statistics** tab. + +### Changed +* [#164](https://github.com/digitalghost-dev/premier-league/issues/164) - Changed the default value `st.selectbox` to `None` in the **Squads** tab. +* [#168](https://github.com/digitalghost-dev/premier-league/issues/168) - Changed the `max_value` for each `st.dataframe` to programatically calaculate based on current max value in the DataFrame under the **League Statistics** section. +* [#171](https://github.com/digitalghost-dev/premier-league/issues/171) - Changed line chart under **Point Progression throughout the Season** section to use plotly instead of Streamlit's built in `st.line_chart` method. + +### Removed +* [#170](https://github.com/digitalghost-dev/premier-league/issues/168) - Removed `for` loop that previously generated the sections for **Goalkeepers**, **Defenders**, **Midfielders**, and **Attackers** under the **Squads** tab. +* [#173](https://github.com/digitalghost-dev/premier-league/issues/173) - Removed `st.container` border from **Top 5 Teams** and **Top 5 Scorers** sections. + +--- + +## [2.14.1] | 2024-01-25 ### Changed * [#169](https://github.com/digitalghost-dev/premier-league/issues/154) - Changed the query for `components/connections.py` to reflect table schema changes for the standings `st.dataframe`. @@ -420,6 +437,8 @@ Top Teams Tab Top Players Tab * Shows the `portrait`, `goals`, `team`, and `nationality` of the current top five goal scorers in the league. +[2.14.1]: https://github.com/digitalghost-dev/premier-league/commit/e4a0ba46fd3dee96544b34b2022140c73a4d2ccd + [2.14.0]: https://github.com/digitalghost-dev/premier-league/commit/62a27e488c3fbc91c585e55e73c91adbe9edf0b8#diff-4dc66906e3c3b7f7a82967d85af564f2d5a6e0bee5829aa5eda607dd9756c87d [2.13.0]: https://github.com/digitalghost-dev/premier-league/commit/dec0426ca5d3de50e8093874635f5bf01718aaa6 diff --git a/README.md b/README.md index 8f040ca..dcbe94c 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@
@@ -69,7 +69,7 @@ 3. The prior steps are orchestrated with Cloud Scheduler as a Docker container hosted on Cloud Run as a Job. #### Pipeline Diagram -![data-pipeline](https://storage.googleapis.com/premier-league/data_pipelines.png) +![data-pipeline](https://storage.googleapis.com/premier_league_bucket/flowcharts/data_pipelines_flowchart.png) ### CI/CD Pipeline The CI/CD pipeline is focused on building the Streamlit app into a Docker container that is then pushed to Artifact Registry and deployed to Cloud Run as a Service. Different architecutres are buit for different machine types and pushed to Docker Hub. @@ -79,7 +79,7 @@ The CI/CD pipeline is focused on building the Streamlit app into a Docker contai 3. The Docker image is then deployed to [Cloud Run](https://cloud.google.com/run/docs/overview/what-is-cloud-run) as a Service. #### Pipeline Diagram -![cicd_pipeline](https://storage.googleapis.com/premier-league/cicd_pipeline.png) +![cicd_pipeline](https://storage.googleapis.com/premier_league_bucket/flowcharts/cicd_pipeline_flowchart.png) --- diff --git a/components/point_progression_section.py b/components/point_progression_section.py index 9c7dc87..7b09532 100644 --- a/components/point_progression_section.py +++ b/components/point_progression_section.py @@ -1,4 +1,5 @@ import pandas as pd +import plotly.graph_objects as go import streamlit as st @@ -35,4 +36,24 @@ def display(self): df = self.create_dataframe(team_forms) st.subheader("Point Progression throughout the Season") - st.line_chart(data=df) + + labels = [str(f"{self.standings_df.iloc[i, 3]} - {self.standings_df.iloc[i, 1]} points") for i in range(5)] + colors = ["#1e90ff", "#ff4500", "#ffd700", "#228b22", "#000000"] + + fig = go.Figure() + + for i in range(5): + fig.add_trace(go.Scatter(x=df.index, y=df.iloc[:, i], name=labels[i], line=dict(color=colors[i], width=2))) + + # add markers + fig.update_traces(mode="markers+lines", marker=dict(size=8, line=dict(width=2))) + + fig.update_layout( + xaxis_title="Gameweek", + yaxis_title="Points", + legend_title="Team", + legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01), + height=600, + ) + + st.plotly_chart(fig, use_container_width=True) diff --git a/components/point_slider_section.py b/components/point_slider_section.py new file mode 100644 index 0000000..f0c574e --- /dev/null +++ b/components/point_slider_section.py @@ -0,0 +1,52 @@ +import streamlit as st +import plotly.graph_objects as go + + +class PointSliderSection: + def __init__(self, standings_df): + self.standings_df = standings_df + + def display(self): + st.subheader("Points per Team:") + # Creating the slider. + points = self.standings_df["points"].tolist() + points_selection = st.slider( + "Select a Range of Points:", min_value=min(points), max_value=max(points), value=(min(points), max(points)) + ) + # Picking colors to use for the bar chart. + colors = [ + "indigo", + ] * 20 + # Making sure the bar chart changes with the slider. + mask = self.standings_df["points"].between(*points_selection) + amount_of_teams = self.standings_df[mask].shape[0] + + df_grouped = self.standings_df[mask] + df_grouped = df_grouped.reset_index() + lowest_number = df_grouped["points"].min() + st.markdown(f"Number of teams with {lowest_number} or more points: {amount_of_teams}") + # Creating the bar chart. + points_chart = go.Figure( + data=[ + go.Bar( + x=df_grouped["team"], + y=df_grouped["points"], + marker_color=colors, + text=df_grouped["points"], + textposition="auto", + ) + ] + ) + # Rotating x axis lables. + points_chart.update_layout( + xaxis_tickangle=-35, + autosize=False, + margin=dict( + l=0, # left + r=0, # right + b=0, # bottom + t=0, # top + ), + ) + + st.plotly_chart(points_chart, use_container_width=True) diff --git a/components/social_media_section.py b/components/social_media_section.py index 97e448b..bb03ec3 100644 --- a/components/social_media_section.py +++ b/components/social_media_section.py @@ -7,12 +7,12 @@ def __init__(self): self.social_links = [ { "url": "https://hub.docker.com/r/digitalghostdev/premier-league/tags", - "icon_url": "https://storage.googleapis.com/premier-league/docker.svg", + "icon_url": "https://storage.googleapis.com/premier_league_bucket/icons/companies/docker.svg", "alt_text": "Docker", }, { "url": "https://github.com/digitalghost-dev/", - "icon_url": "https://storage.googleapis.com/premier-league/github.svg", + "icon_url": "https://storage.googleapis.com/premier_league_bucket/icons/companies/github.svg", "alt_text": "GitHub", }, ] diff --git a/components/top_scorers_section.py b/components/top_scorers_section.py index 11846a9..fe67cf9 100644 --- a/components/top_scorers_section.py +++ b/components/top_scorers_section.py @@ -17,7 +17,7 @@ def generate_scorer_html(self, index): ] def display(self): - with st.container(border=True): + with st.container(): st.subheader("Top 5 Scorers") columns = st.columns(5) diff --git a/components/top_teams_section.py b/components/top_teams_section.py index 56c779b..0606272 100644 --- a/components/top_teams_section.py +++ b/components/top_teams_section.py @@ -16,7 +16,7 @@ def generate_team_html(self, index): ] def display(self): - with st.container(border=True): + with st.container(): st.subheader("Top 5 Teams") columns = st.columns(5) diff --git a/streamlit_app.py b/streamlit_app.py index 3c944b1..fe4fe67 100644 --- a/streamlit_app.py +++ b/streamlit_app.py @@ -10,6 +10,7 @@ from components.highlights_section import HighlightsSection from components.league_form_section import LeagueFormsSection from components.point_progression_section import PointProgressionSection +from components.point_slider_section import PointSliderSection from components.social_media_section import SocialMediaSection from components.squads_section import SquadSection from components.stadiums_map_section import StadiumMapSection @@ -73,10 +74,11 @@ def get_suffix(day): st.write(f"{formatted_date}") # Tab menu. - tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs( + tab1, tab2, tab3, tab4, tab5, tab6, tab7 = st.tabs( [ "Standings & Overview", - "Top Teams & Scorers", + "Teams Statistics", + "Players Statistics", "Fixtures", "Squads", "News & Hightlights", @@ -93,11 +95,12 @@ def get_suffix(day): # Average goals scored column. with col1: teams_df_average_goals = teams_df.sort_values(by=["average_goals"], ascending=False) + max_average_goals = teams_df_average_goals.iloc[0, 6] average_goals_df = pd.DataFrame( { "Average Goals": [ - teams_df_average_goals.iloc[0, 6], + max_average_goals, teams_df_average_goals.iloc[1, 6], teams_df_average_goals.iloc[2, 6], teams_df_average_goals.iloc[3, 6], @@ -121,7 +124,7 @@ def get_suffix(day): help="The Average Goals Scored by Each Team.", format="%f", min_value=0, - max_value=8, + max_value=int(round(max_average_goals, 2)) * 2, ), }, hide_index=True, @@ -129,11 +132,12 @@ def get_suffix(day): with col2: teams_df_penalties_scored = teams_df.sort_values(by=["penalties_scored"], ascending=False) + max_penalties_scored = teams_df_penalties_scored.iloc[0, 4] penalties_scored_df = pd.DataFrame( { "Penalties Scored": [ - teams_df_penalties_scored.iloc[0, 4], + max_penalties_scored, teams_df_penalties_scored.iloc[1, 4], teams_df_penalties_scored.iloc[2, 4], teams_df_penalties_scored.iloc[3, 4], @@ -157,7 +161,7 @@ def get_suffix(day): help="The Amount of Penalties Scored by Each Team.", format="%d", min_value=0, - max_value=20, + max_value=int(max_penalties_scored) * 2, ), }, hide_index=True, @@ -165,11 +169,12 @@ def get_suffix(day): with col3: teams_df_win_streak = teams_df.sort_values(by=["win_streak"], ascending=False) + max_win_streak = teams_df_win_streak.iloc[0, 7] win_streak_df = pd.DataFrame( { "Biggest Win Streak": [ - teams_df_win_streak.iloc[0, 7], + max_win_streak, teams_df_win_streak.iloc[1, 7], teams_df_win_streak.iloc[2, 7], teams_df_win_streak.iloc[3, 7], @@ -193,7 +198,7 @@ def get_suffix(day): help="The Biggest Win Streak by Each Team.", format="%d", min_value=0, - max_value=10, + max_value=int(max_win_streak) * 2, ), }, hide_index=True, @@ -262,39 +267,41 @@ def standings_table() -> DeltaGenerator: stadium_map_section = StadiumMapSection() stadium_map_section.display(stadiums_df) - # --------- Statistics Tab --------- + # --------- Team Statistics Tab --------- # Tab 2 holds the following sections: [Top Teams, Point Progression, Top Scorers, League Forms]. with tab2: + top_teams_section = TopTeamsSection(teams_df) with st.container(): - sections = [ - (TopTeamsSection, teams_df, None), - (PointProgressionSection, teams_df, standings_df), - (TopScorersSection, top_scorers_df, None), - (LeagueFormsSection, teams_df, None), - ] - - first_section = True - for section_class, dataframe_1, dataframe_2 in sections: - if not first_section: - st.subheader("") - else: - first_section = False - - if dataframe_2 is not None: - section = section_class(dataframe_1, dataframe_2) - else: - section = section_class(dataframe_1) - section.display() + top_teams_section.display() - # --------- Fixtures Tab --------- - # Tab 3 holds the following sections: [Fixtures]. + point_progression_section = PointProgressionSection(teams_df, standings_df) + with st.container(): + point_progression_section.display() + + point_slider_section = PointSliderSection(standings_df) + with st.container(): + point_slider_section.display() + + league_forms_section = LeagueFormsSection(teams_df) + with st.container(): + league_forms_section.display() + + # --------- Player Statistics Tab --------- + # Tab 3 holds the following sections: [Player Statistics]. with tab3: + top_scorers_section = TopScorersSection(top_scorers_df) + with st.container(): + top_scorers_section.display() + + # --------- Fixtures Tab --------- + # Tab 4 holds the following sections: [Fixtures]. + with tab4: # Fixtures section. fixtures_section.display() # --------- Squads Tab --------- - # Tab 4 holds the following sections: [Squads]. - with tab4: + # Tab 5 holds the following sections: [Squads]. + with tab5: st.subheader("Team Squads") st.markdown("**Note:** Double click on the player's photo to expand it.") squads = SquadSection(squads_df) @@ -302,16 +309,19 @@ def standings_table() -> DeltaGenerator: col1, _, _ = st.columns(3) with col1: option = st.selectbox( - label="**Select a team to view their squad:**", + index=None, + label="Use the dropdown menu to select a team:", options=squads.teams, - placeholder="Please select a team to view their squad.", + placeholder="Please make a selection", ) if option: + selected_team_logo = teams_df[teams_df["team"] == option]["logo"].iloc[0] + st.image(selected_team_logo, width=75) squads.display(option) # --------- News Tab --------- - # Tab 5 holds the following sections: [News]. - with tab5: + # Tab 6 holds the following sections: [News, Highlights]. + with tab6: st.header("Recent News") col1, col2, col3, col4 = st.columns(4) @@ -374,8 +384,8 @@ def standings_table() -> DeltaGenerator: HighlightsSection(highlights_df).display_second_row() # --------- About Tab --------- - # Tab 6 holds the following sections: [About]. - with tab6: + # Tab 7 holds the following sections: [About]. + with tab7: # About about_section = AboutSection() about_section.display()