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 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 ]
);
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