Skip to main content
The Ironman Rankings feature provides a comprehensive player ranking system that emphasizes durability and availability while accounting for production, efficiency, and value.

What Are Ironman Rankings?

Ironman rankings combine multiple factors to identify reliable, high-value fantasy basketball players:
  • Durability: Games played, weighted toward recent seasons with median tracking
  • Production: Per-game statistics across all fantasy categories
  • Efficiency: Shooting percentages and turnover management
  • Minutes: Average playing time indicating opportunity
  • Value: Performance relative to average draft position (ADP)
The system generates two ranking variants:
  • Ironman Rank: Durability-first approach emphasizing availability
  • Good Ironman Rank: Balanced view incorporating production quality

How Rankings Are Calculated

The pipeline uses z-scores to normalize player performance across dimensions:
// ironman/app.js:91-116
const NUMERIC_FIELDS = [
  'IronMan_Rank',           // Pure durability rank
  'Good_IronMan_Rank',      // Production-weighted rank
  'IronMan_Score',          // Composite durability score
  'Good_IronMan_Score',     // Composite production score
  'DurabilityZ',            // Z-score for availability
  'ProductionZ',            // Z-score for per-game output
  'EfficiencyZ',            // Z-score for shooting efficiency
  'MinutesZ',               // Z-score for playing time
  'ValueZ',                 // Z-score vs ADP
  'Weighted_GP',            // Recent-season weighted games played
  'GP_Median',              // Median GP across seasons
  'Durability_Composite',   // Combined durability metric
  'Durability_Penalty',     // Penalty for low availability/small sample
];

Rank Calculation Logic

Players are ranked by their composite score:
// ironman/app.js:495-503
function relabelRanks(players) {
  const basisKey = state.rankBasis === 'goodIronman' 
    ? 'Good_IronMan_Score' 
    : 'IronMan_Score';
  
  const ranked = [...players].sort((a, b) => 
    Number(b[basisKey] ?? 0) - Number(a[basisKey] ?? 0)
  );
  
  ranked.forEach((player, index) => {
    player.displayRank = index + 1;
    player.currentScore = Number(player[basisKey] ?? 0);
  });
}

Data Source

Rankings are loaded from a CSV file generated by the backend pipeline:
// ironman/app.js:368-392
async function loadData() {
  try {
    const response = await fetch(CSV_PATH);
    if (!response.ok) throw new Error(`Failed to load CSV (${response.status})`);

    const csvText = await response.text();
    const parsed = Papa.parse(csvText, {
      header: true,
      dynamicTyping: false,
      skipEmptyLines: true,
      transformHeader: (header) => header.replace(/^\uFEFF/, '').trim(),
    });

    state.players = parsed.data.map((row, index) => normalizeRow(row, index));
    populateFilterOptions(state.players);
    recompute();
  } catch (error) {
    console.error(error);
    elements.summary.textContent = 'Unable to load player data.';
  }
}

Row Normalization

// ironman/app.js:394-421
function normalizeRow(row, index) {
  const normalized = { ...row };

  // Convert numeric fields
  NUMERIC_FIELDS.forEach((field) => {
    if (field in row) {
      normalized[field] = coerceNumber(row[field]);
    }
  });

  // Handle ADP (can be null/empty)
  const hasAdp = row.ADP && String(row.ADP).trim() !== '';
  normalized.ADP = hasAdp ? coerceNumber(row.ADP) : null;
  normalized.hasAdp = hasAdp;

  // Calculate MPG from total minutes and games played
  const gp = coerceNumber(row.GP);
  const minutes = coerceNumber(row.MIN);
  const mpg = gp > 0 ? +(minutes / gp).toFixed(1) : 0;

  normalized.id = `${row.name_full || 'player'}-${index}`;
  normalized.GP = gp;
  normalized.MPG = mpg;
  normalized.displayRank = index + 1;
  normalized.currentScore = 0;
  normalized.active = true;
  normalized.seasons = parseSeasons(row.Seasons_Used);

  return normalized;
}

Interactive Features

Filtering System

Multiple filter options narrow down the player pool:
// ironman/app.js:465-494
function applyFilters(players) {
  return players
    .filter((player) => player.active)
    .filter(bySearch)
    .filter(byTeam)
    .filter(byPosition)
    .filter(byGamesPlayed);
}

function bySearch(player) {
  if (!state.filters.search) return true;
  return player.name_full?.toLowerCase().includes(state.filters.search.toLowerCase());
}

function byTeam(player) {
  if (state.filters.teams.size === 0) return true;
  return state.filters.teams.has(player.team);
}

function byPosition(player) {
  if (state.filters.positions.size === 0) return true;
  return player.pos
    ?.split(/[,\s]+/)
    .map((pos) => pos.trim())
    .some((pos) => state.filters.positions.has(pos));
}

function byGamesPlayed(player) {
  return player.GP >= state.filters.minGames;
}

Live Re-Ranking

Deleting players simulates draft picks and triggers instant re-ranking:
// ironman/app.js:750-765
function handleDelete(id) {
  const player = state.players.find((item) => item.id === id);
  if (!player) return;
  
  player.active = false;
  state.removedStack.push(player);
  state.expanded.delete(id);
  recompute();
}

function handleUndo() {
  const player = state.removedStack.pop();
  if (!player) return;
  
  player.active = true;
  recompute();
}

Player Detail Cards

Expanded views show comprehensive metrics:
// ironman/app.js:921-976
function buildDetailSections(player) {
  const rankingMetrics = [
    detailMetric('Ironman Rank', formatNumber(player.IronMan_Rank, 0), 'ironmanRank'),
    detailMetric('Good Ironman Rank', formatNumber(player.Good_IronMan_Rank, 0), 'goodIronmanRank'),
    detailMetric('Good Score', formatNumber(player.Good_IronMan_Score), 'goodScore'),
    detailMetric('Ironman Score', formatNumber(player.IronMan_Score), 'ironmanScore'),
  ].join('');

  const scoreMetrics = [
    detailMetric('Durability Z', formatNumber(player.DurabilityZ), 'durabilityZ'),
    detailMetric('Production Z', formatNumber(player.ProductionZ), 'productionZ'),
    detailMetric('Efficiency Z', formatNumber(player.EfficiencyZ), 'efficiencyZ'),
    detailMetric('Minutes Z', formatNumber(player.MinutesZ), 'minutesZ'),
    detailMetric('Value Z', formatNumber(player.ValueZ), 'valueZ'),
  ].join('');

  const availabilityMetrics = [
    detailMetric('Weighted GP', formatNumber(player.Weighted_GP, 1), 'weightedGP'),
    detailMetric('GP Median', formatNumber(player.GP_Median, 1), 'gpMedian'),
    detailMetric('Durability Composite', formatNumber(player.Durability_Composite), 'durabilityComposite'),
    detailMetric('Durability Penalty', formatNumber(player.Durability_Penalty), 'durabilityPenalty'),
    detailMetric('Seasons Used', formatSeasons(player.seasons), 'seasonsUsed'),
  ].join('');

  const perGameMetrics = [
    detailMetric('PTS', formatNumber(player.PTS_PG, 1), 'pts'),
    detailMetric('REB', formatNumber(player.REB_PG, 1), 'reb'),
    detailMetric('AST', formatNumber(player.AST_PG, 1), 'ast'),
    detailMetric('STL', formatNumber(player.STL_PG, 2), 'stl'),
    detailMetric('BLK', formatNumber(player.BLK_PG, 2), 'blk'),
    detailMetric('3PM', formatNumber(player.FG3M_PG, 2), 'fg3m'),
    detailMetric('3P%', formatNumber(player.FG3_PCT, 3), 'fg3Pct'),
    detailMetric('FT%', formatNumber(player.FT_PCT, 3), 'ftPct'),
    detailMetric('TOV', formatNumber(player.TOV_PG, 2), 'tov'),
    detailMetric('DD2', formatNumber(player.DD2_PG, 2), 'dd2'),
  ].join('');

  return {
    ranking: rankingMetrics,
    score: scoreMetrics,
    availability: availabilityMetrics,
    perGame: perGameMetrics,
  };
}

Tier Visualization

Players are color-coded by rank tier:
// ironman/app.js:887-906
function getTierClass(rank) {
  if (rank <= 12) return 'tier tier-top12';
  if (rank <= 25) return 'tier tier-top25';
  if (rank <= 50) return 'tier tier-top50';
  return '';
}

function getTierLabel(rank) {
  if (rank <= 12) return 'Top 12';
  if (rank <= 25) return 'Top 25';
  if (rank <= 50) return 'Top 50';
  return '';
}

function getTierTooltip(rank) {
  if (rank <= 12) return 'Currently grades inside the top 12 of the live board.';
  if (rank <= 25) return 'Currently grades inside the top 25 of the live board.';
  if (rank <= 50) return 'Currently grades inside the top 50 of the live board.';
  return '';
}

Responsive Design

The interface adapts between table and card views:
// ironman/app.js:298-307
function handleViewportChange(isMobile) {
  state.isMobile = Boolean(isMobile);
  if (!state.isMobile) {
    closeFilterDrawer({ skipFocus: true, immediate: true });
  }
  syncMobileSortControls();
  if (state.players.length > 0) {
    render();
  }
}

Mobile Sorting

// ironman/app.js:353-366
function handleMobileSortChange(event) {
  const key = event.target.value;
  if (!key || state.sort.key === key) return;
  
  const defaultDirection = key === 'displayRank' || key === 'name_full' ? 'asc' : 'desc';
  state.sort = { key, direction: defaultDirection };
  recompute();
}

function handleMobileSortDirection() {
  const nextDirection = state.sort.direction === 'asc' ? 'desc' : 'asc';
  state.sort.direction = nextDirection;
  recompute();
}
Rankings are static snapshots from the pipeline. For real-time updates, re-run the backend ranking generation process and reload the page.

Using During a Draft

The Ironman Rankings excel as a draft board:
1

Filter by Position

Select positions you need to fill to narrow the board.
2

Set Minimum Games Played

Use the slider to exclude injury-prone players based on your risk tolerance.
3

Delete as Players Are Picked

Click “Delete” on each player as they’re drafted. The remaining players instantly re-rank.
4

Undo Mistakes

Use the “Undo Delete” button if you accidentally remove a player.
The “Delete” action only removes players from the current view. Refreshing the page restores all players. Use “Reset View” to clear all filters and deletions without refreshing.

Summary Display

The summary bar shows active filters and player count:
// ironman/app.js:539-546
function renderSummary() {
  const totalActive = state.players.filter((player) => player.active).length;
  const visible = state.visiblePlayers.length;
  const removed = state.players.length - totalActive;
  
  elements.summary.textContent = 
    `${visible} players showing (${removed} removed, ${totalActive} active of ${state.players.length}).`;
  elements.undoButton.disabled = state.removedStack.length === 0;
  renderChips(buildSummaryChips());
}

Build docs developers (and LLMs) love