# Visual Fixes + Spray/Bat Tracking Cleanup Ship Summary

Date: 2026-05-17

## Commit Ladder

- `7929ceb` - Visual Fix Step 1: Daily page diagnosis and content layout
- `4814725` - Visual Fix Step 2: pitch arsenal current-season refresh
- `301f3e3` - Visual Fix Step 3: spray chart direction bar
- `d56450f` - Visual Fix Step 4: bat tracking vertical layout
- Visual Fix Step 5: verification and ship summary - this commit

## What Shipped

### Daily Page Diagnosis + Layout Fix

The Daily page was not actually empty. Playwright and loader inspection showed the loader returned real content: 15 slate games, a lede game, 3 write-ups, 8 starter matchups, 6 pitcher-form rows, and 3 conditions/callout rows. The rendered page looked empty because the Daily rail was in normal document flow at viewport height, pushing the populated content below the fold.

Fix:

- Converted the Daily frame into a grid layout so the rail and content render side by side.
- Collapsed the rail/content layout cleanly on mobile.
- Reworked oversized write-up stat typography into compact definition-list stat rows.

Diagnostic screenshots:

- `outputs/diagnostics/daily_2026-05-17.png`
- `outputs/diagnostics/daily_2026-05-17_fixed_final_1280.png`
- `outputs/diagnostics/daily_2026-05-17_fixed4_375.png`

### Pitch Arsenal Current-Season Refresh

Investigation found the profile threshold logic was already reasonable: current-season arsenal is used when the latest arsenal file has at least 100 tracked pitches and 2 pitch types, otherwise it falls back to the prior season. The actual issue was that no 2026 pitcher arsenal file existed yet, even though 2026 pitcher seasonal Statcast data showed Cam Schlittler with 63.1 IP and 960 pitches.

Fix:

- Extended `fetch_statcast_seasonal_current.py` to emit current-season pitcher arsenal artifacts alongside current-season hitter/pitcher seasonal leaderboards.
- Generated `arsenal_2026.parquet` locally from existing Statcast current-season data.
- Verified Schlittler now shows 2026 pitch-level arsenal aggregates with 6 pitch types and 957 classified pitches.

Sample chart outputs:

- `outputs/chart_engine_samples/pitch_arsenal_schlittler_2026.svg`
- `outputs/chart_engine_samples/pitch_movement_schlittler_2026.svg`

### Spray Viz Matches Available Data

The old spray chart rendered three aggregate direction buckets as dots on a field polygon. That was honestly labeled, but visually implied per-ball landing coordinates that the data layer does not have.

Decision:

- Chose Option A, a horizontal stacked direction bar, because the available data is aggregate pull/center/oppo percentages.
- Removed the aggregate-dot field rendering path for this chart type.
- Added in-chart disclosure that true landing-coordinate spray charts require event-level batted-ball data.

Sample chart output:

- `outputs/chart_engine_samples/spray_direction_bar_caminero.svg`

### Bat Tracking Vertical Layout

The bat-tracking section previously used the generic 3-column feature grid, leaving Swing Path, Attack Angle, and Bat Speed too narrow to read. The section now uses a dedicated `mm-chart-stack--bat` vertical layout on player and projection-player pages.

Decision:

- Kept the bat-tracking stack vertical rather than restoring a wide-screen 3-column layout. Even around 1200px, the profile rail and gutters leave the charts cramped, so readability wins over density here.

Sample chart outputs:

- `outputs/chart_engine_samples/bat_tracking_swing_path_caminero.svg`
- `outputs/chart_engine_samples/bat_tracking_attack_angle_caminero.svg`
- `outputs/chart_engine_samples/bat_tracking_bat_speed_caminero.svg`

## Verification

- Affected route checks:
  - `/daily` returned 200 and contained Slate strip, Pitching, and Write-ups content.
  - `/players/693645` returned 200 and contained 2026 pitch-level arsenal aggregates.
  - `/projections/pitcher/693645` returned 200 and contained 2026 pitch-level arsenal aggregates.
  - `/players/691406` returned 200 and contained aggregate direction buckets plus vertical bat-tracking stack markup.
  - `/projections/hitter/691406` returned 200 and contained aggregate direction buckets plus vertical bat-tracking stack markup.
- Tests: `python -m unittest discover tests` ran 87 tests, all passing.
- Route timing profile:
  - import app: 2.674s
  - create app: 0.038s
  - `/`: 1.640s
  - `/daily`: 0.415s
  - `/projections`: 0.645s
  - `/leaderboards`: 0.071s
  - `/methods`: 2.782s
  - `/betting/consensus`: 0.903s
  - `/design-system`: 0.016s
- Split scheduler check: forced `morning_core` cycle completed successfully in 124.585s with all tasks successful.
- Chalk discipline: template-level chalk usage remains isolated to `daily.html`; shared token/style definitions remain in the CSS system.
- OneDrive runtime-path scan: zero hits in Python/JSON runtime files.
- Generated artifacts stayed uncommitted under ignored `data/` and `outputs/` paths.

## Carried Debts

- Projected game totals around 17.2 / 16.0 are systematically about 2x league-average run environment. The game-totals output needs model magnitude calibration in a future modeling milestone.
- True per-ball spray charts require a Statcast event-level batted-ball landing-coordinate data layer.
- Defense, baserunning, and stealing percentile metrics are still queued as a separate milestone.
- Rolling projections / Bayesian updating remains a separate milestone.
- True 50/100 PA rolling-window xwOBA charts require a per-PA event log data layer.
- `run_daily_mithrandir` remains a long-running postgame/card-generation path, carried from the scheduler performance milestone.

## Explicit Non-Goals

- No new chart-type family was added.
- No broad new data source family was introduced; the current-season arsenal output reuses the existing current-season Statcast pull.
- No rolling projections work was done.
- No defense or baserunning percentile work was done.
