diff --git a/src/Teams.py b/src/Teams.py index 8e14d20..fff8bca 100644 --- a/src/Teams.py +++ b/src/Teams.py @@ -20,8 +20,8 @@ # Generate the input section of the `Teams` page. team_number = team_manager.generate_input_section() - metric_tab, auto_graphs_tab, teleop_graphs_tab = st.tabs( - ["📊 Metrics", "🤖 Autonomous Graphs", "🎮 Teleop + Endgame Graphs"] + metric_tab, auto_graphs_tab, teleop_graphs_tab, qualitative_graphs_tab = st.tabs( + ["📊 Metrics", "🤖 Autonomous Graphs", "🎮 Teleop + Endgame Graphs", "📝 Qualitative Graphs"] ) with metric_tab: @@ -69,4 +69,9 @@ team_number, type_of_graph=GraphType.POINT_CONTRIBUTIONS ) - + + with qualitative_graphs_tab: + st.write("#### 📝 Qualitative Graphs") + team_manager.generate_qualitative_graphs( + team_number, + ) diff --git a/src/data/match_schedule.json b/src/data/match_schedule.json new file mode 100644 index 0000000..f38ca2e --- /dev/null +++ b/src/data/match_schedule.json @@ -0,0 +1,769 @@ +[ + { + "match_key": "qm1", + "red_alliance": [ + 3373, + 5954, + 2998 + ], + "blue_alliance": [ + 612, + 2421, + 422 + ] + }, + { + "match_key": "qm2", + "red_alliance": [ + 8592, + 836, + 4286 + ], + "blue_alliance": [ + 9684, + 2199, + 6863 + ] + }, + { + "match_key": "qm3", + "red_alliance": [ + 4099, + 2534, + 1599 + ], + "blue_alliance": [ + 617, + 539, + 8590 + ] + }, + { + "match_key": "qm4", + "red_alliance": [ + 339, + 8326, + 1895 + ], + "blue_alliance": [ + 620, + 540, + 1731 + ] + }, + { + "match_key": "qm5", + "red_alliance": [ + 9709, + 2106, + 1522 + ], + "blue_alliance": [ + 5587, + 4505, + 3136 + ] + }, + { + "match_key": "qm6", + "red_alliance": [ + 1731, + 1599, + 8592 + ], + "blue_alliance": [ + 6863, + 8590, + 9709 + ] + }, + { + "match_key": "qm7", + "red_alliance": [ + 422, + 2106, + 4505 + ], + "blue_alliance": [ + 339, + 1895, + 2421 + ] + }, + { + "match_key": "qm8", + "red_alliance": [ + 836, + 617, + 3136 + ], + "blue_alliance": [ + 620, + 9684, + 5954 + ] + }, + { + "match_key": "qm9", + "red_alliance": [ + 4099, + 5587, + 1522 + ], + "blue_alliance": [ + 612, + 539, + 540 + ] + }, + { + "match_key": "qm10", + "red_alliance": [ + 2998, + 8326, + 2534 + ], + "blue_alliance": [ + 2199, + 3373, + 4286 + ] + }, + { + "match_key": "qm11", + "red_alliance": [ + 8590, + 2199, + 540 + ], + "blue_alliance": [ + 1731, + 422, + 620 + ] + }, + { + "match_key": "qm12", + "red_alliance": [ + 339, + 836, + 539 + ], + "blue_alliance": [ + 612, + 2421, + 2106 + ] + }, + { + "match_key": "qm13", + "red_alliance": [ + 1599, + 4286, + 2998 + ], + "blue_alliance": [ + 9684, + 6863, + 8326 + ] + }, + { + "match_key": "qm14", + "red_alliance": [ + 617, + 9709, + 1522 + ], + "blue_alliance": [ + 4099, + 2534, + 8592 + ] + }, + { + "match_key": "qm15", + "red_alliance": [ + 4505, + 5954, + 5587 + ], + "blue_alliance": [ + 3373, + 1895, + 3136 + ] + }, + { + "match_key": "qm16", + "red_alliance": [ + 8592, + 1522, + 1599 + ], + "blue_alliance": [ + 4099, + 5954, + 6863 + ] + }, + { + "match_key": "qm17", + "red_alliance": [ + 1731, + 620, + 2998 + ], + "blue_alliance": [ + 3136, + 2106, + 2199 + ] + }, + { + "match_key": "qm18", + "red_alliance": [ + 612, + 9709, + 422 + ], + "blue_alliance": [ + 539, + 836, + 8326 + ] + }, + { + "match_key": "qm19", + "red_alliance": [ + 617, + 8590, + 540 + ], + "blue_alliance": [ + 4286, + 1895, + 9684 + ] + }, + { + "match_key": "qm20", + "red_alliance": [ + 2534, + 2421, + 339 + ], + "blue_alliance": [ + 3373, + 5587, + 4505 + ] + }, + { + "match_key": "qm21", + "red_alliance": [ + 540, + 2106, + 2534 + ], + "blue_alliance": [ + 2421, + 1522, + 617 + ] + }, + { + "match_key": "qm22", + "red_alliance": [ + 3373, + 8590, + 1895 + ], + "blue_alliance": [ + 620, + 3136, + 9684 + ] + }, + { + "match_key": "qm23", + "red_alliance": [ + 539, + 6863, + 8326 + ], + "blue_alliance": [ + 4099, + 8592, + 836 + ] + }, + { + "match_key": "qm24", + "red_alliance": [ + 1599, + 339, + 4505 + ], + "blue_alliance": [ + 1731, + 9709, + 2998 + ] + }, + { + "match_key": "qm25", + "red_alliance": [ + 612, + 4286, + 5954 + ], + "blue_alliance": [ + 422, + 2199, + 5587 + ] + }, + { + "match_key": "qm26", + "red_alliance": [ + 836, + 1522, + 5954 + ], + "blue_alliance": [ + 1731, + 8590, + 612 + ] + }, + { + "match_key": "qm27", + "red_alliance": [ + 8592, + 4505, + 9709 + ], + "blue_alliance": [ + 3136, + 1599, + 339 + ] + }, + { + "match_key": "qm28", + "red_alliance": [ + 4099, + 2421, + 422 + ], + "blue_alliance": [ + 540, + 2106, + 1895 + ] + }, + { + "match_key": "qm29", + "red_alliance": [ + 9684, + 8326, + 2998 + ], + "blue_alliance": [ + 2534, + 4286, + 539 + ] + }, + { + "match_key": "qm30", + "red_alliance": [ + 2199, + 5587, + 620 + ], + "blue_alliance": [ + 3373, + 617, + 6863 + ] + }, + { + "match_key": "qm31", + "red_alliance": [ + 9709, + 1599, + 6863 + ], + "blue_alliance": [ + 422, + 2534, + 8592 + ] + }, + { + "match_key": "qm32", + "red_alliance": [ + 2998, + 2106, + 9684 + ], + "blue_alliance": [ + 8326, + 836, + 4505 + ] + }, + { + "match_key": "qm33", + "red_alliance": [ + 5587, + 3373, + 1895 + ], + "blue_alliance": [ + 539, + 620, + 2199 + ] + }, + { + "match_key": "qm34", + "red_alliance": [ + 4099, + 1522, + 4286 + ], + "blue_alliance": [ + 617, + 5954, + 2421 + ] + }, + { + "match_key": "qm35", + "red_alliance": [ + 612, + 8590, + 540 + ], + "blue_alliance": [ + 339, + 3136, + 1731 + ] + }, + { + "match_key": "qm36", + "red_alliance": [ + 2106, + 5587, + 3136 + ], + "blue_alliance": [ + 8592, + 4505, + 1599 + ] + }, + { + "match_key": "qm37", + "red_alliance": [ + 1895, + 2998, + 1522 + ], + "blue_alliance": [ + 5954, + 9709, + 8590 + ] + }, + { + "match_key": "qm38", + "red_alliance": [ + 2199, + 4286, + 1731 + ], + "blue_alliance": [ + 540, + 612, + 6863 + ] + }, + { + "match_key": "qm39", + "red_alliance": [ + 422, + 4099, + 539 + ], + "blue_alliance": [ + 836, + 8326, + 9684 + ] + }, + { + "match_key": "qm40", + "red_alliance": [ + 2421, + 2534, + 620 + ], + "blue_alliance": [ + 339, + 617, + 3373 + ] + }, + { + "match_key": "qm41", + "red_alliance": [ + 2421, + 2106, + 3136 + ], + "blue_alliance": [ + 2199, + 540, + 339 + ] + }, + { + "match_key": "qm42", + "red_alliance": [ + 836, + 2998, + 3373 + ], + "blue_alliance": [ + 5587, + 5954, + 4505 + ] + }, + { + "match_key": "qm43", + "red_alliance": [ + 422, + 9684, + 1731 + ], + "blue_alliance": [ + 1522, + 1895, + 1599 + ] + }, + { + "match_key": "qm44", + "red_alliance": [ + 8326, + 612, + 617 + ], + "blue_alliance": [ + 4286, + 620, + 539 + ] + }, + { + "match_key": "qm45", + "red_alliance": [ + 9709, + 8592, + 4099 + ], + "blue_alliance": [ + 2534, + 8590, + 6863 + ] + }, + { + "match_key": "qm46", + "red_alliance": [ + 2998, + 1599, + 8592 + ], + "blue_alliance": [ + 2106, + 2199, + 8590 + ] + }, + { + "match_key": "qm47", + "red_alliance": [ + 339, + 2534, + 3136 + ], + "blue_alliance": [ + 5954, + 6863, + 4286 + ] + }, + { + "match_key": "qm48", + "red_alliance": [ + 1895, + 540, + 836 + ], + "blue_alliance": [ + 422, + 1522, + 620 + ] + }, + { + "match_key": "qm49", + "red_alliance": [ + 3373, + 4505, + 9709 + ], + "blue_alliance": [ + 612, + 8326, + 5587 + ] + }, + { + "match_key": "qm50", + "red_alliance": [ + 1731, + 617, + 2421 + ], + "blue_alliance": [ + 9684, + 4099, + 539 + ] + }, + { + "match_key": "qm51", + "red_alliance": [ + 617, + 2998, + 8592 + ], + "blue_alliance": [ + 540, + 5954, + 9709 + ] + }, + { + "match_key": "qm52", + "red_alliance": [ + 9684, + 4099, + 620 + ], + "blue_alliance": [ + 1522, + 4505, + 5587 + ] + }, + { + "match_key": "qm53", + "red_alliance": [ + 8590, + 4286, + 339 + ], + "blue_alliance": [ + 836, + 2421, + 1599 + ] + }, + { + "match_key": "qm54", + "red_alliance": [ + 1895, + 2106, + 8326 + ], + "blue_alliance": [ + 6863, + 539, + 3373 + ] + }, + { + "match_key": "qm55", + "red_alliance": [ + 2199, + 1731, + 422 + ], + "blue_alliance": [ + 612, + 3136, + 2534 + ] + }, + { + "match_key": "qm56", + "red_alliance": [ + 4505, + 8592, + 4099 + ], + "blue_alliance": [ + 5587, + 620, + 836 + ] + }, + { + "match_key": "qm57", + "red_alliance": [ + 2998, + 1895, + 4286 + ], + "blue_alliance": [ + 5954, + 2106, + 422 + ] + }, + { + "match_key": "qm58", + "red_alliance": [ + 3136, + 540, + 617 + ], + "blue_alliance": [ + 539, + 8590, + 8326 + ] + }, + { + "match_key": "qm59", + "red_alliance": [ + 612, + 2534, + 339 + ], + "blue_alliance": [ + 2199, + 3373, + 2421 + ] + } +] diff --git a/src/page_managers/__init__.py b/src/page_managers/__init__.py index 1dff197..5066ee2 100644 --- a/src/page_managers/__init__.py +++ b/src/page_managers/__init__.py @@ -3,4 +3,3 @@ from .match_manager import MatchManager from .picklist_manager import PicklistManager from .team_manager import TeamManager -from .alliance_selection_manager import AllianceSelectionManager diff --git a/src/page_managers/alliance_selection_manager.py b/src/page_managers/alliance_selection_manager.py deleted file mode 100644 index 596cfc0..0000000 --- a/src/page_managers/alliance_selection_manager.py +++ /dev/null @@ -1,543 +0,0 @@ -"""Creates the `MatchManager` class used to set up the Match page and its graphs.""" - -import numpy as np -import streamlit as st -from scipy.integrate import quad -from scipy.stats import norm - -from .page_manager import PageManager -from utils import ( - alliance_breakdown, - bar_graph, - box_plot, - CalculatedStats, - colored_metric, - Criteria, - GeneralConstants, - GraphType, - multi_line_graph, - plotly_chart, - populate_missing_data, - Queries, - retrieve_match_schedule, - retrieve_pit_scouting_data, - retrieve_team_list, - retrieve_scouting_data, - scouting_data_for_team, - stacked_bar_graph, - win_percentages, -) - - -class AllianceSelectionManager(PageManager): - """The page manager for the `Alliance Selection` page.""" - - def __init__(self): - self.calculated_stats = CalculatedStats(retrieve_scouting_data()) - self.pit_scouting_data = retrieve_pit_scouting_data() - - def generate_input_section(self) -> list[list, list]: - """Creates the input section for the `Alliance Selection` page. - - Creates 3 dropdowns to choose teams - - :return: List with 3 choices - """ - team_list = retrieve_team_list() - - # Create the different dropdowns to choose the three teams for Red Alliance. - team_1_col, team_2_col, team_3_col = st.columns(3) - team_1 = team_1_col.selectbox( - "Team 1", - team_list, - index=0 - ) - team_2 = team_2_col.selectbox( - "Team 2", - team_list, - index=1 - ) - team_3 = team_3_col.selectbox( - "Team 3", - team_list, - index=2 - ) - - return [team_1, team_2, team_3] - - def generate_alliance_dashboard(self, team_numbers: list[int], color_gradient: list[str]) -> None: - """Generates an alliance dashboard in the `Match` page. - - :param team_numbers: The teams to generate the alliance dashboard for. - :param color_gradient: The color gradient to use for graphs, depending on the alliance. - :return: - """ - fastest_cycler_col, second_fastest_cycler_col, slowest_cycler_col = st.columns(3) - - fastest_cyclers = sorted( - { - team: self.calculated_stats.driving_index(team) for team in team_numbers - }.items(), - key=lambda pair: pair[1], - reverse=True - ) - - # Colored metric displaying the fastest cycler in the alliance - with fastest_cycler_col: - colored_metric( - "Fastest Cycler", - fastest_cyclers[0][0], - background_color=color_gradient[0], - opacity=0.4, - border_opacity=0.9 - ) - - # Colored metric displaying the second fastest cycler in the alliance - with second_fastest_cycler_col: - colored_metric( - "Second Fastest Cycler", - fastest_cyclers[1][0], - background_color=color_gradient[1], - opacity=0.4, - border_opacity=0.9 - ) - - # Colored metric displaying the slowest cycler in the alliance - with slowest_cycler_col: - colored_metric( - "Slowest Cycler", - fastest_cyclers[2][0], - background_color=color_gradient[2], - opacity=0.4, - border_opacity=0.9 - ) - - def generate_drivetrain_dashboard(self, team_numbers: list[int], color_gradient: list[str]) -> None: - """Generates an drivetrain dashboard in the `Alliance Selection` page. - - :param team_numbers: The teams to generate the drivetrain dashboard for. - :param color_gradient: The color gradient to use for graphs. - :return: - """ - - team1_col, team2_col, team3_col = st.columns(3) - - drivetrain_data = [ - self.pit_scouting_data[ - self.pit_scouting_data["Team Number"] == team - ].iloc[0]["Drivetrain"] - for team in team_numbers] - - # Colored metric displaying the fastest cycler in the alliance - with team1_col: - colored_metric( - "Team " + str(team_numbers[0]) + " Drivetrain:", - drivetrain_data[0], - background_color=color_gradient[0], - opacity=0.4, - border_opacity=0.9 - ) - - # Colored metric displaying the second fastest cycler in the alliance - with team2_col: - colored_metric( - "Team " + str(team_numbers[1]) + " Drivetrain:", - drivetrain_data[1], - background_color=color_gradient[1], - opacity=0.4, - border_opacity=0.9 - ) - - # Colored metric displaying the slowest cycler in the alliance - with team3_col: - colored_metric( - "Team " + str(team_numbers[2]) + " Drivetrain:", - drivetrain_data[2], - background_color=color_gradient[2], - opacity=0.4, - border_opacity=0.9 - ) - - def generate_autonomous_graphs( - self, - team_numbers: list[int], - type_of_graph: str, - color_gradient: list[str] - ) -> None: - """Generates the autonomous graphs for the `Match` page. - - :param team_numbers: The teams to generate the graphs for. - :param type_of_graph: The type of graph to make (cycle contributions/point contributions). - :param color_gradient: The color gradient to use for graphs, depending on the alliance. - :return: - """ - display_cycle_contributions = type_of_graph == GraphType.CYCLE_CONTRIBUTIONS - - auto_configuration_col, auto_engage_stats_col = st.columns(2) - auto_cycle_distribution_col, auto_cycles_over_time = st.columns(2) - - # Determine the best auto configuration for an alliance. - with auto_configuration_col: - average_auto_cycles_by_team = [ - ( - self.calculated_stats.cycles_by_match(team, Queries.AUTO_GRID) - if display_cycle_contributions - else self.calculated_stats.points_contributed_by_match(team, Queries.AUTO_GRID) - ).mean() - for team in team_numbers - ] - - plotly_chart( - bar_graph( - team_numbers, - average_auto_cycles_by_team, - x_axis_label="Teams", - y_axis_label=( - "Cycles in Auto" - if display_cycle_contributions - else "Points Scored in Auto" - ), - title="Average Auto Contribution", - color=color_gradient[1] - ) - ) - - # Determine the accuracy of teams when it comes to engaging onto the charge station - with auto_engage_stats_col: - successful_engages_by_team = [ - self.calculated_stats.cumulative_stat( - team, - Queries.AUTO_CHARGING_STATE, - Criteria.SUCCESSFUL_ENGAGE_CRITERIA - ) - for team in team_numbers - ] - successful_docks_by_team = [ - self.calculated_stats.cumulative_stat( - team, - Queries.AUTO_CHARGING_STATE, - Criteria.SUCCESSFUL_DOCK_CRITERIA - ) - for team in team_numbers - ] - missed_attempts_by_team = [ - self.calculated_stats.cumulative_stat( - team, - Queries.AUTO_ENGAGE_ATTEMPTED, - Criteria.AUTO_ATTEMPT_CRITERIA - ) - successful_docks_by_team[idx] - successful_engages_by_team[idx] - for idx, team in enumerate(team_numbers) - ] - - plotly_chart( - stacked_bar_graph( - team_numbers, - [missed_attempts_by_team, successful_docks_by_team, successful_engages_by_team], - x_axis_label="Teams", - y_axis_label=["# of Missed Engages", "# of Docks", "# of Engages"], - y_axis_title="", - color_map=dict( - zip( - ["# of Missed Engages", "# of Docks", "# of Engages"], - color_gradient - ) - ), - title="Auto Engage Stats" - ) - ) - - # Box plot showing the distribution of cycles - with auto_cycle_distribution_col: - cycles_by_team = [ - ( - self.calculated_stats.cycles_by_match(team, Queries.AUTO_GRID) - if display_cycle_contributions - else self.calculated_stats.points_contributed_by_match(team, Queries.AUTO_GRID) - ) - for team in team_numbers - ] - - plotly_chart( - box_plot( - team_numbers, - cycles_by_team, - x_axis_label="Teams", - y_axis_label=( - "# of Cycles" - if display_cycle_contributions - else "Points Contributed" - ), - title=( - "Distribution of Auto Cycles" - if display_cycle_contributions - else "Distribution of Points Contributed During Auto" - ), - show_underlying_data=True, - color_sequence=color_gradient - ) - ) - - # Plot cycles over time - with auto_cycles_over_time: - cycles_by_team = [ - ( - self.calculated_stats.cycles_by_match(team, Queries.AUTO_GRID) - if display_cycle_contributions - else self.calculated_stats.points_contributed_by_match(team, Queries.AUTO_GRID) - ) - for team in team_numbers - ] - - plotly_chart( - multi_line_graph( - *populate_missing_data(cycles_by_team), - x_axis_label="Match Index", - y_axis_label=team_numbers, - y_axis_title=( - "# of Cycles" - if display_cycle_contributions - else "Points Contributed" - ), - title=( - "Auto Cycles Over Time" - if display_cycle_contributions - else "Points Contributed in Auto Over Time" - ), - color_map=dict(zip(team_numbers, color_gradient)) - ) - ) - - def generate_teleop_graphs( - self, - team_numbers: list[int], - type_of_graph: str, - color_gradient: list[str] - ) -> None: - """Generates the teleop graphs for the `Match` page. - - :param team_numbers: The teams to generate the graphs for. - :param type_of_graph: The type of graph to make (cycle contributions/point contributions). - :param color_gradient: The color gradient to use for graphs, depending on the alliance. - :return: - """ - display_cycle_contributions = type_of_graph == GraphType.CYCLE_CONTRIBUTIONS - - teleop_cycles_by_level_col, teleop_game_piece_breakdown_col = st.columns(2) - teleop_cycles_over_time_col, teleop_cycles_distribution_col = st.columns(2) - - # Graph the teleop cycles per team by level (High/Mid/Low) - with teleop_cycles_by_level_col: - cycles_by_height = [] - - for height in (Queries.HIGH, Queries.MID, Queries.LOW): - cycles_by_height.append([ - self.calculated_stats.average_cycles_for_height( - team, - Queries.TELEOP_GRID, - height - ) * (1 if display_cycle_contributions else Criteria.TELEOP_GRID_POINTAGE[height]) - for team in team_numbers - ]) - - plotly_chart( - stacked_bar_graph( - team_numbers, - cycles_by_height, - x_axis_label="Teams", - y_axis_label=["High", "Mid", "Low"], - y_axis_title="", - color_map=dict( - zip( - ["High", "Mid", "Low"], - GeneralConstants.LEVEL_GRADIENT - ) - ), - title=( - "Average Cycles by Height" - if display_cycle_contributions - else "Average Points Contributed by Height" - ) - ).update_layout(xaxis={"categoryorder": "total descending"}) - ) - - # Graph the breakdown of game pieces by each team - with teleop_game_piece_breakdown_col: - cones_scored_by_team = [ - self.calculated_stats.cycles_by_game_piece_per_match( - team, - Queries.TELEOP_GRID, - Queries.CONE - ).sum() - for team in team_numbers - ] - cubes_scored_by_team = [ - self.calculated_stats.cycles_by_game_piece_per_match( - team, - Queries.TELEOP_GRID, - Queries.CUBE - ).sum() - for team in team_numbers - ] - - plotly_chart( - stacked_bar_graph( - team_numbers, - [cones_scored_by_team, cubes_scored_by_team], - x_axis_label="Teams", - y_axis_label=["Total # of Cones Scored", "Total # of Cubes Scored"], - y_axis_title="", - color_map=dict( - zip( - ["Total # of Cones Scored", "Total # of Cubes Scored"], - [GeneralConstants.CONE_COLOR, GeneralConstants.CUBE_COLOR] - ) - ), - title="Game Piece Breakdown by Team" - ).update_layout(xaxis={"categoryorder": "total descending"}) - ) - - # Box plot showing the distribution of cycles - with teleop_cycles_distribution_col: - cycles_by_team = [ - ( - self.calculated_stats.cycles_by_match(team, Queries.TELEOP_GRID) - if display_cycle_contributions - else self.calculated_stats.points_contributed_by_match(team, Queries.TELEOP_GRID) - ) - for team in team_numbers - ] - - plotly_chart( - box_plot( - team_numbers, - cycles_by_team, - x_axis_label="Teams", - y_axis_label=( - "# of Cycles" - if display_cycle_contributions - else "Points Contributed" - ), - title=( - "Distribution of Teleop Cycles" - if display_cycle_contributions - else "Distribution of Points Contributed During Teleop" - ), - show_underlying_data=True, - color_sequence=color_gradient - ) - ) - - # Plot cycles over time - with teleop_cycles_over_time_col: - cycles_by_team = [ - ( - self.calculated_stats.cycles_by_match(team, Queries.TELEOP_GRID) - if display_cycle_contributions - else self.calculated_stats.points_contributed_by_match(team, Queries.TELEOP_GRID) - ) - for team in team_numbers - ] - - plotly_chart( - multi_line_graph( - *populate_missing_data(cycles_by_team), - x_axis_label="Match Index", - y_axis_label=team_numbers, - y_axis_title=( - "# of Cycles" - if display_cycle_contributions - else "Points Contributed" - ), - title=( - "Teleop Cycles Over Time" - if display_cycle_contributions - else "Points Contributed in Teleop Over Time" - ), - color_map=dict(zip(team_numbers, color_gradient)) - ) - ) - - def generate_rating_graphs( - self, - team_numbers: list[int], - color_gradient: list[str] - ) -> None: - """Generates the teleop graphs for the `Match` page. - - :param team_numbers: The teams to generate the graphs for. - :param type_of_graph: The type of graph to make (cycle contributions/point contributions). - :param color_gradient: The color gradient to use for graphs, depending on the alliance. - :return: - """ - - driver_rating_col, defense_rating_col = st.columns(2) - disables_col, drivetrain_width_col = st.columns(2) - - with driver_rating_col: - driver_ratings = [ - self.calculated_stats.average_driver_rating(team) for team in team_numbers - ] - - plotly_chart( - bar_graph( - team_numbers, - driver_ratings, - x_axis_label="Teams", - y_axis_label="Driver Rating", - title="Driver Rating", - color=color_gradient[1] - ) - ) - - with defense_rating_col: - defense_ratings = [ - self.calculated_stats.average_defense_rating(team) for team in team_numbers - ] - - plotly_chart( - bar_graph( - team_numbers, - defense_ratings, - x_axis_label="Teams", - y_axis_label="Defense Rating", - title="Defense Rating", - color=color_gradient[1] - ) - ) - - with disables_col: - - disables_by_team = [ - self.calculated_stats.disables_by_team(team) for team in team_numbers - ] - - plotly_chart( - multi_line_graph( - *populate_missing_data(disables_by_team), - x_axis_label="Match Index", - y_axis_label=team_numbers, - y_axis_title="Disabled", - title=( - "Disables Over Time" - ), - color_map=dict(zip(team_numbers, color_gradient)) - ) - ) - - with drivetrain_width_col: - drivetrain_widths = [ - self.calculated_stats.drivetrain_width_by_team(team) for team in team_numbers - ] - - plotly_chart( - bar_graph( - team_numbers, - drivetrain_widths, - x_axis_label="Teams", - y_axis_label="Drivetrain Width", - title="Drivetrain Width", - color=color_gradient[1] - ) - ) - - \ No newline at end of file diff --git a/src/page_managers/custom_graphs_manager.py b/src/page_managers/custom_graphs_manager.py index f685268..f3a7324 100644 --- a/src/page_managers/custom_graphs_manager.py +++ b/src/page_managers/custom_graphs_manager.py @@ -40,7 +40,7 @@ def generate_input_section(self) -> list[list, list, Callable, str]: names_to_methods = { name.replace("_", " ").capitalize(): method for name, method in inspect.getmembers(self.calculated_stats, predicate=inspect.ismethod) - if not name.startswith("__") + if not name.startswith("__") and "(ignore)" not in method.__doc__ } st.write("#### 📈 Data to Display") diff --git a/src/page_managers/event_manager.py b/src/page_managers/event_manager.py index 89b1d5f..6a6c0df 100644 --- a/src/page_managers/event_manager.py +++ b/src/page_managers/event_manager.py @@ -11,8 +11,8 @@ GeneralConstants, GraphType, plotly_chart, - Queries, - retrieve_team_list, + Queries, + retrieve_team_list, retrieve_scouting_data ) @@ -28,47 +28,102 @@ def __init__(self): ) @st.cache_data(ttl=GeneralConstants.SECONDS_TO_CACHE) - def _retrieve_cycle_distributions(_self, type_of_grid: str) -> list: + def _retrieve_cycle_distributions(_self, mode: str) -> list: """Retrieves cycle distributions across an event for autonomous/teleop. - :param type_of_grid: The mode to retrieve cycle data for (autonomous/teleop). + :param mode: The mode to retrieve cycle data for (autonomous/teleop). :return: A list containing the cycle distirbutions for each team. """ teams = retrieve_team_list() return [ - _self.calculated_stats.cycles_by_match(team, type_of_grid) + _self.calculated_stats.cycles_by_match(team, mode) for team in teams ] @st.cache_data(ttl=GeneralConstants.SECONDS_TO_CACHE) - def _retrieve_point_distributions(_self, type_of_grid: str) -> list: + def _retrieve_point_distributions(_self, mode: str) -> list: """Retrieves point distributions across an event for autonomous/teleop. - :param type_of_grid: The mode to retrieve point contribution data for (autonomous/teleop). - :return: A list containing the point distirbutions for each team. + :param mode: The mode to retrieve point contribution data for (autonomous/teleop). + :return: A list containing the point distributions for each team. """ teams = retrieve_team_list() return [ - _self.calculated_stats.points_contributed_by_match(team, type_of_grid) + _self.calculated_stats.points_contributed_by_match(team, mode) for team in teams ] + @st.cache_data(ttl=GeneralConstants.SECONDS_TO_CACHE) + def _retrieve_speaker_cycle_distributions(_self) -> list: + """Retrieves the distribution of speaker cycles for each team across an event for auto/teleop. + + :return: A list containing the speaker cycle distributions for each team. + """ + teams = retrieve_team_list() + return [ + _self.calculated_stats.cycles_by_structure_per_match(team, (Queries.AUTO_SPEAKER, Queries.TELEOP_SPEAKER)) + for team in teams + ] + + @st.cache_data(ttl=GeneralConstants.SECONDS_TO_CACHE) + def _retrieve_amp_cycle_distributions(_self) -> list: + """Retrieves the distribution of amp cycles for each team across an event for auto/teleop. + + :return: A list containing the amp cycle distributions for each team. + """ + teams = retrieve_team_list() + return [ + _self.calculated_stats.cycles_by_structure_per_match(team, (Queries.AUTO_AMP, Queries.TELEOP_AMP)) + for team in teams + ] + + @st.cache_data(ttl=GeneralConstants.SECONDS_TO_CACHE) + def _retrieve_speaker_cycle_distributions(_self) -> list: + """Retrieves the distribution of speaker cycles for each team across an event for auto/teleop. + + :return: A list containing the speaker cycle distributions for each team. + """ + teams = retrieve_team_list() + return [ + _self.calculated_stats.cycles_by_structure_per_match(team, (Queries.AUTO_SPEAKER, Queries.TELEOP_SPEAKER)) + for team in teams + ] + + @st.cache_data(ttl=GeneralConstants.SECONDS_TO_CACHE) + def _retrieve_amp_cycle_distributions(_self) -> list: + """Retrieves the distribution of amp cycles for each team across an event for auto/teleop. + + :return: A list containing the amp cycle distributions for each team. + """ + teams = retrieve_team_list() + return [ + _self.calculated_stats.cycles_by_structure_per_match(team, (Queries.AUTO_AMP, Queries.TELEOP_AMP)) + for team in teams + ] + + @st.cache_data(ttl=GeneralConstants.SECONDS_TO_CACHE) + def _retrieve_teleop_distributions(_self) -> list: + teams = retrieve_team_list() + return [ + _self.calculated_stats.cycles_by_match(team, Queries.TELEOP) for team in teams + ] + def generate_input_section(self) -> None: """Defines that there are no inputs for the event page, showing event-wide graphs.""" return - + def generate_event_breakdown(self) -> None: """Creates metrics that breakdown the events and display the average cycles of the top 8, 16 and 24 teams.""" top_8_col, top_16_col, top_24_col = st.columns(3) - + average_cycles_per_team = sorted( [ - self.calculated_stats.average_cycles(team, Queries.TELEOP_GRID) + self.calculated_stats.average_cycles(team, Queries.TELEOP) for team in retrieve_team_list() ], reverse=True ) - + # Metric displaying the average cycles of the top 8 teams/likely alliance captains with top_8_col: colored_metric( @@ -87,7 +142,7 @@ def generate_event_breakdown(self) -> None: opacity=0.4, border_opacity=0.75, ) - + # Metric displaying the average cycles of the top 24 teams/likely alliance captains with top_24_col: colored_metric( @@ -106,17 +161,17 @@ def generate_event_graphs(self, type_of_graph: str) -> None: display_cycle_contributions = type_of_graph == GraphType.CYCLE_CONTRIBUTIONS teams = retrieve_team_list() auto_cycles_col, teleop_cycles_col = st.columns(2, gap="large") + speaker_cycles_col, amp_cycles_col = st.columns(2, gap="large") - # Display event-wide graph surrounding each team and their cycle/point contributions in autonomous. + # Display event-wide graph surrounding each team and their cycle distributions in the Autonomous period. with auto_cycles_col: variable_key = f"auto_cycles_col_{type_of_graph}" auto_distributions = ( - self._retrieve_cycle_distributions(Queries.AUTO_GRID) + self._retrieve_cycle_distributions(Queries.AUTO) if display_cycle_contributions - else self._retrieve_point_distributions(Queries.AUTO_GRID) + else self._retrieve_point_distributions(Queries.AUTO) ) - auto_sorted_distributions = dict( sorted( zip(teams, auto_distributions), @@ -134,14 +189,14 @@ def generate_event_graphs(self, type_of_graph: str) -> None: plotly_chart( box_plot( auto_sorted_teams[ - st.session_state[variable_key]:st.session_state[variable_key] + self.TEAMS_TO_SPLIT_BY + st.session_state[variable_key]:st.session_state[variable_key] + self.TEAMS_TO_SPLIT_BY ], auto_distributions[ - st.session_state[variable_key]:st.session_state[variable_key] + self.TEAMS_TO_SPLIT_BY + st.session_state[variable_key]:st.session_state[variable_key] + self.TEAMS_TO_SPLIT_BY ], x_axis_label="Teams", - y_axis_label=f"{'Cycle' if display_cycle_contributions else 'Point'} Distribution", - title=f"{'Cycle' if display_cycle_contributions else 'Point'} Contributions in Autonomous" + y_axis_label="Cycle Distribution" if display_cycle_contributions else "Point Distribution", + title="Cycle Contributions in Auto" if display_cycle_contributions else "Point Contributions in Auto" ).update_layout( showlegend=False ) @@ -150,33 +205,32 @@ def generate_event_graphs(self, type_of_graph: str) -> None: previous_col, next_col = st.columns(2) if previous_col.button( - f"Previous {self.TEAMS_TO_SPLIT_BY} Teams", - use_container_width=True, - key=f"prevAuto{type_of_graph}", - disabled=(st.session_state[variable_key] - self.TEAMS_TO_SPLIT_BY < 0) + f"Previous {self.TEAMS_TO_SPLIT_BY} Teams", + use_container_width=True, + key=f"prevAuto{type_of_graph}", + disabled=(st.session_state[variable_key] - self.TEAMS_TO_SPLIT_BY < 0) ): st.session_state[variable_key] -= self.TEAMS_TO_SPLIT_BY st.experimental_rerun() if next_col.button( - f"Next {self.TEAMS_TO_SPLIT_BY} Teams", - use_container_width=True, - key=f"nextAuto{type_of_graph}", - disabled=(st.session_state[variable_key] + self.TEAMS_TO_SPLIT_BY >= len(teams)) + f"Next {self.TEAMS_TO_SPLIT_BY} Teams", + use_container_width=True, + key=f"nextAuto{type_of_graph}", + disabled=(st.session_state[variable_key] + self.TEAMS_TO_SPLIT_BY >= len(teams)) ): st.session_state[variable_key] += self.TEAMS_TO_SPLIT_BY st.experimental_rerun() - # Display event-wide graph surrounding each team and their cycle/point contributions in teleop. + # Display event-wide graph surrounding each team and their cycle distributions in the Teleop period. with teleop_cycles_col: - variable_key = f"teleop_cycles_col_{type_of_graph}" + variable_key = f"auto_cycles_col_{type_of_graph}" teleop_distributions = ( - self._retrieve_cycle_distributions(Queries.TELEOP_GRID) + self._retrieve_cycle_distributions(Queries.TELEOP) if display_cycle_contributions - else self._retrieve_point_distributions(Queries.TELEOP_GRID) + else self._retrieve_point_distributions(Queries.TELEOP) ) - teleop_sorted_distributions = dict( sorted( zip(teams, teleop_distributions), @@ -200,8 +254,8 @@ def generate_event_graphs(self, type_of_graph: str) -> None: st.session_state[variable_key]:st.session_state[variable_key] + self.TEAMS_TO_SPLIT_BY ], x_axis_label="Teams", - y_axis_label=f"{'Cycle' if display_cycle_contributions else 'Point'} Distribution", - title=f"{'Cycle' if display_cycle_contributions else 'Point'} Contributions in Teleop" + y_axis_label="Cycle Distribution" if display_cycle_contributions else "Point Distribution", + title="Cycle Contributions in Teleop" if display_cycle_contributions else "Point Contributions in Teleop" ).update_layout( showlegend=False ) @@ -226,3 +280,113 @@ def generate_event_graphs(self, type_of_graph: str) -> None: ): st.session_state[variable_key] += self.TEAMS_TO_SPLIT_BY st.experimental_rerun() + + # Display event-wide graph surrounding each team and their cycle distributions with the Speaker. + with speaker_cycles_col: + variable_key = f"speaker_cycles_col_{type_of_graph}" + + speaker_distributions = self._retrieve_speaker_cycle_distributions() + speaker_sorted_distributions = dict( + sorted( + zip(teams, speaker_distributions), + key=lambda pair: (pair[1].median(), pair[1].mean()), + reverse=True + ) + ) + + speaker_sorted_teams = list(speaker_sorted_distributions.keys()) + speaker_distributions = list(speaker_sorted_distributions.values()) + + if not st.session_state.get(variable_key): + st.session_state[variable_key] = 0 + + plotly_chart( + box_plot( + speaker_sorted_teams[ + st.session_state[variable_key]:st.session_state[variable_key] + self.TEAMS_TO_SPLIT_BY + ], + speaker_distributions[ + st.session_state[variable_key]:st.session_state[variable_key] + self.TEAMS_TO_SPLIT_BY + ], + x_axis_label="Teams", + y_axis_label=f"Cycle Distribution", + title=f"Speaker Cycle Distributions by Team" + ).update_layout( + showlegend=False + ) + ) + + previous_col, next_col = st.columns(2) + + if previous_col.button( + f"Previous {self.TEAMS_TO_SPLIT_BY} Teams", + use_container_width=True, + key=f"prevSpeaker{type_of_graph}", + disabled=(st.session_state[variable_key] - self.TEAMS_TO_SPLIT_BY < 0) + ): + st.session_state[variable_key] -= self.TEAMS_TO_SPLIT_BY + st.experimental_rerun() + + if next_col.button( + f"Next {self.TEAMS_TO_SPLIT_BY} Teams", + use_container_width=True, + key=f"nextSpeaker{type_of_graph}", + disabled=(st.session_state[variable_key] + self.TEAMS_TO_SPLIT_BY >= len(teams)) + ): + st.session_state[variable_key] += self.TEAMS_TO_SPLIT_BY + st.experimental_rerun() + + # Display event-wide graph surrounding each team and their cycle contributions to the Amp. + with amp_cycles_col: + variable_key = f"amp_cycles_col_{type_of_graph}" + + amp_distributions = self._retrieve_amp_cycle_distributions() + amp_sorted_distributions = dict( + sorted( + zip(teams, amp_distributions), + key=lambda pair: (pair[1].median(), pair[1].mean()), + reverse=True + ) + ) + + amp_sorted_teams = list(amp_sorted_distributions.keys()) + amp_distributions = list(amp_sorted_distributions.values()) + + if not st.session_state.get(variable_key): + st.session_state[variable_key] = 0 + + plotly_chart( + box_plot( + amp_sorted_teams[ + st.session_state[variable_key]:st.session_state[variable_key] + self.TEAMS_TO_SPLIT_BY + ], + amp_distributions[ + st.session_state[variable_key]:st.session_state[variable_key] + self.TEAMS_TO_SPLIT_BY + ], + x_axis_label="Teams", + y_axis_label=f"Cycle Distribution", + title=f"Amp Cycle Distributions By Team" + ).update_layout( + showlegend=False + ) + ) + + previous_col, next_col = st.columns(2) + + if previous_col.button( + f"Previous {self.TEAMS_TO_SPLIT_BY} Teams", + use_container_width=True, + key=f"prevAmp{type_of_graph}", + disabled=(st.session_state[variable_key] - self.TEAMS_TO_SPLIT_BY < 0) + ): + st.session_state[variable_key] -= self.TEAMS_TO_SPLIT_BY + st.experimental_rerun() + + if next_col.button( + f"Next {self.TEAMS_TO_SPLIT_BY} Teams", + use_container_width=True, + key=f"nextAmp{type_of_graph}", + disabled=(st.session_state[variable_key] + self.TEAMS_TO_SPLIT_BY >= len(teams)) + ): + st.session_state[variable_key] += self.TEAMS_TO_SPLIT_BY + st.experimental_rerun() diff --git a/src/page_managers/match_manager.py b/src/page_managers/match_manager.py index ce76715..5882005 100644 --- a/src/page_managers/match_manager.py +++ b/src/page_managers/match_manager.py @@ -58,12 +58,12 @@ def generate_input_section(self) -> list[list, list]: # Filter through matches where the selected team plays in. match_schedule = match_schedule[ match_schedule["red_alliance"] - .apply(lambda alliance: ",".join(map(str, alliance))) - .str.contains(filter_by_team_number) + .apply(lambda alliance: ",".join(map(str, alliance))) + .str.contains(filter_by_team_number) | match_schedule["blue_alliance"] - .apply(lambda alliance: ",".join(map(str, alliance))) - .str.contains(filter_by_team_number) - ] + .apply(lambda alliance: ",".join(map(str, alliance))) + .str.contains(filter_by_team_number) + ] match_chosen = match_selector_col.selectbox( "Choose Match", match_schedule["match_key"] @@ -128,7 +128,7 @@ def generate_hypothetical_input_section(self) -> list[list, list]: ] def generate_match_prediction_dashboard( - self, red_alliance: list[int], blue_alliance: list[int] + self, red_alliance: list[int], blue_alliance: list[int] ) -> None: """Generates metrics for match predictions (Red vs. Blue Tab). @@ -152,13 +152,13 @@ def generate_match_prediction_dashboard( # Calculate mean and standard deviation of the point distribution of the red alliance. red_alliance_std = ( - sum( - [ - np.std(team_distribution) ** 2 - for team_distribution in red_alliance_points - ] - ) - ** 0.5 + sum( + [ + np.std(team_distribution) ** 2 + for team_distribution in red_alliance_points + ] + ) + ** 0.5 ) red_alliance_mean = sum( [ @@ -169,13 +169,13 @@ def generate_match_prediction_dashboard( # Calculate mean and standard deviation of the point distribution of the blue alliance. blue_alliance_std = ( - sum( - [ - np.std(team_distribution) ** 2 - for team_distribution in blue_alliance_points - ] - ) - ** 0.5 + sum( + [ + np.std(team_distribution) ** 2 + for team_distribution in blue_alliance_points + ] + ) + ** 0.5 ) blue_alliance_mean = sum( [ @@ -185,7 +185,7 @@ def generate_match_prediction_dashboard( ) # Calculate mean and standard deviation of the point distribution of red alliance - blue alliance - compared_std = (red_alliance_std**2 + blue_alliance_std**2) ** 0.5 + compared_std = (red_alliance_std ** 2 + blue_alliance_std ** 2) ** 0.5 compared_mean = red_alliance_mean - blue_alliance_mean # Use sentinel value if there isn't enough of a distribution yet to determine standard deviation. @@ -253,12 +253,12 @@ def generate_match_prediction_dashboard( [ ( team, - average_points_contributed[idx], - scouting_data_for_team(team)["DriverRating"].mean(), + self.calculated_stats.average_driver_rating(team), + self.calculated_stats.average_counter_defense_skill(team) ) for idx, team in enumerate(red_alliance) ], - key=lambda info: (info[1] / info[2], 5 - info[2]), + key=lambda info: info[1] / info[2], )[-1][0] alliance_breakdown( @@ -277,12 +277,12 @@ def generate_match_prediction_dashboard( [ ( team, - average_points_contributed[idx], - scouting_data_for_team(team)["DriverRating"].mean(), + self.calculated_stats.average_driver_rating(team), + self.calculated_stats.average_counter_defense_skill(team) ) for idx, team in enumerate(blue_alliance) ], - key=lambda info: (info[1] / info[2], 5 - info[2]), + key=lambda info: info[1] / info[2], )[-1][0] alliance_breakdown( @@ -293,7 +293,7 @@ def generate_match_prediction_dashboard( ) def generate_match_prediction_graphs( - self, red_alliance: list[int], blue_alliance: list[int], type_of_graph: str + self, red_alliance: list[int], blue_alliance: list[int], type_of_graph: str ) -> None: """Generate graphs for match prediction (Red vs. Blue tab). @@ -305,32 +305,37 @@ def generate_match_prediction_graphs( display_cycle_contributions = type_of_graph == GraphType.CYCLE_CONTRIBUTIONS color_sequence = ["#781212", "#163ba1"] # Bright red # Bright blue - game_piece_breakdown_col, auto_cycles_col = st.columns(2) + structure_breakdown_col, auto_cycles_col = st.columns(2) teleop_cycles_col, cumulative_cycles_col = st.columns(2) - # Breaks down game pieces between cones/cubes among the six teams - with game_piece_breakdown_col: - game_piece_breakdown = [ + # Breaks down where the different teams scored among the six teams + with structure_breakdown_col: + structure_breakdown = [ [ - self.calculated_stats.cycles_by_game_piece_per_match( - team, Queries.TELEOP_GRID, game_piece + self.calculated_stats.cycles_by_structure_per_match( + team, structures ).sum() for team in combined_teams ] - for game_piece in (Queries.CONE, Queries.CUBE) + for structures in ( + (Queries.AUTO_AMP, Queries.TELEOP_AMP), + (Queries.AUTO_SPEAKER, Queries.TELEOP_SPEAKER), + Queries.TELEOP_TRAP + ) ] plotly_chart( stacked_bar_graph( combined_teams, - game_piece_breakdown, + structure_breakdown, "Teams", - ["Total # of Cones Scored", "Total # of Cubes Scored"], - "Total Game Pieces Scored", - title="Game Piece Breakdown", + ["# of Amp Cycles", "# of Speaker Cycles", "# of Trap Cycles"], + "Total Cycles Scored into Structures", + title="Structure Breakdown", color_map={ - "Total # of Cones Scored": GeneralConstants.CONE_COLOR, # Cone color - "Total # of Cubes Scored": GeneralConstants.CUBE_COLOR, # Cube color + "# of Amp Cycles": GeneralConstants.GOLD_GRADIENT[0], + "# of Speaker Cycles": GeneralConstants.GOLD_GRADIENT[1], + "# of Trap Cycles": GeneralConstants.GOLD_GRADIENT[2] }, ).update_layout(xaxis={"categoryorder": "total descending"}) ) @@ -342,10 +347,10 @@ def generate_match_prediction_graphs( for alliance in (red_alliance, blue_alliance): cycles_in_alliance = [ ( - self.calculated_stats.cycles_by_match(team, Queries.AUTO_GRID) + self.calculated_stats.cycles_by_match(team, Queries.AUTO) if display_cycle_contributions else self.calculated_stats.points_contributed_by_match( - team, Queries.AUTO_GRID + team, Queries.AUTO ) ) for team in alliance @@ -361,12 +366,12 @@ def generate_match_prediction_graphs( ["Red Alliance", "Blue Alliance"], auto_alliance_distributions, y_axis_label=( - "Cycles" + "Notes Scored" if display_cycle_contributions else "Points Contributed" ), title=( - f"Cycles During Autonomous (N={len(auto_alliance_distributions[0])})" + f"Notes During Autonomous (N={len(auto_alliance_distributions[0])})" if display_cycle_contributions else f"Points Contributed During Autonomous (N={len(auto_alliance_distributions[0])})" ), @@ -381,10 +386,10 @@ def generate_match_prediction_graphs( for alliance in (red_alliance, blue_alliance): cycles_in_alliance = [ ( - self.calculated_stats.cycles_by_match(team, Queries.TELEOP_GRID) + self.calculated_stats.cycles_by_match(team, Queries.TELEOP) if display_cycle_contributions else self.calculated_stats.points_contributed_by_match( - team, Queries.TELEOP_GRID + team, Queries.TELEOP ) ) for team in alliance @@ -400,12 +405,12 @@ def generate_match_prediction_graphs( ["Red Alliance", "Blue Alliance"], teleop_alliance_distributions, y_axis_label=( - "Cycles" + "Notes Scored" if display_cycle_contributions else "Points Contributed" ), title=( - f"Cycles During Teleop (N={len(teleop_alliance_distributions[0])})" + f"Notes During Teleop (N={len(teleop_alliance_distributions[0])})" if display_cycle_contributions else f"Points Contributed During Teleop (N={len(teleop_alliance_distributions[0])})" ), @@ -427,12 +432,12 @@ def generate_match_prediction_graphs( ["Red Alliance", "Blue Alliance"], cumulative_alliance_distributions, y_axis_label=( - "Cycles" + "Notes Scored" if display_cycle_contributions else "Points Contributed" ), title=( - f"Cycles During Auto + Teleop (N={len(cumulative_alliance_distributions[0])})" + f"Notes During Auto + Teleop (N={len(cumulative_alliance_distributions[0])})" if display_cycle_contributions else f"Points Contributed During Auto + Teleop (N={len(cumulative_alliance_distributions[0])})" ), @@ -447,30 +452,7 @@ def generate_alliance_dashboard(self, team_numbers: list[int], color_gradient: l :param color_gradient: The color gradient to use for graphs, depending on the alliance. :return: """ - if self.pit_scouting_data is not None: - fastest_cycler_col, second_fastest_cycler_col, slowest_cycler_col, tolerance_col = st.columns(4) - - # Colored metric that displays the tolerance when engaging on the charge station. - with tolerance_col: - total_width = 0 - - for team in team_numbers: - try: - total_width += self.pit_scouting_data[ - self.pit_scouting_data["Team Number"] == team - ].iloc[0]["Drivetrain Width"] / 12 - except IndexError: - print(f"{team} has no pit scouting data.") # For debugging purposes when looking at logs. - - colored_metric( - "Tolerance When Engaging (ft.)", - f"{GeneralConstants.CHARGE_STATION_LENGTH - total_width:.1f}", - background_color=color_gradient[3], - opacity=0.4, - border_opacity=0.9 - ) - else: - fastest_cycler_col, second_fastest_cycler_col, slowest_cycler_col = st.columns(3) + fastest_cycler_col, second_fastest_cycler_col, slowest_cycler_col, reaches_coop_col = st.columns(4) fastest_cyclers = sorted( { @@ -510,11 +492,24 @@ def generate_alliance_dashboard(self, team_numbers: list[int], color_gradient: l border_opacity=0.9 ) + # Colored metric displaying the chance of reaching the co-op bonus (1 amp cycle in 45 seconds + auto) + with reaches_coop_col: + coop_by_match = [self.calculated_stats.reaches_coop_bonus_by_match(team) for team in team_numbers] + possible_coop_combos = self.calculated_stats.cartesian_product(*coop_by_match) + + colored_metric( + "Chance of Co-Op Bonus", + f"{len([combo for combo in possible_coop_combos if any(combo)]) / len(possible_coop_combos):.0%}", + background_color=color_gradient[3], + opacity=0.4, + border_opacity=0.9 + ) + def generate_autonomous_graphs( - self, - team_numbers: list[int], - type_of_graph: str, - color_gradient: list[str] + self, + team_numbers: list[int], + type_of_graph: str, + color_gradient: list[str] ) -> None: """Generates the autonomous graphs for the `Match` page. @@ -525,324 +520,271 @@ def generate_autonomous_graphs( """ display_cycle_contributions = type_of_graph == GraphType.CYCLE_CONTRIBUTIONS - auto_configuration_col, auto_engage_stats_col = st.columns(2) - auto_cycle_distribution_col, auto_cycles_over_time = st.columns(2) - - # Determine the best auto configuration for an alliance. - with auto_configuration_col: - teams_sorted_by_point_contribution = dict( - sorted( - { - team: ( - self.calculated_stats.cycles_by_match(team, Queries.AUTO_GRID) - if display_cycle_contributions - else self.calculated_stats.points_contributed_by_match(team, Queries.AUTO_GRID) - ) - for team in team_numbers - }.items(), - key=lambda pair: pair[1].max(), - reverse=True - ) - ) - - # Y values of plot - points_by_grid = {} - full_grid = [Queries.LEFT, Queries.COOP, Queries.RIGHT] - grids_occupied = set() + best_auto_config_col, auto_cycles_breakdown_col = st.columns(2, gap="large") - for team, point_contributions in teams_sorted_by_point_contribution.items(): - grid_placements = self.calculated_stats.classify_autos_by_match(team) - autos_sorted = sorted( - zip(point_contributions, grid_placements), - key=lambda pair: pair[0], + # Best auto configuration graph + with best_auto_config_col: + if display_cycle_contributions: + best_autos_by_team = sorted( + [ + (team_number, self.calculated_stats.cycles_by_match(team_number, Queries.AUTO).max()) + for team_number in team_numbers + ], + key=lambda pair: pair[1], reverse=True ) - - for auto_pointage, grid in autos_sorted: - if grid not in grids_occupied: - points_by_grid[team] = (auto_pointage, grid) - grids_occupied.add(grid) - break - else: - # Add a placeholder in the worst-case scenario - placeholder_grid = next(iter(set(full_grid).difference(grids_occupied))) - points_by_grid[team] = (point_contributions.max(), placeholder_grid) - grids_occupied.add(placeholder_grid) - - # Sort points by grid in order to go from left to right (left, coop, right). - points_by_grid = dict( - sorted( - points_by_grid.items(), - key=lambda pair: full_grid.index(pair[1][1]) + else: + best_autos_by_team = sorted( + [ + ( + team_number, self.calculated_stats.points_contributed_by_match(team_number, Queries.AUTO).max()) + for team_number in team_numbers + ], + key=lambda pair: pair[1], + reverse=True ) - ) plotly_chart( bar_graph( - list(points_by_grid.keys()), - [value[0] for value in points_by_grid.values()], - x_axis_label="Teams (Left, Coop, Right)", + [pair[0] for pair in best_autos_by_team], + [pair[1] for pair in best_autos_by_team], + x_axis_label="Teams", y_axis_label=( - "Cycles in Auto" + "# of Cycles in Auto" if display_cycle_contributions - else "Points Scored in Auto" + else "# of Points in Auto" ), title="Best Auto Configuration", color=color_gradient[1] ) ) - # Determine the accuracy of teams when it comes to engaging onto the charge station - with auto_engage_stats_col: - successful_engages_by_team = [ - self.calculated_stats.cumulative_stat( - team, - Queries.AUTO_CHARGING_STATE, - Criteria.SUCCESSFUL_ENGAGE_CRITERIA - ) - for team in team_numbers - ] - successful_docks_by_team = [ - self.calculated_stats.cumulative_stat( - team, - Queries.AUTO_CHARGING_STATE, - Criteria.SUCCESSFUL_DOCK_CRITERIA - ) - for team in team_numbers - ] - missed_attempts_by_team = [ - self.calculated_stats.cumulative_stat( - team, - Queries.AUTO_ENGAGE_ATTEMPTED, - Criteria.AUTO_ATTEMPT_CRITERIA - ) - successful_docks_by_team[idx] - successful_engages_by_team[idx] - for idx, team in enumerate(team_numbers) - ] + # Auto cycle breakdown graph + with auto_cycles_breakdown_col: + if display_cycle_contributions: + average_speaker_cycles_by_team = [ + self.calculated_stats.average_cycles_for_structure(team, Queries.AUTO_SPEAKER) + for team in team_numbers + ] + average_amp_cycles_by_team = [ + self.calculated_stats.average_cycles_for_structure(team, Queries.AUTO_AMP) + for team in team_numbers + ] + else: + average_speaker_cycles_by_team = [ + self.calculated_stats.average_cycles_for_structure(team, Queries.AUTO_SPEAKER) * 5 + for team in team_numbers + ] + average_amp_cycles_by_team = [ + self.calculated_stats.average_cycles_for_structure(team, Queries.AUTO_AMP) * 2 + for team in team_numbers + ] plotly_chart( stacked_bar_graph( team_numbers, - [missed_attempts_by_team, successful_docks_by_team, successful_engages_by_team], - x_axis_label="Teams", - y_axis_label=["# of Missed Engages", "# of Docks", "# of Engages"], - y_axis_title="", - color_map=dict( - zip( - ["# of Missed Engages", "# of Docks", "# of Engages"], - color_gradient - ) - ), - title="Auto Engage Stats" - ) + [average_speaker_cycles_by_team, average_amp_cycles_by_team], + "Teams", + [ + ("Avg. Speaker Cycles" if display_cycle_contributions else "Avg. Speaker Points"), + ("Avg. Amp Cycles" if display_cycle_contributions else "Avg. Amp Points") + ], + ("Total Auto Cycles" if display_cycle_contributions else "Total Auto Points"), + title="Auto Scoring Breakdown", + color_map={ + ("Avg. Speaker Cycles" if display_cycle_contributions else "Avg. Speaker Points"): color_gradient[1], + ("Avg. Amp Cycles" if display_cycle_contributions else "Avg. Amp Points"): color_gradient[2] + } + ).update_layout(xaxis={"categoryorder": "total descending"}) ) - # Box plot showing the distribution of cycles - with auto_cycle_distribution_col: + def generate_teleop_graphs( + self, + team_numbers: list[int], + type_of_graph: str, + color_gradient: list[str] + ) -> None: + """Generates the teleop graphs for the `Match` page. + + :param team_numbers: The teams to generate the graphs for. + :param type_of_graph: The type of graph to make (cycle contributions/point contributions). + :param color_gradient: The color gradient to use for graphs, depending on the alliance. + :return: + """ + teams_data = [scouting_data_for_team(team) for team in team_numbers] + display_cycle_contributions = type_of_graph == GraphType.CYCLE_CONTRIBUTIONS + + speaker_cycles_over_time_col, amp_periods_over_time_col = st.columns(2, gap="large") + climb_breakdown_by_team_col, climb_speed_by_team = st.columns(2, gap="large") + + short_gradient = [ + GeneralConstants.LIGHT_RED, + GeneralConstants.RED_TO_GREEN_GRADIENT[2], + GeneralConstants.LIGHT_GREEN + ] + + # Display the teleop speaker cycles of each team over time + with speaker_cycles_over_time_col: cycles_by_team = [ + self.calculated_stats.cycles_by_structure_per_match(team, Queries.TELEOP_SPEAKER) * ( - self.calculated_stats.cycles_by_match(team, Queries.AUTO_GRID) - if display_cycle_contributions - else self.calculated_stats.points_contributed_by_match(team, Queries.AUTO_GRID) + 1 if display_cycle_contributions else 2 ) for team in team_numbers ] + best_teams = sorted(zip(team_numbers, cycles_by_team), key=lambda pair: pair[1].mean()) + color_map = { + pair[0]: color + for pair, color in zip(best_teams, short_gradient) + } plotly_chart( - box_plot( - team_numbers, - cycles_by_team, - x_axis_label="Teams", - y_axis_label=( + multi_line_graph( + *populate_missing_data(cycles_by_team), + x_axis_label="Match Index", + y_axis_label=team_numbers, + y_axis_title=( "# of Cycles" if display_cycle_contributions else "Points Contributed" ), title=( - "Distribution of Auto Cycles" + "Teleop Speaker Cycles Over Time" if display_cycle_contributions - else "Distribution of Points Contributed During Auto" + else "Points Contributed in the Speaker Over Time" ), - show_underlying_data=True, - color_sequence=color_gradient + color_map=color_map ) ) - # Plot cycles over time - with auto_cycles_over_time: - cycles_by_team = [ - ( - self.calculated_stats.cycles_by_match(team, Queries.AUTO_GRID) - if display_cycle_contributions - else self.calculated_stats.points_contributed_by_match(team, Queries.AUTO_GRID) - ) + # Display the teleop speaker cycles of each team over time + with amp_periods_over_time_col: + amp_periods_by_team = [ + self.calculated_stats.potential_amplification_periods_by_match(team) for team in team_numbers ] + best_teams = sorted(zip(team_numbers, amp_periods_by_team), key=lambda pair: pair[1].mean()) + color_map = { + pair[0]: color + for pair, color in zip(best_teams, short_gradient) + } plotly_chart( multi_line_graph( - *populate_missing_data(cycles_by_team), + *populate_missing_data(amp_periods_by_team), x_axis_label="Match Index", y_axis_label=team_numbers, - y_axis_title=( - "# of Cycles" - if display_cycle_contributions - else "Points Contributed" - ), - title=( - "Auto Cycles Over Time" - if display_cycle_contributions - else "Points Contributed in Auto Over Time" - ), - color_map=dict(zip(team_numbers, color_gradient)) + y_axis_title="# of Potential Amplification Periods", + title="Potential Amplification Periods Produced by Alliance", + color_map=color_map ) ) - def generate_teleop_graphs( - self, - team_numbers: list[int], - type_of_graph: str, - color_gradient: list[str] - ) -> None: - """Generates the teleop graphs for the `Match` page. - - :param team_numbers: The teams to generate the graphs for. - :param type_of_graph: The type of graph to make (cycle contributions/point contributions). - :param color_gradient: The color gradient to use for graphs, depending on the alliance. - :return: - """ - display_cycle_contributions = type_of_graph == GraphType.CYCLE_CONTRIBUTIONS - - teleop_cycles_by_level_col, teleop_game_piece_breakdown_col = st.columns(2) - teleop_cycles_over_time_col, teleop_cycles_distribution_col = st.columns(2) - - # Graph the teleop cycles per team by level (High/Mid/Low) - with teleop_cycles_by_level_col: - cycles_by_height = [] - - for height in (Queries.HIGH, Queries.MID, Queries.LOW): - cycles_by_height.append([ - self.calculated_stats.average_cycles_for_height( - team, - Queries.TELEOP_GRID, - height - ) * (1 if display_cycle_contributions else Criteria.TELEOP_GRID_POINTAGE[height]) - for team in team_numbers - ]) + with climb_breakdown_by_team_col: + harmonized_climbs_by_team = [ + team_data[Queries.HARMONIZED_ON_CHAIN].sum() + for team_data in teams_data + ] + normal_climbs_by_team = [ + team_data[Queries.CLIMBED_CHAIN].sum() - harmonized_climbs + for team_data, harmonized_climbs in zip(teams_data, harmonized_climbs_by_team) + ] plotly_chart( stacked_bar_graph( team_numbers, - cycles_by_height, + [normal_climbs_by_team, harmonized_climbs_by_team], x_axis_label="Teams", - y_axis_label=["High", "Mid", "Low"], - y_axis_title="", - color_map=dict( - zip( - ["High", "Mid", "Low"], - GeneralConstants.LEVEL_GRADIENT - ) - ), - title=( - "Average Cycles by Height" - if display_cycle_contributions - else "Average Points Contributed by Height" - ) - ).update_layout(xaxis={"categoryorder": "total descending"}) + y_axis_label=["Normal Climbs", "Harmonized Climbs"], + y_axis_title="# of Climb Types", + title="Climbs by Team", + color_map={"Normal Climbs": color_gradient[0], "Harmonized Climbs": color_gradient[1]} + ) ) - # Graph the breakdown of game pieces by each team - with teleop_game_piece_breakdown_col: - cones_scored_by_team = [ - self.calculated_stats.cycles_by_game_piece_per_match( - team, - Queries.TELEOP_GRID, - Queries.CONE - ).sum() - for team in team_numbers + with climb_speed_by_team: + slow_climbs = [ + (team_data[Queries.CLIMB_SPEED] == "Slow").sum() + for team_data in teams_data ] - cubes_scored_by_team = [ - self.calculated_stats.cycles_by_game_piece_per_match( - team, - Queries.TELEOP_GRID, - Queries.CUBE - ).sum() - for team in team_numbers + + fast_climbs = [ + (team_data[Queries.CLIMB_SPEED] == "Fast").sum() + for team_data in teams_data ] plotly_chart( stacked_bar_graph( team_numbers, - [cones_scored_by_team, cubes_scored_by_team], + [slow_climbs, fast_climbs], x_axis_label="Teams", - y_axis_label=["Total # of Cones Scored", "Total # of Cubes Scored"], - y_axis_title="", - color_map=dict( - zip( - ["Total # of Cones Scored", "Total # of Cubes Scored"], - [GeneralConstants.CONE_COLOR, GeneralConstants.CUBE_COLOR] - ) - ), - title="Game Piece Breakdown by Team" - ).update_layout(xaxis={"categoryorder": "total descending"}) + y_axis_label=["Slow Climbs", "Fast Climbs"], + y_axis_title="# of Climb Speeds", + title="Climb Speeds by Team", + color_map={"Slow Climbs": GeneralConstants.LIGHT_RED, "Fast Climbs": GeneralConstants.LIGHT_GREEN} + ) ) - # Box plot showing the distribution of cycles - with teleop_cycles_distribution_col: - cycles_by_team = [ - ( - self.calculated_stats.cycles_by_match(team, Queries.TELEOP_GRID) - if display_cycle_contributions - else self.calculated_stats.points_contributed_by_match(team, Queries.TELEOP_GRID) - ) + def generate_qualitative_graphs( + self, + team_numbers: list[int], + color_gradient: list[str] + ): + """Generates the qualitative graphs for the `Match` page. + + :param team_numbers: The teams to generate the graphs for. + :param color_gradient: The color gradient to use for graphs, depending on the alliance. + :return: + """ + driver_rating_by_team_col, defense_rating_by_team_col, disables_by_team_col = st.columns(3) + + with driver_rating_by_team_col: + driver_rating_by_team = [ + self.calculated_stats.average_driver_rating(team) for team in team_numbers ] plotly_chart( - box_plot( + bar_graph( team_numbers, - cycles_by_team, + driver_rating_by_team, x_axis_label="Teams", - y_axis_label=( - "# of Cycles" - if display_cycle_contributions - else "Points Contributed" - ), - title=( - "Distribution of Teleop Cycles" - if display_cycle_contributions - else "Distribution of Points Contributed During Teleop" - ), - show_underlying_data=True, - color_sequence=color_gradient + y_axis_label="Driver Rating (1-5)", + title="Average Driver Rating by Team", + color=color_gradient[0] ) ) + + with defense_rating_by_team_col: + defense_rating_by_team = [ + self.calculated_stats.average_defense_skill(team) + for team in team_numbers + ] - # Plot cycles over time - with teleop_cycles_over_time_col: - cycles_by_team = [ - ( - self.calculated_stats.cycles_by_match(team, Queries.TELEOP_GRID) - if display_cycle_contributions - else self.calculated_stats.points_contributed_by_match(team, Queries.TELEOP_GRID) + plotly_chart( + bar_graph( + team_numbers, + defense_rating_by_team, + x_axis_label="Teams", + y_axis_label="Defense Rating (1-5)", + title="Average Defense Rating by Team", + color=color_gradient[1] ) + ) + + with disables_by_team_col: + disables_by_team = [ + self.calculated_stats.cumulative_stat(team, Queries.DISABLE, Criteria.BOOLEAN_CRITERIA) for team in team_numbers ] plotly_chart( - multi_line_graph( - *populate_missing_data(cycles_by_team), - x_axis_label="Match Index", - y_axis_label=team_numbers, - y_axis_title=( - "# of Cycles" - if display_cycle_contributions - else "Points Contributed" - ), - title=( - "Teleop Cycles Over Time" - if display_cycle_contributions - else "Points Contributed in Teleop Over Time" - ), - color_map=dict(zip(team_numbers, color_gradient)) + bar_graph( + team_numbers, + disables_by_team, + x_axis_label="Teams", + y_axis_label="Disables", + title="Disables by Team", + color=color_gradient[2] ) ) diff --git a/src/page_managers/picklist_manager.py b/src/page_managers/picklist_manager.py index b456639..1630075 100644 --- a/src/page_managers/picklist_manager.py +++ b/src/page_managers/picklist_manager.py @@ -1,12 +1,18 @@ """Creates the `PicklistManager` class used to set up the Picklist page and its table.""" +import os from functools import partial import streamlit as st +from dotenv import load_dotenv +from notion_client import Client +from notion_client.helpers import get_id from pandas import DataFrame from .page_manager import PageManager -from utils import CalculatedStats, Queries, retrieve_scouting_data, retrieve_team_list +from utils import CalculatedStats, Criteria, EventSpecificConstants, Queries, retrieve_scouting_data, retrieve_team_list + +load_dotenv() class PicklistManager(PageManager): @@ -18,17 +24,49 @@ def __init__(self): retrieve_scouting_data() ) self.teams = retrieve_team_list() + self.client = Client(auth=os.getenv("NOTION_TOKEN")) # Requested stats is used to define the stats wanted in the picklist generation. self.requested_stats = { "Average Auto Cycles": partial( self.calculated_stats.average_cycles, - type_of_grid=Queries.AUTO_GRID + mode=Queries.AUTO ), "Average Teleop Cycles": partial( self.calculated_stats.average_cycles, - type_of_grid=Queries.TELEOP_GRID - ) + mode=Queries.TELEOP + ), + "Average Speaker Cycles": partial( + self.calculated_stats.average_cycles_for_structure, + structure=(Queries.AUTO_SPEAKER, Queries.TELEOP_SPEAKER) + ), + "Average Amp Cycles": partial( + self.calculated_stats.average_cycles_for_structure, + structure=(Queries.AUTO_AMP, Queries.TELEOP_AMP) + ), + "Average Trap Cycles": partial( + self.calculated_stats.average_cycles_for_structure, + structure=Queries.TELEOP_TRAP + ), + "# of Times Climbed": partial( + self.calculated_stats.cumulative_stat, + stat=Queries.CLIMBED_CHAIN, + criteria=Criteria.BOOLEAN_CRITERIA + ), + "# of Times Harmonized": partial( + self.calculated_stats.cumulative_stat, + stat=Queries.HARMONIZED_ON_CHAIN, + criteria=Criteria.BOOLEAN_CRITERIA + ), + "# of Disables": partial( + self.calculated_stats.cumulative_stat, + stat=Queries.DISABLE, + criteria=Criteria.BOOLEAN_CRITERIA + ), + "Average Driver Rating": self.calculated_stats.average_driver_rating, + "Average Defense Skill": self.calculated_stats.average_defense_skill, + "Average Defense Time": self.calculated_stats.average_defense_time, + "Average Counter Defense Skill": self.calculated_stats.average_counter_defense_skill } def generate_input_section(self) -> list[list, list]: @@ -59,3 +97,96 @@ def generate_picklist(self, stats_requested: list[str]) -> DataFrame: for team in self.teams ] return DataFrame.from_dict(requested_picklist) + + def write_to_notion(self, dataframe: DataFrame) -> None: + """Writes to a Notion picklist entered by the user in the constants file. + + :param dataframe: The dataframe containing all the statistics of each team. + :return: + """ + # Generate Notion Database first + properties = { + "Team Name": {"title": {}} + } | { + column: {"number": {}} for column in dataframe.columns if column != "Team Number" + } + icon = {"type": "emoji", "emoji": "🗒️"} + self.client.databases.update( + database_id=(db_id := get_id(EventSpecificConstants.PICKLIST_URL)), properties=properties, icon=icon + ) + + # Find percentiles across all teams + percentile_75 = self.calculated_stats.quantile_stat( + 0.75, + lambda self_, team: self_.average_cycles(team) + ) + percentile_50 = self.calculated_stats.quantile_stat( + 0.5, + lambda self_, team: self_.average_cycles(team) + ) + percentile_25 = self.calculated_stats.quantile_stat( + 0.25, + lambda self_, team: self_.average_cycles(team) + ) + + for _, row in dataframe.iterrows(): + team_name = row["Team Number"] + query_page = self.client.databases.query( + database_id=db_id, + filter={ + "property": "Team Name", + "title": { + "contains": team_name, + }, + } + ) + # Based off of the percentile between all their stats + team_number = int(team_name.split()[1]) + team_cycles = self.calculated_stats.average_cycles(team_number) + + if team_cycles > percentile_75: + emoji = "🔵" + elif percentile_50 <= team_cycles < percentile_75: + emoji = "🟢" + elif percentile_25 <= team_cycles < percentile_50: + emoji = "🟠" + else: + emoji = "🔴" + + # No page created yet. + if not query_page["results"]: + self.client.pages.create( + database_id=db_id, + icon={"type": "emoji", "emoji": emoji}, + parent={"type": "database_id", "database_id": db_id}, + properties={ + column: { + "number": dataframe[dataframe["Team Number"] == team_name][column].iloc[0] + } for column in dataframe.columns if column != "Team Number" + } | { + "Team Name": {"id": "title", "title": [{"text": {"content": team_name}}]}, + }, + children=[ + { + "object": "block", + "type": "embed", + "embed": { + "url": f"https://falconvis-{EventSpecificConstants.EVENT_CODE[-3:]}.streamlit.app?team_number={team_number}" + } + } + ] + ) + # Page already created + else: + self.client.pages.update( + page_id=query_page["results"][0]["id"], + icon={"type": "emoji", "emoji": emoji}, + parent={"type": "database_id", "database_id": db_id}, + properties={ + column: { + "number": dataframe[dataframe["Team Number"] == team_name][column].iloc[0] + } for column in dataframe.columns if column != "Team Number" + } | { + "Team Name": {"id": "title", "title": [{"text": {"content": team_name}}]}, + } + ) diff --git a/src/page_managers/team_manager.py b/src/page_managers/team_manager.py index ffb461b..0380aec 100644 --- a/src/page_managers/team_manager.py +++ b/src/page_managers/team_manager.py @@ -1,6 +1,9 @@ """Creates the `TeamManager` class used to set up the Teams page and its graphs.""" +import re import streamlit as st +from annotated_text import annotated_text +from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer from .contains_metrics import ContainsMetrics from .page_manager import PageManager @@ -13,13 +16,16 @@ GeneralConstants, GraphType, line_graph, + multi_line_graph, plotly_chart, Queries, retrieve_team_list, retrieve_pit_scouting_data, retrieve_scouting_data, scouting_data_for_team, - stacked_bar_graph + stacked_bar_graph, + colored_metric_with_two_values, + populate_missing_data ) @@ -39,9 +45,11 @@ def generate_input_section(self) -> int: :return: The team number selected to create graphs for. """ + queried_team = int(st.experimental_get_query_params().get("team_number", [0])[0]) return st.selectbox( "Team Number", - retrieve_team_list() + (team_list := retrieve_team_list()), + index=team_list.index(queried_team) if queried_team in team_list else 0 ) def generate_metrics(self, team_number: int) -> None: @@ -50,7 +58,7 @@ def generate_metrics(self, team_number: int) -> None: :param team_number: The team number to calculate the metrics for. """ points_contributed_col, drivetrain_col, auto_cycle_col, teleop_cycle_col = st.columns(4) - iqr_col, auto_engage_col, auto_engage_accuracy_col, auto_accuracy_col = st.columns(4) + iqr_col, trap_ability_col, climb_breakdown_col, disables_col = st.columns(4) # Metric for avg. points contributed with points_contributed_col: @@ -73,7 +81,7 @@ def generate_metrics(self, team_number: int) -> None: drivetrain = self.pit_scouting_data[ self.pit_scouting_data["Team Number"] == team_number ].iloc[0]["Drivetrain"].split("/")[0] # The splitting at / is used to shorten the drivetrain type. - except IndexError: + except (IndexError, TypeError): drivetrain = "—" colored_metric( @@ -85,34 +93,58 @@ def generate_metrics(self, team_number: int) -> None: # Metric for average auto cycles with auto_cycle_col: - average_auto_cycles = self.calculated_stats.average_cycles( + average_auto_speaker_cycles = self.calculated_stats.average_cycles_for_structure( team_number, - Queries.AUTO_GRID + Queries.AUTO_SPEAKER ) - auto_cycles_for_percentile = self.calculated_stats.quantile_stat( + average_auto_amp_cycles = self.calculated_stats.average_cycles_for_structure( + team_number, + Queries.AUTO_AMP + ) + average_auto_speaker_cycles_for_percentile = self.calculated_stats.quantile_stat( 0.5, - lambda self, team: self.average_cycles(team, Queries.AUTO_GRID) + lambda self, team: self.average_cycles_for_structure(team, Queries.AUTO_SPEAKER) ) - colored_metric( + average_auto_amp_cycles_for_percentile = self.calculated_stats.quantile_stat( + 0.5, + lambda self, team: self.average_cycles_for_structure(team, Queries.AUTO_AMP) + ) + + colored_metric_with_two_values( "Average Auto Cycles", - round(average_auto_cycles, 2), - threshold=auto_cycles_for_percentile + "Speaker / Amp", + round(average_auto_speaker_cycles, 2), + round(average_auto_amp_cycles, 2), + first_threshold=average_auto_speaker_cycles_for_percentile, + second_threshold=average_auto_amp_cycles_for_percentile ) # Metric for average teleop cycles with teleop_cycle_col: - average_teleop_cycles = self.calculated_stats.average_cycles( + average_teleop_speaker_cycles = self.calculated_stats.average_cycles_for_structure( + team_number, + Queries.TELEOP_SPEAKER + ) + average_teleop_amp_cycles = self.calculated_stats.average_cycles_for_structure( team_number, - Queries.TELEOP_GRID + Queries.TELEOP_AMP ) - teleop_cycles_for_percentile = self.calculated_stats.quantile_stat( + average_teleop_speaker_cycles_for_percentile = self.calculated_stats.quantile_stat( 0.5, - lambda self, team: self.average_cycles(team, Queries.TELEOP_GRID) + lambda self, team: self.average_cycles_for_structure(team, Queries.TELEOP_SPEAKER) ) - colored_metric( + average_teleop_amp_cycles_for_percentile = self.calculated_stats.quantile_stat( + 0.5, + lambda self, team: self.average_cycles_for_structure(team, Queries.TELEOP_AMP) + ) + + colored_metric_with_two_values( "Average Teleop Cycles", - round(average_teleop_cycles, 2), - threshold=teleop_cycles_for_percentile + "Speaker / Amp", + round(average_teleop_speaker_cycles, 2), + round(average_teleop_amp_cycles, 2), + first_threshold=average_teleop_speaker_cycles_for_percentile, + second_threshold=average_teleop_amp_cycles_for_percentile ) # Metric for IQR of points contributed (consistency) @@ -135,61 +167,68 @@ def generate_metrics(self, team_number: int) -> None: invert_threshold=True ) - # Metric for total auto engage attempts - with auto_engage_col: - total_auto_engage_attempts = self.calculated_stats.cumulative_stat( + # Metric for ability to score trap + with trap_ability_col: + average_trap_cycles = self.calculated_stats.average_stat( team_number, - Queries.AUTO_ENGAGE_ATTEMPTED, - Criteria.AUTO_ATTEMPT_CRITERIA + Queries.TELEOP_TRAP, + Criteria.BOOLEAN_CRITERIA ) - auto_engage_attempts_for_percentile = self.calculated_stats.quantile_stat( - 0.5, - lambda self, team: self.cumulative_stat( - team, - Queries.AUTO_ENGAGE_ATTEMPTED, - Criteria.AUTO_ATTEMPT_CRITERIA - ) + colored_metric( + "Can they score in the trap?", + average_trap_cycles, + threshold=0.01, + value_formatter=lambda value: "Yes" if value > 0 else "No" ) - colored_metric( - "Auto Engage Attempts", - total_auto_engage_attempts, - threshold=auto_engage_attempts_for_percentile + # Metric for total times climbed and total harmonizes + with climb_breakdown_col: + times_climbed = self.calculated_stats.cumulative_stat( + team_number, + Queries.CLIMBED_CHAIN, + Criteria.BOOLEAN_CRITERIA + ) + times_climbed_for_percentile = self.calculated_stats.quantile_stat( + 0.5, + lambda self, team: self.cumulative_stat(team, Queries.CLIMBED_CHAIN, Criteria.BOOLEAN_CRITERIA) ) - # Metric for auto engage accuracy - with auto_engage_accuracy_col: - total_successful_engages = self.calculated_stats.cumulative_stat( + times_harmonized = self.calculated_stats.cumulative_stat( team_number, - Queries.AUTO_CHARGING_STATE, - Criteria.SUCCESSFUL_ENGAGE_CRITERIA + Queries.HARMONIZED_ON_CHAIN, + Criteria.BOOLEAN_CRITERIA ) - auto_engage_accuracy = ( - total_successful_engages / total_auto_engage_attempts - if total_auto_engage_attempts - else 0.0 + times_harmonized_for_percentile = self.calculated_stats.quantile_stat( + 0.5, + lambda self, team: self.cumulative_stat(team, Queries.HARMONIZED_ON_CHAIN, Criteria.BOOLEAN_CRITERIA) ) - colored_metric( - "Auto Engage Accuracy", - auto_engage_accuracy, - threshold=0.75, - value_formatter=lambda value: f"{value:.1%}" + colored_metric_with_two_values( + "Climb Breakdown", + "# of Times Climbed/Harmonized", + times_climbed, + times_harmonized, + first_threshold=times_climbed_for_percentile, + second_threshold=times_harmonized_for_percentile ) - # Metric for average auto accuracy by match - with auto_accuracy_col: - average_auto_accuracy = self.calculated_stats.average_auto_accuracy(team_number) - auto_accuracy_for_percentile = self.calculated_stats.quantile_stat( + # Metric for number of disables + with disables_col: + times_disabled = self.calculated_stats.cumulative_stat( + team_number, + Queries.DISABLE, + Criteria.BOOLEAN_CRITERIA + ) + times_disabled_for_percentile = self.calculated_stats.quantile_stat( 0.5, - lambda self, team: self.average_auto_accuracy(team) + lambda self, team: self.cumulative_stat(team, Queries.DISABLE, Criteria.BOOLEAN_CRITERIA) ) colored_metric( - "Average Auto Accuracy (%)", - average_auto_accuracy, - threshold=auto_accuracy_for_percentile, - value_formatter=lambda value: f"{value:.1%}" + "# of Times Disabled", + times_disabled, + threshold=times_disabled_for_percentile, + invert_threshold=True ) def generate_autonomous_graphs( @@ -203,64 +242,50 @@ def generate_autonomous_graphs( :param type_of_graph: The type of graph to use for the graphs on said page (cycle contribution / point contributions). :return: """ - team_data = scouting_data_for_team(team_number) using_cycle_contributions = type_of_graph == GraphType.CYCLE_CONTRIBUTIONS - auto_cycles_over_time_col, auto_engage_stats_col = st.columns(2) - - # Graph for auto cycles over time - with auto_cycles_over_time_col: - auto_cycles_over_time = ( - self.calculated_stats.cycles_by_match(team_number, Queries.AUTO_GRID) - if using_cycle_contributions - else self.calculated_stats.points_contributed_by_match(team_number, Queries.AUTO_GRID) - ) - - plotly_chart( - line_graph( - x=team_data[Queries.MATCH_KEY], - y=auto_cycles_over_time, - x_axis_label="Match Key", - y_axis_label=( - "# of Auto Cycles" - if using_cycle_contributions - else "Points Contributed" - ), - title=( - "Auto Cycles Over Time" - if using_cycle_contributions - else "Auto Points Contributed Over Time" - ) - ) - ) + # Metric for how many times they left the starting zone + times_left_starting_zone = self.calculated_stats.cumulative_stat( + team_number, + Queries.LEFT_STARTING_ZONE, + Criteria.BOOLEAN_CRITERIA + ) + times_left_for_percentile = self.calculated_stats.quantile_stat( + 0.5, + lambda self, team: self.cumulative_stat(team, Queries.LEFT_STARTING_ZONE, Criteria.BOOLEAN_CRITERIA) + ) - # Bar graph for displaying how successful a team is at their auto engaging. - with auto_engage_stats_col: - total_successful_engages = self.calculated_stats.cumulative_stat( - team_number, - Queries.AUTO_CHARGING_STATE, - Criteria.SUCCESSFUL_ENGAGE_CRITERIA - ) - total_successful_docks = self.calculated_stats.cumulative_stat( - team_number, - Queries.AUTO_CHARGING_STATE, - {"Dock": 1} - ) - total_missed_engages = self.calculated_stats.cumulative_stat( - team_number, - Queries.AUTO_ENGAGE_ATTEMPTED, - Criteria.AUTO_ATTEMPT_CRITERIA - ) - total_successful_engages - total_successful_docks + colored_metric( + "# of Leaves from the Starting Zone", + times_left_starting_zone, + threshold=times_left_for_percentile + ) - plotly_chart( - bar_graph( - x=["# of Successful Engages", "# of Successful Docks", "# of Missed Engages"], - y=[total_successful_engages, total_successful_docks, total_missed_engages], - x_axis_label="", - y_axis_label="# of Occurences", - title="Auto Charge Station Statistics" - ) + # Auto Speaker/amp over time graph + speaker_cycles_by_match = self.calculated_stats.cycles_by_structure_per_match( + team_number, + Queries.AUTO_SPEAKER + ) * (1 if using_cycle_contributions else 5) + amp_cycles_by_match = self.calculated_stats.cycles_by_structure_per_match( + team_number, + Queries.AUTO_AMP + ) * (1 if using_cycle_contributions else 2) + line_names = [ + ("# of Speaker Cycles" if using_cycle_contributions else "# of Speaker Points"), + ("# of Amp Cycles" if using_cycle_contributions else "# of Amp Points") + ] + + plotly_chart( + multi_line_graph( + range(len(speaker_cycles_by_match)), + [speaker_cycles_by_match, amp_cycles_by_match], + x_axis_label="Match Index", + y_axis_label=line_names, + y_axis_title=f"# of Autonomous {'Cycles' if using_cycle_contributions else 'Points'}", + title=f"Speaker/Amp {'Cycles' if using_cycle_contributions else 'Points'} During Autonomous Over Time", + color_map=dict(zip(line_names, (GeneralConstants.GOLD_GRADIENT[0], GeneralConstants.GOLD_GRADIENT[-1]))) ) + ) def generate_teleop_graphs( self, @@ -273,96 +298,214 @@ def generate_teleop_graphs( :param type_of_graph: The type of graph to use for the graphs on said page (cycle contribution / point contributions). :return: """ + times_climbed_col, times_harmonized_col = st.columns(2) + speaker_amp_col, climb_speed_col = st.columns(2) + + team_data = scouting_data_for_team(team_number) using_cycle_contributions = type_of_graph == GraphType.CYCLE_CONTRIBUTIONS - cycles_by_height_col, teleop_cycles_over_time_col, breakdown_cycles_col = st.columns(3) - - # Bar graph for displaying average # of cycles per height - with cycles_by_height_col: - cycles_for_low = self.calculated_stats.average_cycles_for_height( + # Teleop Speaker/amp over time graph + with speaker_amp_col: + speaker_cycles_by_match = self.calculated_stats.cycles_by_structure_per_match( team_number, - Queries.TELEOP_GRID, - Queries.LOW + Queries.TELEOP_SPEAKER + ) * (1 if using_cycle_contributions else 5) + amp_cycles_by_match = self.calculated_stats.cycles_by_structure_per_match( + team_number, + Queries.TELEOP_AMP ) * (1 if using_cycle_contributions else 2) - cycles_for_mid = self.calculated_stats.average_cycles_for_height( + line_names = [ + ("# of Speaker Cycles" if using_cycle_contributions else "# of Speaker Points"), + ("# of Amp Cycles" if using_cycle_contributions else "# of Amp Points") + ] + + plotly_chart( + multi_line_graph( + range(len(speaker_cycles_by_match)), + [speaker_cycles_by_match, amp_cycles_by_match], + x_axis_label="Match Index", + y_axis_label=line_names, + y_axis_title=f"# of Teleop {'Cycles' if using_cycle_contributions else 'Points'}", + title=f"Speaker/Amp {'Cycles' if using_cycle_contributions else 'Points'} During Teleop Over Time", + color_map=dict(zip(line_names, (GeneralConstants.GOLD_GRADIENT[0], GeneralConstants.GOLD_GRADIENT[-1]))) + ) + ) + + # Climb speed over time graph + with climb_speed_col: + slow_climbs = self.calculated_stats.cumulative_stat( team_number, - Queries.TELEOP_GRID, - Queries.MID - ) * (1 if using_cycle_contributions else 3) - cycles_for_high = self.calculated_stats.average_cycles_for_height( + Queries.CLIMB_SPEED, + {"Slow": 1} + ) + fast_climbs = self.calculated_stats.cumulative_stat( team_number, - Queries.TELEOP_GRID, - Queries.HIGH - ) * (1 if using_cycle_contributions else 5) + Queries.CLIMB_SPEED, + {"Fast": 1} + ) plotly_chart( bar_graph( - x=["Hybrid Avr.", "Mid Avr.", "High Avr."], - y=[cycles_for_low, cycles_for_mid, cycles_for_high], - x_axis_label="Node Height", - y_axis_label=( - "Average # of Teleop Cycles" - if using_cycle_contributions - else "Average Pts. Contributed" - ), - title=( - "Average # of Teleop Cycles by Height" - if using_cycle_contributions - else "Average Pts. Contributed by Height" - ) + ["Slow Climbs", "Fast Climbs"], + [slow_climbs, fast_climbs], + x_axis_label="Type of Climb", + y_axis_label="# of Climbs", + title=f"Climb Speed Breakdown", + color={"Slow Climbs": GeneralConstants.LIGHT_RED, "Fast Climbs": GeneralConstants.LIGHT_GREEN}, + color_indicator="Type of Climb" ) ) - # Graph for teleop cycles over time - with teleop_cycles_over_time_col: - teleop_cycles_over_time = ( - self.calculated_stats.cycles_by_match(team_number, Queries.TELEOP_GRID) - if using_cycle_contributions - else self.calculated_stats.points_contributed_by_match(team_number, Queries.TELEOP_GRID) - ) + def generate_qualitative_graphs(self, team_number: int) -> None: + """Generates the qualitative graphs for the `Team` page. - plotly_chart( - line_graph( - x=team_data[Queries.MATCH_KEY], - y=teleop_cycles_over_time, - x_axis_label="Match Key", - y_axis_label=( - "# of Teleop Cycles" - if using_cycle_contributions - else "Points Contributed" - ), - title=( - "Teleop Cycles Over Time" - if using_cycle_contributions - else "Teleop Points Contributed Over Time" + :param team_number: The team to generate the graphs for. + :return: + """ + # Constants used for the sentiment analysis + ml_weight = 1 + estimate_weight = 1 + + sentiment = SentimentIntensityAnalyzer() + positivity_scores = [] + scouting_data = scouting_data_for_team(team_number) + + # Split into two tabs + qualitative_graphs_tab, note_scouting_analysis_tab = st.tabs( + ["📊 Qualitative Graphs", "✏️ Note Scouting Analysis"] + ) + + with qualitative_graphs_tab: + driver_rating_col, defense_skill_col, counter_defense_skill = st.columns(3) + + with driver_rating_col: + driver_rating_types = Criteria.DRIVER_RATING_CRITERIA.keys() + driver_rating_by_type = [ + self.calculated_stats.cumulative_stat(team_number, Queries.DRIVER_RATING, {driver_rating_type: 1}) + for driver_rating_type in driver_rating_types + ] + + plotly_chart( + bar_graph( + driver_rating_types, + driver_rating_by_type, + x_axis_label="Driver Rating", + y_axis_label="# of Occurrences", + title="Driver Rating Breakdown", + color=dict(zip(driver_rating_types, GeneralConstants.RED_TO_GREEN_GRADIENT[::-1])), + color_indicator="Driver Rating" ) ) - ) - # Stacked bar graph displaying the breakdown of cones and cubes in Teleop - with breakdown_cycles_col: - total_cones_scored = self.calculated_stats.cycles_by_game_piece_per_match( - team_number, - Queries.TELEOP_GRID, - Queries.CONE - ).sum() - total_cubes_scored = self.calculated_stats.cycles_by_game_piece_per_match( - team_number, - Queries.TELEOP_GRID, - Queries.CUBE - ).sum() + with defense_skill_col: + defense_skill_types = Criteria.BASIC_RATING_CRITERIA.keys() + defense_skill_by_type = [ + self.calculated_stats.cumulative_stat(team_number, Queries.DEFENSE_SKILL, {defense_skill_type: 1}) + for defense_skill_type in defense_skill_types + ] + + plotly_chart( + bar_graph( + defense_skill_types, + defense_skill_by_type, + x_axis_label="Defense Skill", + y_axis_label="# of Occurrences", + title="Defense Skill Breakdown", + color=dict(zip(defense_skill_types, GeneralConstants.RED_TO_GREEN_GRADIENT[::-1])), + color_indicator="Defense Skill" + ) + ) - plotly_chart( - stacked_bar_graph( - x=[str(team_number)], - y=[[total_cones_scored], [total_cubes_scored]], - x_axis_label="Team Number", - y_axis_label=["Total # of Cones Scored", "Total # of Cubes Scored"], - title="Game Piece Breakdown", - color_map={ - "Total # of Cones Scored": GeneralConstants.CONE_COLOR, # Cone color - "Total # of Cubes Scored": GeneralConstants.CUBE_COLOR # Cube color - } + with counter_defense_skill: + counter_defense_skill_types = Criteria.BASIC_RATING_CRITERIA.keys() + counter_defense_skill_by_type = [ + self.calculated_stats.cumulative_stat( + team_number, + Queries.COUNTER_DEFENSE_SKIll, + {counter_defense_skill_type: 1} + ) + for counter_defense_skill_type in defense_skill_types + ] + + plotly_chart( + bar_graph( + counter_defense_skill_types, + counter_defense_skill_by_type, + x_axis_label="Counter Defense Skill", + y_axis_label="# of Occurrences", + title="Counter Defense Skill Breakdown", + color=dict(zip(counter_defense_skill_types, GeneralConstants.RED_TO_GREEN_GRADIENT[::-1])), + color_indicator="Counter Defense Skill" + ) + ) + + with note_scouting_analysis_tab: + notes_col, metrics_col = st.columns(2, gap="medium") + notes_by_match = dict( + zip( + scouting_data[Queries.MATCH_KEY], + ( + scouting_data[Queries.AUTO_NOTES].apply(lambda note: (note + " ").lower() if note else "") + + scouting_data[Queries.TELEOP_NOTES].apply( + lambda note: (note + " ").lower() if note else "") + + scouting_data[Queries.ENDGAME_NOTES].apply( + lambda note: (note + " ").lower() if note else "") + + scouting_data[Queries.RATING_NOTES].apply(lambda note: note.lower()) + + ) ) ) + + with notes_col: + st.write("##### Notes") + st.markdown("