Puck Analytics

NHL data pipeline & visualization platform

Data EngineeringUI/UX DesignDevelopment

This project was commissioned to narrow the scope of modern hockey analytics and analyze the impact of non-conventional metrics on roster construction.

Puck Analytics
Role

Designer & Developer

Timeline

2026 - present

Live site

puck-analytics.com

Status

In Progress

Problem

The client believed that modern analytics were not helpful and wanted to analyze the impact of several non-conventional metrics on roster construction.

Solution

A PostgreSQL database ingests and normalizes data from the NHL Stats API, while a Next.js dashboard surfaces interactive visualizations — shot charts, player trends, and team performance breakdowns — without requiring SQL knowledge.

Database

Purpose

Why create a database instead of fetching data from the existing API?

The NHL API was missing several key associated data points, and had some other inconsistencies:

  • Player amateur league data was not associated with any player related endpoints.

    The NHL API does not provide direct amateur league data for players, which is important for analyzing player development and performance. Because this was the top ask from the client, it was necessary to create a database that could fill in this missing data point.

  • Player draft position and year were not associated with any player related endpoints.

    The NHL API does not provide direct draft position and year data for players, which is important for analyzing player development and performance.

  • Inconsistent roster data existed for players who have been traded mid-season.

    Inconsistent roster data resulted in gaps in inferred line combinations.

  • The existing API can still be leveraged for certain data points

    By preserving team and player IDs from the NHL API, the existing API can still be leveraged for certain data points, such as player statistics and team information.

Methods

Creating and hosting the database

Python was utilized to fetch existing data from the NHL API and to fill in the missing data points. The data was then normalized and stored in a PostgreSQL database.

  • Player ameture league was deduced from the last played league prior to the NHL from the player statistics endpoint.

  • Player draft position and year was added utilizing the NHL API draft endpoint.

  • Player line combinations were created from the NHL shift charts API

  • The database was hosted on Neon, a serverless Postgres platform.

Structure

Tables

  • seasons

    One row per NHL season (2005-06 onward). Stores the season ID (e.g. 20052006), start/end years, rule flags (wild card, ties, OT loss point), and key dates (regular season end, playoff end).

  • teams

    One row per NHL franchise entry. Stores the team's numeric ID, full name, three-letter abbreviation, and franchise ID.

  • team_seasons

    Junction table linking teams to seasons. Each row represents a team participating in a given season and stores end-of-season standings data: wins, losses, OT losses, points, and a reference to the division. Its primary key (id / team_season_id) is the FK used throughout the rest of the schema.

  • players

    One row per NHL player. Stores the player's numeric ID, first/last name, birthdate, birth country, shoots/catches handedness, and amateur league (the last non-NHL/AHL league the player appeared in before turning pro).

  • rosters

    Links players to a specific team-season. Stores jersey number, position code, height (inches), and weight (pounds). A player can appear on multiple team_seasons rows (trades, multiple seasons).

  • conferences

    One row per conference per season (e.g. Eastern, Western). Stores the conference name and the season it belongs to. Created on-the-fly when standings are inserted.

  • divisions

    One row per division per season (e.g. Atlantic, Metropolitan). Stores the division name, its parent conference, and the season it belongs to. Created on-the-fly when standings are inserted.

  • player_stats

    Regular-season skater stats (game_type_id = 2) per player per team-season. Stores goals, assists, points, plus/minus, average time on ice, penalty minutes, and games played. Goalies are excluded.

  • player_stats_playoffs

    Playoff skater stats (any game_type_id other than 2) per player per team-season. Same columns as player_stats. Goalies are excluded.

  • playoffs

    One row per season that had a playoff. Acts as a parent record for playoff_series rows, keyed by season_id.

  • playoff_series

    One row per playoff series (e.g. first-round matchup). Stores the round number, series letter (A–O), the home and away team_season_ids, and each team's game wins within the series.

  • playoff_games

    One row per individual playoff game. Stores the parent playoff_series_id, game number within the series, home/away team_season_ids, and final scores for each team.

Relations

  • seasons
    • conferences
      • divisons
        • team_seasons
          • rosters
            • players
          • player_stats
          • player_stats_playoffs
          • playoff_series
          • playoff_games
    • teams
      • team_seasons (see above)
    • playoffs
      • playoff_series
Automation

How the database is kept up to date

The database is updated daily with new data from the NHL API. A Python ETL pipeline is used to fetch new data and update the database. Unique identifiers, such as player ID and team ID, are shared between the NHL API and the database to ensure that data is updated correctly.

  • The ETL pipeline is scheduled to run daily using GitHub Actions.

  • The pipeline fetches new data from the NHL API and updates the database with any new or changed data for the most recent season.

UI/UX Design

Sitemap

Not many routes were needed to satisfy client requirements. The particular interest was in the team lineup and team comparison pages.

Unique design challenge

An old-fashioned design to emphasize an old-fashioned view of statistics

The client wanted the interface to resemble a newspaper to reflect a more traditional approach to statistics. The challenge was to create a modern, responsive layouts that had the same feel as traditional printed media.

Images of newspapers were collected and utilized for reference and inspiration. The design was then iterated on to create a modern, responsive layout that had the same feel as traditional printed media.

Design elements

Common design elements were identified accross traditional printed media.

  • Serif fonts
  • Thick horizontal and vertical rules
  • Multiple column layout
  • Rigid grid structure
  • Large headlines
  • Justfied text
  • Minimal to no color
Design Tokens

The design tokens were defined in Figma utilizing variables.

Concept and base layout

An example page layout demonstrating the base style, layout, navigation, and design principles was presented. The base layout was created in Figma utilizing slots and grid auto layout, with variants for desktop and mobile views.

03

Development

Tech stack

Application

Next.js

React

TypeScript

Tailwind CSS

Visualization

D3.js

Infrastructure

Neon

Vercel

Data architecture

From raw API data to interactive dashboard

The application layers two data sources through Next.js server components before reaching the browser — a pre-built PostgreSQL database for roster and standings data, and live NHL API calls for per-game shift chart data.

  1. NHL Stats API

    Live shift chart data fetched per-game to reconstruct line combinations.

  2. Python ETL

    Ingests and normalizes roster, standings, and playoff data from the NHL API into PostgreSQL.

  3. PostgreSQL on Neon

    Persistent storage for all historical season, team, player, and playoff data.

  4. Next.js server components

    Query the database and NHL API at request time — data arrives fully rendered with no client-side loading states.

  5. React + D3

    Interactive client components handle navigation and D3-powered chart rendering.

Lineup algorithm

Reconstructing line combinations from shift data

The NHL does not publish an official lineup API. This algorithm infers line combinations from the shift chart endpoint, which records each player's exact on-ice intervals throughout a game.

  1. 1

    Fetch shift chart

    All player shifts for the team's most recent game are requested from the NHL shift chart endpoint, providing each player's on-ice intervals to the second.

  2. 2

    Filter to 5v5 regulation

    Shifts outside of regulation 5-on-5 play are discarded. Power plays, penalty kills, and overtime are excluded to isolate even-strength deployment patterns.

  3. 3

    Detect and remove goalies

    Goalies are identified by position code and shift-duration heuristics — goalies take long continuous shifts while skaters rotate frequently.

  4. 4

    Rank by ice time

    Skaters are ranked by total 5v5 time-on-ice. The top 12 forwards and top 6 defensemen form the pools for line and pairing construction.

  5. 5

    Compute co-ice time

    For every pair of skaters, the total seconds they shared the ice is calculated. High co-ice time identifies players the coach regularly deployed together.

  6. 6

    Build forward lines

    The highest-TOI forward seeds each line. Remaining forwards are slotted by position preference (C → LW → RW, filling gaps as needed), weighted by co-ice time to form 4 complete lines.

  7. 7

    Build defensive pairings

    Defensemen are paired by handedness — a left-handed and right-handed defender per pairing — matched by co-ice time to construct 3 pairings that reflect actual deployment.