Skip to main content
The Team Comparison feature allows you to view and compare team performance using either season-long totals or weekly averages. This provides a big-picture view of league standings beyond Yahoo’s default rankings.

Overview

Team Comparison operates similarly to Category Strengths but focuses on cumulative season data rather than individual weeks. You can switch between two modes:
  • Season Totals: Raw cumulative stats from all weeks played
  • Weekly Averages: Per-week averages calculated by dividing totals by weeks played
Weekly averages help normalize performance for teams that may have played different numbers of games due to postponements or roster changes.

How It Works

Data Loading

Season data is fetched once and cached for the session:
loadSeasonStats: async () => {
  const r = await fetch('/api/season_avg');
  const raw = await r.json();
  const payload = DataService.transformSeasonData(raw);
  
  STATE.compare.seasonPayload = payload;
  STATE.compare.userTeamIndex = payload.teams.findIndex(t => t.isMine);
}

Data Transformation

The system transforms raw API data into a standardized format:
transformSeasonData: (data) => {
  const blocks = data.fantasy_content.league || [];
  const meta = blocks.find(b => b.current_week) || {};
  const teamsO = (blocks.find(b => b.teams) || {}).teams || {};

  const currentWeek = parseInt(meta.current_week ?? CONFIG.MAX_WEEKS, 10);
  const teams = [];
  
  Object.values(teamsO).forEach(tw => {
    const t = tw.team;
    let name = '—', isMine = false;
    
    (t[0] || []).forEach(it => {
      if (it?.name) name = it.name;
      if (Utils.yes(it?.is_current_login)) isMine = true;
    });
    
    const statMap = {};
    (t[1]?.team_stats?.stats || []).forEach(s => {
      statMap[s.stat.stat_id] = s.stat.value;
    });
    
    teams.push({ name, isMine, statMap });
  });
  
  return { teams, currentWeek };
}

Switching Modes

Season Totals Mode

Displays the raw cumulative statistics for the entire season:
if (mode === 'tot') {
  CONFIG.COLS.forEach(([id]) => {
    sm[id] = Utils.formatStatValue(id, numVal);
  });
}
This mode is useful for:
  • Seeing overall production levels
  • Identifying high-volume contributors
  • Comparing total output regardless of games played

Weekly Averages Mode

Calculates per-week averages by dividing totals by the current week number:
if (mode === 'avg') {
  if (CONFIG.PERCENTAGE_STATS.includes(id)) {
    sm[id] = Utils.formatStatValue(id, numVal);
  } else {
    sm[id] = (numVal / currentWeek).toFixed(0);
  }
}
Percentage stats (FG%, FT%, 3PT%) are never averaged—they remain as-is since they’re already normalized metrics.

Calculating Rankings

Rankings are computed using the same algorithm as Category Strengths:
computeRanks: teams => {
  const ranks = Array.from({ length: teams.length }, () => ({}));
  
  CONFIG.COLS.forEach(([id, , dir]) => {
    const vals = teams.map((t, i) => ({ num: parseFloat(t.statMap[id]), i }));
    vals.sort((a, b) => {
      if (isNaN(a.num)) return 1;
      if (isNaN(b.num)) return -1;
      return dir === 'high' ? b.num - a.num : a.num - b.num;
    });
    
    let curr = 0, prev = null;
    vals.forEach(({ num, i }, idx) => {
      if (isNaN(num)) { 
        ranks[i][id] = '-'; 
        return;
      }
      if (prev === null || num !== prev) { 
        curr = idx + 1; 
        prev = num; 
      }
      ranks[i][id] = curr;
    });
  });
  
  return ranks;
}
The ranking algorithm:
  1. Sorts teams by stat value (ascending or descending based on category)
  2. Handles ties by giving them the same rank
  3. Skips missing or invalid data (shown as ’-‘)

Table and Card Views

Both visualization modes are available in the Team Comparison tab:

Table View Features

renderCompareTable: (teams, selectedTeamIndex = 0) => {
  // Selected team highlighted at top
  const selectedTeamTr = document.createElement('tr');
  selectedTeamTr.className = 'selected-team-row';
  
  // Other teams shown below with color coding
  teams.forEach((t, idx) => {
    if (idx === selectedTeamIndex) return;
    
    CONFIG.COLS.forEach(([id, , dir]) => {
      const raw = t.statMap[id];
      let cls = '';
      
      if (raw !== '–' && baseTeam.statMap[id] !== '–') {
        const a = parseFloat(raw), b = parseFloat(baseTeam.statMap[id]);
        if (!isNaN(a) && !isNaN(b) && a !== b) {
          cls = (dir === 'high' ? b > a : b < a) ? 'better' : 'worse';
        }
      }
    });
  });
}

Card View Features

renderCompareCards: (teams, selectedTeamIndex = 0) => {
  const baseTeam = teams[selectedTeamIndex];
  
  teams.forEach((t, idx) => {
    if (idx === selectedTeamIndex) return;
    
    const card = document.createElement('div');
    card.className = 'card';
    
    CONFIG.COLS.forEach(([id, label, dir]) => {
      const raw = t.statMap[id];
      const ur = baseTeam.statMap[id];
      let diff = '';
      
      if (raw !== '–' && ur !== '–') {
        const a = parseFloat(raw), b = parseFloat(ur);
        let d = a - b;
        diff = CONFIG.PERCENTAGE_STATS.includes(id) 
          ? d.toFixed(3).replace(/^-?0\./, '.')
          : d.toFixed(0);
        if (d > 0) diff = '+' + diff;
      }
    });
  });
}

Desktop Experience

Table view shows all teams simultaneously for quick scanning

Mobile Experience

Card view adapts to smaller screens with touch-friendly cards

Automated Analysis

The season analysis summary works identically to the weekly version:
const analysis = Utils.generateCategoryAnalysis(processedData, ranks, selectedTeamIndex);
if (DOM.compare.analysisSummary) {
  DOM.compare.analysisSummary.innerHTML = Utils.formatAnalysisSummary(analysis, 'season');
}

Analysis Output

  • Team Name: Shows which team is being analyzed
  • Strongest Categories: Top 3 categories where the team ranks in the top 30%
  • Areas to Improve: Top 3 categories where the team ranks in the bottom 30%

Selecting a Comparison Team

The team selector works identically to Category Strengths:
DOM.compare.teamSel?.addEventListener('change', () => {
  const selectedIndex = parseInt(DOM.compare.teamSel.value);
  STATE.compare.selectedTeamIndex = selectedIndex;
  
  if (STATE.compare.seasonPayload) {
    API.renderCompare(
      STATE.compare.seasonPayload, 
      STATE.compare.seasonMode, 
      selectedIndex
    );
  }
});

Responsive Design

The view automatically adapts based on screen size:
initResponsiveView: () => {
  let currentBreakpointIsMobile = window.innerWidth <= 768;

  const setInitialViews = () => {
    Utils.switchView(currentBreakpointIsMobile ? 'cards' : 'table', DOM.compare);
  };
  
  window.addEventListener('resize', () => {
    const newBreakpointIsMobile = window.innerWidth <= 768;
    if (newBreakpointIsMobile !== currentBreakpointIsMobile) {
      Utils.switchView(newBreakpointIsMobile ? 'cards' : 'table', DOM.compare);
      currentBreakpointIsMobile = newBreakpointIsMobile;
    }
  });
}

Technical Implementation

API Endpoint

Season data comes from the /api/season_avg endpoint:
@app.route("/api/season_avg")
def api_season_avg():
    if "league_key" not in session: 
        return jsonify({"error": "no league chosen"}), 400
    return jsonify(
        yahoo_api(f"fantasy/v2/league/{session['league_key']}/teams;out=stats;type=season")
    )

State Management

const STATE = {
  compare: {
    seasonPayload: null,
    seasonMode: 'tot',
    selectedTeamIndex: 0,
    userTeamIndex: 0
  }
};

Next Steps

Category Strengths

Analyze week-by-week performance

Player Contributions

See individual player impact

Build docs developers (and LLMs) love