Created
January 9, 2018 12:17
-
-
Save valex/028ddf32f9c2df8f5da613ec53ef2595 to your computer and use it in GitHub Desktop.
Spray+Plot
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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%; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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°"); | |
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 = "°"; | |
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°"); | |
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)) + "°"); | |
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 += '°'; | |
} | |
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(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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" | |
} | |
] | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{"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