Skip to content
// morris-labs
liveobservablesports-analyticsclaude

Sleeper analytics dashboard

Fantasy football analytics for a Sleeper league — power rankings, all-play records, trade grading, and a scoring system for bad lineup decisions.

Thirteen pages of analytics over a Sleeper fantasy football league, built on Observable Framework and deployed to GitHub Pages. Standings and power rankings, draft and trade retrospectives, all-play records, a ring of honor — and an atrocity tracker, which exists to rank the worst lineup decisions anybody in the league has made.

Static site, computed data

Observable Framework runs data loaders at build time: every *.json.js under src/data/ executes during the build, hits the Sleeper API, and its stdout becomes a static JSON file the page loads. The deployed site is entirely static — no server, no API keys in the browser — but the numbers in it were computed by real code with real dependencies.

It is the split worth reaching for whenever a dashboard has an expensive or credentialed step in it: do that part offline, commit the result, and let the thing that ships do nothing but render.

Valuing a trade at the time it happened

The problem with grading a trade after the fact is that you already know how it turned out. Player values move constantly, so scoring a September trade against today’s values measures hindsight rather than judgment.

So value-over-replacement snapshots are captured weekly and never recomputedvor-snapshots-history.json accumulates a point-in-time record, and a historical trade is graded against the values that existed the week it was made. Positional scarcity comes from FantasyCalc, with DynastyProcess as the dynasty-league source and static multipliers as a fallback when neither answers.

The atrocity score

A weighted composite for starting the wrong guy, scaled 0–100:

component weight what it catches
actual points gap 40% the pain you actually felt, as a position z-score
projection gap 25% information available before kickoff
start-percentage gap 20% consensus of thousands of other managers
ranking gap 15% where the players sat in the rankings

Then multiplied by a position factor and a context multiplier. The weighting is the argument: the largest share goes to what happened, but 45% sits on projection and start percentage — evidence that was on the screen at the time. It distinguishes bad luck from a bad decision, which is the only version of this worth having.

Commentary

Weekly recaps and trade analysis are generated with claude-sonnet-4-5 through the Anthropic SDK, in a voice picked by a COMMENTATOR_REGION variable — US sports-desk, UK football-pundit, or European-manager personas. GitHub Actions regenerates trade analysis nightly and weekly summaries on Wednesday mornings, then commits the output, so the AI calls happen in CI and never at page load.

Stack

  • Observable Framework — markdown pages, build-time data loaders, Plot for charts
  • Sleeper API — league, rosters, users, matchups; public and unauthenticated
  • FantasyCalc / DynastyProcess — player valuation and positional scarcity
  • Anthropic SDK — scheduled commentary generation
  • GitHub Actions → Pages — build, regenerate, deploy