diff --git a/.idea/FSelo.iml b/.idea/FSelo.iml index 9c09204358cc9fb2e2a4178493c656e50f501ddb..e48623ae5d5911d460adfbaf30c0f8dea4a8f37e 100644 --- a/.idea/FSelo.iml +++ b/.idea/FSelo.iml @@ -3,6 +3,9 @@ <component name="NewModuleRootManager"> <content url="file://$MODULE_DIR$"> <excludeFolder url="file://$MODULE_DIR$/.venv" /> + <excludeFolder url="file://$MODULE_DIR$/public/elo_website_combustion" /> + <excludeFolder url="file://$MODULE_DIR$/public/elo_website_combustion/images" /> + <excludeFolder url="file://$MODULE_DIR$/public/elo_website_electric" /> </content> <orderEntry type="jdk" jdkName="Python 3.10 (FSelo)" jdkType="Python SDK" /> <orderEntry type="sourceFolder" forTests="false" /> diff --git a/main.py b/main.py index 353e4f9261bc45823722cb2d3fd00936ed82e650..b585c1e17bb0b645bd59712eb289b1788b087c7c 100644 --- a/main.py +++ b/main.py @@ -1,18 +1,18 @@ -import os import csv import math -import glob -from operator import truediv +import multiprocessing as mp +import os +from datetime import datetime -import pandas as pd -import matplotlib.pyplot as plt import matplotlib.patches as patches -from matplotlib.colors import LinearSegmentedColormap -from pathlib import Path +import matplotlib.pyplot as plt import numpy as np -from datetime import datetime +import pandas as pd +from matplotlib.colors import LinearSegmentedColormap -import multiprocessing as mp +from team_page import get_team_page_html +from tools import * +from world_ranking_list import get_world_ranking_html # Constants for Elo calculation K_FACTOR = 10 @@ -24,26 +24,6 @@ def expected_score(rating_a, rating_b): return 1 / (1 + math.pow(10, (rating_b - rating_a) / 400)) -def clean_filename(filename): - """Clean a string to make it suitable for use as a 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): self.competition_type = competition_type @@ -229,7 +209,7 @@ class EloSystem: def generate_website(self): """Generate static HTML website with Elo ratings and visualizations""" # Create main index page - self.create_index_page() + self.create_world_ranking() # # Create team-specific pages # for team_name in self.teams: @@ -248,248 +228,11 @@ class EloSystem: print(f"Website generated in {self.output_directory}") - def create_index_page(self): - """Create the main index.html page""" + def create_world_ranking(self): + """Create the index.html page for the vehicle type""" # Sort teams by Elo rating sorted_teams = sorted(self.teams.items(), key=lambda x: x[1]['elo'], reverse=True) - - html_content = f"""<!DOCTYPE html> -<html lang="en"> -<head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>Formula Student {self.competition_type} Elo Rankings</title> - {self.header_snippet} - <style> - body {{ - 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; - width: 100%; - animation: fadeInUp 0.5s ease-out forwards; - }} - - 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 {{ - 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: 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: {self.LINK_COLOR}; - text-decoration: none; - transition: color 0.3s ease; - }} - - a:hover {{ - 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> - <div class="content-container"> - <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> -""" - - # Add rows for each team - for rank, (team_name, team_data) in enumerate(sorted_teams, 1): - html_content += f""" - <tr> - <td>{rank}</td> - <td><a href="team_{clean_filename(team_name)}.html">{team_name}</a></td> - <td>{int(team_data['elo'])}</td> - <td>{team_data['competitions']}</td> - </tr>""" - - html_content += """ - </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> -</body> -</html> -""".format(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + html_content = get_world_ranking_html(self, sorted_teams) # Write the HTML file with open(os.path.join(self.output_directory, 'index.html'), 'w', encoding='utf-8') as f: @@ -507,208 +250,7 @@ class EloSystem: # Filter history for this team's competitions team_competitions = sorted(team_data['history'], key=lambda x: x['date'], reverse=True) - html_content = f"""<!DOCTYPE html> -<html lang="en"> -<head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>{team_name} - Elo Profile</title> - {self.header_snippet} - <style> - body {{ - font-family: Arial, sans-serif; - line-height: 1.6; - margin: 0; - padding: 20px; - max-width: 1200px; - margin: 0 auto; - }} - 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 {{ - 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: {self.LINK_COLOR}; - text-decoration: none; - transition: color 0.3s ease; - }} - a:hover {{ - 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; - border: 1px solid #ddd; - border-radius: 5px; - margin: 10px; - flex: 1; - min-width: 150px; - text-align: center; - background-color: #f9f9f9; - }} - .stat-value {{ - font-size: 24px; - font-weight: bold; - color: #333; - }} - .stat-label {{ - font-size: 14px; - color: #666; - }} - .positive {{ - color: green; - }} - .negative {{ - color: red; - }} - @keyframes fadeInUp {{ - from {{ - opacity: 0; - transform: translateY(20px); - }} - to {{ - opacity: 1; - transform: translateY(0); - }} - }} - </style> -</head> -<body> - <h1>{team_name} - Elo Profile</h1> - <p><a href="index.html">← Back to Rankings</a></p> - - <div class="summary-stats"> - <div class="stat-box"> - <div class="stat-value">{int(team_data['elo'])}</div> - <div class="stat-label">Current Elo</div> - </div> - <div class="stat-box"> - <div class="stat-value">{team_data['competitions']}</div> - <div class="stat-label">Competitions</div> - </div> -""" - - # Calculate average ranking if available - if team_competitions: - avg_rank = sum(comp['rank'] for comp in team_competitions) / len(team_competitions) - best_rank = min(comp['rank'] for comp in team_competitions) - - html_content += f""" - <div class="stat-box"> - <div class="stat-value">{avg_rank:.1f}</div> - <div class="stat-label">Avg. Placement</div> - </div> - <div class="stat-box"> - <div class="stat-value">{best_rank}</div> - <div class="stat-label">Best Placement</div> - </div> -""" - - html_content += """ - </div> - - <div class="chart-container"> - <img src="images/{}_history.png" alt="{} Elo History"> - </div> - - <h2>Competition History</h2> - <table> - <tr> - <th>Date</th> - <th>Competition</th> - <th>Placement</th> - <th>Elo Before</th> - <th>Elo After</th> - <th>Elo Change</th> - </tr> -""".format(clean_filename(team_name), team_name) - - # Add rows for each competition - for comp in team_competitions: - elo_change = comp['elo_change'] - change_class = "positive" if elo_change > 0 else "negative" if elo_change < 0 else "" - - html_content += f""" - <tr> - <td>{comp['date']}</td> - <td>{comp['competition']}</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> - </tr>""" - - html_content += """ - </table> - - <p><small>Generated on {}</small></p> -</body> -</html> -""".format(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + html_content = get_team_page_html(self, team_name, team_data, team_competitions) # Write the HTML file with open(os.path.join(self.output_directory, f'team_{clean_filename(team_name)}.html'), 'w', @@ -906,7 +448,6 @@ class EloSystem: def main(): """Main function to run the Elo ranking system""" import argparse - from time import time parser = argparse.ArgumentParser(description='Process competition results and generate Elo rankings website') parser.add_argument('--input', default='csv_files', help='Directory containing CSV result files') diff --git a/public/index.html b/public/index.html index 5c7b3b1b2d8f479f1de4b1642f175722f81a54c8..a3a899331f39391e2e751affcc926a3e0999f926 100644 --- a/public/index.html +++ b/public/index.html @@ -127,7 +127,7 @@ <div class="center"> <div class="by"> <div><h3>by</h3></div> - <div><img src="GET racing schwarz.svg" width="200em"></div> + <div><img src="GET_racing_schwarz.svg" width="200em"></div> </div> </div> <div class="center"> diff --git a/team_page.py b/team_page.py new file mode 100644 index 0000000000000000000000000000000000000000..e603b7ead9e585c4754345243533a9ddaaabe754 --- /dev/null +++ b/team_page.py @@ -0,0 +1,208 @@ +from datetime import datetime +from tools import clean_filename + +def get_team_page_html(self, team_name, team_data, team_competitions): + html_content = f"""<!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>{team_name} - Elo Profile</title> + {self.header_snippet} + <style> + body {{ + font-family: Arial, sans-serif; + line-height: 1.6; + margin: 0; + padding: 20px; + max-width: 1200px; + margin: 0 auto; + }} + 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 {{ + 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: {self.LINK_COLOR}; + text-decoration: none; + transition: color 0.3s ease; + }} + a:hover {{ + 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; + border: 1px solid #ddd; + border-radius: 5px; + margin: 10px; + flex: 1; + min-width: 150px; + text-align: center; + background-color: #f9f9f9; + }} + .stat-value {{ + font-size: 24px; + font-weight: bold; + color: #333; + }} + .stat-label {{ + font-size: 14px; + color: #666; + }} + .positive {{ + color: green; + }} + .negative {{ + color: red; + }} + @keyframes fadeInUp {{ + from {{ + opacity: 0; + transform: translateY(20px); + }} + to {{ + opacity: 1; + transform: translateY(0); + }} + }} + </style> + </head> + <body> + <h1>{team_name} - Elo Profile</h1> + <p><a href="index.html">← Back to Rankings</a></p> + + <div class="summary-stats"> + <div class="stat-box"> + <div class="stat-value">{int(team_data['elo'])}</div> + <div class="stat-label">Current Elo</div> + </div> + <div class="stat-box"> + <div class="stat-value">{team_data['competitions']}</div> + <div class="stat-label">Competitions</div> + </div> + """ + + # Calculate average ranking if available + if team_competitions: + avg_rank = sum(comp['rank'] for comp in team_competitions) / len(team_competitions) + best_rank = min(comp['rank'] for comp in team_competitions) + + html_content += f""" + <div class="stat-box"> + <div class="stat-value">{avg_rank:.1f}</div> + <div class="stat-label">Avg. Placement</div> + </div> + <div class="stat-box"> + <div class="stat-value">{best_rank}</div> + <div class="stat-label">Best Placement</div> + </div> + """ + + html_content += """ + </div> + + <div class="chart-container"> + <img src="images/{}_history.png" alt="{} Elo History"> + </div> + + <h2>Competition History</h2> + <table> + <tr> + <th>Date</th> + <th>Competition</th> + <th>Placement</th> + <th>Elo Before</th> + <th>Elo After</th> + <th>Elo Change</th> + </tr> + """.format(clean_filename(team_name), team_name) + + # Add rows for each competition + for comp in team_competitions: + elo_change = comp['elo_change'] + change_class = "positive" if elo_change > 0 else "negative" if elo_change < 0 else "" + + html_content += f""" + <tr> + <td>{comp['date']}</td> + <td>{comp['competition']}</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> + </tr>""" + + html_content += """ + </table> + + <p><small>Generated on {}</small></p> + </body> + </html> + """.format(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + + return html_content \ No newline at end of file diff --git a/tools.py b/tools.py new file mode 100644 index 0000000000000000000000000000000000000000..7b08ea95dbcc98b1dd8131c6ff1a10bb7c5f2a03 --- /dev/null +++ b/tools.py @@ -0,0 +1,18 @@ +def clean_filename(filename): + """Clean a string to make it suitable for use as a 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 \ No newline at end of file diff --git a/world_ranking_list.py b/world_ranking_list.py new file mode 100644 index 0000000000000000000000000000000000000000..a530828c9f0b14620737cc70c3fc20d875a0ac80 --- /dev/null +++ b/world_ranking_list.py @@ -0,0 +1,243 @@ +from datetime import datetime +from tools import clean_filename + +def get_world_ranking_html(self, sorted_teams): + html_content = f"""<!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Formula Student {self.competition_type} Elo Rankings</title> + {self.header_snippet} + <style> + body {{ + 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; + width: 100%; + animation: fadeInUp 0.5s ease-out forwards; + }} + + 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 {{ + 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: 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: {self.LINK_COLOR}; + text-decoration: none; + transition: color 0.3s ease; + }} + + a:hover {{ + 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> + <div class="content-container"> + <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> + """ + + # Add rows for each team + for rank, (team_name, team_data) in enumerate(sorted_teams, 1): + html_content += f""" + <tr> + <td>{rank}</td> + <td><a href="team_{clean_filename(team_name)}.html">{team_name}</a></td> + <td>{int(team_data['elo'])}</td> + <td>{team_data['competitions']}</td> + </tr>""" + + html_content += """ + </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> + </body> + </html> + """.format(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + + return html_content \ No newline at end of file