Skip to main content
The Player Contributions feature breaks down how each player on your roster contributes to your team’s overall statistics. Using interactive pie charts powered by Chart.js, you can identify your most valuable players in each category.

Overview

Player Contributions displays:
  • Percentage breakdowns of each player’s contribution to team stats
  • Weekly or season-long views of player impact
  • Interactive pie charts for all fantasy categories
  • Weighted calculations for percentage stats (FG%, FT%, 3PT%)
This feature helps identify which players are carrying your team and which categories need roster adjustments.

Weekly vs Season Modes

Weekly Mode

Analyze player contributions for a specific week:
const players = STATE.viewMode === 'weekly' 
  ? (STATE.weeklyPlayerStats[STATE.selectedWeek] || []) 
  : (STATE.seasonPlayerStats || []);
Use cases for weekly mode:
  • Identify hot/cold streaks
  • Evaluate lineup decisions
  • Analyze matchup-specific performance
  • Track rest-of-season outlook changes

Season Mode

View cumulative contributions across all weeks:
fetchSeasonPlayerStats: async () => {
  const response = await fetch(`/api/player_stats_season`);
  const data = await response.json();
  return DataService.extractPlayers(data);
}
Use cases for season mode:
  • Identify consistent producers
  • Evaluate trade value
  • Plan keeper decisions
  • Assess overall roster construction

How Percentage Stats Are Weighted

Percentage stats (FG%, FT%, 3PT%) require special handling since they can’t be simply averaged.

The Weighting System

if (CONFIG.PERCENTAGE_STATS.includes(statId)) {
  let weightedSum = 0, totalWeight = 0;
  
  players.forEach(player => {
    let attemptsKey, madeKey;
    if (statId === '11') { attemptsKey = '9'; madeKey = '10'; }  // 3PTA, 3PTM
    else if (statId === '5') { attemptsKey = '3'; madeKey = '4'; }  // FGA, FGM
    else if (statId === '8') { attemptsKey = '6'; madeKey = '7'; }  // FTA, FTM

    const attempts = Utils.getStatValue(player, attemptsKey);
    const made = Utils.getStatValue(player, madeKey);

    if (attempts > 0) {
      weightedSum += made;
      totalWeight += attempts;
    }
  });
  
  return totalWeight > 0 ? (weightedSum / totalWeight) : 0;
}

Contribution Percentage Calculation

For percentage stats, contribution is based on volume (attempts), not the percentage itself:
if (CONFIG.PERCENTAGE_STATS.includes(statId)) {
  let attemptsKey;
  if (statId === '11') attemptsKey = '9';      // 3PTA for 3PT%
  else if (statId === '5') attemptsKey = '3';  // FGA for FG%
  else if (statId === '8') attemptsKey = '6';  // FTA for FT%
  
  const playerAttempts = Utils.getStatValue(player, attemptsKey);
  
  const totalAttempts = players.reduce((sum, p) => {
    return sum + Utils.getStatValue(p, attemptsKey);
  }, 0);
  
  contributionPercentage = totalAttempts > 0 
    ? (playerAttempts / totalAttempts) * 100 
    : 0;
}
A player taking 10 of your team’s 20 3-point attempts contributes 50% to your 3PT%, regardless of their individual shooting percentage.

Counting Stats

For counting stats, contribution is straightforward:
if (overallTeamStatTotal > 0) {
  contributionPercentage = (rawPlayerStatValue / overallTeamStatTotal) * 100;
}
Example: If a player scores 25 points and your team scores 100 points, they contribute 25%.

Player Data Extraction

Player stats are extracted from the Yahoo Fantasy API response:
extractPlayers: (data) => {
  const players = [];
  const teamRoster = data.fantasy_content?.team?.[1]?.players;
  if (!teamRoster) return players;
  
  Object.keys(teamRoster).forEach(key => {
    if (key === 'count') return;
    if (teamRoster[key]?.player) {
      players.push(teamRoster[key].player);
    }
  });
  
  return players;
}

Getting Player Names

getPlayerName: playerObj => {
  if (!playerObj) return 'Unknown Player';
  if (Array.isArray(playerObj) && playerObj.length > 0) {
    const metadata = playerObj[0];
    if (Array.isArray(metadata)) {
      for (const item of metadata) {
        if (item?.name) return item.name.full || item.name;
      }
    } else if (metadata?.name) {
      return metadata.name.full || metadata.name;
    }
  }
  return 'Unknown Player';
}

Getting Stat Values

getStatValue: (playerObj, statId) => {
  if (!playerObj || !Array.isArray(playerObj)) return 0;
  const playerStats = playerObj[1]?.player_stats?.stats;
  if (!Array.isArray(playerStats)) return 0;
  
  for (const stat of playerStats) {
    if (stat.stat?.stat_id === statId.toString()) {
      const rawValue = stat.stat.value;
      if (rawValue === null || rawValue === '' || rawValue === '-') return 0;
      const numVal = parseFloat(rawValue);
      return isNaN(numVal) ? 0 : numVal;
    }
  }
  return 0;
}

Chart Rendering

Processing Player Data

The system aggregates players with minimal contributions:
processPlayerData: (players, statId) => {
  // Calculate contribution percentages for all players
  const playerStats = players.map(player => ({
    name: Utils.getPlayerName(player),
    value: Utils.getStatValue(player, statId),
    percentage: /* calculated as shown above */
  }))
  .filter(p => p.percentage > 0)
  .sort((a, b) => b.percentage - a.percentage);
  
  // If more than 12 players, group the rest as "Others"
  if (playerStats.length > 12) {
    const topPlayersStats = playerStats.slice(0, 11);
    const otherPlayersStats = playerStats.slice(11);
    
    chartLabels = topPlayersStats.map(p => p.name);
    chartData = topPlayersStats.map(p => p.percentage);
    
    // Add aggregated "Others" slice
    const othersPercentageSum = otherPlayersStats.reduce(
      (sum, p) => sum + p.percentage, 0
    );
    if (othersPercentageSum > 0.01) {
      chartLabels.push('Others');
      chartData.push(othersPercentageSum);
    }
  }
}

Pie Chart Configuration

DOM.chart = new Chart(ctx, {
  type: 'pie',
  data: {
    labels: labels,  // Player names
    datasets: [{
      data: data,  // Contribution percentages
      rawValues: rawValues,  // Actual stat values for tooltip
      backgroundColor: colors,
      borderColor: colors.map(c => darkenColor(c)),
      borderWidth: 1
    }]
  },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    plugins: {
      title: {
        display: true,
        text: `${statName} Contributions ${viewMode === 'weekly' ? `- Week ${week}` : '- Season Totals'}`
      },
      legend: {
        position: 'right',
        labels: { boxWidth: 15, padding: 10, font: { size: 12 }}
      }
    }
  }
});

Color Palette

The chart uses a predefined set of pastel colors:
PLAYER_COLORS: [
  '#FFB6C1', '#B0E0E6', '#FFEFD5', '#CCCCFF', '#FFE4B5', '#E6FFE0',
  '#FFD1DC', '#F0FFF0', '#FFDAB9', '#E0FFFF', '#FFE4E1', '#D8BFD8',
  '#FFFACD', '#F0E68C'
]
Colors are assigned sequentially and cycle if there are more than 14 contributors:
const colors = labels.map((_, i) => 
  CONFIG.PLAYER_COLORS[i % CONFIG.PLAYER_COLORS.length]
);

Interactive Tooltips

Tooltips show both contribution percentage and actual stat value:
tooltip: {
  callbacks: {
    label: function(context) {
      const label = context.label || '';  // Player name
      const percentageContribution = context.parsed;  // From data array
      
      let lines = [];
      lines.push(`${label}: ${percentageContribution.toFixed(1)}%`);

      const rawPlayerStat = context.dataset.rawValues[context.dataIndex];
      let formattedRawValue = Utils.formatStatValue(STATE.selectedStat, rawPlayerStat);

      if (!CONFIG.PERCENTAGE_STATS.includes(STATE.selectedStat)) {
        const statDisplayName = STATE.statCategories.find(
          cat => cat[0] === STATE.selectedStat
        )?.[1] || '';
        formattedRawValue = `${formattedRawValue} ${statDisplayName}`;
      }
      
      lines.push(`Stat Value: ${formattedRawValue}`);
      return lines;
    }
  }
}
Example tooltip output:
Stephen Curry: 28.5%
Stat Value: 25.3 PTS

Data Loading Process

The feature loads all weeks of player data in parallel:
fetchAllWeeksPlayerStats: async () => {
  // 1. Fetch league settings
  const settingsResult = await DataService.tryGetLeagueSettings();
  STATE.currentWeek = parseInt(leagueData.fantasy_content.league[0]?.current_week || "1", 10);
  
  // 2. Fetch all weeks in parallel
  const weeklyPlayerStatsPromises = [];
  for (let week = 1; week <= STATE.currentWeek; week++) {
    const promise = fetch(`/api/player_stats_week/${week}`)
      .then(response => response.ok ? response.json() : null)
      .then(weekData => ({ 
        week, 
        players: weekData ? DataService.extractPlayers(weekData) : null 
      }));
    weeklyPlayerStatsPromises.push(promise);
  }
  
  // 3. Wait for all to complete
  const settledResults = await Promise.allSettled(weeklyPlayerStatsPromises);
  
  // 4. Populate state
  const populatedWeeklyStats = {};
  settledResults.forEach(result => {
    if (result.status === 'fulfilled' && result.value) {
      const { week, players } = result.value;
      populatedWeeklyStats[week] = players;
    }
  });
  
  return populatedWeeklyStats;
}

Week Selector

The week selector is populated based on the current week:
initWeekSelector: () => {
  DOM.weekSelector.innerHTML = '';
  for (let week = 1; week <= STATE.currentWeek; week++) {
    const option = document.createElement('option');
    option.value = week;
    option.textContent = `Week ${week}`;
    DOM.weekSelector.appendChild(option);
  }
  
  STATE.selectedWeek = Math.min(
    STATE.selectedWeek, 
    STATE.currentWeek > 0 ? STATE.currentWeek : 1
  );
  DOM.weekSelector.value = STATE.selectedWeek;
  
  DOM.weekSelector.addEventListener('change', () => {
    STATE.selectedWeek = parseInt(DOM.weekSelector.value, 10);
    ChartRenderer.renderChart();
  });
}

View Toggle Controls

Buttons allow switching between weekly and season views:
DOM.viewToggle.weeklyBtn.addEventListener('click', () => {
  if (STATE.viewMode === 'weekly') return;
  STATE.viewMode = 'weekly';
  DOM.viewToggle.weeklyBtn.classList.add('active');
  DOM.viewToggle.seasonBtn.classList.remove('active');
  DOM.weekSelector.parentElement.style.display = 'flex';
  ChartRenderer.renderChart();
});

DOM.viewToggle.seasonBtn.addEventListener('click', () => {
  if (STATE.viewMode === 'season') return;
  STATE.viewMode = 'season';
  DOM.viewToggle.seasonBtn.classList.add('active');
  DOM.viewToggle.weeklyBtn.classList.remove('active');
  DOM.weekSelector.parentElement.style.display = 'none';
  ChartRenderer.renderChart();
});

Stat Selector

Defaults to Points (PTS) if available:
initStatSelector: () => {
  DOM.statSelector.innerHTML = '';
  STATE.statCategories.forEach(([id, name]) => {
    const option = document.createElement('option');
    option.value = id;
    option.textContent = name;
    DOM.statSelector.appendChild(option);
  });
  
  // Default to Points if available
  const pointsStat = STATE.statCategories.find(cat => cat[0] === '12');
  STATE.selectedStat = pointsStat 
    ? pointsStat[0] 
    : (DOM.statSelector.options[0]?.value || null);
    
  if (STATE.selectedStat) DOM.statSelector.value = STATE.selectedStat;
}

Loading States

Progress indicators show data fetching status:
updateLoadingMessage: (message, weekNum = null, totalWeeks = null) => {
  if (DOM.loadingText) DOM.loadingText.textContent = message;
  
  if (weekNum !== null && totalWeeks !== null) {
    const progressPercent = Math.round((weekNum / totalWeeks) * 100);
    DOM.loadingProgress.style.width = `${progressPercent}%`;
    DOM.loadingCurrentWeek.textContent = `Fetching: Week ${weekNum} of ${totalWeeks}`;
  }
}

API Endpoints Used

Weekly Player Stats

@app.route("/api/player_stats_week/<int:week>")
def api_player_stats_week(week):
    team_key = get_user_team_key(session["league_key"], session["team_name"])
    data = yahoo_api(f"fantasy/v2/team/{team_key}/players/stats;type=week;week={week}")
    return jsonify(data)

Season Player Stats

@app.route("/api/player_stats_season")
def api_player_stats_season():
    team_key = get_user_team_key(session["league_key"], session["team_name"])
    data = yahoo_api(f"fantasy/v2/team/{team_key}/players/stats;type=season")
    return jsonify(data)

Error Handling

try {
  STATE.weeklyPlayerStats = await DataService.fetchAllWeeksPlayerStats();
  STATE.seasonPlayerStats = await DataService.fetchSeasonPlayerStats();
  
  if (Object.keys(STATE.weeklyPlayerStats).length === 0 && !STATE.seasonPlayerStats) {
    throw new Error('No player data could be loaded.');
  }
  
  STATE.dataLoaded = true;
  ChartRenderer.renderChart();
} catch (error) {
  console.error('Error loading player data:', error);
  errorMsgElement.textContent = `Error: ${error.message || 'Please try again.'}`;
}

Next Steps

Trends

See how team performance evolves over time

Category Strengths

Analyze team-level category rankings

Build docs developers (and LLMs) love