Goal
Build the first real Player analytics vertical slice on top of the Player directory and persisted PlayerSeasonHitting foundation.
The page should answer one clear baseball question:
What did this player's overall offensive season look like?
This issue begins Player analytics after the UI/information-architecture work in #30. Keep the first slice intentionally small, season-level, DB-only, and based only on data already persisted locally.
Route
Add a real Player hitting destination using shareable query parameters, preferably:
/players/hitting?season=2025&player_id=592450
Do not add a generic Player-detail routing framework.
The Player directory should provide a real path into this page once a Player is selected.
Data source
Use the existing persisted Player foundation:
players
player_seasons
player_season_hitting
get_player(...)
get_player_season_hitting(...)
Normal browser rendering must remain DB-only.
Do not add new MLB ingestion, schema migrations, live API calls, or team-stint modeling in this issue.
Season and membership semantics
A Player hitting page must be tied to a real persisted Player/season selection.
player_id and season must be explicit and shareable.
- Player identity alone does not establish season membership.
- Preserve the Player catalog's season-membership semantics.
- If the Player is not in the selected season's persisted catalog, render the established useful not-found state.
- If catalog membership exists but hitting data has not been stored for that Player/season, render a clear data-unavailable state rather than fabricating zeroes or calling MLB.
Overview metrics
Derive familiar season-level hitting rates from the raw persisted counting stats rather than persisting duplicate rate statistics.
Candidate overview metrics:
- Games
- Plate Appearances
- AVG
- OBP
- SLG
- OPS
- Home Runs
- Walks
- Strikeouts
- Stolen Bases
Use standard baseball formulas from the stored components and handle zero denominators explicitly.
Do not silently coerce an undefined rate to zero.
First visualization
If the existing chart architecture supports it cleanly, add one small, interpretable Player visualization based on a shared denominator:
- K% = strikeouts / plate appearances
- BB% = walks / plate appearances
- HR% = home runs / plate appearances
The chart should be described as a plate-appearance rate profile, not as a ranking or quality score.
Do not mix AVG, OBP, SLG, OPS, and counting stats into an arbitrary bar chart merely because they are available.
If the rate-profile visualization proves awkward or misleading during implementation, keep the page to the season overview and document why rather than forcing a chart.
Navigation / Player UI
Extend the real Player domain established in #56 without redesigning Team analytics.
The Player directory should remain the selection/search home.
Once a Player is selected and hitting data exists, provide a clear link to the hitting overview.
Establish Player-domain navigation only to the extent required by real destinations. Do not add dead future Player metric links.
Analytics ownership
Derived baseball calculations belong under app/analytics/, not in routes, templates, or repositories.
Prefer a small typed Player hitting analysis model/function over passing raw persistence models directly into presentation logic.
Keep routes thin.
Presentation
Follow the existing server-rendered FastAPI/Jinja/Plotly visual language:
- existing shell and primary navigation
- cards and typography
- responsive layout
- semantic HTML
- visible keyboard focus
- local Plotly only if a chart is included
Make clear that the page represents a full-season aggregate. It is not a game-log trend and does not provide historical team-stint detail.
Testing
Cover at minimum:
- correct AVG/OBP/SLG/OPS formulas
- correct PA-rate calculations if the visualization is included
- zero-denominator behavior
- selected Player must belong to the selected catalog season
- missing hitting data produces a useful state
- existing stored hitting data renders without MLB calls
- shareable
season and player_id
- directory-to-hitting navigation
- Player primary navigation remains correct
- Team routes and
/health remain unchanged
- browser rendering remains DB-only
Out of scope
- Player game logs
- rolling Player trends
- multi-season trend charts
- Player-vs-MLB context
- leaderboards/rankings
- Player comparisons
- Player pitching
- team-stint history
- new Player ingestion
- schema migrations
- generic metric/dashboard frameworks
- route migrations for existing Team pages
Completion
A user can search/select a locally stored Player, navigate to a real Player season hitting overview, understand the Player's season-level offensive line from persisted data, and encounter honest empty/error states when the local database cannot support the view.
This should be the first Player analytics slice, not the entire Player analytics roadmap.
Goal
Build the first real Player analytics vertical slice on top of the Player directory and persisted
PlayerSeasonHittingfoundation.The page should answer one clear baseball question:
This issue begins Player analytics after the UI/information-architecture work in #30. Keep the first slice intentionally small, season-level, DB-only, and based only on data already persisted locally.
Route
Add a real Player hitting destination using shareable query parameters, preferably:
Do not add a generic Player-detail routing framework.
The Player directory should provide a real path into this page once a Player is selected.
Data source
Use the existing persisted Player foundation:
playersplayer_seasonsplayer_season_hittingget_player(...)get_player_season_hitting(...)Normal browser rendering must remain DB-only.
Do not add new MLB ingestion, schema migrations, live API calls, or team-stint modeling in this issue.
Season and membership semantics
A Player hitting page must be tied to a real persisted Player/season selection.
player_idandseasonmust be explicit and shareable.Overview metrics
Derive familiar season-level hitting rates from the raw persisted counting stats rather than persisting duplicate rate statistics.
Candidate overview metrics:
Use standard baseball formulas from the stored components and handle zero denominators explicitly.
Do not silently coerce an undefined rate to zero.
First visualization
If the existing chart architecture supports it cleanly, add one small, interpretable Player visualization based on a shared denominator:
The chart should be described as a plate-appearance rate profile, not as a ranking or quality score.
Do not mix AVG, OBP, SLG, OPS, and counting stats into an arbitrary bar chart merely because they are available.
If the rate-profile visualization proves awkward or misleading during implementation, keep the page to the season overview and document why rather than forcing a chart.
Navigation / Player UI
Extend the real Player domain established in #56 without redesigning Team analytics.
The Player directory should remain the selection/search home.
Once a Player is selected and hitting data exists, provide a clear link to the hitting overview.
Establish Player-domain navigation only to the extent required by real destinations. Do not add dead future Player metric links.
Analytics ownership
Derived baseball calculations belong under
app/analytics/, not in routes, templates, or repositories.Prefer a small typed Player hitting analysis model/function over passing raw persistence models directly into presentation logic.
Keep routes thin.
Presentation
Follow the existing server-rendered FastAPI/Jinja/Plotly visual language:
Make clear that the page represents a full-season aggregate. It is not a game-log trend and does not provide historical team-stint detail.
Testing
Cover at minimum:
seasonandplayer_id/healthremain unchangedOut of scope
Completion
A user can search/select a locally stored Player, navigate to a real Player season hitting overview, understand the Player's season-level offensive line from persisted data, and encounter honest empty/error states when the local database cannot support the view.
This should be the first Player analytics slice, not the entire Player analytics roadmap.