diff --git a/main.py b/main.py index 1963ffe94590104baeee2cb848cace37d3869f85..5bae51c8ff280e80700241e7046244906f5550bd 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,8 @@ from operator import truediv import pandas as pd import matplotlib.pyplot as plt +import matplotlib.patches as patches +from matplotlib.colors import LinearSegmentedColormap from pathlib import Path import numpy as np from datetime import datetime @@ -27,6 +29,20 @@ def clean_filename(filename): # Replace spaces and special characters return ''.join(c if c.isalnum() else '_' for c in filename).lower() +def hex_to_normalized_rgb(hex_color): + # Remove the '#' symbol if present + hex_color = hex_color.lstrip('#') + + # Convert the hex values to integers + red = int(hex_color[0:2], 16) + green = int(hex_color[2:4], 16) + blue = int(hex_color[4:6], 16) + + # Normalize the values by dividing by 255 + normalized_rgb = (red / 255, green / 255, blue / 255) + + return normalized_rgb + class EloSystem: def __init__(self, csv_directory, output_directory, competition_type): @@ -39,6 +55,16 @@ class EloSystem: self.competition_history = {} # Dictionary to store competition results self.initialize_output_directory() self.competition_meta_data = self.load_competition_meta_data() + if self.competition_type == "electric": + self.BORDER_COLOR = "#ffff1e" + self.LINK_COLOR = "#ffc400" + self.LINK_COLOR_HOVER = "#cc9d00" + elif self.competition_type == "combustion": + self.BORDER_COLOR = "#ff8c00" + self.LINK_COLOR = "#ff4500" + self.LINK_COLOR_HOVER = "#cc3700" + else: + raise def load_competition_meta_data(self): if self.competition_type == "combustion": @@ -61,6 +87,9 @@ class EloSystem: def get_competition_date(self, id): date_string = self.competition_meta_data[self.competition_meta_data["id"] == int(id)]['date'].values[0] return datetime.strptime(date_string, "%Y-%m-%d") + + def get_competition_type(self, id): + return self.competition_meta_data[self.competition_meta_data["id"] == int(id)]['kind'].values[0].strip() def load_and_process_csv_files(self): """Load all CSV files and process them to calculate Elo ratings""" @@ -95,16 +124,18 @@ class EloSystem: rank = row[3].strip() team_name = row[0].strip() - - # Initialize the team if not seen before - if team_name not in self.teams: - self.teams[team_name] = { - 'elo': INITIAL_ELO, - 'competitions': 0, - 'history': [], - 'delta_elo': 0 - } - teams_in_competition.append((rank, team_name)) + kind = row[2].strip() + + if kind.lower() == self.competition_type: + # Initialize the team if not seen before + if team_name not in self.teams: + self.teams[team_name] = { + 'elo': INITIAL_ELO, + 'competitions': 0, + 'history': [], + 'delta_elo': 0 + } + teams_in_competition.append((rank, team_name)) except FileNotFoundError: print(f"file {csv_file} not found: skipping") continue @@ -131,7 +162,6 @@ class EloSystem: def update_elo_for_competition(self, teams_in_order, competition_id): """Update Elo ratings for all teams in a competition""" - # HIER ÄNDERN, UM EV UND CV AUSEINANDERZUSORTIEREN # For each team, compare with teams ranked below them date = self.get_competition_date(competition_id) # Use current date as competition date @@ -152,6 +182,7 @@ class EloSystem: 'date': date.strftime("%Y-%m-%d"), 'competition': self.get_competition_name(competition_id), 'rank': rank, + 'participants': len(teams_in_order), 'previous_elo': self.teams[team]['elo'], 'new_elo': self.teams[team]['elo'] + self.teams[team]['delta_elo'], 'elo_change': self.teams[team]['delta_elo'] @@ -228,70 +259,204 @@ class EloSystem: <title>Formula Student {self.competition_type} Elo Rankings</title> <style> body {{ - font-family: Arial, sans-serif; - line-height: 1.6; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + min-height: 100vh; margin: 0; + font-family: 'Helvetica Neue', sans-serif; + background: linear-gradient(135deg, #ebebeb, #c9c9c9); + animation: gradientAnimation 10s ease infinite; + background-size: 200% 200%; padding: 20px; + }} + + @keyframes gradientAnimation {{ + 0%, 100% {{ + background-position: 0% 50%; + }} + 50% {{ + background-position: 100% 50%; + }} + }} + + .content-container {{ max-width: 1200px; - margin: 0 auto; + width: 100%; + animation: fadeInUp 0.5s ease-out forwards; }} - h1, h2 {{ + + h1 {{ + font-size: 40px; + text-transform: uppercase; + text-align: center; color: #333; + animation: fadeInUp 0.5s ease-out forwards; + opacity: 0; }} + + h2 {{ + text-align: center; + color: #555; + animation: fadeInUp 0.5s ease-out forwards; + opacity: 0; + margin-top: 30px; + }} + + p {{ + text-align: center; + color: #777; + animation: fadeInUp 0.5s ease-out forwards; + opacity: 0; + }} + table {{ border-collapse: collapse; width: 100%; margin-top: 20px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + border-radius: 5px; + overflow: hidden; + animation: fadeInUp 0.5s ease-out forwards; + opacity: 0; }} + th, td {{ - border: 1px solid #ddd; - padding: 8px; + padding: 12px 15px; text-align: left; }} + th {{ background-color: #f2f2f2; + color: #333; font-weight: bold; + text-transform: uppercase; + border-bottom: 2px solid {self.BORDER_COLOR}; + }} + + tr {{ + background-color: white; + transition: background-color 0.3s ease; }} + tr:nth-child(even) {{ background-color: #f9f9f9; }} + tr:hover {{ background-color: #f5f5f5; }} + .chart-container {{ - margin-top: 30px; + margin: 30px 0; display: flex; justify-content: center; + width: 100%; + animation: fadeInUp 0.5s ease-out forwards; + opacity: 0; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + border-radius: 5px; + overflow: hidden; + background-color: white; + padding: 20px; + box-sizing: border-box; }} + .chart-container img {{ max-width: 100%; height: auto; }} + a {{ - color: #0066cc; + color: {self.LINK_COLOR}; text-decoration: none; + transition: color 0.3s ease; }} + a:hover {{ - text-decoration: underline; + color: {self.LINK_COLOR_HOVER}; + text-decoration: none; + }} + + .timestamp {{ + font-size: 12px; + color: #777; + text-align: center; + margin-top: 20px; + animation: fadeInUp 0.5s ease-out 0.7s forwards; + opacity: 0; + }} + + .nav-button {{ + margin-top: 30px; + padding: 1em 2em; + font-size: larger; + text-transform: uppercase; + width: 12em; + text-align: center; + border: none; + border-radius: 5px; + background: linear-gradient(135deg, #ffff1e, #ffc400); + color: black; + cursor: pointer; + box-shadow: 0 10px 20px rgba(255, 255, 30, 0.3); + animation: fadeInUp 0.5s ease-out 0.8s forwards; + opacity: 0; + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + position: relative; + overflow: hidden; + }} + + .nav-button:hover {{ + box-shadow: 0 15px 30px rgba(255, 255, 30, 0.4); + transform: translateY(-2px); + }} + + .nav-button::before {{ + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transition: left 0.7s ease; + }} + + .nav-button:hover::before {{ + left: 100%; + }} + + @keyframes fadeInUp {{ + from {{ + opacity: 0; + transform: translateY(20px); + }} + to {{ + opacity: 1; + transform: translateY(0); + }} }} </style> </head> <body> - <h1>Formula Student {self.competition_type} Elo Rankings</h1> - <p>Rankings based on {len(self.competition_history)} competitions.</p> - - <div class="chart-container"> - <img src="images/top_teams_elo.png" alt="Top Teams Elo Chart"> - </div> - - <h2>Current Rankings</h2> - <table> - <tr> - <th>Rank</th> - <th>Team</th> - <th>Elo Rating</th> - <th>Competitions</th> - </tr> + <div class="content-container"> + <h1>Formula Student electric Elo Rankings</h1> + <p>Rankings based on 126 competitions.</p> + + <div class="chart-container"> + <img src="images/top_teams_elo.png" alt="Top Teams Elo Chart"> + </div> + + <h2>Current Rankings</h2> + <table> + <tr> + <th>Rank</th> + <th>Team</th> + <th>Elo Rating</th> + <th>Competitions</th> + </tr> """ # Add rows for each team @@ -305,13 +470,20 @@ class EloSystem: </tr>""" html_content += """ - </table> - - <div class="chart-container"> - <img src="images/elo_distribution.png" alt="Elo Distribution"> + </table> + + <div class="chart-container"> + <img src="images/elo_distribution.png" alt="Elo Distribution"> + </div> + + <div class="timestamp"> + <p>Generated on {}</p> + </div> + + <div style="display: flex; justify-content: center;"> + <a href="index.html"><button class="nav-button">Back to Home</button></a> + </div> </div> - - <p><small>Generated on {}</small></p> </body> </html> """.format(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) @@ -349,48 +521,80 @@ class EloSystem: }} h1, h2 {{ color: #333; + animation: fadeInUp 0.5s ease-out forwards; }} table {{ border-collapse: collapse; width: 100%; margin-top: 20px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + border-radius: 5px; + overflow: hidden; + animation: fadeInUp 0.5s ease-out forwards; + opacity: 0; }} + th, td {{ - border: 1px solid #ddd; - padding: 8px; + padding: 12px 15px; text-align: left; }} + th {{ background-color: #f2f2f2; + color: #333; font-weight: bold; + text-transform: uppercase; + border-bottom: 2px solid {self.BORDER_COLOR}; + }} + + tr {{ + background-color: white; + transition: background-color 0.3s ease; }} + tr:nth-child(even) {{ background-color: #f9f9f9; }} + tr:hover {{ background-color: #f5f5f5; }} + p {{ + animation: fadeInUp 0.5s ease-out forwards; + }} .chart-container {{ margin-top: 30px; display: flex; justify-content: center; + width: 100%; + animation: fadeInUp 0.5s ease-out forwards; + opacity: 0; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + border-radius: 5px; + overflow: hidden; + background-color: white; + padding: 20px; + box-sizing: border-box; }} .chart-container img {{ max-width: 100%; height: auto; }} a {{ - color: #0066cc; + color: {self.LINK_COLOR}; text-decoration: none; + transition: color 0.3s ease; }} a:hover {{ - text-decoration: underline; + color: {self.LINK_COLOR_HOVER}; + text-decoration: none; }} .summary-stats {{ display: flex; justify-content: space-around; margin: 20px 0; flex-wrap: wrap; + animation: fadeInUp 0.5s ease-out forwards; }} .stat-box {{ padding: 15px; @@ -417,6 +621,16 @@ class EloSystem: .negative {{ color: red; }} + @keyframes fadeInUp {{ + from {{ + opacity: 0; + transform: translateY(20px); + }} + to {{ + opacity: 1; + transform: translateY(0); + }} + }} </style> </head> <body> @@ -478,7 +692,7 @@ class EloSystem: <tr> <td>{comp['date']}</td> <td>{comp['competition']}</td> - <td>{comp['rank']}</td> + <td>{comp['rank']} / {comp['participants']}</td> <td>{int(comp['previous_elo'])}</td> <td>{int(comp['new_elo'])}</td> <td class="{change_class}">{'+' if elo_change > 0 else ''}{elo_change:.1f}</td> @@ -507,7 +721,8 @@ class EloSystem: def create_top_teams_chart(self): """Create a bar chart of the top teams by Elo rating""" - plt.figure(figsize=(12, 8)) + fig = plt.figure(figsize=(12, 8)) + ax = fig.add_axes([0.3, 0.1, 0.6, 0.8]) # Get top 15 teams by Elo top_teams = sorted(self.teams.items(), key=lambda x: x[1]['elo'], reverse=True)[:15] @@ -516,16 +731,56 @@ class EloSystem: elo_ratings = [team[1]['elo'] for team in top_teams] # Create bar chart - bars = plt.barh(team_names, elo_ratings, color='skyblue') + bars = plt.barh(team_names, elo_ratings, color='none') # Add Elo values at the end of each bar for i, bar in enumerate(bars): plt.text(bar.get_width() + 5, bar.get_y() + bar.get_height() / 2, f"{int(elo_ratings[i])}", va='center') + + # Create custom colormap + colors = [hex_to_normalized_rgb(self.LINK_COLOR), hex_to_normalized_rgb(self.BORDER_COLOR)] # RGB values for #ffff1e and #ffc400 + custom_yellow_cmap = LinearSegmentedColormap.from_list('custom_yellow', colors) + + for bar in bars: + # Get the coordinates of the bar + x0, y0 = bar.get_xy() + width = bar.get_width() + height = bar.get_height() + + # Create a gradient for the bar + gradient = np.linspace(0, 1, 100).reshape(1, -1) + + # Get colors from the colormap + colors = custom_yellow_cmap(gradient) + + # Set the bar's face color to the gradient + bar.set_facecolor('none') # Remove default color + + # Add the gradient rectangle + gradient_rect = patches.Rectangle((x0, y0), width, height, + linewidth=0, + fill=True, + facecolor=custom_yellow_cmap(0.5)) + + # To create a gradient, add an axial gradient transform + gradient_rect.set_facecolor('none') + ax.add_patch(gradient_rect) + + # Create multiple thin rectangles to simulate a gradient + steps = 50 + for i in range(steps): + left = x0 + (i/steps) * width + rect_width = width/steps + color = custom_yellow_cmap(i/steps) + rect = patches.Rectangle((left, y0), rect_width, height, + linewidth=0, + color=color) + ax.add_patch(rect) plt.xlabel('Elo Rating') plt.title('Top Teams by Elo Rating') - plt.tight_layout() + # plt.tight_layout() # Save the chart plt.savefig(os.path.join(self.output_directory, 'images', 'top_teams_elo.png'), dpi=100) @@ -538,8 +793,64 @@ class EloSystem: # Get all Elo ratings elo_ratings = [team_data['elo'] for team_data in self.teams.values()] + # Create custom yellow gradient colormap (#ffff1e to #ffc400) + colors = [hex_to_normalized_rgb(self.LINK_COLOR), hex_to_normalized_rgb(self.BORDER_COLOR)] + custom_yellow_cmap = LinearSegmentedColormap.from_list('custom_yellow', colors) + # Create histogram - plt.hist(elo_ratings, bins=20, color='skyblue', edgecolor='black') + counts, bin_edges, _ = plt.hist(elo_ratings, bins=20, alpha=0) + + # Draw bars with gradients + bin_width = bin_edges[1] - bin_edges[0] + for i in range(len(counts)): + # For each bar in the histogram + left = bin_edges[i] + bottom = 0 + width = bin_width + height = counts[i] + + # Create the gradient effect by stacking multiple thin rectangles + gradient_steps = 50 + for step in range(gradient_steps): + # Calculate position and width of each thin rectangle + rect_left = left + rect_width = width + # Calculate height of this piece of the gradient + rect_bottom = bottom + (step / gradient_steps) * height + rect_height = height / gradient_steps + + # Get color from our custom colormap + color = custom_yellow_cmap(step / gradient_steps) + + # Add the rectangle patch + rect = patches.Rectangle( + (rect_left, rect_bottom), + rect_width, + rect_height, + linewidth=0, + facecolor=color, + edgecolor=None + ) + plt.gca().add_patch(rect) + + # Add black edges to each bar + for i in range(len(counts)): + left = bin_edges[i] + width = bin_width + height = counts[i] + rect = patches.Rectangle( + (left, 0), + width, + height, + fill=False, + edgecolor='black', + linewidth=1 + ) + plt.gca().add_patch(rect) + + # Set axis limits to make sure the entire histogram is visible + plt.xlim(bin_edges[0], bin_edges[-1]) + plt.ylim(0, max(counts) * 1.1) plt.xlabel('Elo Rating') plt.ylabel('Number of Teams') @@ -564,7 +875,7 @@ class EloSystem: elo_values = [entry['new_elo'] for entry in team_history] # Create line chart - plt.plot(range(len(dates)), elo_values, marker='o', linestyle='-', color='blue') + plt.plot(range(len(dates)), elo_values, marker='o', linestyle='-', color='gray') # Add competition labels for i, entry in enumerate(team_history):