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.
- package/dist/bin/bbdata.js +935 -30
- package/dist/bin/bbdata.js.map +1 -1
- package/dist/src/index.d.ts +52 -1
- package/dist/src/index.js +875 -31
- package/dist/src/index.js.map +1 -1
- package/dist/templates/queries/hitter-raw-bip.ts +65 -0
- package/dist/templates/queries/hitter-zone-grid.ts +90 -0
- package/dist/templates/queries/index.ts +27 -24
- package/dist/templates/queries/pitcher-raw-pitches.ts +62 -0
- package/dist/templates/queries/trend-rolling-average.ts +15 -3
- package/dist/templates/reports/advance-sp.hbs +66 -60
- package/dist/templates/reports/pro-hitter-eval.hbs +77 -65
- package/dist/templates/reports/pro-pitcher-eval.hbs +81 -69
- package/package.json +68 -63
- package/src/templates/reports/advance-sp.hbs +66 -60
- package/src/templates/reports/pro-hitter-eval.hbs +77 -65
- package/src/templates/reports/pro-pitcher-eval.hbs +81 -69
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
type
|
|
24
|
-
|
|
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 [{
|
|
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]} → ${
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
{{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
###
|
|
55
|
-
- *To be filled
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
{{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{{
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{{
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
##
|
|
56
|
-
|
|
57
|
-
*To be
|
|
58
|
-
|
|
59
|
-
##
|
|
60
|
-
|
|
61
|
-
*To be
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
*
|
|
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}}*
|