bbdata-cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ import { registerTemplate, type QueryTemplate } from './registry.js';
2
+ import type { PitchData } from '../../adapters/types.js';
3
+
4
+ /**
5
+ * Raw batted-ball projection — one row per ball in play with hit coordinates.
6
+ * Powers the spray chart. Filters to pitches where launch_speed is recorded
7
+ * (i.e., the ball was actually put in play).
8
+ */
9
+ const template: QueryTemplate = {
10
+ id: 'hitter-raw-bip',
11
+ name: 'Hitter Raw Batted Balls',
12
+ category: 'hitter',
13
+ description: 'One row per batted ball with hit coordinates, exit velo, and launch angle',
14
+ preferredSources: ['savant'],
15
+ requiredParams: ['player'],
16
+ optionalParams: ['season'],
17
+ examples: [
18
+ 'bbdata query hitter-raw-bip --player "Aaron Judge" --season 2025 --format json',
19
+ ],
20
+
21
+ buildQuery(params) {
22
+ return {
23
+ player_name: params.player,
24
+ season: params.season ?? new Date().getFullYear(),
25
+ stat_type: 'batting',
26
+ };
27
+ },
28
+
29
+ columns() {
30
+ return [
31
+ 'hc_x',
32
+ 'hc_y',
33
+ 'launch_speed',
34
+ 'launch_angle',
35
+ 'events',
36
+ 'bb_type',
37
+ 'game_date',
38
+ ];
39
+ },
40
+
41
+ transform(data) {
42
+ const pitches = data as PitchData[];
43
+ if (pitches.length === 0) return [];
44
+
45
+ return pitches
46
+ .filter(
47
+ (p) =>
48
+ p.launch_speed != null &&
49
+ p.launch_speed > 0 &&
50
+ p.hc_x != null &&
51
+ p.hc_y != null,
52
+ )
53
+ .map((p) => ({
54
+ hc_x: p.hc_x,
55
+ hc_y: p.hc_y,
56
+ launch_speed: p.launch_speed,
57
+ launch_angle: p.launch_angle,
58
+ events: p.events ?? 'unknown',
59
+ bb_type: p.bb_type ?? 'unknown',
60
+ game_date: p.game_date,
61
+ }));
62
+ },
63
+ };
64
+
65
+ registerTemplate(template);
@@ -0,0 +1,90 @@
1
+ import { registerTemplate, type QueryTemplate } from './registry.js';
2
+ import type { PitchData } from '../../adapters/types.js';
3
+
4
+ // 3x3 strike zone grid (copied from hitter-hot-cold-zones.ts)
5
+ // row: 0 = high, 2 = low. col: 0 = inside (catcher POV left), 2 = outside.
6
+ const ZONES: { name: string; row: number; col: number; xMin: number; xMax: number; zMin: number; zMax: number }[] = [
7
+ { name: 'High-In', row: 0, col: 0, xMin: -0.83, xMax: -0.28, zMin: 2.83, zMax: 3.5 },
8
+ { name: 'High-Mid', row: 0, col: 1, xMin: -0.28, xMax: 0.28, zMin: 2.83, zMax: 3.5 },
9
+ { name: 'High-Out', row: 0, col: 2, xMin: 0.28, xMax: 0.83, zMin: 2.83, zMax: 3.5 },
10
+ { name: 'Mid-In', row: 1, col: 0, xMin: -0.83, xMax: -0.28, zMin: 2.17, zMax: 2.83 },
11
+ { name: 'Mid-Mid', row: 1, col: 1, xMin: -0.28, xMax: 0.28, zMin: 2.17, zMax: 2.83 },
12
+ { name: 'Mid-Out', row: 1, col: 2, xMin: 0.28, xMax: 0.83, zMin: 2.17, zMax: 2.83 },
13
+ { name: 'Low-In', row: 2, col: 0, xMin: -0.83, xMax: -0.28, zMin: 1.5, zMax: 2.17 },
14
+ { name: 'Low-Mid', row: 2, col: 1, xMin: -0.28, xMax: 0.28, zMin: 1.5, zMax: 2.17 },
15
+ { name: 'Low-Out', row: 2, col: 2, xMin: 0.28, xMax: 0.83, zMin: 1.5, zMax: 2.17 },
16
+ ];
17
+
18
+ /**
19
+ * 3x3 zone grid with numeric row/col indices and averaged xwOBA.
20
+ * Powers the zone-profile heatmap visualization. Unlike hitter-hot-cold-zones
21
+ * which returns strings for human display, this returns numerics for charting.
22
+ */
23
+ const template: QueryTemplate = {
24
+ id: 'hitter-zone-grid',
25
+ name: 'Hitter Zone Grid (numeric)',
26
+ category: 'hitter',
27
+ description: '3x3 strike zone grid with numeric row/col/xwoba for heatmap visualization',
28
+ preferredSources: ['savant'],
29
+ requiredParams: ['player'],
30
+ optionalParams: ['season'],
31
+ examples: [
32
+ 'bbdata query hitter-zone-grid --player "Shohei Ohtani" --format json',
33
+ ],
34
+
35
+ buildQuery(params) {
36
+ return {
37
+ player_name: params.player,
38
+ season: params.season ?? new Date().getFullYear(),
39
+ stat_type: 'batting',
40
+ };
41
+ },
42
+
43
+ columns() {
44
+ return ['zone', 'row', 'col', 'pitches', 'xwoba'];
45
+ },
46
+
47
+ transform(data) {
48
+ const pitches = data as PitchData[];
49
+ if (pitches.length === 0) return [];
50
+
51
+ return ZONES.map((z) => {
52
+ const inZone = pitches.filter(
53
+ (p) =>
54
+ p.plate_x >= z.xMin && p.plate_x < z.xMax &&
55
+ p.plate_z >= z.zMin && p.plate_z < z.zMax,
56
+ );
57
+
58
+ // Compute actual xwOBA across all plate-appearance-ending events in
59
+ // this zone (not just balls in play). Walks and HBPs use their canonical
60
+ // wOBA weights; strikeouts contribute zero; batted balls use the
61
+ // Statcast expected wOBA for that event.
62
+ const paEnding = inZone.filter((p) => p.events != null);
63
+ let xwobaSum = 0;
64
+ for (const p of paEnding) {
65
+ if (p.events === 'walk') {
66
+ xwobaSum += 0.69;
67
+ } else if (p.events === 'hit_by_pitch') {
68
+ xwobaSum += 0.72;
69
+ } else if (p.events === 'strikeout') {
70
+ xwobaSum += 0;
71
+ } else {
72
+ // Batted ball: use Statcast estimated wOBA for the contact event
73
+ xwobaSum += p.estimated_woba ?? 0;
74
+ }
75
+ }
76
+ const xwoba = paEnding.length > 0 ? xwobaSum / paEnding.length : 0;
77
+
78
+ return {
79
+ zone: z.name,
80
+ row: z.row,
81
+ col: z.col,
82
+ pitches: inZone.length,
83
+ pa: paEnding.length,
84
+ xwoba: Number(xwoba.toFixed(3)),
85
+ };
86
+ });
87
+ },
88
+ };
89
+
90
+ registerTemplate(template);
@@ -1,24 +1,27 @@
1
- // Import all templates to trigger registration
2
- import './pitcher-arsenal.js';
3
- import './pitcher-velocity-trend.js';
4
- import './pitcher-handedness-splits.js';
5
- import './hitter-batted-ball.js';
6
- import './hitter-vs-pitch-type.js';
7
- import './hitter-hot-cold-zones.js';
8
- import './matchup-pitcher-vs-hitter.js';
9
- import './matchup-situational.js';
10
- import './leaderboard-custom.js';
11
- import './leaderboard-comparison.js';
12
- import './trend-rolling-average.js';
13
- import './trend-year-over-year.js';
14
-
15
- export {
16
- getTemplate,
17
- getAllTemplates,
18
- getTemplatesByCategory,
19
- listTemplates,
20
- type QueryTemplate,
21
- type QueryTemplateParams,
22
- type QueryResult,
23
- type QueryCategory,
24
- } from './registry.js';
1
+ // Import all templates to trigger registration
2
+ import './pitcher-arsenal.js';
3
+ import './pitcher-velocity-trend.js';
4
+ import './pitcher-handedness-splits.js';
5
+ import './hitter-batted-ball.js';
6
+ import './hitter-vs-pitch-type.js';
7
+ import './hitter-hot-cold-zones.js';
8
+ import './matchup-pitcher-vs-hitter.js';
9
+ import './matchup-situational.js';
10
+ import './leaderboard-custom.js';
11
+ import './leaderboard-comparison.js';
12
+ import './trend-rolling-average.js';
13
+ import './trend-year-over-year.js';
14
+ import './pitcher-raw-pitches.js';
15
+ import './hitter-raw-bip.js';
16
+ import './hitter-zone-grid.js';
17
+
18
+ export {
19
+ getTemplate,
20
+ getAllTemplates,
21
+ getTemplatesByCategory,
22
+ listTemplates,
23
+ type QueryTemplate,
24
+ type QueryTemplateParams,
25
+ type QueryResult,
26
+ type QueryCategory,
27
+ } from './registry.js';
@@ -0,0 +1,62 @@
1
+ import { registerTemplate, type QueryTemplate } from './registry.js';
2
+ import type { PitchData } from '../../adapters/types.js';
3
+
4
+ /**
5
+ * Raw pitch-level projection — one row per pitch, coordinate columns preserved.
6
+ * Powers the pitch-movement visualization (pfx_x, pfx_z scatter).
7
+ * Unlike pitcher-arsenal this does NOT aggregate; viz builders need per-pitch points.
8
+ */
9
+ const template: QueryTemplate = {
10
+ id: 'pitcher-raw-pitches',
11
+ name: 'Pitcher Raw Pitches',
12
+ category: 'pitcher',
13
+ description: 'One row per pitch with coordinate columns for visualization (movement, location)',
14
+ preferredSources: ['savant'],
15
+ requiredParams: ['player'],
16
+ optionalParams: ['season', 'pitchType'],
17
+ examples: [
18
+ 'bbdata query pitcher-raw-pitches --player "Corbin Burnes" --season 2025 --format json',
19
+ ],
20
+
21
+ buildQuery(params) {
22
+ return {
23
+ player_name: params.player,
24
+ season: params.season ?? new Date().getFullYear(),
25
+ stat_type: 'pitching',
26
+ pitch_type: params.pitchType ? [params.pitchType] : undefined,
27
+ };
28
+ },
29
+
30
+ columns() {
31
+ return [
32
+ 'pitch_type',
33
+ 'release_speed',
34
+ 'release_spin_rate',
35
+ 'pfx_x',
36
+ 'pfx_z',
37
+ 'plate_x',
38
+ 'plate_z',
39
+ 'game_date',
40
+ ];
41
+ },
42
+
43
+ transform(data) {
44
+ const pitches = data as PitchData[];
45
+ if (pitches.length === 0) return [];
46
+
47
+ return pitches
48
+ .filter((p) => p.pitch_type)
49
+ .map((p) => ({
50
+ pitch_type: p.pitch_type,
51
+ release_speed: p.release_speed,
52
+ release_spin_rate: p.release_spin_rate,
53
+ pfx_x: p.pfx_x,
54
+ pfx_z: p.pfx_z,
55
+ plate_x: p.plate_x,
56
+ plate_z: p.plate_z,
57
+ game_date: p.game_date,
58
+ }));
59
+ },
60
+ };
61
+
62
+ registerTemplate(template);
@@ -22,7 +22,7 @@ const template: QueryTemplate = {
22
22
  },
23
23
 
24
24
  columns() {
25
- return ['Window', 'Games', 'AVG', 'SLG', 'K %', 'Avg EV', 'Hard Hit %'];
25
+ return ['Window', 'Window End', 'Games', 'AVG', 'SLG', 'K %', 'Avg EV', 'Hard Hit %'];
26
26
  },
27
27
 
28
28
  transform(data) {
@@ -41,7 +41,16 @@ const template: QueryTemplate = {
41
41
  const windowSize = 15;
42
42
 
43
43
  if (dates.length < windowSize) {
44
- return [{ Window: 'Insufficient data', Games: dates.length, AVG: '—', SLG: '—', 'K %': '—', 'Avg EV': '—', 'Hard Hit %': '—' }];
44
+ return [{
45
+ Window: 'Insufficient data',
46
+ 'Window End': '',
47
+ Games: dates.length,
48
+ AVG: '—',
49
+ SLG: '—',
50
+ 'K %': '—',
51
+ 'Avg EV': '—',
52
+ 'Hard Hit %': '—',
53
+ }];
45
54
  }
46
55
 
47
56
  // Calculate rolling windows
@@ -68,8 +77,11 @@ const template: QueryTemplate = {
68
77
  : null;
69
78
  const hardHit = batted.filter((p) => p.launch_speed! >= 95).length;
70
79
 
80
+ const windowEnd = windowDates[windowDates.length - 1]!;
81
+
71
82
  results.push({
72
- Window: `${windowDates[0]} → ${windowDates[windowDates.length - 1]}`,
83
+ Window: `${windowDates[0]} → ${windowEnd}`,
84
+ 'Window End': windowEnd,
73
85
  Games: windowDates.length,
74
86
  AVG: pas.length > 0 ? (hits.length / pas.length).toFixed(3) : '—',
75
87
  SLG: pas.length > 0 ? (totalBases / pas.length).toFixed(3) : '—',
@@ -1,60 +1,66 @@
1
- # Advance Report: {{player}} (SP)
2
-
3
- **Season:** {{season}} | **For:** {{audience}} | **Generated:** {{date}}
4
-
5
- ---
6
-
7
- ## Recent Form
8
-
9
- *Last 5 starts — to be populated with game log data*
10
-
11
- ## Pitch Mix & Sequencing
12
-
13
- {{#if data.pitcher-arsenal}}
14
- ### Arsenal Overview
15
-
16
- | Pitch | Usage | Velo | Spin | Whiff % |
17
- |-------|-------|------|------|---------|
18
- {{#each data.pitcher-arsenal}}
19
- | {{this.[Pitch Type]}} | {{this.[Usage %]}} | {{this.[Avg Velo]}} | {{this.[Avg Spin]}} | {{this.[Whiff %]}} |
20
- {{/each}}
21
-
22
- ### By Count (to be populated)
23
- - **Ahead in count:** Primary pitch selection
24
- - **Behind in count:** Go-to pitch
25
- - **Two-strike:** Put-away pitch
26
- {{else}}
27
- *Arsenal data not available*
28
- {{/if}}
29
-
30
- ## Times Through the Order
31
-
32
- *3rd time adjustment, velocity drop patterns — to be analyzed from pitch-level data*
33
-
34
- ## Platoon Vulnerabilities
35
-
36
- {{#if data.pitcher-handedness-splits}}
37
- | Split | PA | AVG | SLG | K % | BB % |
38
- |-------|-----|-----|-----|-----|------|
39
- {{#each data.pitcher-handedness-splits}}
40
- | {{this.vs}} | {{this.PA}} | {{this.AVG}} | {{this.SLG}} | {{this.[K %]}} | {{this.[BB %]}} |
41
- {{/each}}
42
- {{else}}
43
- *Splits data not available*
44
- {{/if}}
45
-
46
- ## How to Attack
47
-
48
- ### Early in Game (1st time through)
49
- - *To be filled: patient approach? aggressive?*
50
-
51
- ### Late in Game (3rd+ time through)
52
- - *To be filled: exploit fatigue patterns?*
53
-
54
- ### Key Weaknesses
55
- - *To be filled by evaluator*
56
-
57
- ---
58
-
59
- *Generated by bbdata CLI · Data sources: {{sources}}*
60
- *This is a 1-page advance report designed for in-game use.*
1
+ # Advance Report: {{player}} (SP)
2
+
3
+ **Season:** {{season}} | **For:** {{audience}} | **Generated:** {{date}}
4
+
5
+ ---
6
+
7
+ ## Recent Form
8
+
9
+ *Last 5 starts — to be populated with game log data*
10
+
11
+ ## Pitch Mix & Sequencing
12
+
13
+ {{#if data.pitcher-arsenal}}
14
+ ### Arsenal Overview
15
+
16
+ | Pitch | Usage | Velo | Spin | Whiff % |
17
+ |-------|-------|------|------|---------|
18
+ {{#each data.pitcher-arsenal}}
19
+ | {{this.[Pitch Type]}} | {{this.[Usage %]}} | {{this.[Avg Velo]}} | {{this.[Avg Spin]}} | {{this.[Whiff %]}} |
20
+ {{/each}}
21
+
22
+ ### By Count (to be populated)
23
+ - **Ahead in count:** Primary pitch selection
24
+ - **Behind in count:** Go-to pitch
25
+ - **Two-strike:** Put-away pitch
26
+ {{else}}
27
+ *Arsenal data not available*
28
+ {{/if}}
29
+
30
+ {{#if graphs.movementChart}}
31
+ ### Pitch Movement Profile
32
+
33
+ {{{svgOrEmpty graphs.movementChart}}}
34
+ {{/if}}
35
+
36
+ ## Times Through the Order
37
+
38
+ *3rd time adjustment, velocity drop patterns — to be analyzed from pitch-level data*
39
+
40
+ ## Platoon Vulnerabilities
41
+
42
+ {{#if data.pitcher-handedness-splits}}
43
+ | Split | PA | AVG | SLG | K % | BB % |
44
+ |-------|-----|-----|-----|-----|------|
45
+ {{#each data.pitcher-handedness-splits}}
46
+ | {{this.vs}} | {{this.PA}} | {{this.AVG}} | {{this.SLG}} | {{this.[K %]}} | {{this.[BB %]}} |
47
+ {{/each}}
48
+ {{else}}
49
+ *Splits data not available*
50
+ {{/if}}
51
+
52
+ ## How to Attack
53
+
54
+ ### Early in Game (1st time through)
55
+ - *To be filled: patient approach? aggressive?*
56
+
57
+ ### Late in Game (3rd+ time through)
58
+ - *To be filled: exploit fatigue patterns?*
59
+
60
+ ### Key Weaknesses
61
+ - *To be filled by evaluator*
62
+
63
+ ---
64
+
65
+ *Generated by bbdata CLI · Data sources: {{sources}}*
66
+ *This is a 1-page advance report designed for in-game use.*
@@ -1,65 +1,77 @@
1
- # Pro Hitter Evaluation: {{player}}
2
-
3
- **Season:** {{season}} | **Audience:** {{audience}} | **Generated:** {{date}}
4
-
5
- ---
6
-
7
- ## Batted Ball Profile
8
-
9
- {{#if data.hitter-batted-ball}}
10
- | Metric | Value |
11
- |--------|-------|
12
- {{#each data.hitter-batted-ball}}
13
- | {{this.Metric}} | {{this.Value}} |
14
- {{/each}}
15
- {{else}}
16
- *Batted ball data not available*
17
- {{/if}}
18
-
19
- ## Approach & Discipline
20
-
21
- {{#if data.hitter-vs-pitch-type}}
22
- | Pitch Type | Seen | Swing % | Whiff % | Foul % | In Play | Avg EV | SLG |
23
- |------------|------|---------|---------|--------|---------|--------|-----|
24
- {{#each data.hitter-vs-pitch-type}}
25
- | {{this.[Pitch Type]}} | {{this.Seen}} | {{this.[Swing %]}} | {{this.[Whiff %]}} | {{this.[Foul %]}} | {{this.[In Play]}} | {{this.[Avg EV]}} | {{this.SLG}} |
26
- {{/each}}
27
- {{else}}
28
- *Pitch type breakdown not available*
29
- {{/if}}
30
-
31
- ## Hot/Cold Zones
32
-
33
- {{#if data.hitter-hot-cold-zones}}
34
- | Zone | Pitches | Swings | Whiff % | AVG | SLG |
35
- |------|---------|--------|---------|-----|-----|
36
- {{#each data.hitter-hot-cold-zones}}
37
- | {{this.Zone}} | {{this.Pitches}} | {{this.Swings}} | {{this.[Whiff %]}} | {{this.AVG}} | {{this.SLG}} |
38
- {{/each}}
39
- {{else}}
40
- *Zone data not available*
41
- {{/if}}
42
-
43
- ## Splits Analysis
44
-
45
- *To be populated: vs LHP/RHP, home/away*
46
-
47
- ## Trend Analysis
48
-
49
- *To be populated: rolling average trends*
50
-
51
- ## Risk Assessment
52
-
53
- *To be filled by evaluator: injury history, age curve, contract implications*
54
-
55
- ## Comparable Player
56
-
57
- *To be filled by evaluator: statistical comp*
58
-
59
- ## Role Projection
60
-
61
- *To be filled by evaluator: lineup position, expected production, Acquire / Pass / Monitor*
62
-
63
- ---
64
-
65
- *Generated by bbdata CLI · Data sources: {{sources}}*
1
+ # Pro Hitter Evaluation: {{player}}
2
+
3
+ **Season:** {{season}} | **Audience:** {{audience}} | **Generated:** {{date}}
4
+
5
+ ---
6
+
7
+ ## Batted Ball Profile
8
+
9
+ {{#if data.hitter-batted-ball}}
10
+ | Metric | Value |
11
+ |--------|-------|
12
+ {{#each data.hitter-batted-ball}}
13
+ | {{this.Metric}} | {{this.Value}} |
14
+ {{/each}}
15
+ {{else}}
16
+ *Batted ball data not available*
17
+ {{/if}}
18
+
19
+ {{#if graphs.sprayChart}}
20
+ ### Spray Chart
21
+
22
+ {{{svgOrEmpty graphs.sprayChart}}}
23
+ {{/if}}
24
+
25
+ ## Approach & Discipline
26
+
27
+ {{#if data.hitter-vs-pitch-type}}
28
+ | Pitch Type | Seen | Swing % | Whiff % | Foul % | In Play | Avg EV | SLG |
29
+ |------------|------|---------|---------|--------|---------|--------|-----|
30
+ {{#each data.hitter-vs-pitch-type}}
31
+ | {{this.[Pitch Type]}} | {{this.Seen}} | {{this.[Swing %]}} | {{this.[Whiff %]}} | {{this.[Foul %]}} | {{this.[In Play]}} | {{this.[Avg EV]}} | {{this.SLG}} |
32
+ {{/each}}
33
+ {{else}}
34
+ *Pitch type breakdown not available*
35
+ {{/if}}
36
+
37
+ ## Hot/Cold Zones
38
+
39
+ {{#if data.hitter-hot-cold-zones}}
40
+ | Zone | Pitches | Swings | Whiff % | AVG | SLG |
41
+ |------|---------|--------|---------|-----|-----|
42
+ {{#each data.hitter-hot-cold-zones}}
43
+ | {{this.Zone}} | {{this.Pitches}} | {{this.Swings}} | {{this.[Whiff %]}} | {{this.AVG}} | {{this.SLG}} |
44
+ {{/each}}
45
+ {{else}}
46
+ *Zone data not available*
47
+ {{/if}}
48
+
49
+ {{#if graphs.zoneChart}}
50
+ ### Zone Heatmap
51
+
52
+ {{{svgOrEmpty graphs.zoneChart}}}
53
+ {{/if}}
54
+
55
+ ## Splits Analysis
56
+
57
+ *To be populated: vs LHP/RHP, home/away*
58
+
59
+ ## Trend Analysis
60
+
61
+ *To be populated: rolling average trends*
62
+
63
+ ## Risk Assessment
64
+
65
+ *To be filled by evaluator: injury history, age curve, contract implications*
66
+
67
+ ## Comparable Player
68
+
69
+ *To be filled by evaluator: statistical comp*
70
+
71
+ ## Role Projection
72
+
73
+ *To be filled by evaluator: lineup position, expected production, Acquire / Pass / Monitor*
74
+
75
+ ---
76
+
77
+ *Generated by bbdata CLI · Data sources: {{sources}}*