Back to the map

How the Shade Map's numbers are made

Methods, data, checks and known errors, as of the data refresh of 2026-10-01: all 30 MLB parks and the spring training parks in Arizona and Florida, 19 of the 23 mapped. Every number below comes from the committed data and code; the companion file VALIDATION.md holds the full check tables.

Summary

For a chosen park, date, first pitch and game length, the page computes the sun's position every five minutes from an hour before first pitch to an hour after the expected end, using the NOAA Solar Calculator algorithm in the park's own time zone. Each park is reduced to two measured or assumed surfaces in a frame centred on home plate: a rim, the top edge of whatever surrounds the field, as one point per degree of bearing with a height, and a seat surface, where the seats are and how high. A seat is in shade at a given moment if the straight line from a viewer's eye, 1.1 m above that seat, toward the sun passes below the rim. Every number on the page is a count or an average of those yes-or-no answers over sample seats and time steps. The rims come from USGS 3DEP airborne lidar at 41 of the 48 mapped parks; the seat surface is measured from the same lidar at 20 parks, with the outfield seating at 17 of them, and assumed at the rest. Five parks have no map: Tropicana Field is under a fixed roof, and four Florida spring parks have no lidar of their stands as they are now.

  1. What the page reports
  2. Pipeline
  3. Data and provenance
  4. Frames and conventions
  5. The sun
  6. The rim
  7. Home plate and center field
  8. The seats
  9. The shade test
  10. From seats to the numbers
  11. Checks
  12. Uncertainty and known errors
  13. Reproducing it
  14. Sources and licenses

1. What the page reports

Notation: the game window is the set of five-minute steps k from first pitch to first pitch plus the game length, inclusive. A cell is a group of sample seats: a section and row at Sutter Health Park (one seat, at the middle of the row), or a zone and a depth row elsewhere (five seats, one at each of five bearings across the zone). For a cell at step k, fk is the fraction of its sample seats in shade.

On the pageDefinition
Map color, "At this time"Zone parks: fk of each depth-row cell, binned as direct sun (f = 0), some shade (0 < f < 1/3), mostly shade (1/3 ≤ f < 1), shade (f = 1). Sutter: each row is sun or shade.
Map color, "Whole game"The same four bins applied to the cell's share (below).
At [time]For the seat card's cell at the scrubber's step: "Sun" if f = 0, "Shade" if f = 1, "Partly shaded (p% of the zone)" otherwise; at Sutter, the state of the one seat (sun, shade from the stands, shade under the overhang). "Sun is down" when the sun's apparent altitude is at or below 0 deg.
Shade arrivesThe clock time of the first step in the game window with fk ≥ 0.5. "At first pitch" if that is the first step; "Shaded from first pitch" if the share is 1; "Not during the game" if no step qualifies. Marked "(sunset)" if the sun is down at that step.
Share of the game in shadeThe mean of fk over the game window, as a percentage.
Rows (or depth rows) in shade nowSutter: rows of the chosen section whose seat is not in sun at the step. Zone parks: depth rows of the chosen zone with fk ≥ 0.5.
Row-by-time chartOne column per step of the game window, one band per row (or depth row); colored by the same bins as the map.
Whole game, in shortCounted from the cells' shares: rows (or depth rows) with share exactly 1 are "shade for the whole game", share exactly 0 are "sun for the whole game", counted from the back and from the front of each zone; Sutter's overhang rows are counted apart, because they are shaded all day by rule.

Night counts as shade throughout: a step with the sun at or below the horizon puts every seat in the "sun is down" state, which is not sun.

2. Pipeline

Pipeline: three sources feed per-park geometry, which the browser combines with the sun to shade each seat every five minutes. MLB Stats API snapshotparks, time zones, roofs, fences, clubs Vendor lidar dataset (21 parks)rims, plates, CF bearings USGS 3DEP lidar (20 parks)Sutter 2019; FL 2018-19; AZ 2020 build_parks.mjsparks.json: plate, bearing,seat surface, notesrims.json: 360 rim pointsper park Sun every 5 min (solar.js)NOAA algorithm, park time zone Shade test per seat (shade.js)eye-to-sun line against the rim Numbers on the page (core.js)fractions, share, arrival, summary offline, committed (scripts/shademap/)in the browser, per input change
Figure 1. Everything left of the arrow into the browser is computed offline and committed; the page recomputes the right-hand column whenever an input changes (well under 100 ms per park on a Raspberry Pi 5).

3. Data and provenance

SourceUsed forVersionLicense
MLB Stats API teams endpoint (snapshot; URL below)Venue ids, names, cities, IANA time zones, roof types, published fence distances; each club's spring venue and league; at parks without lidar, the venue point and centre-field azimuthSnapshot 2026-10-01 (the 30 MLB parks it yields are byte for byte those of the 2026-09-25 snapshot)Copyright MLB Advanced Media, L.P.
MLB Stats API schedule"Pick a game", fetched live by the browserLiveCopyright MLB Advanced Media, L.P.
ballpark-shadow-geometry (Adrien Peltzer)Rims, home plates and centre-field bearings at 21 open-air MLB parks; the shadow calendar used as a checkCommit dec934bData CC BY 4.0, code MIT
USGS 3DEP lidar, project USGS_LPC_CA_NoCAL_Wildfires_B5a_2018Sutter Health Park: rim, plate, bearing, seat heightsFlown 2019-02-10 and 2019-02-12Public domain
USGS 3DEP lidar, project AZ_MaricopaPinal_1_2020The ten Arizona parks: rim, plate, bearing, seat surface, outfield wallFlown 2020-11-20 to 2020-11-28Public domain
USGS 3DEP lidar, seven Florida projects (listed below)The nine mapped Florida parks: the same measurementsFlown 2018-06-10 to 2019-04-15 (each park's dates in VALIDATION 10)Public domain
OpenStreetMap stadium outlinesOnly to centre the lidar download box, at 16 spring parksQueried 2026-10-01ODbL
NOAA Solar CalculatorThe sun algorithm (Meeus, Astronomical Algorithms), reimplementedmain.js as of 2026-09-25US government work

The Florida projects, each the most recent 3DEP project whose footprint contains the park: FL_Peninsular_Pinellas_2018 (BayCare Ballpark), FL_Peninsular_FDEM_Charlotte_2018 (Charlotte Sports Park), FL_Peninsular_FDEM_Manatee_2018 (LECOM Park), FL_Peninsular_FDEM_Polk_2018 (Joker Marchant Stadium), FL_Peninsular_FDEM_Sarasota_2018 (Ed Smith Stadium), FL_Peninsular_FDEM_PalmBeach_2019 (CACTI Park of the Palm Beaches and Roger Dean Chevrolet Stadium) and USGS_LPC_FL_Southwest_A_2018_LAS_2019 (JetBlue Park and Hammond Stadium).

The snapshot also says which clubs train where: fifteen Cactus League clubs at ten venues in Arizona, fifteen Grapefruit League clubs at thirteen in Florida. Four of the Florida venues are listed with no map, because their only USGS lidar predates the stands: George M. Steinbrenner Field (2007 lidar, renovated 2017), TD Ballpark (December 2018 lidar, rebuilt 2019 to 2020), Clover Park (March 2019 lidar, renovated for 2020) and CoolToday Park (December 2018 lidar, while it was being built). No newer USGS lidar exists for any of them (checked against the 3DEP index and The National Map).

The snapshot URL is https://statsapi.mlb.com/api/v1/teams?sportId=1&hydrate=venue(location,fieldInfo,timezone),springVenue(location,fieldInfo,timezone). The lidar is read from USGS's public Entwine point tiles on AWS (s3://usgs-lidar-public/<project>/ept.json): only the octree nodes that intersect a box round each park, down to about 0.3 m point spacing. Flight dates are read from each return's GPS time.

4. Frames and conventions

5. The sun

solar.js implements the NOAA Solar Calculator: Julian century from UTC, geometric mean longitude and anomaly, equation of centre, apparent longitude with the nutation term, corrected obliquity, declination and equation of time, then hour angle, zenith and azimuth for the park's latitude and longitude, plus NOAA's piecewise refraction (zero above 85 deg). The game track runs from first pitch minus 60 min to first pitch plus the game length plus 60 min, in 5 min steps; the scrubber covers all of it and the statistics use only the game window.

Against the calculator's own code at five parks and five dates the azimuth and altitude agree to 0.02 deg, and JPL Horizons agrees to the same order (VALIDATION.md sections 2 and 3, which also test the clock either side of each 2026 daylight-saving change).

6. The rim

The rim is the top of whatever bounds the view from home plate in each direction: grandstand, roof, scoreboard, building, hill or tree. It is stored as 360 points, one per 1 deg bin of bearing from the plate, each with its plan position and its height above the field, and joined into a closed polyline.

Definition (lidar parks). In each 1 deg bin, of all returns 15 to 250 m from the plate, take the one with the 4th highest elevation angle seen from the plate at field level,

elevation = atan( (z - z0) / horizontal distance from the plate )

(the 4th rather than the highest so a bird or a wire cannot win a bin). Then narrow tall features, a run of bins at most 32 deg wide rising more than 3 deg above a 61 deg rolling median with a step of at least 4 deg at both ends, are replaced by the rolling median: light towers and masts, which cast thin moving shadows rather than shade a section. No bin is edited by hand. This is the vendor's method; Sutter Health Park and the spring parks run a port of the same code on fresh lidar. At the spring parks the playing field is cleared first: in fair territory, returns more than 2 m inside the outfield wall are dropped, because equipment left on the field when it was flown (a batting-practice screen, a cart) would otherwise win a bin. The clearing never reaches past MLB's published fence line, because behind a see-through fence the wall the lidar finds is the next structure out. That changes the rim at three Florida parks (35 bins at Joker Marchant, 4 at Roger Dean, 1 at JetBlue) and none in Arizona.

Salt River Fields from the lidar, seen from above: field and stands in grey, tall structures tinted dark, with the measured rim in orange, the front row of seats in cyan, the last visible row in yellow, home plate in magenta and the centre-field axis and foul lines in white.
Figure 2. Salt River Fields at Talking Stick from the November 2020 lidar. Grey: lidar intensity of the ground and the seats; dark blue tint: structures more than 2 m above the field, darker with height to 25 m. Orange: the rim. Cyan and yellow dots: the front row and the last visible row every 2.5 deg, as the model uses them after the cleaning of section 8 (filled), and where the profile found a bank that the cleaning dropped (hollow rings: a walkway, a batter's eye, the top of a tall wall). Magenta: home plate. White: the centre-field axis and the foul lines. 20 m scale bar; north up. The long orange spokes in the outfield are distant light poles on the practice fields, 185 to 249 m out, which the rule keeps because from the plate they are no higher than the near rim.

7. Home plate and center field

Everything is measured from home plate, so its position and the centre-field bearing set where the seats are and which way the zones face. They come from three places.

The spring park field finder

  1. Mound candidates. On a 0.5 m grid of ground returns, the mean height within 1.5 m of a cell minus the mean over a 4.5 to 9 m ring, where the ring is flat (standard deviation under 0.06 m) and the rise is 0.12 to 0.5 m (a mound is 10 in high by rule). Up to 60 local maxima within 120 m of the download anchor.
  2. The stadium field. A candidate qualifies if structures stand round it (90th percentile height of all returns 30 to 60 m away at least 4 m) and if the lidar intensity 6 to 40 m from it is mirror-symmetric about some line through it, with correlation at least 0.7 (an infield is: dirt cutouts, base paths, chalk, and the seating bowl round it). Of those within 80 m of the anchor, the nearest; only at Roger Dean did two qualify.
  3. Mound centre: the dz-weighted centroid of the bump above a plane fitted to a 4.5 to 9 m ring.
  4. Centre-field axis: the line through the mound centre that maximises the correlation between the intensity image and its mirror image, searched in 0.25 deg steps over 180 deg, then in 0.05 deg steps within 1 deg combined with a lateral offset of up to 1 m. Home is the end with the grandstand.
  5. Home plate: the mound centre projected onto the axis, then 59 ft (17.98 m) toward home, because the mound circle's centre is 59 ft from the plate's back point by rule:
plate = mound_on_axis - 17.98 m · ( sin cf , cos cf )

When the finder sees no mound. On the measure of step 1 the 17 mounds the finder chose rise 0.17 to 0.24 m. At LECOM Park, flown in November 2018, the mound was gone: nothing within 15 m of where it belongs rises more than 0.04 m. At Hammond Stadium, flown in June 2018, it is there (0.19 m), but those returns are not classified as ground, the only class step 1 reads. At these two parks the plate is the centre of the home-plate dirt circle, the darkest 13 ft disc within 4 m of a rough guess read off the lidar intensity image (the guess is recorded in the fetch script), placed on the symmetry axis found about the point 20 m from it toward the open field. At Hammond the mound, read from ground and unclassified returns together, is 0.4 m from where that plate and axis put it.

At every park the chosen axis correlates at 0.78 to 0.96 and the best axis more than 20 deg away at 0.27 to 0.55, so there is never a second candidate. The independent checks of the plate and bearing are in section 11.

8. The seats

A seat is a point on the seat surface; the viewer's eye is 1.1 m above it. Three kinds of seat surface are in use.

Generic bowl (21 MLB parks, 7 retractable-roof parks)

Along each bearing b from the plate the bowl runs from the field wall to 1.5 m short of the rim. The wall is the nearest of a backstop 18 m behind the plate and walls 15 m outside each foul line, capped so the bowl is at least 10 m deep. Seats rise in a straight line from 2.0 m above the field at the front to 3.0 m below the rim at the back, never steeper than 35 deg on average. A seat at depth d (0 = front row, 1 = last row):

dist(d) = front + d (back - front) z(d) = z_front + d (z_back - z_front) z_back = max( z_front, min( rim_height - 3.0, z_front + tan(35 deg) (back - front) ) )

Sutter Health Park

Twenty-two sections with estimated boundaries and row counts; each section's front distance, last-row distance and the heights of both are read from the lidar profile at the section's middle bearing; two or three overhang rows at the back of most sections are shaded all day by rule. VALIDATION.md sections 7 to 9.

Measured profile (the 19 mapped spring parks)

These bowls are low: behind home the last row the lidar sees is 1.9 to 5.2 m above the field in Arizona and 2.3 to 8.0 m in Florida (median at each park), under a rim 12 to 22 m high, where the generic bowl would put it 4.8 to 12.9 m higher. So the seat surface is measured:

  1. Every 2.5 deg of bearing, the median height of returns within 1 deg, in 0.5 m radial bins: on the foul side from 12 to 120 m from the plate; in fair territory, for the outfield seating, from 1 m behind the outfield wall to 200 m. Behind a see-through fence (no wall at MLB's published distance, and the first one more than 6 m beyond it) the wall found is something farther out, so the outfield profile starts 1 m behind the published fence line instead; between published points only beside such a point, because the line there is interpolated and an odd wall (JetBlue's) can sit metres outside it.
  2. Front: the first bin more than 0.5 m above the field. Front height: the median of the bins 1 to 2.5 m behind it (past the wall cap).
  3. Last row: the last bin before the height jumps more than 2.5 m within 1.5 m (a roof, a deck or a building), or drops more than 2.5 m within 1.5 m and does not come back within 4 m (the back of an open stand; a vomitory comes back).
  4. Cleaning, the foul side and fair territory separately: an entry counts only if its last row is at least 1 m up, at least 6 m behind its front and higher than its front, and in fair territory only if its front is at most 4 m up (a batter's eye or a scoreboard starts higher than any lawn or bleacher); a front under 0.3 m up takes the nearest good fronts on either side within 15 deg; each value is then the median of the good entries within 5 deg.
  5. A seat at depth d along bearing b sits on the plate ray at the same straight-line formulas as above with the measured front, last row and their heights, each interpolated linearly between the two 2.5 deg entries either side of b. No seats next to a missing entry.
Figure 3. Straight back from home plate at Salt River Fields (top) and Sloan Park (bottom). Dots: median lidar height in each 0.5 m step within 1 deg of the bearing, the numbers the seat profile reads. Orange line: the seats as modelled. Pink circle: the rim point in that direction. At Sloan Park a roof 15 m up starts 32 m from the plate and runs 20 m back: the lidar sees only the rows in front of it, so the covered rows are not on the map.

Plan views of all 19 mapped spring parks, drawn the same way as Figure 2 (click for full size), Arizona then Florida:

9. The shade test

Side view of the shade test: the eye 1.1 m above a seat, the rim at horizontal distance t and height H along the sun's azimuth, and a sun lower than the rim angle, so the seat is in shade. t eye: seat + 1.1 m seat rim, height H rim angle = atan((H − z_eye) / t) sun, altitude h: lower than the rim angle
Figure 4. Side view along the sun's azimuth. The seat is in shade when the rim, seen from the eye in the sun's direction, stands higher than the sun: here the rim angle is larger than the sun's altitude h, so the line to the sun hits the rim. t is the horizontal distance from the eye to the rim.

For a seat with eye at (x, y, zeye) and a sun at azimuth A and apparent altitude h > 0, walk in plan from the eye along u = (sin A, cos A). For every rim segment the walk crosses, at horizontal distance t > 0, read the rim height H at the crossing by linear interpolation along the segment. Then

shaded if and only if tan(h) < max over crossings of (H - z_eye) / t

A walk that crosses nothing sees open sky. A seat is also in shade when the sun is at or below the horizon ("sun is down"), or when it is an overhang row at Sutter Health Park (shaded all day by rule). For speed, rim segments are bucketed once per time step by their offset across the sun's direction, so each seat tests only the few segments its walk can cross; the result is checked equal to testing every segment.

The rim is a skyline seen from home plate, not a full 3-D model: a structure hidden behind a taller one from the plate's point of view is missing, even if it would shade seats beside it, and anything above a seat's row that is not on the skyline (an upper deck, a roof) is missing too. Sutter's overhang rows are the only exception.

10. From seats to the numbers

The map's zones are drawn schematically, the same shape at every zone park (the outfield bands just behind the park's published fence line); only the colors come from the park's geometry. The numbers do not depend on the drawing.

11. Checks

WhatAgainstResultWhere
Sun positionNOAA's own code; JPL HorizonsWithin 0.02 deg of NOAA at five parks and dates; JPL agrees to the same orderVALIDATION 2
Park clocksKnown UTC instants either side of every 2026 DST changeExact, including the hours that do not exist or repeatVALIDATION 3
Rim and sun together (21 MLB parks)The vendor's independent shadow calendar: the minute home plate goes into shade, on four dates75 of the 84 park-days comparable: median difference 1 min, 90th percentile 6 min, every gap over 6 min traced to a rim feature 1 to 3 deg wide. With the calendar's own rim method, this sun and clock reproduce all 84 to the minuteVALIDATION 4
Sutter Health Park shadeNine dated photos, 2025 and 2026Matches four midday photos, the 5:24 pm one and two evening ones; 30 to 45 min late on the third-base side around 4:30 to 5 pm, cause not foundVALIDATION 9
Spring park plate and bearingMLB's published fence distances (independent of the lidar method)47 of the 50 checks where the lidar shows a wall at the published distance are within 5 ft. Arizona: foul lines 19 of 20 within 4 ft, median −1 ft; center field 9 of 10 within 5 ft, median −3 ft (Goodyear +8 ft, where no wall is seen at the published 400 ft). Seven checks have no wall to see (see-through fences) and say nothingVALIDATION 10, Figure 5
Spring park plateThe home-plate dirt circle in lidar intensity (independent of the mound)Within 0.79 m at ten of the eleven mound-method parks where the circle is clear; 2.06 m at JetBlue ParkVALIDATION 10
Plate and bearing at Hammond Stadium (dirt-circle method)The mound in ground and unclassified returns, which the method did not use0.4 m from where the plate and axis put itVALIDATION 10
Spring park bearingMLB API azimuth (Salt River Fields only)72 deg against 72.8VALIDATION 10
Spring park shadeDated photosNot done yet
Figure 5. For each mapped spring park, the outfield wall seen from the lidar plate along the lidar bearing minus MLB's published distance: left-field line, center field and right-field line. The wall is the first 0.5 m step past 60 m holding at least three returns 1.2 to 8 m above the field that continues: two of the four neighbouring degrees have such a step within 3 m of it, so equipment left on the field is skipped (a degree where nothing continues has no wall). Each value is the median of three bearings (1 to 3 deg inside each foul line, and straightaway center plus or minus 1 deg). Hollow markers: the lidar shows no wall at the published distance (a see-through fence), so the step it finds is something farther back and the check says nothing. JetBlue Park's two filled misses are its odd wall: the published 420 ft is the deepest corner rather than straightaway center, and the right-field wall bends away from the pole. A plate 3 m off along the axis would move center field by about 10 ft.

12. Uncertainty and known errors

13. Reproducing it

Inputs that are not in the repository live outside it (on the machine that built the data, in ~/.cache/shademap/): the MLB snapshot, a clone of the vendor repository at dec934b, and the lidar caches. Software: Node 22, Python 3.13 with numpy 2.2, laspy 2.7 (lazrs backend) and pyproj 3.8; Pillow for the figures.

# The MLB snapshot and the vendor clone
mkdir -p ~/.cache/shademap/inputs
export SHADEMAP_SNAPSHOT=~/.cache/shademap/inputs/teams-2026-10-01.json
export SHADEMAP_VENDOR=~/.cache/shademap/inputs/ballpark-shadow-geometry
curl -o $SHADEMAP_SNAPSHOT 'https://statsapi.mlb.com/api/v1/teams?sportId=1&hydrate=venue(location,fieldInfo,timezone),springVenue(location,fieldInfo,timezone)'
git clone https://github.com/apelt001/ballpark-shadow-geometry $SHADEMAP_VENDOR
git -C $SHADEMAP_VENDOR checkout dec934ba48b4fdc2f01ac18c4495610e9595e784

# Spring park lidar (about 45 min of downloads), then the measurement (about 5 min;
# it reads the published fence distances from the snapshot)
python scripts/shademap/spring_lidar_fetch.py --cache ~/.cache/shademap/spring/lidar
python scripts/shademap/spring_lidar.py --cache ~/.cache/shademap/spring/lidar --snapshot $SHADEMAP_SNAPSHOT

# Park data from the snapshot, the vendor clone and the two lidar files
node scripts/shademap/build_parks.mjs --snapshot $SHADEMAP_SNAPSHOT --vendor $SHADEMAP_VENDOR

# The wall check, then the figures on this page
python scripts/shademap/wall_check.py --cache ~/.cache/shademap/spring/lidar
python scripts/shademap/spring_figures.py --cache ~/.cache/shademap/spring/lidar
node scripts/shademap/method_figures.mjs

# Tests, including a byte-for-byte rebuild of parks.json and rims.json, which runs only
# when the two inputs above are present (the variables, or those default paths)
npm test

The measurement is deterministic: re-running it reproduces spring-lidar.json byte for byte, and the test suite rebuilds parks.json and rims.json byte for byte from the pinned inputs when they are present; without them that one test reports itself skipped rather than failing. A fresh API call can drift from the 2026-10-01 snapshot over time (a renamed park, a new fence distance), and then the rebuild differs where the API did; the snapshot date is recorded in parks.json.

14. Sources and licenses

I built the Shade Map pairing with Claude Code. The code, the data and these figures are in the site's repository under public/shademap/ and scripts/shademap/.