Skip to content

Instantly share code, notes, and snippets.

@valex
Created January 9, 2018 12:17
Show Gist options
  • Save valex/028ddf32f9c2df8f5da613ec53ef2595 to your computer and use it in GitHub Desktop.
Save valex/028ddf32f9c2df8f5da613ec53ef2595 to your computer and use it in GitHub Desktop.
Spray+Plot
circle.ball.active{
fill: #ff0000;
}
circle.ball.hovered{
fill: #c6c6c6;
}
table {
border-collapse: collapse;
}
.table {
width: 100%;
max-width: 100%;
margin-bottom: 1rem;
background-color: transparent;
font-size: 0.92rem;
}
.table thead th {
vertical-align: bottom;
border-bottom: 2px solid #e9ecef;
}
.table tr.active{
background-color: #c5e8f5;
}
.table td, .table th {
padding: .3rem;
vertical-align: top;
border-top: 1px solid #e9ecef;
text-align: center;
}
.table td, .table th {
padding: .3rem;
vertical-align: top;
border-top: 1px solid #e9ecef;
}
div.tooltip {
position: absolute;
text-align: center;
width: 70px;
height: 28px;
line-height: 28px;
padding: 6px 2px 2px 2px;
font: 12px sans-serif;
background: black;
border: 0px;
-webkit-border-radius: 8px;
-moz-border-radius: 8px;
border-radius: 8px;
pointer-events: none;
color:white;
}
#distance-module{
position: relative;
}
.heatmap-wrapper {
position: absolute;
}
.heatmap {
position: relative;
width: 100%;
height: 100%;
}
/* pitch styles */
circle.pitch-ball.active{
stroke: #ee83d8;
stroke-width: 3;
}
circle.pitch-ball.hovered{
fill: #c6c6c6;
}
div.pitch-tooltip {
position: absolute;
text-align: center;
width: 70px;
height: 28px;
line-height: 28px;
padding: 6px 2px 2px 2px;
font: 12px sans-serif;
background: black;
border: 0px;
-webkit-border-radius: 8px;
-moz-border-radius: 8px;
border-radius: 8px;
pointer-events: none;
color:white;
}
.pitch-heatmap-wrapper{
position: absolute;
}
.pitch-heatmap {
position: relative;
width: 100%;
height: 100%;
}
var BASEBALL_ANIM_APP = {
url: 'spraychart.json',
data: [],
eventsData: [],
activeIndex: null,
activeEvent: '',
timer: null,
carousel: true,
animation: {
duration: 1000,
delay: 4000
},
selectModuleOptions:{
appendToSelector: '#select-module',
},
tableModuleOptions:{
id: 'baseball-app-table',
appendToSelector: '#table-module',
rowActiveClass:"active",
tbody: null,
columns: [
{
key: 'exitSpeed',
label: 'Exit velocity',
fixed: 1,
},
{
key: 'vAngle',
label: 'Vertical Launch',
fixed: 0,
},
{
key: 'hAngle',
label: 'Horiztonal Launch',
fixed: 0,
},
{
key: 'distance',
label: 'Distance',
fixed: 0,
},
{
key: 'video_icon',
label: '',
},
]
},
distanceModuleOptions: {
id: 'distance',
appendToSelector: '#distance-module',
margins: {top: 10, right: 10, bottom: 10, left: 10},
width: 300,
height: 300,
defaultBallColor:'#b3b3b3',
ballActiveClass:"active",
ballHoverClass:"hovered",
radii: [320, 220, 120],
mainGroup: null,
scaleRadius: null,
tooltip: null,
schemeRadius: 0,
schemeBasePoint: {x:0, y:0},
arcGen: null,
heatmap: {
show: false,
instance: null,
}
},
exitLaunchAngleModuleOptions: {
id: 'exit-angle',
appendToSelector: '#exit-launch-angle-module',
margins: {top: 10, right: 10, bottom: 10, left: 10},
width: 300,
height: 200,
mainGroup: null,
schemeRadius: 0,
vectorLength: 0,
vectorArcGenerator: null,
schemeBasePoint: {x:0, y:0},
},
exitDirectionModuleOptions: {
id: 'exit-dir',
appendToSelector: '#exit-direction-module',
margins: {top: 10, right: 10, bottom: 10, left: 10},
width: 300,
height: 200,
mainGroup: null,
schemeRadius: 0,
schemeBasePoint: {x:0, y:0},
vectorArcGenerator: null,
},
videoModuleOptions: {
videoJs: null,
ready: false,
appendToSelector: '#video-module'
},
mainColor: '#005d80',
mainLightColor: '#3bafda',
start: function(){
this.loadData();
},
loadData: function(){
var that = this;
d3.json(this.url, function (error, rawData) {
if (error) throw error;
var data = rawData['RECORDS'].map(function (d) {
return {
exitSpeed: +d["exitvelo"],
vAngle: +d["vlaunch"],
hAngle: +d["hlaunch"],
distance: +d["distance"],
videoSrc: (''+d["videoPath"]).trim(),
eventID: +d["eventID"],
eventName: (''+d["eventName"]).trim(),
eventDate: moment(d["eventDate"], "YYYY-MM-DD HH:mm:ss"),
}
});
that.eventsData = d3.nest()
.key(function(d) { return d.eventID; })
.entries(data);
that.updateData();
that.initModules();
that.updateModules();
if(data.length > 0 ){
// initialize index
that.setIndexAndAnimate(that.getNextIndex());
that.startTimer();
}
});
},
initModules: function(){
this.initSelectModule();
this.initTableModule();
this.initDistanceModule();
this.initExitLaunchAngleModule();
this.initExitDirectionModule();
},
updateModules: function(){
this.updateExitLaunchAngleModule();
this.updateExitDirectionModule();
this.updateDistanceModule();
this.updateTableModule();
},
animateModules:function(){
this.animateExitLaunchAngleModule();
this.animateExitDirectionModule();
this.animateDistanceModule();
this.animateTableModule();
this.updateVideoModule();
},
updateData: function(){
var that = this;
that.data = d3.merge(
this.eventsData.map(function(d){
return d.values.filter(function(dd){
if(that.activeEvent.length <=0)
return true;
return dd.eventID == that.activeEvent
})
})
);
},
onActiveEventChange: function(){
if(this.carousel === true) this.stopTimer();
this.updateData();
this.updateModules();
this.setIndexAndAnimate(0);
if(this.carousel === true) this.startTimer();
},
startTimer: function(){
var animationDelay = this.animation.duration + this.animation.delay;
this.timer = d3.interval(this.timerCallback, animationDelay);
},
timerCallback: function(elapsed){
BASEBALL_ANIM_APP.setIndexAndAnimate(BASEBALL_ANIM_APP.getNextIndex());
},
onVideoPlayerReady: function(){
this.videoModuleOptions.ready = true;
this.videoModuleOptions.videoJs.play();
},
setVideoSrc: function(data){
this.videoModuleOptions.videoJs.src({
src: data.videoSrc,
type: 'video/mp4'
});
},
getNextIndex: function(){
var dataLength = this.dataLength();
if(dataLength <= 0)
return null;
if(this.activeIndex === null)
return 0;
var nextIndex = this.activeIndex + 1;
if(nextIndex === dataLength){
nextIndex = 0;
}
return nextIndex;
},
ballManuallySelected: function(index){
this.stopTimer();
this.carousel = false;
this.setIndexAndAnimate(index);
},
setIndexAndAnimate: function(index){
this.setActiveIndex(index);
this.animateModules();
},
setActiveIndex:function(index){
this.activeIndex = index;
},
hasVideo: function(data){
return data.videoSrc.length > 0;
},
stopTimer: function(){
if(this.timer === null)
return;
this.timer.stop();
},
isDataLoaded: function(){
return this.dataLength();
},
dataLength: function(){
return this.data.length;
},
// ============================== EXIT DIRECTION MODULE ==============================
initExitDirectionModule: function(){
var options = this.exitDirectionModuleOptions;
var graphWidth = this.exitDirectionModuleOptions.width - this.exitDirectionModuleOptions.margins.left - this.exitDirectionModuleOptions.margins.right,
graphHeight = this.exitDirectionModuleOptions.height - this.exitDirectionModuleOptions.margins.top - this.exitDirectionModuleOptions.margins.bottom;
var svg = d3.select(options.appendToSelector)
.append("svg")
.attr("width", options.width)
.attr("height", options.height);
options.mainGroup = svg.append('g')
.attr('transform', 'translate(' + options.margins.left + ',' + options.margins.top + ')')
.attr('id', options.id);
// Add the Title
options.mainGroup.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("font-weight", "bold")
.attr("font-size", "14px")
.attr("alignment-baseline", "hanging")
.attr("dominant-baseline", "hanging") // for firefox
.attr("class", "title")
.style("fill", this.mainColor)
.text("EXIT DIRECTION");
options.mainGroup.append("text")
.attr("id", "angle-label")
// .attr("x", graphWidth)
// .attr("y", 0)
.attr("x", 160)
.attr("y", 40)
.attr("font-weight", "bold")
.attr("font-size", "28px")
.attr("alignment-baseline", "hanging")
.attr("dominant-baseline", "hanging") // for firefox
.attr("class", "angle")
.attr("text-anchor", "end")
.style("fill", this.mainColor)
.html("0&#176;");
options.schemeRadius = Math.round(graphWidth * 0.5 * 0.66);
options.schemeBasePoint = {
x: Math.round(graphWidth / 2),
y: graphHeight
};
// outer arc
var outerArc = d3.arc()
.innerRadius(0)
.outerRadius(4/5 * options.schemeRadius)
.startAngle(-45 * Math.PI / 180)
.endAngle(45 * Math.PI / 180);
options.mainGroup.append('path')
.attr("d", outerArc)
.attr("transform", 'translate('+options.schemeBasePoint.x+', '+options.schemeBasePoint.y+')')
.attr("fill", '#dee0df')
.attr("stroke-width", '0');
// inner arc
var innerArc = d3.arc()
.innerRadius(0)
.outerRadius(4/7 * options.schemeRadius)
.startAngle(-45 * Math.PI / 180)
.endAngle(45 * Math.PI / 180);
options.mainGroup.append('path')
.attr("d", innerArc)
.attr("transform", 'translate('+options.schemeBasePoint.x+', '+options.schemeBasePoint.y+')')
.attr("fill", this.mainLightColor)
.attr("stroke", this.mainColor)
.attr("stroke-width", 2);
// vector arc
options.vectorArcGenerator = d3.arc()
.innerRadius(0)
.outerRadius(4/7 * options.schemeRadius)
.startAngle(0)
.endAngle(function(d) { return d * Math.PI / 180; });
options.mainGroup.append('path')
.attr("id", "vector-arc")
.attr("d", options.vectorArcGenerator(0))
.attr("transform", 'translate('+options.schemeBasePoint.x+', '+options.schemeBasePoint.y+')')
.attr("fill", this.mainColor);
options.mainGroup.append("line")
.style("stroke", this.mainColor) // colour the line
.attr("x1", options.schemeBasePoint.x) // x position of the first end of the line
.attr("y1", options.schemeBasePoint.y) // y position of the first end of the line
.attr("x2", options.schemeBasePoint.x + options.schemeRadius) // x position of the second end of the line
.attr("y2", options.schemeBasePoint.y - options.schemeRadius)
.attr("stroke-width", 3);
options.mainGroup.append("line")
.style("stroke", this.mainColor) // colour the line
.attr("x1", options.schemeBasePoint.x) // x position of the first end of the line
.attr("y1", options.schemeBasePoint.y) // y position of the first end of the line
.attr("x2", options.schemeBasePoint.x - options.schemeRadius) // x position of the second end of the line
.attr("y2", options.schemeBasePoint.y - options.schemeRadius)
.attr("stroke-width", 3);
// vector
options.mainGroup.append("line")
.style("stroke", this.mainColor) // colour the line
.attr("id", "vector") // x position of the first end of the line
.attr("x1", options.schemeBasePoint.x) // x position of the first end of the line
.attr("y1", options.schemeBasePoint.y) // y position of the first end of the line
.attr("x2", options.schemeBasePoint.x) // x position of the second end of the line
.attr("y2", options.schemeBasePoint.y - options.schemeRadius)
.attr("stroke-width", 1.5);
},
updateExitDirectionModule: function(){
},
animateExitDirectionModule: function(){
var options = this.exitDirectionModuleOptions;
var data = this.data[this.activeIndex];
options.mainGroup
.select('#'+options.id+' #vector')
.datum(data)
.transition()
.duration(this.animation.duration)
.tween("animations", function(d){
var el = d3.select(this);
var angleLabel = d3.select('#'+options.id+' #angle-label');
var vectorArc = d3.select('#'+options.id+' #vector-arc');
var X2 = +el.attr("x2");
var Y2 = +el.attr("y2");
var fromAngle = 180 / Math.PI * Math.asin((X2 - options.schemeBasePoint.x) / options.schemeRadius);
var toAngle = d['hAngle'];
var iAngle = d3.interpolateNumber(fromAngle, toAngle);
return function(t){
el.attr("x2", options.schemeBasePoint.x + (options.schemeRadius * Math.sin(iAngle(t) * Math.PI / 180)));
el.attr("y2", options.schemeBasePoint.y - (options.schemeRadius * Math.cos(iAngle(t) * Math.PI / 180)));
var suffix = "&#176;";
if(iAngle(t) >= 0){
suffix += 'R';
}else{
suffix += 'L';
}
angleLabel.html(Math.round(iAngle(t)) + suffix);
vectorArc.attr("d", options.vectorArcGenerator(iAngle(t)))
}
})
},
// ============================== EXIT LAUNCH ANGLE MODULE ==============================
initExitLaunchAngleModule: function(){
var options = this.exitLaunchAngleModuleOptions;
var graphWidth = options.width - options.margins.left - options.margins.right,
graphHeight = options.height - options.margins.top - options.margins.bottom;
var svg = d3.select(options.appendToSelector)
.append("svg")
.attr("width", options.width)
.attr("height", options.height);
options.mainGroup = svg.append('g')
.attr('transform', 'translate(' + options.margins.left + ',' + options.margins.top + ')')
.attr('id', options.id);
// title text
options.mainGroup.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("font-weight", "bold")
.attr("font-size", "14px")
.attr("alignment-baseline", "hanging")
.attr("dominant-baseline", "hanging") // for firefox
.attr("class", "title")
.style("fill", this.mainColor)
.text("EXIT LAUNCH ANGLE");
// angle text
options.mainGroup.append("text")
.attr("id", "angle-label")
// .attr("x", graphWidth)
// .attr("y", 0)
.attr("x", 70)
.attr("y", 82)
.attr("font-weight", "bold")
.attr("font-size", "28px")
.attr("alignment-baseline", "hanging")
.attr("dominant-baseline", "hanging") // for firefox
.attr("class", "angle")
.attr("text-anchor", "end")
.style("fill", this.mainColor)
.html("0&#176;");
options.schemeRadius = Math.round((graphHeight - 30) * 0.5);
options.vectorLength = Math.round(options.schemeRadius * 0.85);
options.schemeBasePoint = {
x: Math.round(1/3 * graphWidth),
y: graphHeight - options.schemeRadius
};
// outer arc
var outerArc = d3.arc()
.innerRadius(0)
.outerRadius(0.6 * options.schemeRadius)
.startAngle(0)
.endAngle(180 * Math.PI / 180);
options.mainGroup.append('path')
.attr("d", outerArc)
.attr("transform", 'translate('+options.schemeBasePoint.x+', '+options.schemeBasePoint.y+')')
.attr("fill", '#dee0df')
.attr("stroke-width", '0');
// inner arc
var innerArc = d3.arc()
.innerRadius(0)
.outerRadius(0.44 * options.schemeRadius)
.startAngle(0)
.endAngle(180 * Math.PI / 180);
options.mainGroup.append('path')
.attr("d", innerArc)
.attr("transform", 'translate('+options.schemeBasePoint.x+', '+options.schemeBasePoint.y+')')
.attr("fill", this.mainLightColor)
.attr("stroke", this.mainColor)
.attr("stroke-width", 0);
// vector arc
options.vectorArcGenerator = d3.arc()
.innerRadius(0)
.outerRadius(0.44 * options.schemeRadius)
.startAngle(90 * Math.PI / 180)
.endAngle(function(d) { return (90 - d) * Math.PI / 180; });
options.mainGroup.append('path')
.attr("id", "vector-arc")
.attr("d", options.vectorArcGenerator(0))
.attr("transform", 'translate('+options.schemeBasePoint.x+', '+options.schemeBasePoint.y+')')
.attr("fill", this.mainColor);
// vector
options.mainGroup.append("line")
.style("stroke", this.mainColor)
.attr("id", "vector")
.attr("x1", options.schemeBasePoint.x)
.attr("y1", options.schemeBasePoint.y)
.attr("x2", options.schemeBasePoint.x + options.vectorLength)
.attr("y2", options.schemeBasePoint.y)
.attr("stroke-width", 1.5);
// x axis
options.mainGroup.append("line")
.style("stroke", this.mainColor) // colour the line
.attr("x1", options.schemeBasePoint.x) // x position of the first end of the line
.attr("y1", options.schemeBasePoint.y) // y position of the first end of the line
.attr("x2", options.schemeBasePoint.x + options.schemeRadius) // x position of the second end of the line
.attr("y2", options.schemeBasePoint.y)
.attr("stroke-width", 3);
// y axis
options.mainGroup.append("line")
.style("stroke", this.mainColor) // colour the line
.attr("x1", options.schemeBasePoint.x) // x position of the first end of the line
.attr("y1", options.schemeBasePoint.y + options.schemeRadius) // y position of the first end of the line
.attr("x2", options.schemeBasePoint.x) // x position of the second end of the line
.attr("y2", options.schemeBasePoint.y - options.schemeRadius)
.attr("stroke-width", 3);
},
updateExitLaunchAngleModule: function(){
},
animateExitLaunchAngleModule: function(){
var options = this.exitLaunchAngleModuleOptions;
var data = this.data[this.activeIndex];
options.mainGroup
.select('#'+options.id+' #vector')
.datum(data)
.transition()
.duration(this.animation.duration)
.tween("animations", function(d){
var el = d3.select(this);
var angleLabel = d3.select('#'+options.id+' #angle-label');
var vectorArc = d3.select('#'+options.id+' #vector-arc');
var X2 = +el.attr("x2");
var Y2 = +el.attr("y2");
var fromAngle = 180 / Math.PI * Math.asin((options.schemeBasePoint.y - Y2) / options.vectorLength);
var toAngle = d['vAngle'];
var iAngle = d3.interpolateNumber(fromAngle, toAngle);
return function(t){
el.attr("x2", options.schemeBasePoint.x + (options.vectorLength * Math.cos(iAngle(t) * Math.PI / 180)));
el.attr("y2", options.schemeBasePoint.y - (options.vectorLength * Math.sin(iAngle(t) * Math.PI / 180)));
angleLabel.html(Math.round(iAngle(t)) + "&#176;");
vectorArc.attr("d", options.vectorArcGenerator(iAngle(t)))
}
})
},
// ============================== DISTANCE MODULE ==============================
initDistanceModule: function(){
var that = this;
var options = this.distanceModuleOptions;
var graphWidth = options.width - options.margins.left - options.margins.right,
graphHeight = options.height - options.margins.top - options.margins.bottom;
var svg = d3.select(options.appendToSelector)
.append("svg")
.attr("width", options.width)
.attr("height", options.height);
options.mainGroup = svg.append('g')
.attr('transform', 'translate(' + options.margins.left + ',' + options.margins.top + ')')
.attr('id', options.id);
// title text
options.mainGroup.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("font-weight", "bold")
.attr("font-size", "14px")
.attr("alignment-baseline", "hanging")
.attr("dominant-baseline", "hanging") // for firefox
.attr("class", "title")
.style("fill", this.mainColor)
.text("DISTANCE");
// ft label
options.mainGroup.append("text")
.attr("x", graphWidth)
.attr("y", 28)
.attr("font-size", "18px")
.attr("alignment-baseline", "baseline")
.attr("dominant-baseline", "baseline") // for firefox
.attr("text-anchor", "end")
.style("fill", this.mainColor)
.html("ft");
// ft value
options.mainGroup.append("text")
.attr("id", "ft-value")
.attr("x", graphWidth - 14)
.attr("y", 28)
.attr("font-weight", "bold")
.attr("font-size", "28px")
.attr("alignment-baseline", "baseline")
.attr("dominant-baseline", "baseline") // for firefox
.attr("text-anchor", "end")
.style("fill", this.mainColor)
.html("0");
// Define the div for the tooltip
options.tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
options.schemeRadius = Math.round(0.5 * graphWidth / Math.cos(45 * Math.PI / 180));
options.schemeBasePoint = {
x: Math.round(0.5 * graphWidth),
y: graphHeight
};
options.arcGen = d3.arc()
.startAngle(-45 * Math.PI / 180)
.endAngle(45 * Math.PI / 180);
options.scaleRadius = d3.scaleLinear()
.domain([0, d3.max(options.radii)])
.range([0, options.schemeRadius]);
this.initHeatmap();
this.drawBackground();
this.drawDistanceLabels();
},
initHeatmap: function(){
this.initHeatmapCheckbox();
var that = this;
var options = this.distanceModuleOptions;
var heatmapWidth = options.width,
heatmapHeight = options.height;
var heatmapWrapper = d3.select(options.appendToSelector)
.insert("div", ':first-child')
.attr("class", 'heatmap-wrapper')
.style("width", heatmapWidth+"px")
.style("height", heatmapHeight+"px")
.style("z-index", -1);
var heatmap = heatmapWrapper
.append('div')
.attr("class", 'heatmap');
// minimal heatmap instance configuration
options.heatmap.instance = h337.create({
// only container is required, the rest will be defaults
container: document.querySelector('.heatmap')
});
},
drawHeatmap: function(){
var that = this;
var options = this.distanceModuleOptions;
// now generate some random data
var points = [];
var max = 1;
for(var i=0; i < that.data.length; i++){
var coordinates = that.ballXY(that.data[i]);
var point = {
x: Math.round(coordinates.x + options.margins.left),
y: Math.round(coordinates.y + options.margins.top),
value: 0.7
};
points.push(point);
}
// heatmap data format
var data = {
max: max,
data: points
};
// if you have a set of datapoints always use setData instead of addData
// for data initialization
options.heatmap.instance.setData(data);
},
clearHeatmap: function(){
var that = this;
var options = this.distanceModuleOptions;
options.heatmap.instance.setData({
max: 0,
min: 0,
data: [
]
});
},
initHeatmapCheckbox: function(){
var that = this;
var heatmapCheckbox = d3.select("#heatmap-checkbox");
heatmapCheckbox.on("change", function(){
that.distanceModuleOptions.heatmap.show = !!this.checked;
that.onHeatmapCheckboxChange();
});
},
drawBackground: function () {
var that = this;
var options = this.distanceModuleOptions;
if(true === options.heatmap.show){
that.clearScatterBackground();
that.drawHeatmap();
}else{
that.clearHeatmap();
that.drawScatterBackground();
}
},
onHeatmapCheckboxChange: function(){
this.drawBackground();
},
drawScatterBackground: function(){
var that = this;
var options = this.distanceModuleOptions;
// outfield
options.mainGroup.insert('path', '.distance-label')
.attr("d", options.arcGen({
innerRadius: 0,
outerRadius: options.scaleRadius(d3.max(options.radii))
}))
.attr("transform", 'translate('+options.schemeBasePoint.x+', '+options.schemeBasePoint.y+')')
.attr("fill", '#0a6d08')
.attr("class", 'scatter-background');
// infield
options.mainGroup.insert('path', '.distance-label')
.attr("d", options.arcGen({
innerRadius: 0,
outerRadius: options.scaleRadius(d3.min(options.radii))
}))
.attr("transform", 'translate('+options.schemeBasePoint.x+', '+options.schemeBasePoint.y+')')
.attr("fill", '#5e3d0b')
.attr("class", 'scatter-background');
},
clearScatterBackground:function(){
var that = this;
var options = this.distanceModuleOptions;
d3.selectAll('.scatter-background').remove();
},
drawDistanceLabels: function() {
var that = this;
var options = this.distanceModuleOptions;
var radii = options.radii;
var innerRadius, outerRadius;
for(var i=0; i < radii.length; i++){
innerRadius = options.scaleRadius(radii[i]);
outerRadius = innerRadius;
if(i == 0){
innerRadius = 0;
}
options.mainGroup.append('path')
.attr("d", options.arcGen({
innerRadius: innerRadius,
outerRadius: outerRadius
}))
.attr("transform", 'translate('+options.schemeBasePoint.x+', '+options.schemeBasePoint.y+')')
.attr("fill", 'none')
.attr("stroke", '#777777')
.attr("stroke-width", '1')
.attr("class", 'distance-label');
// labels left
options.mainGroup.append("text")
.attr("x", options.schemeBasePoint.x)
.attr("y", options.schemeBasePoint.y - options.scaleRadius(radii[i]))
.attr("transform", "translate(-3,0) rotate(-45,"+options.schemeBasePoint.x+", "+options.schemeBasePoint.y+")")
.attr("text-anchor", i == 0 ? "start" : "end")
.attr("font-size", "12px")
.attr("font-weight", "bold")
.attr("alignment-baseline", i == 0 ? "auto" : "middle")
.attr("dominant-baseline", i == 0 ? "auto" : "middle") // for firefox
.style("fill", '#777777')
.text(radii[i])
.attr("class", 'distance-label');
// labels right
options.mainGroup.append("text")
.attr("x", options.schemeBasePoint.x)
.attr("y", options.schemeBasePoint.y - options.scaleRadius(radii[i]))
.attr("transform", "translate(3,0) rotate(45,"+options.schemeBasePoint.x+", "+options.schemeBasePoint.y+")")
.attr("text-anchor", i == 0 ? "end" : "start")
.attr("font-size", "12px")
.attr("font-weight", "bold")
.attr("alignment-baseline", i == 0 ? "auto" : "middle")
.attr("dominant-baseline", i == 0 ? "auto" : "middle") // for firefox
.style("fill", '#777777')
.text(radii[i])
.attr("class", 'distance-label');
}
},
updateDistanceModule: function(){
var that = this;
var options = this.distanceModuleOptions;
var circlesSelection = options.mainGroup.selectAll('circle')
.data(this.data);
circlesSelection.exit().remove();
var circlesEnter = circlesSelection
.enter()
.append('circle')
.attr('class', 'ball')
.attr('fill', options.defaultBallColor)
.attr('stroke', 'none')
.attr('r', 5)
.on('click', function(d, i) {
var el = d3.select(this);
that.ballManuallySelected(i);
})
.on('mouseenter', function(d) {
var el = d3.select(this);
// tooltip
options.tooltip.transition()
.duration(200)
.style("opacity", .8);
options.tooltip.html(parseFloat(d.exitSpeed).toFixed(1)+' mph')
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
// classes
if(el.classed(options.ballActiveClass))
return;
el.classed(options.ballHoverClass, true);
})
.on('mouseout', function(d) {
var el = d3.select(this);
// tooltip
options.tooltip.transition()
.duration(500)
.style("opacity", 0);
// classes
if(el.classed(options.ballActiveClass))
return;
el.classed(options.ballHoverClass, false);
});
var circles = circlesSelection.merge(circlesEnter);
circles
.attr('id', function(d,i){return 'ball-'+i;})
.attr('cx', function(d, i){return that.ballXY(d).x})
.attr('cy', function(d, i){return that.ballXY(d).y});
this.drawBackground();
},
animateDistanceModule: function(){
var options = this.distanceModuleOptions;
var el = d3.select('#'+options.id+' #ball-'+this.activeIndex);
var datum = el.datum();
// update ft-value
options.mainGroup.select('#'+options.id+' #ft-value')
.text(Math.round(datum['distance']));
// only one active
var alreadyIsActive = el.classed(options.ballActiveClass);
if( ! alreadyIsActive){
options.mainGroup.selectAll('#'+options.id+' .ball')
.classed(options.ballActiveClass, false);
el.classed(options.ballActiveClass, !alreadyIsActive);
el.classed(options.ballHoverClass, false);
}
},
ballXY: function(d){
var that = this;
var options = this.distanceModuleOptions;
return {
x: options.schemeBasePoint.x + options.scaleRadius(d.distance * Math.sin(d.hAngle * Math.PI / 180)),
y: options.schemeBasePoint.y - options.scaleRadius(d.distance * Math.cos(d.hAngle * Math.PI / 180))
}
},
// ============================== TABLE MODULE ==============================
initTableModule: function() {
var options = this.tableModuleOptions;
var table = d3.select(options.appendToSelector).append('table')
.attr('id', options.id)
.attr('class', 'table');
var thead = table.append('thead');
options.tbody = table.append('tbody');
// append the header row
thead.append('tr')
.selectAll('th')
.data(options.columns)
.enter()
.append('th')
.text(function (column) { return column.label; });
},
updateTableModule:function(){
var options = this.tableModuleOptions;
// create a row for each object in the data
var rowsSelection = options.tbody.selectAll('tr')
.data(this.data);
rowsSelection.exit().remove();
var rowsEnter = rowsSelection
.enter()
.append('tr')
.attr('id', function(d,i){ return 'ball-'+i;})
.on('click', function(d, i){
BASEBALL_ANIM_APP.ballManuallySelected(i);
});
var rows = rowsSelection.merge(rowsEnter);
// create a cell in each row for each column
var cellsSelection = rows.selectAll('td')
.data(function (row) {
return options.columns.map(function (column) {
var value;
switch(column.key){
case 'video_icon':
value = '';
if(BASEBALL_ANIM_APP.hasVideo(row)){
value = '<i class="fa fa-video-camera" aria-hidden="true"></i>';
}
break;
default:
value = parseFloat(row[column.key]).toFixed(column.fixed);
if(column.key == 'vAngle' || column.key == 'hAngle'){
value += '&#176;';
}
break;
}
return {value: value};
});
});
var cellsEnter = cellsSelection
.enter()
.append('td');
var cells = cellsSelection.merge(cellsEnter);
cells.html(function (d) { return d.value; });
},
animateTableModule: function(){
this.setRowActive(this.activeIndex);
},
setRowActive: function(index){
var options = this.tableModuleOptions;
var table = d3.select('#'+options.id);
var tr = table.select('#ball-'+index);
// only one active
var alreadyIsActive = tr.classed(options.rowActiveClass);
if(alreadyIsActive)
return;
table.selectAll('#'+options.id+' tr')
.classed(options.rowActiveClass, false);
tr.classed(options.rowActiveClass, !alreadyIsActive);
},
// ============================== VIDEO MODULE ==============================
initVideoPlayer: function(){
var that = this;
var options = this.videoModuleOptions;
var outerBlock = d3.select(options.appendToSelector).html("");
var innerBlock = outerBlock
.append('div')
.attr('class', 'embed-responsive embed-responsive-16by9');
var videoTag = innerBlock.append('video')
.attr('class', 'video-js embed-responsive-item');
var videoUnsupportedWarn = videoTag.append('p')
.attr('class', 'vjs-no-js')
.html('To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="http://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a>');
options.videoJs = videojs(document.querySelector('.video-js'), {
aspectRatio: '16:9',
controls: true,
autoplay: false,
preload: 'auto',
});
options.videoJs.on('playing', function() {
});
options.videoJs.on('ended', function() {
if(that.carousel === true){
that.startTimer();
that.setIndexAndAnimate(that.getNextIndex());
}
});
options.videoJs.ready(function() {
that.onVideoPlayerReady();
});
},
updateVideoModule: function(){
var options = this.videoModuleOptions;
if(options.videoJs !== null){
this.disposeVideoPlayer();
}
var datum = this.data[this.activeIndex];
if(this.hasVideo(datum)){
this.stopTimer();
this.initVideoPlayer();
this.setVideoSrc(datum);
}
},
disposeVideoPlayer: function(){
this.videoModuleOptions.videoJs.dispose();
this.videoModuleOptions.videoJs = null;
this.videoModuleOptions.ready = false;
},
// ============================== SELECT MODULE ==============================
initSelectModule:function(){
var options = this.selectModuleOptions;
var select = d3.select(options.appendToSelector).append('select')
.attr('name', 'active_event')
.attr('class', 'form-control');
var nullOption = select.append('option')
.attr('value', '')
.text('ALL EVENTS');
for(var i=0; i<this.eventsData.length; i++){
select.append('option')
.attr('value', this.eventsData[i].values[0].eventID)
.text(this.eventsData[i].values[0].eventName);
}
select.on('change', this.onSelectChange);
},
onSelectChange: function (e) {
var old = BASEBALL_ANIM_APP.activeEvent;
BASEBALL_ANIM_APP.activeEvent = this.value;
if(old != BASEBALL_ANIM_APP.activeEvent){
BASEBALL_ANIM_APP.onActiveEventChange();
}
}
};
BASEBALL_ANIM_APP.start();
var PITCH_APP = {
url: 'pitchplot.json',
eventsData: [],
activeEvent: null,
data: [],
activeIndex: null,
options: {
selectModule: {
appendToSelector: '#pitch-select-module',
},
chartModule: {
id: 'pitch-chart',
appendToSelector: '#pitch-chart-module',
width: 400,
height: 400,
margins: {top: 10, right: 10, bottom: 20, left: 10},
color: null,
defaultBallColor:'#b3b3b3',
ballHoverClass:"hovered",
ballActiveClass:"active",
svg: null,
mainGroup: null,
tooltip: null,
scaleX: null,
scaleY: null,
strikeZone:{
color: '#2ad146',
width: 1
},
heatmap: {
show: false,
instance: null,
}
},
videoModule: {
videoJs: null,
ready: false,
appendToSelector: '#pitch-video-module'
},
tableModule:{
appendToSelector: '#pitch-table-module',
rowActiveClass:"active",
tbody: null,
columns: [
{
key: 'pitchSpeed',
label: 'Exit velocity',
fixed: 1,
},
{
key: 'pitchType',
label: 'Type',
},
{
key: 'pitchResult',
label: 'Result',
},
{
key: 'video_icon',
label: '',
},
]
},
},
start: function(){
this.loadData();
},
loadData: function(){
var that = this;
d3.json(this.url, function (error, rawData) {
if (error) throw error;
var data = rawData['RECORDS'].map(function (d) {
return {
pitchSpeed: +d["pitchvelo"],
pitchX: +d["pitchX"],
pitchY: +d["pitchY"],
pitchType: (''+d["ptype"]).trim().toLowerCase(),
pitchResult: (''+d["presult"]).trim().toLowerCase(),
eventID: +d["eventID"],
eventName: (''+d["eventName"]).trim(),
eventDate: moment(d["eventDate"], "YYYY-MM-DD HH:mm:ss"),
videoSrc: typeof d["videoPath"] !== 'undefined' ? (''+d["videoPath"]).trim() : '',
}
});
that.eventsData = d3.nest()
.key(function(d) { return d.eventID; })
.entries(data);
that.updateData();
that.initModules();
that.updateModules();
});
},
initModules: function(){
this.initSelectModule();
this.initTableModule();
this.initChart();
},
updateModules: function(){
this.updateTableModule();
this.updateChart();
},
animateModules:function(){
this.animateTableModule();
this.updateVideoModule();
this.animateChart();
},
updateData: function(){
var that = this;
that.data = d3.merge(
this.eventsData.map(function(d){
return d.values.filter(function(dd){
if(that.activeEvent === null)
return true;
return dd.eventID == that.activeEvent;
})
})
);
},
ballManuallySelected: function(index){
this.setIndexAndAnimate(index);
},
setIndexAndAnimate: function(index){
this.setActiveIndex(index);
this.animateModules();
},
setActiveIndex:function(index){
this.activeIndex = index;
},
hasVideo: function(data){
return data.videoSrc.length > 0;
},
// ============================== VIDEO MODULE ==============================
initVideoPlayer: function(){
var that = this;
var options = this.options.videoModule;
var outerBlock = d3.select(options.appendToSelector).html("");
var innerBlock = outerBlock
.append('div')
.attr('class', 'embed-responsive embed-responsive-16by9');
var videoTag = innerBlock.append('video')
.attr('class', 'pitch-video-js video-js embed-responsive-item');
var videoUnsupportedWarn = videoTag.append('p')
.attr('class', 'vjs-no-js')
.html('To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="http://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a>');
options.videoJs = videojs(document.querySelector('.pitch-video-js'), {
aspectRatio: '16:9',
controls: true,
autoplay: false,
preload: 'auto',
});
options.videoJs.on('playing', function() {
});
options.videoJs.on('ended', function() {
});
options.videoJs.ready(function() {
that.onVideoPlayerReady();
});
},
onVideoPlayerReady: function(){
this.options.videoModule.ready = true;
this.options.videoModule.videoJs.play();
},
updateVideoModule: function(){
var options = this.options.videoModule;
if(options.videoJs !== null){
this.disposeVideoPlayer();
}
if(null !== this.activeIndex){
var datum = this.data[this.activeIndex];
if(this.hasVideo(datum)){
this.initVideoPlayer();
this.setVideoSrc(datum);
}
}
},
setVideoSrc: function(data){
this.options.videoModule.videoJs.src({
src: data.videoSrc,
type: 'video/mp4'
});
},
disposeVideoPlayer: function(){
this.options.videoModule.videoJs.dispose();
this.options.videoModule.videoJs = null;
this.options.videoModule.ready = false;
},
// ============================== TABLE MODULE ==============================
initTableModule: function() {
var options = this.options.tableModule;
var table = d3.select(options.appendToSelector).append('table')
.attr('class', 'table');
var thead = table.append('thead');
options.tbody = table.append('tbody');
// append the header row
thead.append('tr')
.selectAll('th')
.data(options.columns)
.enter()
.append('th')
.text(function (column) { return column.label; });
},
updateTableModule:function(){
var options = this.options.tableModule;
// create a row for each object in the data
var rowsSelection = options.tbody.selectAll('tr')
.data(this.data);
rowsSelection.exit().remove();
var rowsEnter = rowsSelection
.enter()
.append('tr')
.on('click', function(d, i){
PITCH_APP.ballManuallySelected(i);
});
var rows = rowsSelection.merge(rowsEnter);
// create a cell in each row for each column
var cellsSelection = rows.selectAll('td')
.data(function (row) {
return options.columns.map(function (column) {
var value;
switch(column.key){
case 'video_icon':
value = '';
if(PITCH_APP.hasVideo(row)){
value = '<i class="fa fa-video-camera" aria-hidden="true"></i>';
}
break;
case 'pitchType':
case 'pitchResult':
value = row[column.key];
break;
default:
value = parseFloat(row[column.key]).toFixed(column.fixed);
break;
}
return {value: value};
});
});
var cellsEnter = cellsSelection
.enter()
.append('td');
var cells = cellsSelection.merge(cellsEnter);
cells.html(function (d) { return d.value; });
},
animateTableModule: function(){
this.setRowActive(this.activeIndex);
},
setRowActive: function(index){
var options = this.options.tableModule;
// only one active
options.tbody.selectAll('tr')
.classed(options.rowActiveClass, false);
var tr = options.tbody.selectAll('tr').filter(function(d,i){return i === index});
if(tr.size() > 0){
var alreadyIsActive = tr.classed(options.rowActiveClass);
if( ! alreadyIsActive){
tr.classed(options.rowActiveClass, ! alreadyIsActive);
}
}
},
// ============================== CHART MODULE ==============================
initChart: function(){
var that = this;
var options = this.options.chartModule;
var chartWidth = options.width - options.margins.left - options.margins.right,
chartHeight = options.height - options.margins.top - options.margins.bottom;
options.svg = d3.select(options.appendToSelector)
.append("svg")
.attr("width", options.width)
.attr("height", options.height);
options.mainGroup = options.svg.append('g')
.attr('transform', 'translate(' + options.margins.left + ',' + options.margins.top + ')')
.attr('id', options.id);
options.scaleX = d3.scaleLinear()
.domain([-4, 4])
.range([0, chartWidth]);
options.scaleY = d3.scaleLinear()
.domain([0, 6])
.range([chartHeight, 0]);
options.color = d3.scaleOrdinal()
.domain(["fastball", "curveball", "changeup", "slider", "knuckleball", "splitter"])
.range(["red", "blue", "yellow", "green", "purple", "orange", "#b3b3b3"]);
this.initHeatmap();
// tooltip
options.tooltip = d3.select("body")
.append("div")
.attr("class", "pitch-tooltip")
.style("opacity", 0);
// strikeZone
var strikeZone = options.svg.append('g')
.attr('transform', 'translate(' + (options.scaleX(-1.1) + options.margins.left) + ',' + (options.scaleY(3.5) + options.margins.top) + ')');
strikeZone.append('rect')
.attr('width', options.scaleX(1.1) - options.scaleX(-1.1))
.attr('height', options.scaleY(1.0) - options.scaleY(3.5))
.attr('fill', 'none')
.attr('stroke', options.strikeZone.color)
.attr('stroke-width', options.strikeZone.width);
// axes
var axisXGroup = options.svg.append('g')
.attr('transform', 'translate(' + options.margins.left + ',' + (options.height - options.margins.bottom) + ')');
var axisX = d3.axisBottom(options.scaleX);
axisXGroup.call(axisX);
var axisYGroup = options.svg.append('g')
.attr('transform', 'translate(' + (options.margins.left + options.scaleX(0)) + ',' + options.margins.top + ')');
var axisY = d3.axisLeft(options.scaleY).ticks(6);
axisYGroup.call(axisY);
},
initHeatmap: function(){
this.initHeatmapCheckbox();
var that = this;
var options = this.options.chartModule;
var heatmapWidth = options.width,
heatmapHeight = options.height;
var heatmapWrapper = d3.select(options.appendToSelector)
.insert("div", ':first-child')
.attr("class", 'pitch-heatmap-wrapper')
.style("width", heatmapWidth+"px")
.style("height", heatmapHeight+"px")
.style("z-index", -1);
var heatmap = heatmapWrapper
.append('div')
.attr("class", 'pitch-heatmap');
// minimal heatmap instance configuration
options.heatmap.instance = h337.create({
// only container is required, the rest will be defaults
container: document.querySelector('.pitch-heatmap')
});
},
initHeatmapCheckbox: function(){
var that = this;
var heatmapCheckbox = d3.select("#pitch-heatmap-checkbox");
heatmapCheckbox.on("change", function(){
that.options.chartModule.heatmap.show = !!this.checked;
that.onHeatmapCheckboxChange();
});
},
onHeatmapCheckboxChange: function(){
this.drawBackground();
},
drawBackground: function () {
var that = this;
var options = this.options.chartModule;
if(true === options.heatmap.show){
that.drawHeatmap();
}else{
that.clearHeatmap();
}
},
drawHeatmap: function(){
var that = this;
var options = this.options.chartModule;
// now generate some random data
var points = [];
var max = 1;
for(var i=0; i < that.data.length; i++){
var coordinates = that.ballXY(that.data[i]);
var point = {
x: Math.round(coordinates.x + options.margins.left),
y: Math.round(coordinates.y + options.margins.top),
value: 0.7
};
points.push(point);
}
// heatmap data format
var data = {
max: max,
data: points
};
// if you have a set of datapoints always use setData instead of addData
// for data initialization
options.heatmap.instance.setData(data);
},
clearHeatmap: function(){
var that = this;
var options = this.options.chartModule;
options.heatmap.instance.setData({
max: 0,
min: 0,
data: [
]
});
},
updateChart: function(){
var that = this;
var options = this.options.chartModule;
var circlesSelection = options.mainGroup.selectAll('circle')
.data(this.data);
circlesSelection.exit().remove();
var circlesEnter = circlesSelection
.enter()
.append('circle')
.attr('class', 'pitch-ball')
.attr('stroke', 'none')
.attr('r', 5)
.on('click', function(d, i) {
var el = d3.select(this);
that.ballManuallySelected(i);
})
.on('mouseenter', function(d) {
var el = d3.select(this);
// tooltip
options.tooltip.transition()
.duration(200)
.style("opacity", .8);
options.tooltip.html(parseFloat(d.pitchSpeed).toFixed(1)+' mph')
.style("left", (d3.event.pageX + 5) + "px")
.style("top", (d3.event.pageY - 28 - 5) + "px");
// classes
el.classed(options.ballHoverClass, true);
})
.on('mouseout', function(d) {
var el = d3.select(this);
// tooltip
options.tooltip.transition()
.duration(500)
.style("opacity", 0);
// classes
el.classed(options.ballHoverClass, false);
});
var circles = circlesSelection.merge(circlesEnter);
circles
.attr('fill', function(d,i){return options.color(d.pitchType)})
.attr('cx', function(d, i){return that.ballXY(d).x})
.attr('cy', function(d, i){return that.ballXY(d).y});
this.drawBackground();
},
animateChart: function(){
var that = this;
var options = this.options.chartModule;
options.mainGroup.selectAll('circle.pitch-ball')
.classed(options.ballActiveClass, false);
var el = options.mainGroup.selectAll('circle.pitch-ball').filter(function(d,i){return i===that.activeIndex});
if(el.size() <= 0)
return;
// only one active
var alreadyIsActive = el.classed(options.ballActiveClass);
if( ! alreadyIsActive){
el.classed(options.ballActiveClass, ! alreadyIsActive);
el.classed(options.ballHoverClass, false);
}
},
ballXY: function(d){
var that = this;
var options = this.options.chartModule;
return {
x: options.scaleX(d.pitchX),
y: options.scaleY(d.pitchY),
}
},
// ============================== SELECT MODULE ==============================
initSelectModule:function(){
var options = this.options.selectModule;
var select = d3.select(options.appendToSelector).append('select')
.attr('name', 'active_event')
.attr('class', 'form-control');
var nullOption = select.append('option')
.attr('value', '')
.text('ALL PITCHES');
for(var i=0; i < this.eventsData.length; i++){
select.append('option')
.attr('value', this.eventsData[i].values[0].eventID)
.text(this.eventsData[i].values[0].eventName);
}
select.on('change', this.onSelectChange);
},
onSelectChange: function (e) {
var old = PITCH_APP.activeEvent;
PITCH_APP.activeEvent = this.value.length > 0 ? this.value : null;
if(old != PITCH_APP.activeEvent){
PITCH_APP.onActiveEventChange();
}
},
onActiveEventChange: function(){
this.updateData();
this.updateModules();
this.setIndexAndAnimate(null);
},
};
PITCH_APP.start();
<!DOCTYPE html>
<meta charset='utf-8'>
<html>
<head>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/video.js/6.6.0/alt/video-js-cdn.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<link rel='stylesheet' href='bp_data.css'>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-sm-4" id="select-module"></div>
<div class="col-sm-4" id="heatmap-checkbox-block">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="heatmap-checkbox">
<label class="form-check-label" for="heatmap-checkbox">Heatmap</label>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-sm-4">
<div id="distance-module"></div>
</div>
<div class="col-sm-4" id="exit-launch-angle-module"></div>
<div class="col-sm-4" id="exit-direction-module"></div>
</div>
<div class="row">
<div class="col-sm-4" id="table-module"></div>
<div class="col-sm-8" id="video-module"></div>
</div>
<!-- PITHC MODULE -->
<div class="row">
<div class="col-sm-4" id="pitch-select-module"></div>
<div class="col-sm-4">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="pitch-heatmap-checkbox">
<label class="form-check-label" for="pitch-heatmap-checkbox">Heatmap</label>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-4">
<div id="pitch-chart-module"></div>
</div>
</div>
<div class="row">
<div class="col-sm-4" id="pitch-table-module"></div>
<div class="col-sm-8" id="pitch-video-module"></div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/video.js/6.6.0/video.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/heatmap.js/2.0.2/heatmap.min.js"></script>
<script type='text/javascript' src='charts_batter_spray.js'></script>
<script type='text/javascript' src='charts_pitcher_plot.js'></script>
</body>
</html>
{
"RECORDS": [
{
"playID": "834",
"eventName": "Tryouts 2",
"eventDate": "16/12/2017 23:47:23",
"eventID": "9",
"name": "Elian Almanzar",
"pitchvelo": "80.520",
"pitchX": "0.850",
"pitchY": "3.40",
"ptype": "slider",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/248_ElianAlmanzar_Bullpen.mp4",
"video_id": "802"
},
{
"playID": "835",
"eventName": "Tryouts 2",
"eventDate": "16/12/2017 23:47:23",
"eventID": "9",
"name": "Elian Almanzar",
"pitchvelo": "80.520",
"pitchX": "0.750",
"pitchY": "3.20",
"ptype": "changeup",
"presult": "Ball",
"videoPath": "",
"video_id": "802"
},
{
"playID": "836",
"eventName": "Tryouts 2",
"eventDate": "16/12/2017 23:47:23",
"eventID": "9",
"name": "Elian Almanzar",
"pitchvelo": "80.520",
"pitchX": "0.750",
"pitchY": "3.0",
"ptype": "curveball",
"presult": "Ball",
"video_id": "802"
},
{
"playID": "837",
"eventName": "Tryouts 2",
"eventDate": "16/12/2017 23:47:23",
"eventID": "9",
"name": "Elian Almanzar",
"pitchvelo": "80.520",
"pitchX": "0.750",
"pitchY": "2.80",
"ptype": "fastball",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/248_ElianAlmanzar_Bullpen.mp4",
"video_id": "802"
},
{
"playID": "838",
"eventName": "Tryouts 2",
"eventDate": "16/12/2017 23:47:23",
"eventID": "9",
"name": "Elian Almanzar",
"pitchvelo": "80.520",
"pitchX": "0.750",
"pitchY": "3.650",
"ptype": "splitter",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/248_ElianAlmanzar_Bullpen.mp4",
"video_id": "802"
},
{
"playID": "839",
"eventName": "Tryouts 2",
"eventDate": "16/12/2017 23:47:23",
"eventID": "9",
"name": "Elian Almanzar",
"pitchvelo": "75.520",
"pitchX": "0.350",
"pitchY": "2.650",
"ptype": "knuckleball",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/248_ElianAlmanzar_Bullpen.mp4",
"video_id": "802"
},
{
"playID": "841",
"eventName": "Tryouts 1",
"eventDate": "16/12/2017 23:47:23",
"eventID": "8",
"name": "Elian Almanzar",
"pitchvelo": "85.520",
"pitchX": "-0.350",
"pitchY": "1.750",
"ptype": "Slider",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/248_ElianAlmanzar_Bullpen.mp4",
"video_id": "802"
},
{
"playID": "842",
"eventName": "Chandler World Tryouts",
"eventDate": "16/12/2017 23:47:23",
"eventID": "7",
"name": "Elian Almanzar",
"pitchvelo": "85.520",
"pitchX": "0.000",
"pitchY": "0.000",
"ptype": "Fastball",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/248_ElianAlmanzar_Bullpen.mp4",
"video_id": "802"
},
{
"playID": "843",
"eventName": "Chandler World Tryouts",
"eventDate": "16/12/2017 23:47:23",
"eventID": "7",
"name": "Elian Almanzar",
"pitchvelo": "83.949",
"pitchX": "-1.760",
"pitchY": "1.050",
"ptype": "Fastball",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/249_ElianAlmanzar_Bullpen.mp4",
"video_id": "803"
},
{
"playID": "844",
"eventName": "Chandler World Tryouts",
"eventDate": "16/12/2017 23:47:23",
"eventID": "7",
"name": "Elian Almanzar",
"pitchvelo": "88.241",
"pitchX": "-0.144",
"pitchY": "2.465",
"ptype": "Fastball",
"presult": "Strike",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/250_ElianAlmanzar_Bullpen.mp4",
"video_id": "804"
},
{
"playID": "845",
"eventName": "Chandler World Tryouts",
"eventDate": "16/12/2017 23:47:23",
"eventID": "7",
"name": "Elian Almanzar",
"pitchvelo": "86.718",
"pitchX": "0.515",
"pitchY": "0.024",
"ptype": "Fastball",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/251_ElianAlmanzar_Bullpen.mp4",
"video_id": "805"
},
{
"playID": "846",
"eventName": "Chandler World Tryouts",
"eventDate": "16/12/2017 23:47:23",
"eventID": "7",
"name": "Elian Almanzar",
"pitchvelo": "87.905",
"pitchX": "0.608",
"pitchY": "1.434",
"ptype": "Fastball",
"presult": "Strike",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/252_ElianAlmanzar_Bullpen.mp4",
"video_id": "806"
},
{
"playID": "847",
"eventName": "Chandler World Tryouts",
"eventDate": "16/12/2017 23:47:23",
"eventID": "7",
"name": "Elian Almanzar",
"pitchvelo": "87.519",
"pitchX": "2.190",
"pitchY": "1.084",
"ptype": "Fastball",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/253_ElianAlmanzar_Bullpen.mp4",
"video_id": "807"
},
{
"playID": "848",
"eventName": "Chandler World Tryouts",
"eventDate": "16/12/2017 23:47:23",
"eventID": "7",
"name": "Elian Almanzar",
"pitchvelo": "86.924",
"pitchX": "0.000",
"pitchY": "0.000",
"ptype": "Fastball",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/254_ElianAlmanzar_Bullpen.mp4",
"video_id": "808"
},
{
"playID": "849",
"eventName": "Chandler World Tryouts",
"eventDate": "16/12/2017 23:47:23",
"eventID": "7",
"name": "Elian Almanzar",
"pitchvelo": "84.432",
"pitchX": "-3.377",
"pitchY": "0.431",
"ptype": "Fastball",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/255_ElianAlmanzar_Bullpen.mp4",
"video_id": "809"
},
{
"playID": "850",
"eventName": "Chandler World Tryouts",
"eventDate": "16/12/2017 23:47:23",
"eventID": "7",
"name": "Elian Almanzar",
"pitchvelo": "86.713",
"pitchX": "-0.455",
"pitchY": "3.587",
"ptype": "Fastball",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/256_ElianAlmanzar_Bullpen.mp4",
"video_id": "810"
},
{
"playID": "851",
"eventName": "Chandler World Tryouts",
"eventDate": "16/12/2017 23:47:23",
"eventID": "7",
"name": "Elian Almanzar",
"pitchvelo": "74.994",
"pitchX": "-2.375",
"pitchY": "1.023",
"ptype": "Curveball",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/257_ElianAlmanzar_Bullpen.mp4",
"video_id": "811"
},
{
"playID": "852",
"eventName": "Chandler World Tryouts",
"eventDate": "16/12/2017 23:47:23",
"eventID": "7",
"name": "Elian Almanzar",
"pitchvelo": "68.233",
"pitchX": "-0.301",
"pitchY": "4.668",
"ptype": "Curveball",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/258_ElianAlmanzar_Bullpen.mp4",
"video_id": "812"
},
{
"playID": "853",
"eventName": "Chandler World Tryouts",
"eventDate": "16/12/2017 23:47:23",
"eventID": "7",
"name": "Elian Almanzar",
"pitchvelo": "72.246",
"pitchX": "-1.486",
"pitchY": "2.132",
"ptype": "Curveball",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/259_ElianAlmanzar_Bullpen.mp4",
"video_id": "813"
},
{
"playID": "854",
"eventName": "Chandler World Tryouts",
"eventDate": "16/12/2017 23:47:23",
"eventID": "7",
"name": "Elian Almanzar",
"pitchvelo": "72.823",
"pitchX": "-1.894",
"pitchY": "3.345",
"ptype": "Curveball",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/260_ElianAlmanzar_Bullpen.mp4",
"video_id": "814"
},
{
"playID": "855",
"eventName": "Chandler World Tryouts",
"eventDate": "16/12/2017 23:47:23",
"eventID": "7",
"name": "Elian Almanzar",
"pitchvelo": "78.118",
"pitchX": "0.000",
"pitchY": "0.000",
"ptype": "Changeup",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/261_ElianAlmanzar_Bullpen.mp4",
"video_id": "815"
},
{
"playID": "856",
"eventName": "Chandler World Tryouts",
"eventDate": "16/12/2017 23:47:23",
"eventID": "7",
"name": "Elian Almanzar",
"pitchvelo": "78.518",
"pitchX": "-2.766",
"pitchY": "1.780",
"ptype": "Changeup",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/262_ElianAlmanzar_Bullpen.mp4",
"video_id": "816"
},
{
"playID": "857",
"eventName": "Chandler World Tryouts",
"eventDate": "16/12/2017 23:47:23",
"eventID": "7",
"name": "Elian Almanzar",
"pitchvelo": "84.582",
"pitchX": "-0.376",
"pitchY": "3.587",
"ptype": "Fastball",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/263_ElianAlmanzar_Bullpen.mp4",
"video_id": "817"
},
{
"playID": "858",
"eventName": "Chandler World Tryouts",
"eventDate": "16/12/2017 23:47:23",
"eventID": "7",
"name": "Elian Almanzar",
"pitchvelo": "87.359",
"pitchX": "-0.592",
"pitchY": "4.363",
"ptype": "Fastball",
"presult": "Ball",
"videoPath": "https://s3.amazonaws.com/prospectwire/2017-12-16/ChandlerWorldTryouts/264_ElianAlmanzar_Bullpen.mp4",
"video_id": "818"
}
]
}
{"RECORDS":
[
{
"playID":"461",
"eventName":"Another Tryouts 2",
"eventDate":"2017-12-29 23:47:23",
"eventID":"9",
"name":"Aidan McNamara",
"exitvelo":"96",
"vlaunch":"30.174",
"distance":"290.641",
"hlaunch":"34.069",
"videoPath":"",
"video_id":""
},
{
"playID":"460",
"eventName":"Another Tryouts",
"eventDate":"2017-12-28 23:47:23",
"eventID":"8",
"name":"Aidan McNamara",
"exitvelo":"98",
"vlaunch":"32.174",
"distance":"310",
"hlaunch":"32.069",
"videoPath":"",
"video_id":""
},
{
"playID":"459",
"eventName":"Another Tryouts",
"eventDate":"2017-12-27 23:47:23",
"eventID":"8",
"name":"Aidan McNamara",
"exitvelo":"100",
"vlaunch":"34.174",
"distance":"330.641",
"hlaunch":"30.069",
"videoPath":"",
"video_id":""
},
{
"playID":"458",
"eventName":"Another Tryouts",
"eventDate":"2017-12-26 23:47:23",
"eventID":"8",
"name":"Aidan McNamara",
"exitvelo":"102",
"vlaunch":"36.174",
"distance":"350.641",
"hlaunch":"28.069",
"videoPath":"",
"video_id":""
},
{
"playID":"457",
"eventName":"Chandler World Tryouts",
"eventDate":"2017-12-16 23:47:23",
"eventID":"7",
"name":"Aidan McNamara",
"exitvelo":"105",
"vlaunch":"38.174",
"distance":"380.641",
"hlaunch":"-28.069",
"videoPath":"https:\/\/s3.amazonaws.com\/prospectwire\/2017-12-16\/ChandlerWorldTryouts\/1008_BP_AidanMcNamara.mp4",
"video_id":"422"
},
{"playID":"451","eventName":"Chandler World Tryouts","eventDate":"2017-12-16 23:47:23","eventID":"7","name":"Aidan McNamara","exitvelo":"99.341","vlaunch":"13.783","distance":"322.474","hlaunch":"-33.130","videoPath":"https:\/\/s3.amazonaws.com\/prospectwire\/2017-12-16\/ChandlerWorldTryouts\/175_BP_AidanMcNamara.mp4","video_id":"416"},
{"playID":"452","eventName":"Chandler World Tryouts","eventDate":"2017-12-16 23:47:23","eventID":"7","name":"Aidan McNamara","exitvelo":"72.831","vlaunch":"46.000","distance":"215.248","hlaunch":"-0.745","videoPath":"https:\/\/s3.amazonaws.com\/prospectwire\/2017-12-16\/ChandlerWorldTryouts\/176_BP_AidanMcNamara.mp4","video_id":"417"},
{"playID":"453","eventName":"Chandler World Tryouts","eventDate":"2017-12-16 23:47:23","eventID":"7","name":"Aidan McNamara","exitvelo":"77.538","vlaunch":"33.029","distance":"273.473","hlaunch":"-19.060","videoPath":"https:\/\/s3.amazonaws.com\/prospectwire\/2017-12-16\/ChandlerWorldTryouts\/177_BP_AidanMcNamara.mp4","video_id":"418"},
{"playID":"454","eventName":"Chandler World Tryouts","eventDate":"2017-12-16 23:47:23","eventID":"7","name":"Aidan McNamara","exitvelo":"96.713","vlaunch":"46.153","distance":"360.351","hlaunch":"-22.730","videoPath":"https:\/\/s3.amazonaws.com\/prospectwire\/2017-12-16\/ChandlerWorldTryouts\/178_BP_AidanMcNamara.mp4","video_id":"419"},
{"playID":"455","eventName":"Chandler World Tryouts","eventDate":"2017-12-16 23:47:23","eventID":"7","name":"Aidan McNamara","exitvelo":"63.877","vlaunch":"32.125","distance":"199.605","hlaunch":"-20.147","videoPath":"https:\/\/s3.amazonaws.com\/prospectwire\/2017-12-16\/ChandlerWorldTryouts\/194_BP_AidanMcNamara.mp4","video_id":"420"},{"playID":"456","eventName":"Chandler World Tryouts","eventDate":"2017-12-16 23:47:23","eventID":"7","name":"Aidan McNamara","exitvelo":"96.911","vlaunch":"23.362","distance":"359.380","hlaunch":"-31.264","videoPath":"https:\/\/s3.amazonaws.com\/prospectwire\/2017-12-16\/ChandlerWorldTryouts\/195_BP_AidanMcNamara.mp4","video_id":"421"},{"playID":"458","eventName":"Chandler World Tryouts","eventDate":"2017-12-16 23:47:23","eventID":"7","name":"Aidan McNamara","exitvelo":"85.341","vlaunch":"21.313","distance":"246.031","hlaunch":"-34.929","videoPath":"https:\/\/s3.amazonaws.com\/prospectwire\/2017-12-16\/ChandlerWorldTryouts\/196_BP_AidanMcNamara.mp4","video_id":"423"},{"playID":"459","eventName":"Chandler World Tryouts","eventDate":"2017-12-16 23:47:23","eventID":"7","name":"Aidan McNamara","exitvelo":"77.434","vlaunch":"44.336","distance":"256.928","hlaunch":"-40.860","videoPath":"https:\/\/s3.amazonaws.com\/prospectwire\/2017-12-16\/ChandlerWorldTryouts\/197_BP_AidanMcNamara.mp4","video_id":"424"},{"playID":"460","eventName":"Chandler World Tryouts","eventDate":"2017-12-16 23:47:23","eventID":"7","name":"Aidan McNamara","exitvelo":"91.025","vlaunch":"20.326","distance":"323.653","hlaunch":"-45.412","videoPath":"https:\/\/s3.amazonaws.com\/prospectwire\/2017-12-16\/ChandlerWorldTryouts\/198_BP_AidanMcNamara.mp4","video_id":"425"}]}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment