Skip to main content
The Trends feature provides visual analysis of your team’s performance across the fantasy basketball season. Using interactive Chart.js line graphs, you can track how your statistics evolve week by week.

Overview

Trends displays week-by-week data for your fantasy team in an interactive line chart. You can:
  • Select any stat category to visualize
  • See performance trends across the entire season
  • Identify hot and cold streaks
  • Compare current performance to historical averages
Trends data is fetched for all weeks up to the current week, allowing you to see your complete season progression.

How It Works

Initial Data Loading

When you activate the Trends tab, the system performs an optimized parallel data fetch:
fetchAllWeeklyData: async () => {
  // 1. Fetch league settings and current week in one call
  Utils.updateLoadingMessage('Loading league settings & current week…');
  const settingsResult = await DataService.tryGetLeagueSettingsAndCurrentWeek();
  
  const numWeeksToFetch = STATE.currentWeek;
  
  // 2. Kick off all week requests in parallel
  const weekFetchPromises = Array.from({ length: numWeeksToFetch }, (_, i) => {
    const weekNum = i + 1;
    return fetch(`/api/scoreboard?week=${weekNum}`)
      .then(response => response.ok ? response.json() : null)
      .then(jsonData => {
        if (jsonData) {
          const teams = DataService.extractTeams(jsonData);
          const myTeamData = teams.find(team => team.isMine);
          if (myTeamData) {
            weeklyStatsData[weekNum - 1] = { week: weekNum, stats: myTeamData.statMap };
          }
        }
      });
  });

  await Promise.allSettled(weekFetchPromises);
}
The parallel fetching strategy significantly reduces load times compared to sequential requests, especially for leagues deep into the season.

Progress Tracking

A visual progress indicator shows real-time loading status:
updateLoadingMessage: (msg, done = null, total = null) => {
  if (DOM.loadingText) DOM.loadingText.textContent = msg;
  if (done != null && total != null && DOM.loadingProgress) {
    DOM.loadingProgress.style.width = `${Math.round((done/total)*100)}%`;
    if (DOM.loadingCurrentWeek)
      DOM.loadingCurrentWeek.textContent = `Week ${done} of ${total}`;
  }
}

Chart.js Visualizations

Chart Configuration

The chart is rendered using Chart.js with responsive settings:
DOM.chart = new Chart(ctx, {
  type: 'line',
  data: {
    labels: labels,  // Week numbers
    datasets: [{
      label: statName,
      data: dataPoints,  // Stat values
      borderColor: '#4299e1',
      backgroundColor: 'rgba(66,153,225,.2)',
      pointBackgroundColor: '#4299e1',
      pointBorderColor: '#fff',
      borderWidth: 2,
      pointRadius: 4,
      pointHoverRadius: 6,
      tension: 0.1,
      spanGaps: true  // Connect lines over null data
    }]
  },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    scales: {
      y: {
        beginAtZero: false,
        ticks: {
          precision: isPercentage ? 3 : 0
        }
      },
      x: {
        title: {
          display: true,
          text: 'Week'
        }
      }
    }
  }
});

Color Scheme

The chart uses a consistent blue color palette:
generateChartColors: () => ({
  borderColor:              '#4299e1',
  backgroundColor:          'rgba(66,153,225,.2)',
  pointBackgroundColor:     '#4299e1',
  pointBorderColor:         '#fff',
  pointHoverBackgroundColor:'#fff',
  pointHoverBorderColor:    '#3182ce'
})

Interpreting Trend Lines

Understanding the Visualization

The line chart shows:
  • X-axis: Week numbers (1, 2, 3, …)
  • Y-axis: Stat values (automatically scaled)
  • Data points: Your team’s performance each week
  • Line: Connects weekly performances to show trends

Handling Missing Data

The chart gracefully handles weeks with no data:
STATE.weeklyStats.forEach((weekData, index) => {
  if (weekData) {
    labels.push(index + 1);
    const statValue = Utils.formatStatValue(STATE.selectedStat, weekData.stats[STATE.selectedStat]);
    dataPoints.push(statValue);
  } else {
    labels.push(index + 1);
    dataPoints.push(null);  // spanGaps: true connects over nulls
  }
});
Missing weeks appear as gaps in the trend line, which are automatically bridged to show overall progression.

Stat Selector

Switch between different statistics using the dropdown:
initStatSelector: () => {
  DOM.statSelector.innerHTML = '';
  STATE.statCategories.forEach(([id, name, dir]) => {
    const option = document.createElement('option');
    option.value = id;
    option.textContent = name;
    DOM.statSelector.appendChild(option);
  });
  
  STATE.selectedStat = STATE.statCategories[0][0];
  DOM.statSelector.value = STATE.selectedStat;
  
  DOM.statSelector.addEventListener('change', () => {
    STATE.selectedStat = DOM.statSelector.value;
    ChartRenderer.renderChart();
  });
}

Percentage Stat Formatting

Percentage stats receive special formatting:
formatStatValue: (id, v) => {
  if (v == null || v === '') return null;
  const num = parseFloat(v);
  if (isNaN(num)) return null;
  
  return CONFIG.PERCENTAGE_STATS.includes(id)
    ? num.toFixed(3).replace(/^0\./, '.')  // .450 instead of 0.450
    : num;
}
In the chart tooltip:
label: (context) => {
  let value = context.parsed.y;
  if (value === null) return `${statName}: N/A`; 
  if (isPercentage) {
    return `${statName}: ${parseFloat(value).toFixed(3).replace(/^0\./, '.')}`;
  }
  return `${statName}: ${Number.isInteger(value) ? value : parseFloat(value).toFixed(2)}`;
}

Interactive Tooltips

Hover over any data point to see detailed information:
plugins: {
  tooltip: {
    callbacks: {
      title: (tooltipItems) => `Week ${tooltipItems[0].label}`,
      label: (context) => {
        let value = context.parsed.y;
        if (value === null) return `${statName}: N/A`;
        
        if (isPercentage) {
          return `${statName}: ${parseFloat(value).toFixed(3).replace(/^0\./, '.')}`;
        }
        return `${statName}: ${Number.isInteger(value) ? value : parseFloat(value).toFixed(2)}`;
      }
    }
  }
}

Loading States

The feature includes several loading messages that cycle while fetching data:
loadingMessages: [
  'Initializing trends system…',
  'Getting league settings…',
  'Connecting to Yahoo API…',
  'Analyzing statistics…',
  'Synchronizing data…',
  'Reading performance metrics…',
  'Preparing visualization…',
  'Almost there!'
]
Messages cycle every 2 seconds:
cycleLoadingMessages: () => {
  let idx = 0;
  return setInterval(() => {
    if (!DOM.loadingText) return;
    if (DOM.loadingText.textContent.includes('Week ')) return;  // Don't override progress
    DOM.loadingText.textContent = STATE.loadingMessages[idx];
    idx = (idx + 1) % STATE.loadingMessages.length;
  }, 2000);
}

Configuration Merging

Trends shares configuration with dashboard.js:
if (window.CONFIG) {
  Object.assign(CONFIG, window.CONFIG);
}
This ensures:
  • Consistent category definitions
  • Shared percentage stat identification
  • Unified stat metadata
If dashboard.js hasn’t loaded, trends.js uses its own fallback configuration to ensure functionality.

Responsive Chart

The chart automatically resizes based on container dimensions:
options: {
  responsive: true,
  maintainAspectRatio: false,
  // ...
}
This ensures optimal viewing on all screen sizes.

Technical Details

State Management

const STATE = {
  currentWeek: 0,
  weeklyStats: [],
  statCategories: [],
  selectedStat: null,
  initialized: false,
  dataLoaded: false
};

Team Extraction

extractTeams: data => {
  const league = (data.fantasy_content.league || []).find(l => l.scoreboard);
  if (!league) return [];
  
  const matchups = league.scoreboard['0']?.matchups || league.scoreboard.matchups || {};
  const teams = [];
  
  Object.values(matchups).forEach(mw => {
    const ts = mw.matchup?.teams || mw.matchup?.['0']?.teams || {};
    Object.values(ts).forEach(tw => {
      const arr = tw.team;
      let name = '—', isMine = false;
      
      // Check if this is the user's team
      (arr[0] || []).forEach(it => {
        if (Utils.yes(it?.is_current_login)) isMine = true;
      });
      
      const statMap = {};
      (arr[1]?.team_stats?.stats || []).forEach(s => {
        statMap[s.stat.stat_id] = s.stat.value;
      });
      
      teams.push({ name, isMine, statMap });
    });
  });
  
  return teams;
}

Error Handling

Robust error handling ensures graceful degradation:
try {
  STATE.weeklyStats = await DataService.fetchAllWeeklyData();
  
  if (!STATE.weeklyStats || STATE.weeklyStats.every(s => s === null)) {
    DOM.container.insertAdjacentHTML('beforeend', 
      '<p class="error-message">Could not load trends data. Please try again.</p>'
    );
    STATE.dataLoaded = false;
    return;
  }
  
  STATE.dataLoaded = true;
  ChartRenderer.renderChart();
} catch (err) {
  console.error('Critical error in fetchAllWeeklyData:', err);
  DOM.container.innerHTML = 
    '<p class="error-message">An unexpected error occurred. Please try again.</p>';
}

Next Steps

Player Contributions

See which players drive your trends

Category Strengths

Analyze specific week performance

Build docs developers (and LLMs) love