Created
February 20, 2018 17:49
-
-
Save valex/37d9e79b391675bed2e0ae57fc75ddbf to your computer and use it in GitHub Desktop.
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: 'Game2_AtBats_Query.json', | |
| data: [], | |
| eventsData: [], | |
| activeIndex: null, | |
| activeEvent: '', | |
| defaultVideoIndex: 0, | |
| activeVideoIndex: null, | |
| timer: null, | |
| autoplay: false, | |
| options: { | |
| atBat: false, | |
| }, | |
| animation: { | |
| duration: 1000, | |
| delay: 4000 | |
| }, | |
| selectModuleOptions:{ | |
| appendToSelector: '#select-module', | |
| }, | |
| tableModuleOptions:{ | |
| id: 'baseball-app-table', | |
| appendToSelector: '#table-module', | |
| rowActiveClass:"active", | |
| tbody: null, | |
| columnsTemplates: { | |
| 'default': [ | |
| { | |
| 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: 'videos', | |
| label: '', | |
| }, | |
| ], | |
| 'at_bat': [ | |
| { | |
| key: 'p_result_label', | |
| label: 'Result', | |
| }, | |
| { | |
| key: 'p_type', | |
| label: 'Pitch', | |
| }, | |
| { | |
| key: 'exitSpeed', | |
| label: 'Exit (mph)', | |
| fixed: 1, | |
| }, | |
| { | |
| key: 'vAngle', | |
| label: 'V Launch', | |
| fixed: 0, | |
| }, | |
| { | |
| key: 'hAngle', | |
| label: 'H Launch', | |
| fixed: 0, | |
| }, | |
| { | |
| key: 'distance', | |
| label: 'Distance', | |
| fixed: 0, | |
| }, | |
| { | |
| key: 'videos', | |
| label: '', | |
| }, | |
| ], | |
| }, | |
| columns: [] | |
| }, | |
| 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], | |
| svg: null, | |
| 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, | |
| videoJsCache: null, | |
| ready: false, | |
| cache: true, | |
| appendToSelector: '#video-module' | |
| }, | |
| mainColor: '#005d80', | |
| mainLightColor: '#3bafda', | |
| start: function(){ | |
| // Set-up the export button | |
| d3.select('#export').on('click', function(){ | |
| BASEBALL_ANIM_APP.export(); | |
| }); | |
| this.activeVideoIndex = this.defaultVideoIndex; | |
| 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) { | |
| var videos=[]; | |
| var videoSrc; | |
| if(typeof d["angle1"] != 'undefined' && d["angle1"] !== null){ | |
| videoSrc = (''+d["angle1"]).trim(); | |
| if(videoSrc.length > 0){ | |
| videos.push({ | |
| src: videoSrc | |
| }); | |
| } | |
| } | |
| if(typeof d["angle2"] != 'undefined' && d["angle2"] !== null){ | |
| videoSrc = (''+d["angle2"]).trim(); | |
| if(videoSrc.length > 0){ | |
| videos.push({ | |
| src: videoSrc | |
| }); | |
| } | |
| } | |
| return { | |
| exitSpeed: +d["exitvelo"], | |
| vAngle: +d["vlaunch"], | |
| hAngle: +d["hlaunch"], | |
| distance: +d["distance"], | |
| videos: videos, | |
| eventID: +d["eventID"], | |
| eventName: (''+d["eventName"]).trim(), | |
| eventDatetime: moment(d["eventDate"]+' '+d["eventTime"], "D/M/YYYY HH:mm:ss"), | |
| pitcher: (''+d["pitcher"]).trim().toLowerCase(), | |
| p_type: (''+d["pType"]).trim().toLowerCase(), | |
| p_result: (''+d["result"]).trim().toLowerCase(), | |
| p_result_label: (''+d["result"]).trim(), | |
| } | |
| }); | |
| that.eventsData = d3.nest() | |
| .key(function(d) { return d.eventID; }) | |
| .entries(data); | |
| that.buildData(); | |
| that.initModules(); | |
| that.updateModules(); | |
| if(true === that.autoplay){ | |
| that.playNextVideo(); | |
| } | |
| }); | |
| }, | |
| initModules: function(){ | |
| this.initAtBatCheckbox(); | |
| this.initSelectModule(); | |
| this.initDistanceModule(); | |
| this.initExitLaunchAngleModule(); | |
| this.initExitDirectionModule(); | |
| this.initAutoplayCheckbox(); | |
| }, | |
| updateModules: function(){ | |
| this.updateExitLaunchAngleModule(); | |
| this.updateExitDirectionModule(); | |
| this.updateDistanceModule(); | |
| this.updateTableModule(); | |
| }, | |
| animateModules:function(){ | |
| this.animateExitLaunchAngleModule(); | |
| this.animateExitDirectionModule(); | |
| this.animateDistanceModule(); | |
| this.animateTableModule(); | |
| this.updateVideoModule(); | |
| }, | |
| buildData: function(){ | |
| var that = this; | |
| that.data = d3.merge( | |
| this.eventsData.map(function(d){ | |
| return d.values.filter(function(dd){ | |
| if(true === that.options.atBat && dd.pitcher === 'bp'){ | |
| return false; | |
| } | |
| if(that.activeEvent !== null | |
| && that.activeEvent.length > 0 //if active event sets | |
| && dd.eventID != that.activeEvent | |
| ){ | |
| return false; | |
| } | |
| return true; | |
| }) | |
| }) | |
| ); | |
| that.data.sort(function(a, b) { return d3.ascending(a['eventDatetime'], b['eventDatetime']); }); | |
| // add timing fields | |
| that.data = that.data.map(function(d, i){ | |
| var prevI = that.getPrevIndex(i); | |
| var prevD = that.data[prevI]; | |
| var nextI = that.getNextIndex(i); | |
| var nextD = that.data[nextI]; | |
| d['secondsToNext'] = Math.abs(nextD.eventDatetime.format('X') - d.eventDatetime.format('X')); | |
| d['secondsToPrev'] = Math.abs(d.eventDatetime.format('X') - prevD.eventDatetime.format('X')); | |
| return d; | |
| }); | |
| }, | |
| rebuild: function(){ | |
| this.setActiveIndex(null); | |
| this.setVideoActiveIndex(this.defaultVideoIndex); | |
| this.buildData(); | |
| this.updateModules(); | |
| this.updateVideoModule(); | |
| if(true === this.autoplay){ | |
| this.playNextVideo(); | |
| } | |
| }, | |
| onAtBatCheckboxChange:function(){ | |
| this.rebuild(); | |
| }, | |
| onActiveEventChange: function(){ | |
| this.rebuild(); | |
| }, | |
| onVideoPlayerReady: function(){ | |
| this.videoModuleOptions.ready = true; | |
| this.videoModuleOptions.videoJs.play(); | |
| }, | |
| setVideoSrc: function(videoSrc){ | |
| this.videoModuleOptions.videoJs.src({ | |
| src: videoSrc, | |
| type: 'video/mp4' | |
| }); | |
| }, | |
| getPrevIndex: function(current){ | |
| current = typeof current !== 'undefined' ? current : null; | |
| var dataLength = this.dataLength(); | |
| if(dataLength <= 0) | |
| return null; | |
| if(current === null){ | |
| return dataLength - 1; | |
| } | |
| var prevIndex = current - 1; | |
| if(prevIndex < 0) { | |
| prevIndex = dataLength - 1; | |
| } | |
| return prevIndex; | |
| }, | |
| getNextIndex: function(current){ | |
| current = typeof current !== 'undefined' ? current : null; | |
| var dataLength = this.dataLength(); | |
| if(dataLength <= 0) | |
| return null; | |
| if(current === null) | |
| return 0; | |
| var nextIndex = current + 1; | |
| if(nextIndex === dataLength){ | |
| nextIndex = 0; | |
| } | |
| return nextIndex; | |
| }, | |
| ballManuallySelected: function(index){ | |
| this.setIndexAndAnimate(index); | |
| }, | |
| setIndexAndAnimate: function(index){ | |
| this.setActiveIndex(index); | |
| this.animateModules(); | |
| }, | |
| setActiveIndex:function(index){ | |
| this.activeIndex = index; | |
| }, | |
| datasetHasVideo: function(){ | |
| for(var i=0; i < this.data.length; i++){ | |
| if(this.hasVideo(this.data[i])) | |
| return true; | |
| } | |
| return false; | |
| }, | |
| initAtBatCheckbox: function(){ | |
| var that = this; | |
| var atBatCheckbox = d3.select("#at-bat-checkbox"); | |
| atBatCheckbox.node().checked = that.options.atBat; | |
| atBatCheckbox.on("change", function(){ | |
| that.options.atBat = !!this.checked; | |
| that.onAtBatCheckboxChange(); | |
| }); | |
| }, | |
| hasVideo: function(data){ | |
| if(typeof data === 'undefined') | |
| return false; | |
| return data.videos.length; | |
| }, | |
| 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; | |
| 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); | |
| this.placeTopLabels(); | |
| // 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.initHeatmapCheckbox(); | |
| this.drawBackground(); | |
| this.drawDistanceLabels(); | |
| }, | |
| initHeatmap: function(){ | |
| var that = this; | |
| var options = this.distanceModuleOptions; | |
| var heatmapWidth = options.width, | |
| heatmapHeight = options.height; | |
| var heatmapWrapper = d3.select(options.appendToSelector) | |
| .append("div") | |
| .attr("class", 'heatmap-wrapper') | |
| .style("width", heatmapWidth+"px") | |
| .style("height", heatmapHeight+"px"); | |
| 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; | |
| if(options.heatmap.instance === null){ | |
| this.initHeatmap(); | |
| } | |
| 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; | |
| if(options.heatmap.instance === null) | |
| return; | |
| 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.clearAllActiveCircles(); | |
| 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){ | |
| this.clearAllActiveCircles(); | |
| el.classed(options.ballActiveClass, !alreadyIsActive); | |
| el.classed(options.ballHoverClass, false); | |
| } | |
| }, | |
| clearAllActiveCircles: function(){ | |
| var options = this.distanceModuleOptions; | |
| options.mainGroup.selectAll('#'+options.id+' .ball') | |
| .classed(options.ballActiveClass, false); | |
| }, | |
| placeTopLabels: function(){ | |
| var that = this; | |
| var options = this.distanceModuleOptions; | |
| // 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", options.width - options.margins.right) | |
| .attr("y", 28) | |
| .attr("font-size", "18px") | |
| .attr("alignment-baseline", "baseline") | |
| .attr("dominant-baseline", "baseline") // for firefox | |
| .attr("text-anchor", "end") | |
| .attr("class", "ft-label") | |
| .style("fill", this.mainColor) | |
| .html("ft"); | |
| // ft value | |
| options.mainGroup.append("text") | |
| .attr("id", "ft-value") | |
| .attr("x", options.width - options.margins.right - 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"); | |
| }, | |
| hideTopLabels: function(){ | |
| d3.select('#ft-value').style("opacity", 0.0); | |
| d3.select('.ft-label').style("opacity", 0.0); | |
| d3.select('.title').style("opacity", 0.0); | |
| }, | |
| showTopLabels: function(){ | |
| d3.select('#ft-value').style("opacity", 1.0); | |
| d3.select('.ft-label').style("opacity", 1.0); | |
| d3.select('.title').style("opacity", 1.0); | |
| }, | |
| 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 ============================== | |
| updateTableModule:function(){ | |
| var that = this; | |
| var options = this.tableModuleOptions; | |
| d3.select(options.appendToSelector).html(''); | |
| 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'); | |
| options.columns = options.columnsTemplates['default']; | |
| if(this.options.atBat){ | |
| options.columns = options.columnsTemplates['at_bat']; | |
| } | |
| // append the header row | |
| thead.append('tr') | |
| .selectAll('th') | |
| .data(options.columns) | |
| .enter() | |
| .append('th') | |
| .text(function (column) { return column.label; }); | |
| // 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;}) | |
| .attr('title', function(d,i){ return d.eventDatetime.format('YYYY-MM-DD HH:mm:ss');}) | |
| .style('background-color', function(d,i){ | |
| var color = null; | |
| if(that.isEndAtBat(d)){ | |
| if(that.options.atBat){ | |
| if((d.p_result =='contact out' && d.exitSpeed >= 90) | |
| || d.p_result == 'single' | |
| || d.p_result == 'double') | |
| { | |
| color = '#a3ffac'; | |
| } | |
| else if( | |
| (d.p_result =='contact out' && (d.exitSpeed < 90 && d.exitSpeed >= 75)) | |
| || d.p_result =='hit by pitch' | |
| || d.p_result =='error' | |
| || d.p_result =='ball' | |
| || d.p_result =='intentional ball' | |
| ) | |
| { | |
| color = '#ffff99'; | |
| } | |
| else if((d.p_result =='contact out' && d.exitSpeed < 75) | |
| || d.p_result =='swinging strikeout' | |
| || d.p_result =='called strikeout') | |
| { | |
| color = '#ff6666'; | |
| } | |
| } | |
| } | |
| return color; | |
| }) | |
| .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, i) { | |
| var allZero = that.isAllZero(row); | |
| return options.columns.map(function (column) { | |
| var value, type; | |
| switch(column.key){ | |
| case 'p_result_label': | |
| type = 'string'; | |
| value = row[column.key]; | |
| break; | |
| case 'p_type': | |
| type = 'string'; | |
| switch(row[column.key]){ | |
| case 'fastball': | |
| value = 'FA'; | |
| break; | |
| case 'curveball': | |
| value = 'CB'; | |
| break; | |
| case 'slider': | |
| value = 'SL'; | |
| break; | |
| case 'changeup': | |
| value = 'CH'; | |
| break; | |
| case 'cutter': | |
| value = 'CT'; | |
| break; | |
| case 'splitter': | |
| value = 'SPL'; | |
| break; | |
| case 'knuckleball': | |
| value = 'KN'; | |
| break; | |
| case 'screwball': | |
| value = 'SCR'; | |
| break; | |
| default: | |
| value = row[column.key]; | |
| break; | |
| } | |
| break; | |
| case 'videos': | |
| type = 'videos'; | |
| value = row.videos; | |
| break; | |
| default: | |
| type = 'number'; | |
| if(allZero === true){ | |
| value = ''; | |
| }else{ | |
| value = parseFloat(row[column.key]).toFixed(column.fixed); | |
| if(column.key == 'vAngle' || column.key == 'hAngle'){ | |
| value += '°'; | |
| } | |
| } | |
| break; | |
| } | |
| return { | |
| type: type, | |
| value: value | |
| }; | |
| }); | |
| }); | |
| var cellsEnter = cellsSelection | |
| .enter() | |
| .append('td'); | |
| var cells = cellsSelection.merge(cellsEnter); | |
| cells.each(function (d) { | |
| var el = d3.select(this); | |
| switch(d.type){ | |
| case 'videos': | |
| el.html(''); | |
| el.append('div') | |
| .attr('class', 'text-nowrap') | |
| .selectAll('i') | |
| .data(d.value) | |
| .enter() | |
| .append('i') | |
| .attr('class', function(d,i){ | |
| if(i > 0){ | |
| return 'fa fa-video-camera fa-rotate-315'; | |
| }else{ | |
| return 'fa fa-video-camera'; | |
| } | |
| }) | |
| .style('margin-right', '6px') | |
| .on('click', function(d, video_i){ | |
| that.onClickVideoIcon(video_i); | |
| }); | |
| break; | |
| case 'string': | |
| case 'number': | |
| el.html(function (d) { return d.value; }); | |
| break; | |
| } | |
| }); | |
| this.clearAllActiveRows(); | |
| }, | |
| animateTableModule: function(){ | |
| this.setRowActive(); | |
| }, | |
| onClickVideoIcon: function(video_index){ | |
| this.setVideoActiveIndex(video_index); | |
| }, | |
| setRowActive: function(){ | |
| var options = this.tableModuleOptions; | |
| var table = d3.select('#'+options.id); | |
| var tr = table.select('#ball-'+this.activeIndex); | |
| // only one active | |
| var alreadyIsActive = tr.classed(options.rowActiveClass); | |
| if(alreadyIsActive) | |
| return; | |
| this.clearAllActiveRows(); | |
| tr.classed(options.rowActiveClass, !alreadyIsActive); | |
| }, | |
| clearAllActiveRows: function(){ | |
| var options = this.tableModuleOptions; | |
| var table = d3.select('#'+options.id); | |
| table.selectAll('#'+options.id+' tr') | |
| .classed(options.rowActiveClass, false); | |
| }, | |
| setVideoActiveIndex: function(index){ | |
| this.activeVideoIndex = index; | |
| }, | |
| highlightVideoIcon: function(){ | |
| var tableOptions = this.tableModuleOptions; | |
| this.extinguishAllVideoIcons(); | |
| d3.select('#'+tableOptions.id+' #ball-'+this.activeIndex+' .fa-video-camera:nth-child('+(1+this.activeVideoIndex)+')').classed('green', true); | |
| }, | |
| extinguishAllVideoIcons: function(){ | |
| var tableOptions = this.tableModuleOptions; | |
| d3.selectAll('#'+tableOptions.id+' .fa-video-camera').classed('green', false); | |
| }, | |
| isEndAtBat: function(data){ | |
| // if the result = single, double, triple, homerun, contact out, hit by pitch, strikeout swinging, called strikeout, error. | |
| // All of those values are indicators that the end of the at bat happened. | |
| if(data.p_result === 'single' | |
| || data.p_result === 'double' | |
| || data.p_result === 'triple' | |
| || data.p_result === 'homerun' | |
| || data.p_result === 'contact out' | |
| || data.p_result === 'hit by pitch' | |
| || data.p_result === 'swinging strikeout' | |
| || data.p_result === 'called strikeout' | |
| || data.p_result === 'error') | |
| { | |
| return true; | |
| } | |
| return data.secondsToNext > 360 // 360 seconds = 60 * 6 minutes | |
| }, | |
| isAllZero: function(data){ | |
| if(data['exitSpeed'] <= 0 | |
| && data['vAngle'] <= 0 | |
| && data['hAngle'] <= 0 | |
| && data['distance'] <= 0) | |
| { | |
| return true; | |
| } | |
| return false; | |
| }, | |
| // ============================== 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 video-js-active 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-active'), { | |
| aspectRatio: '16:9', | |
| controls: true, | |
| autoplay: false, | |
| preload: 'auto', | |
| }); | |
| options.videoJs.on('playing', function() { | |
| }); | |
| options.videoJs.on('ended', function() { | |
| if(true == that.autoplay){ | |
| that.playNextVideo(); | |
| } | |
| }); | |
| options.videoJs.ready(function() { | |
| that.onVideoPlayerReady(); | |
| }); | |
| if(options.cache === true){ | |
| var cacheInnerBlock = outerBlock | |
| .append('div') | |
| .style('width', '1px') | |
| .style('height', '1px') | |
| .attr('class', 'embed-responsive embed-responsive-16by9'); | |
| var cacheVideoTag = cacheInnerBlock.append('video') | |
| .attr('class', 'video-js video-js-cache embed-responsive-item'); | |
| options.videoJsCache = videojs(document.querySelector('.video-js-cache'), { | |
| aspectRatio: '16:9', | |
| controls: true, | |
| autoplay: false, | |
| muted: true, | |
| preload: 'auto', | |
| }); | |
| var nextVideo = this.getNextVideoIndex(); | |
| var videoSrc = this.data[nextVideo.row]['videos'][nextVideo.index]['src']; | |
| options.videoJsCache.src({ | |
| src: videoSrc, | |
| type: 'video/mp4' | |
| }); | |
| options.videoJsCache.on('loadedmetadata', function() { | |
| var video = this.children_[0]; | |
| if (video.buffered.length === 0) return; | |
| var bufferedSeconds = video.buffered.end(0) - video.buffered.start(0); | |
| console.log(bufferedSeconds + ' seconds of video are ready to play!'); | |
| }); | |
| } | |
| }, | |
| updateVideoModule: function(){ | |
| var options = this.videoModuleOptions; | |
| this.disposeVideoPlayer(); | |
| var datum = this.data[this.activeIndex]; | |
| if(this.hasVideo(datum)){ | |
| this.initVideoPlayer(); | |
| if(typeof datum['videos'][this.activeVideoIndex] == 'undefined') | |
| this.setVideoActiveIndex(this.defaultVideoIndex); | |
| this.setVideoSrc(datum['videos'][this.activeVideoIndex]['src']); | |
| } | |
| this.highlightVideoIcon(); | |
| }, | |
| playNextVideo: function(){ | |
| if( ! this.datasetHasVideo()) | |
| return; | |
| var currentVideoIndex = this.getCurrentVideoIndex(); | |
| var nextVideoIndex = this.getNextVideoIndex(); | |
| this.setVideoActiveIndex(nextVideoIndex.index); | |
| if(currentVideoIndex.row === nextVideoIndex.row){ | |
| this.updateVideoModule(); | |
| }else{ | |
| this.setIndexAndAnimate(nextVideoIndex.row) | |
| } | |
| }, | |
| getCurrentVideoIndex: function(){ | |
| return { | |
| row: this.activeIndex, | |
| index: this.activeVideoIndex, | |
| }; | |
| }, | |
| getNextVideoIndex: function(){ | |
| var nextVideo = this.getCurrentVideoIndex(); | |
| do{ | |
| if(nextVideo.row === null || nextVideo.index === null){ | |
| nextVideo = { | |
| row: 0, | |
| index: this.defaultVideoIndex, | |
| }; | |
| }else{ | |
| nextVideo['index']++; | |
| } | |
| if(typeof this.data[nextVideo['row']]['videos'][nextVideo['index']] === 'undefined'){ | |
| nextVideo['index'] = this.defaultVideoIndex; | |
| nextVideo['row']++; | |
| if(typeof this.data[nextVideo['row']] === 'undefined'){ | |
| nextVideo['row'] = 0; | |
| } | |
| } | |
| }while(typeof this.data[nextVideo['row']]['videos'][nextVideo['index']] === 'undefined') | |
| return nextVideo; | |
| }, | |
| startAutoplay: function(){ | |
| if(this.autoplay === true) | |
| return; | |
| this.autoplay = true; | |
| var autoplayCheckbox = d3.select("#autoplay-checkbox"); | |
| autoplayCheckbox.node().checked = this.autoplay; | |
| this.playNextVideo(); | |
| }, | |
| stopAutoplay: function(){ | |
| if(this.autoplay === false) | |
| return; | |
| this.autoplay = false; | |
| var autoplayCheckbox = d3.select("#autoplay-checkbox"); | |
| autoplayCheckbox.node().checked = this.autoplay; | |
| }, | |
| initAutoplayCheckbox: function(){ | |
| var that = this; | |
| var autoplayCheckbox = d3.select("#autoplay-checkbox"); | |
| autoplayCheckbox.node().checked = that.autoplay; | |
| autoplayCheckbox.on("change", function(){ | |
| !!this.checked ? that.startAutoplay() : that.stopAutoplay(); | |
| }); | |
| }, | |
| disposeVideoPlayer: function(){ | |
| if(BASEBALL_ANIM_APP.videoModuleOptions.videoJs !== null){ | |
| try{ | |
| BASEBALL_ANIM_APP.videoModuleOptions.videoJs.dispose(); | |
| }catch (e){ | |
| console.log(e); | |
| } | |
| BASEBALL_ANIM_APP.videoModuleOptions.videoJs = null; | |
| BASEBALL_ANIM_APP.videoModuleOptions.ready = false; | |
| } | |
| if(BASEBALL_ANIM_APP.videoModuleOptions.cache === true && BASEBALL_ANIM_APP.videoModuleOptions.videoJsCache !== null){ | |
| try{ | |
| BASEBALL_ANIM_APP.videoModuleOptions.videoJsCache.dispose(); | |
| }catch (e){ | |
| console.log(e); | |
| } | |
| BASEBALL_ANIM_APP.videoModuleOptions.videoJsCache = null; | |
| } | |
| }, | |
| // ============================== 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(); | |
| } | |
| }, | |
| // ============================== EXPORT MODULE ============================== | |
| export: function(){ | |
| var that = this; | |
| this.beforeExport(); | |
| var svgString = this.getSVGString(this.distanceModuleOptions.svg.node()); | |
| this.svgString2Image( svgString, 2*this.distanceModuleOptions.width, 2*this.distanceModuleOptions.height, 'png', save ); // passes Blob and filesize String to the callback | |
| function save( dataBlob, filesize ){ | |
| saveAs( dataBlob, 'spray_chart.png' ); // FileSaver.js function | |
| that.afterExport(); | |
| } | |
| }, | |
| beforeExport:function () { | |
| this.hideTopLabels(); | |
| }, | |
| afterExport:function () { | |
| this.showTopLabels(); | |
| }, | |
| // Below are the functions that handle actual exporting: | |
| // getSVGString ( svgNode ) and svgString2Image( svgString, width, height, format, callback ) | |
| getSVGString: function( svgNode ) { | |
| svgNode.setAttribute('xlink', 'http://www.w3.org/1999/xlink'); | |
| var cssStyleText = getCSSStyles( svgNode ); | |
| appendCSS( cssStyleText, svgNode ); | |
| var serializer = new XMLSerializer(); | |
| var svgString = serializer.serializeToString(svgNode); | |
| svgString = svgString.replace(/(\w+)?:?xlink=/g, 'xmlns:xlink='); // Fix root xlink without namespace | |
| svgString = svgString.replace(/NS\d+:href/g, 'xlink:href'); // Safari NS namespace fix | |
| return svgString; | |
| function getCSSStyles( parentElement ) { | |
| var selectorTextArr = []; | |
| // Add Parent element Id and Classes to the list | |
| selectorTextArr.push( '#'+parentElement.id ); | |
| for (var c = 0; c < parentElement.classList.length; c++) | |
| if ( !contains('.'+parentElement.classList[c], selectorTextArr) ) | |
| selectorTextArr.push( '.'+parentElement.classList[c] ); | |
| // Add Children element Ids and Classes to the list | |
| var nodes = parentElement.getElementsByTagName("*"); | |
| for (var i = 0; i < nodes.length; i++) { | |
| var id = nodes[i].id; | |
| if ( !contains('#'+id, selectorTextArr) ) | |
| selectorTextArr.push( '#'+id ); | |
| var classes = nodes[i].classList; | |
| for (var c = 0; c < classes.length; c++) | |
| if ( !contains('.'+classes[c], selectorTextArr) ) | |
| selectorTextArr.push( '.'+classes[c] ); | |
| } | |
| // Extract CSS Rules | |
| var extractedCSSText = ""; | |
| for (var i = 0; i < document.styleSheets.length; i++) { | |
| var s = document.styleSheets[i]; | |
| try { | |
| if(!s.cssRules) continue; | |
| } catch( e ) { | |
| if(e.name !== 'SecurityError') throw e; // for Firefox | |
| continue; | |
| } | |
| var cssRules = s.cssRules; | |
| for (var r = 0; r < cssRules.length; r++) { | |
| if ( contains( cssRules[r].selectorText, selectorTextArr ) ) | |
| extractedCSSText += cssRules[r].cssText; | |
| } | |
| } | |
| return extractedCSSText; | |
| function contains(str,arr) { | |
| return arr.indexOf( str ) === -1 ? false : true; | |
| } | |
| } | |
| function appendCSS( cssText, element ) { | |
| var styleElement = document.createElement("style"); | |
| styleElement.setAttribute("type","text/css"); | |
| styleElement.innerHTML = cssText; | |
| var refNode = element.hasChildNodes() ? element.children[0] : null; | |
| element.insertBefore( styleElement, refNode ); | |
| } | |
| }, | |
| svgString2Image: function( svgString, width, height, format, callback ) { | |
| var that = this; | |
| var format = format ? format : 'png'; | |
| var imgsrc = 'data:image/svg+xml;base64,'+ btoa( unescape( encodeURIComponent( svgString ) ) ); // Convert SVG string to data URL | |
| var canvas = document.createElement("canvas"); | |
| var context = canvas.getContext("2d"); | |
| canvas.width = width; | |
| canvas.height = height; | |
| var image = new Image(); | |
| image.onload = function() { | |
| context.clearRect ( 0, 0, width, height ); | |
| if(that.distanceModuleOptions.heatmap.show){ | |
| var dataURL = that.distanceModuleOptions.heatmap.instance.getDataURL(); | |
| var heatmap = new Image(); | |
| heatmap.onload = function(){ | |
| context.drawImage(heatmap, 0, 0, width, height); | |
| context.drawImage(image, 0, 0, width, height); | |
| that.onAllImagesLoad(canvas, callback); | |
| }; | |
| heatmap.src = dataURL; | |
| }else{ | |
| context.drawImage(image, 0, 0, width, height); | |
| that.onAllImagesLoad(canvas, callback); | |
| } | |
| }; | |
| image.src = imgsrc; | |
| }, | |
| onAllImagesLoad: function(canvas, callback){ | |
| canvas.toBlob( function(blob) { | |
| var filesize = Math.round( blob.length/1024 ) + ' KB'; | |
| if ( callback ) callback( blob, filesize ); | |
| }); | |
| } | |
| }; | |
| 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
| {"RECORDS":[{"playID":"287","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"77.049","vlaunch":"19.797","distance":"227.988","hlaunch":"12.737","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/287_BP_MaxShor.mp4","angle2":null,"video_id":"5409","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"12:31:40"},{"playID":"288","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"82.573","vlaunch":"20.949","distance":"267.673","hlaunch":"-2.814","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/288_BP_MaxShor.mp4","angle2":null,"video_id":"5410","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"12:31:45"},{"playID":"289","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"88.216","vlaunch":"25.833","distance":"304.774","hlaunch":"-20.362","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/289_BP_MaxShor.mp4","angle2":null,"video_id":"5411","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"12:31:53"},{"playID":"290","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"89.627","vlaunch":"19.140","distance":"277.800","hlaunch":"-16.938","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/290_BP_MaxShor.mp4","angle2":null,"video_id":"5412","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"12:31:59"},{"playID":"291","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"78.818","vlaunch":"40.456","distance":"278.366","hlaunch":"8.570","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/291_BP_MaxShor.mp4","angle2":null,"video_id":"5413","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"12:32:07"},{"playID":"292","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"89.349","vlaunch":"28.246","distance":"311.274","hlaunch":"-27.389","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/292_BP_MaxShor.mp4","angle2":null,"video_id":"5414","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"12:32:32"},{"playID":"322","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"82.421","vlaunch":"44.204","distance":"288.257","hlaunch":"-0.944","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/322_BP_MaxShor.mp4","angle2":null,"video_id":"5444","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"12:38:20"},{"playID":"323","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"67.996","vlaunch":"32.035","distance":"223.226","hlaunch":"-24.933","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/323_BP_MaxShor.mp4","angle2":null,"video_id":"5445","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"12:38:27"},{"playID":"324","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"80.642","vlaunch":"36.866","distance":"284.487","hlaunch":"-12.312","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/324_BP_MaxShor.mp4","angle2":null,"video_id":"5446","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"12:38:34"},{"playID":"325","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"90.126","vlaunch":"8.835","distance":"149.922","hlaunch":"-18.805","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/325_BP_MaxShor.mp4","angle2":null,"video_id":"5447","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"12:38:43"},{"playID":"326","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"84.372","vlaunch":"23.776","distance":"307.864","hlaunch":"9.837","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/326_BP_MaxShor.mp4","angle2":null,"video_id":"5448","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"12:38:50"},{"playID":"327","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"85.381","vlaunch":"18.901","distance":"265.399","hlaunch":"-9.828","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/327_BP_MaxShor.mp4","angle2":null,"video_id":"5449","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"12:39:11"},{"playID":"328","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"64.086","vlaunch":"10.814","distance":"124.993","hlaunch":"-10.618","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/328_BP_MaxShor.mp4","angle2":null,"video_id":"5450","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"12:39:17"},{"playID":"329","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"87.383","vlaunch":"13.714","distance":"221.382","hlaunch":"-30.750","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/329_BP_MaxShor.mp4","angle2":null,"video_id":"5451","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"12:39:28"},{"playID":"490","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"0.000","vlaunch":"0.000","distance":"0.000","hlaunch":"0.000","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/490_TommyGoodin_MaxShor.mp4","angle2":null,"video_id":"5611","video_id2":null,"pType":"","pitcher":"Tommy Goodin","pitchVelo":"63.864","pSpin":"1395","yzone":"2.659","xzone":"-0.708","eventID":"10","eventTime":"13:20:06"},{"playID":"491","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"0.000","vlaunch":"0.000","distance":"0.000","hlaunch":"0.000","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/491_TommyGoodin_MaxShor.mp4","angle2":null,"video_id":"5612","video_id2":null,"pType":"","pitcher":"Tommy Goodin","pitchVelo":"64.730","pSpin":"1541","yzone":"2.728","xzone":"-1.889","eventID":"10","eventTime":"13:20:22"},{"playID":"524","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"0.000","vlaunch":"0.000","distance":"0.000","hlaunch":"0.000","result":"Ball","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/524_JoshGlazer_MaxShor.mp4","angle2":null,"video_id":"5645","video_id2":null,"pType":"Fastball","pitcher":"Josh Glazer","pitchVelo":"63.511","pSpin":"1441","yzone":"4.417","xzone":"0.544","eventID":"10","eventTime":"13:38:27"},{"playID":"525","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"0.000","vlaunch":"0.000","distance":"0.000","hlaunch":"0.000","result":"Ball","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/525_JoshGlazer_MaxShor.mp4","angle2":null,"video_id":"5646","video_id2":null,"pType":"Fastball","pitcher":"Josh Glazer","pitchVelo":"62.975","pSpin":"1523","yzone":"5.523","xzone":"3.009","eventID":"10","eventTime":"13:38:44"},{"playID":"526","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"0.000","vlaunch":"0.000","distance":"0.000","hlaunch":"0.000","result":"Foul Ball","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/526_JoshGlazer_MaxShor.mp4","angle2":null,"video_id":"5647","video_id2":null,"pType":"Fastball","pitcher":"Josh Glazer","pitchVelo":"63.788","pSpin":"1502","yzone":"3.421","xzone":"0.390","eventID":"10","eventTime":"13:39:05"},{"playID":"527","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"0.000","vlaunch":"0.000","distance":"0.000","hlaunch":"0.000","result":"Foul Ball","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/527_JoshGlazer_MaxShor.mp4","angle2":null,"video_id":"5648","video_id2":null,"pType":"Fastball","pitcher":"Josh Glazer","pitchVelo":"65.787","pSpin":"1596","yzone":"2.894","xzone":"-1.160","eventID":"10","eventTime":"13:39:29"},{"playID":"528","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"82.491","vlaunch":"30.698","distance":"299.966","hlaunch":"-7.620","result":"Contact Out","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/528_JoshGlazer_MaxShor.mp4","angle2":null,"video_id":"5649","video_id2":null,"pType":"Fastball","pitcher":"Josh Glazer","pitchVelo":"65.380","pSpin":"1621","yzone":"4.184","xzone":"-0.348","eventID":"10","eventTime":"13:39:55"},{"playID":"539","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"0.000","vlaunch":"0.000","distance":"0.000","hlaunch":"0.000","result":"Ball","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/539_TommyGoodin_MaxShor.mp4","angle2":null,"video_id":"5660","video_id2":null,"pType":"Curveball","pitcher":"Tommy Goodin","pitchVelo":"53.158","pSpin":"0","yzone":"5.656","xzone":"0.407","eventID":"10","eventTime":"13:45:33"},{"playID":"540","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"0.000","vlaunch":"0.000","distance":"0.000","hlaunch":"0.000","result":"Foul Ball","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/540_TommyGoodin_MaxShor.mp4","angle2":null,"video_id":"5661","video_id2":null,"pType":"Fastball","pitcher":"Tommy Goodin","pitchVelo":"62.175","pSpin":"1376","yzone":"2.542","xzone":"0.539","eventID":"10","eventTime":"13:45:49"},{"playID":"541","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"55.143","vlaunch":"19.379","distance":"128.068","hlaunch":"-0.035","result":"Contact Out","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/541_TommyGoodin_MaxShor.mp4","angle2":null,"video_id":"5662","video_id2":null,"pType":"Fastball","pitcher":"Tommy Goodin","pitchVelo":"60.603","pSpin":"1254","yzone":"4.602","xzone":"0.615","eventID":"10","eventTime":"13:46:33"},{"playID":"558","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"74.452","vlaunch":"24.625","distance":"245.046","hlaunch":"-9.213","result":"Contact Out","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/558_TommyGoodin_MaxShor.mp4","angle2":null,"video_id":"5679","video_id2":null,"pType":"Fastball","pitcher":"Tommy Goodin","pitchVelo":"61.920","pSpin":"1352","yzone":"3.017","xzone":"-0.754","eventID":"10","eventTime":"13:56:27"},{"playID":"567","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"77.743","vlaunch":"21.116","distance":"242.643","hlaunch":"7.411","result":"Single","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/567_JoshGlazer_MaxShor.mp4","angle2":null,"video_id":"5688","video_id2":null,"pType":"Fastball","pitcher":"Josh Glazer","pitchVelo":"62.852","pSpin":"1425","yzone":"4.046","xzone":"-0.848","eventID":"10","eventTime":"14:01:05"},{"playID":"770","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"80.824","vlaunch":"11.321","distance":"166.142","hlaunch":"27.760","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/770_BP_MaxShor.mp4","angle2":null,"video_id":"5885","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"16:18:48"},{"playID":"771","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"71.288","vlaunch":"10.472","distance":"133.572","hlaunch":"32.660","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/771_BP_MaxShor.mp4","angle2":null,"video_id":"5886","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"16:19:11"},{"playID":"772","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"85.058","vlaunch":"15.102","distance":"200.032","hlaunch":"8.903","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/772_BP_MaxShor.mp4","angle2":null,"video_id":"5887","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"16:19:31"},{"playID":"773","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"88.036","vlaunch":"-14.030","distance":"10.831","hlaunch":"14.732","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/773_BP_MaxShor.mp4","angle2":null,"video_id":"5888","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"16:19:47"},{"playID":"774","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"70.649","vlaunch":"4.355","distance":"198.354","hlaunch":"10.988","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/774_BP_MaxShor.mp4","angle2":null,"video_id":"5889","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"16:19:56"},{"playID":"775","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"81.093","vlaunch":"22.966","distance":"270.167","hlaunch":"-8.586","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/775_BP_MaxShor.mp4","angle2":null,"video_id":"5890","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"16:20:25"},{"playID":"776","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"90.680","vlaunch":"1.162","distance":"68.470","hlaunch":"-20.563","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/776_BP_MaxShor.mp4","angle2":null,"video_id":"5891","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"16:20:44"},{"playID":"777","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"84.885","vlaunch":"4.256","distance":"44.634","hlaunch":"-14.472","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/777_BP_MaxShor.mp4","angle2":null,"video_id":"5892","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"16:20:56"},{"playID":"778","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"88.578","vlaunch":"35.471","distance":"327.066","hlaunch":"-16.366","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/778_BP_MaxShor.mp4","angle2":null,"video_id":"5893","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"16:21:07"},{"playID":"779","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"82.491","vlaunch":"44.676","distance":"275.035","hlaunch":"-13.798","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/779_BP_MaxShor.mp4","angle2":null,"video_id":"5894","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"16:21:21"},{"playID":"780","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"91.922","vlaunch":"18.677","distance":"283.472","hlaunch":"-17.831","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/780_BP_MaxShor.mp4","angle2":null,"video_id":"5895","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"16:21:42"},{"playID":"781","eventName":"PW Los Angeles","eventDate":"6\/1\/2018","name":"Max Shor","exitvelo":"90.390","vlaunch":"3.005","distance":"85.955","hlaunch":"-29.052","result":"","angle1":"https:\/\/s3.amazonaws.com\/prospectwire\/2018-01-07\/PWLosAngeles\/781_BP_MaxShor.mp4","angle2":null,"video_id":"5896","video_id2":null,"pType":"","pitcher":"BP","pitchVelo":"0.000","pSpin":"0","yzone":"0.000","xzone":"0.000","eventID":"10","eventTime":"16:22:15"}]} |
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='style.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 class="row"> | |
| <div class="col-4"> | |
| <button type="button" id="export" class="btn btn-outline-primary btn-sm">Export to PNG</button> | |
| </div> | |
| <div class="col-4"> | |
| <label class="switch" title="AUTOPLAY"> | |
| <input id="autoplay-checkbox" type="checkbox"> | |
| <span class="slider round"></span> | |
| </label> | |
| </div> | |
| <div class="col-4"> | |
| <div class="form-check"> | |
| <input type="checkbox" class="form-check-input" id="at-bat-checkbox"> | |
| <label class="form-check-label" for="at-bat-checkbox">Hide BP Swings</label> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-sm-8"> | |
| <div class="row"> | |
| <div class="col-sm-6" id="exit-launch-angle-module"></div> | |
| <div class="col-sm-6" id="exit-direction-module"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <div class="col-sm-4" id="table-module"></div> | |
| <div class="col-sm-8" id="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.3/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 src="https://cdnjs.cloudflare.com/ajax/libs/javascript-canvas-to-blob/3.14.0/js/canvas-to-blob.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.3/FileSaver.min.js"></script> | |
| <script type='text/javascript' src='charts_batter_spray.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
| .fa-rotate-45 { | |
| -webkit-transform: rotate(45deg); | |
| -moz-transform: rotate(45deg); | |
| -ms-transform: rotate(45deg); | |
| -o-transform: rotate(45deg); | |
| transform: rotate(45deg); | |
| } | |
| .fa-rotate-315 { | |
| -webkit-transform: rotate(315deg); | |
| -moz-transform: rotate(315deg); | |
| -ms-transform: rotate(315deg); | |
| -o-transform: rotate(315deg); | |
| transform: rotate(315deg); | |
| } | |
| .green{ | |
| color: limegreen; | |
| } | |
| 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 !important; | |
| } | |
| .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; | |
| } | |
| .switch { | |
| position: relative; | |
| display: inline-block; | |
| width: 60px; | |
| height: 34px; | |
| vertical-align: middle; | |
| margin-bottom: 0; | |
| } | |
| .switch input {display:none;} | |
| .slider { | |
| position: absolute; | |
| cursor: pointer; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: #ccc; | |
| -webkit-transition: .4s; | |
| transition: .4s; | |
| } | |
| .slider:before { | |
| position: absolute; | |
| content: ""; | |
| height: 26px; | |
| width: 26px; | |
| left: 4px; | |
| bottom: 4px; | |
| background-color: white; | |
| -webkit-transition: .4s; | |
| transition: .4s; | |
| } | |
| input:checked + .slider { | |
| background-color: #2196F3; | |
| } | |
| input:focus + .slider { | |
| box-shadow: 0 0 1px #2196F3; | |
| } | |
| input:checked + .slider:before { | |
| -webkit-transform: translateX(26px); | |
| -ms-transform: translateX(26px); | |
| transform: translateX(26px); | |
| } | |
| /* Rounded sliders */ | |
| .slider.round { | |
| border-radius: 34px; | |
| } | |
| .slider.round:before { | |
| border-radius: 50%; | |
| } | |
| /* module specific styles */ | |
| circle.ball.active{ | |
| fill: #ff0000; | |
| } | |
| circle.ball.hovered{ | |
| fill: #c6c6c6; | |
| } | |
| 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; | |
| z-index: 2; | |
| } | |
| #video-module{ | |
| } | |
| #distance-module{ | |
| position: relative; | |
| } | |
| #distance-module svg{ | |
| position: relative; | |
| z-index: 1; | |
| } | |
| .heatmap-wrapper { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| z-index: 0; | |
| } | |
| .heatmap { | |
| position: relative; | |
| width: 100%; | |
| height: 100%; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment