Skip to content
Snippets Groups Projects
Select Git revision
  • e87ccee22b54e6cc4646e3a3cb240fef36ff6697
  • main default protected
2 results

main.py

Blame
  • user avatar
    maeries authored
    c6abf2ee
    History
    Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    main.py 30.68 KiB
    import os
    import csv
    import math
    import glob
    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
    
    import multiprocessing as mp
    
    # Constants for Elo calculation
    K_FACTOR = 32  # Standard chess K-factor
    INITIAL_ELO = 1500  # Starting Elo for all teams
    
    
    def expected_score(rating_a, rating_b):
        """Calculate the expected score for a player with rating_a against a player with 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
            self.events_directory = os.path.join(csv_directory, "events")
            self.competition_meta_data_directory = csv_directory
            self.output_directory = output_directory
            self.teams = {}  # Dictionary to store team Elo ratings
            self.match_history = []  # List to store all match results for history tracking
            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":
                meta_data = pd.read_csv(os.path.join(self.competition_meta_data_directory, "FSC.csv"))
            elif self.competition_type == "electric":
                meta_data = pd.read_csv(os.path.join(self.competition_meta_data_directory, "FSE.csv"))
            else:
                raise
            meta_data.sort_values(by=['id'])
            return meta_data
    
        def initialize_output_directory(self):
            """Create output directory if it doesn't exist"""
            os.makedirs(self.output_directory, exist_ok=True)
            os.makedirs(os.path.join(self.output_directory, 'images'), exist_ok=True)
    
        def get_competition_name(self, id):
            return self.competition_meta_data[self.competition_meta_data["id"] == int(id)]['abbr'].values[0].strip()
    
        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"""
            csv_files = self.competition_meta_data
    
            csv_files = csv_files.sort_values('date')
            csv_files = csv_files['id'].tolist()
            csv_files = [os.path.join(self.events_directory, str(x) + ".csv") for x in csv_files]
    
            print(f"Found {len(csv_files)} CSV files to process")
    
            # Process each CSV file (representing a competition)
            for csv_file in csv_files:
                print(f"Processing {os.path.basename(csv_file)}...")
    
                competition_id = os.path.basename(csv_file).split(".")[0]
                teams_in_competition = []
    
                # Read the CSV file
                try:
                    with open(csv_file, 'r', newline='', encoding='utf-8') as file:
                        reader = csv.reader(file)
    
                        skipped = False
    
                        for row in reader:
                            if not skipped:
                                skipped = True
                                continue
                            if not row:  # Skip empty rows
                                continue
    
                            rank = row[3].strip()
                            team_name = row[0].strip()
                            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
    
                # Sort teams by rank
                teams_in_competition.sort(key=lambda x: int(x[0]))
    
                # Store competition results
                date = self.get_competition_date(os.path.basename(csv_file).split(".")[0])
                self.competition_history[competition_id] = {
                    'date': date,
                    'results': teams_in_competition
                }
    
                # Extract just team names in order
                team_names = [team[1] for team in teams_in_competition]
    
                # Update competition count for each team
                for team_name in team_names:
                    self.teams[team_name]['competitions'] += 1
    
                # Update Elo ratings based on this competition's results
                self.update_elo_for_competition(team_names, competition_id)
    
        def update_elo_for_competition(self, teams_in_order, competition_id):
            """Update Elo ratings for all teams in a competition"""
            # For each team, compare with teams ranked below them
            date = self.get_competition_date(competition_id)  # Use current date as competition date
    
            for i in range(len(teams_in_order)):
                for j in range(i + 1, len(teams_in_order)):
                    team_a = teams_in_order[i]
                    team_b = teams_in_order[j]
    
                    # Team A is ranked higher than Team B
                    self.update_elo_pair(team_a, team_b, 1, 0, competition_id, date)
    
            # Store post-competition Elo ratings and changes
            for team in teams_in_order:
                rank = teams_in_order.index(team) + 1
    
                # Store in team history
                self.teams[team]['history'].append({
                    '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']
                })
    
                self.teams[team]['elo'] = self.teams[team]['elo'] + self.teams[team]['delta_elo']
                self.teams[team]['delta_elo'] = 0
    
        def update_elo_pair(self, team_a, team_b, score_a, score_b, competition_id, date):
            """Update Elo ratings for a pair of teams based on their match outcome"""
            # Get current Elo ratings
            rating_a = self.teams[team_a]['elo']
            rating_b = self.teams[team_b]['elo']
    
            # Calculate expected scores
            expected_a = expected_score(rating_a, rating_b)
            expected_b = expected_score(rating_b, rating_a)
    
            # Compute preliminary Elo ratings
            delta_rating_a = K_FACTOR * (score_a - expected_a)
            delta_rating_b = K_FACTOR * (score_b - expected_b)
    
            # Store match in history
            match_info = {
                'date': date,
                'competition': competition_id,
                'team_a': team_a,
                'team_b': team_b,
                'old_elo_a': rating_a,
                'old_elo_b': rating_b,
                'delta_elo_a': delta_rating_a,
                'delta_elo_b': delta_rating_b,
                'score_a': score_a,
                'score_b': score_b
            }
            self.match_history.append(match_info)
    
            # Update team preliminary Elo ratings
            self.teams[team_a]['delta_elo'] = self.teams[team_a]['delta_elo'] + delta_rating_a
            self.teams[team_b]['delta_elo'] = self.teams[team_b]['delta_elo'] + delta_rating_b
    
        def generate_website(self):
            """Generate static HTML website with Elo ratings and visualizations"""
            # Create main index page
            self.create_index_page()
    
            # # Create team-specific pages
            # for team_name in self.teams:
            #     self.create_team_page(team_name)
    
            # If num_processes not specified, use number of CPU cores
            num_processes = int(mp.cpu_count() / 2)
            print(f"staring computation on {num_processes} cores")
    
            # Create a pool of workers
            pool = mp.Pool(processes=num_processes)
            pool.map(self.create_team_page, self.teams)
    
            # Create visualizations
            self.create_visualizations()
    
            print(f"Website generated in {self.output_directory}")
    
        def create_index_page(self):
            """Create the main index.html page"""
            # 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>
        <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 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
            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"))
    
            # Write the HTML file
            with open(os.path.join(self.output_directory, 'index.html'), 'w', encoding='utf-8') as f:
                f.write(html_content)
    
        def create_team_page(self, team_name):
            """Create a dedicated page for a specific team"""
            print(f"Generating Team site for {team_name}")
            team_data = self.teams[team_name]
    
            # Generate Elo history chart for this team
            if team_data['history']:
                self.create_team_elo_history_chart(team_name)
    
            # 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>
        <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"))
    
            # Write the HTML file
            with open(os.path.join(self.output_directory, f'team_{clean_filename(team_name)}.html'), 'w',
                      encoding='utf-8') as f:
                f.write(html_content)
    
        def create_visualizations(self):
            """Create various visualizations for the website"""
            # Top teams Elo chart
            self.create_top_teams_chart()
    
            # Elo distribution histogram
            self.create_elo_distribution_chart()
    
        def create_top_teams_chart(self):
            """Create a bar chart of the top teams by Elo rating"""
            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]
    
            team_names = [team[0] for team in top_teams]
            elo_ratings = [team[1]['elo'] for team in top_teams]
    
            # Create bar chart
            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()
    
            # Save the chart
            plt.savefig(os.path.join(self.output_directory, 'images', 'top_teams_elo.png'), dpi=100)
            plt.close()
    
        def create_elo_distribution_chart(self):
            """Create a histogram of Elo rating distribution"""
            plt.figure(figsize=(10, 6))
    
            # 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
            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')
            plt.title('Distribution of Team Elo Ratings')
            plt.grid(axis='y', alpha=0.75)
            plt.tight_layout()
    
            # Save the chart
            plt.savefig(os.path.join(self.output_directory, 'images', 'elo_distribution.png'), dpi=100)
            plt.close()
    
        def create_team_elo_history_chart(self, team_name):
            """Create a line chart showing a team's Elo history over time"""
            plt.figure(figsize=(10, 6))
    
            team_history = self.teams[team_name]['history']
    
            # Sort history by date
            team_history.sort(key=lambda x: x['date'])
    
            dates = [entry['date'] for entry in team_history]
            elo_values = [entry['new_elo'] for entry in team_history]
    
            # Create line chart
            plt.plot(range(len(dates)), elo_values, marker='o', linestyle='-', color='gray')
    
            # Add competition labels
            for i, entry in enumerate(team_history):
                plt.annotate(entry['competition'] + f" (#{entry['rank']})",
                             (i, entry['new_elo']),
                             textcoords="offset points",
                             xytext=(0, 10),
                             ha='center',
                             fontsize=8,
                             rotation=45)
    
            plt.xticks(range(len(dates)), dates, rotation=45)
            plt.ylabel('Elo Rating')
            plt.title(f'{team_name} - Elo Rating History')
            plt.grid(True, alpha=0.3)
            plt.tight_layout()
    
            # Save the chart
            plt.savefig(os.path.join(self.output_directory, 'images', f'{clean_filename(team_name)}_history.png'),
                        dpi=100)
            plt.close()
    
    
    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')
        parser.add_argument('--output', default='public/elo_website', help='Output directory for the website')
    
        args = parser.parse_args()
    
        # Create and run the Elo system
        elo_system = EloSystem(args.input, args.output + "_combustion", "combustion")
        elo_system.load_and_process_csv_files()
        elo_system.generate_website()
    
        elo_system = EloSystem(args.input, args.output + "_electric", "electric")
        elo_system.load_and_process_csv_files()
        elo_system.generate_website()
    
    if __name__ == "__main__":
        main()