Skip to content

Instantly share code, notes, and snippets.

@valex
Created February 20, 2018 17:49
Show Gist options
  • Select an option

  • Save valex/37d9e79b391675bed2e0ae57fc75ddbf to your computer and use it in GitHub Desktop.

Select an option

Save valex/37d9e79b391675bed2e0ae57fc75ddbf to your computer and use it in GitHub Desktop.
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&#176;");
options.schemeRadius = Math.round(graphWidth * 0.5 * 0.66);
options.schemeBasePoint = {
x: Math.round(graphWidth / 2),
y: graphHeight
};
// outer arc
var outerArc = d3.arc()
.innerRadius(0)
.outerRadius(4/5 * options.schemeRadius)
.startAngle(-45 * Math.PI / 180)
.endAngle(45 * Math.PI / 180);
options.mainGroup.append('path')
.attr("d", outerArc)
.attr("transform", 'translate('+options.schemeBasePoint.x+', '+options.schemeBasePoint.y+')')
.attr("fill", '#dee0df')
.attr("stroke-width", '0');
// inner arc
var innerArc = d3.arc()
.innerRadius(0)
.outerRadius(4/7 * options.schemeRadius)
.startAngle(-45 * Math.PI / 180)
.endAngle(45 * Math.PI / 180);
options.mainGroup.append('path')
.attr("d", innerArc)
.attr("transform", 'translate('+options.schemeBasePoint.x+', '+options.schemeBasePoint.y+')')
.attr("fill", this.mainLightColor)
.attr("stroke", this.mainColor)
.attr("stroke-width", 2);
// vector arc
options.vectorArcGenerator = d3.arc()
.innerRadius(0)
.outerRadius(4/7 * options.schemeRadius)
.startAngle(0)
.endAngle(function(d) { return d * Math.PI / 180; });
options.mainGroup.append('path')
.attr("id", "vector-arc")
.attr("d", options.vectorArcGenerator(0))
.attr("transform", 'translate('+options.schemeBasePoint.x+', '+options.schemeBasePoint.y+')')
.attr("fill", this.mainColor);
options.mainGroup.append("line")
.style("stroke", this.mainColor) // colour the line
.attr("x1", options.schemeBasePoint.x) // x position of the first end of the line
.attr("y1", options.schemeBasePoint.y) // y position of the first end of the line
.attr("x2", options.schemeBasePoint.x + options.schemeRadius) // x position of the second end of the line
.attr("y2", options.schemeBasePoint.y - options.schemeRadius)
.attr("stroke-width", 3);
options.mainGroup.append("line")
.style("stroke", this.mainColor) // colour the line
.attr("x1", options.schemeBasePoint.x) // x position of the first end of the line
.attr("y1", options.schemeBasePoint.y) // y position of the first end of the line
.attr("x2", options.schemeBasePoint.x - options.schemeRadius) // x position of the second end of the line
.attr("y2", options.schemeBasePoint.y - options.schemeRadius)
.attr("stroke-width", 3);
// vector
options.mainGroup.append("line")
.style("stroke", this.mainColor) // colour the line
.attr("id", "vector") // x position of the first end of the line
.attr("x1", options.schemeBasePoint.x) // x position of the first end of the line
.attr("y1", options.schemeBasePoint.y) // y position of the first end of the line
.attr("x2", options.schemeBasePoint.x) // x position of the second end of the line
.attr("y2", options.schemeBasePoint.y - options.schemeRadius)
.attr("stroke-width", 1.5);
},
updateExitDirectionModule: function(){
},
animateExitDirectionModule: function(){
var options = this.exitDirectionModuleOptions;
var data = this.data[this.activeIndex];
options.mainGroup
.select('#'+options.id+' #vector')
.datum(data)
.transition()
.duration(this.animation.duration)
.tween("animations", function(d){
var el = d3.select(this);
var angleLabel = d3.select('#'+options.id+' #angle-label');
var vectorArc = d3.select('#'+options.id+' #vector-arc');
var X2 = +el.attr("x2");
var Y2 = +el.attr("y2");
var fromAngle = 180 / Math.PI * Math.asin((X2 - options.schemeBasePoint.x) / options.schemeRadius);
var toAngle = d['hAngle'];
var iAngle = d3.interpolateNumber(fromAngle, toAngle);
return function(t){
el.attr("x2", options.schemeBasePoint.x + (options.schemeRadius * Math.sin(iAngle(t) * Math.PI / 180)));
el.attr("y2", options.schemeBasePoint.y - (options.schemeRadius * Math.cos(iAngle(t) * Math.PI / 180)));
var suffix = "&#176;";
if(iAngle(t) >= 0){
suffix += 'R';
}else{
suffix += 'L';
}
angleLabel.html(Math.round(iAngle(t)) + suffix);
vectorArc.attr("d", options.vectorArcGenerator(iAngle(t)))
}
})
},
// ============================== EXIT LAUNCH ANGLE MODULE ==============================
initExitLaunchAngleModule: function(){
var options = this.exitLaunchAngleModuleOptions;
var graphWidth = options.width - options.margins.left - options.margins.right,
graphHeight = options.height - options.margins.top - options.margins.bottom;
var svg = d3.select(options.appendToSelector)
.append("svg")
.attr("width", options.width)
.attr("height", options.height);
options.mainGroup = svg.append('g')
.attr('transform', 'translate(' + options.margins.left + ',' + options.margins.top + ')')
.attr('id', options.id);
// title text
options.mainGroup.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("font-weight", "bold")
.attr("font-size", "14px")
.attr("alignment-baseline", "hanging")
.attr("dominant-baseline", "hanging") // for firefox
.attr("class", "title")
.style("fill", this.mainColor)
.text("EXIT LAUNCH ANGLE");
// angle text
options.mainGroup.append("text")
.attr("id", "angle-label")
// .attr("x", graphWidth)
// .attr("y", 0)
.attr("x", 70)
.attr("y", 82)
.attr("font-weight", "bold")
.attr("font-size", "28px")
.attr("alignment-baseline", "hanging")
.attr("dominant-baseline", "hanging") // for firefox
.attr("class", "angle")
.attr("text-anchor", "end")
.style("fill", this.mainColor)
.html("0&#176;");
options.schemeRadius = Math.round((graphHeight - 30) * 0.5);
options.vectorLength = Math.round(options.schemeRadius * 0.85);
options.schemeBasePoint = {
x: Math.round(1/3 * graphWidth),
y: graphHeight - options.schemeRadius
};
// outer arc
var outerArc = d3.arc()
.innerRadius(0)
.outerRadius(0.6 * options.schemeRadius)
.startAngle(0)
.endAngle(180 * Math.PI / 180);
options.mainGroup.append('path')
.attr("d", outerArc)
.attr("transform", 'translate('+options.schemeBasePoint.x+', '+options.schemeBasePoint.y+')')
.attr("fill", '#dee0df')
.attr("stroke-width", '0');
// inner arc
var innerArc = d3.arc()
.innerRadius(0)
.outerRadius(0.44 * options.schemeRadius)
.startAngle(0)
.endAngle(180 * Math.PI / 180);
options.mainGroup.append('path')
.attr("d", innerArc)
.attr("transform", 'translate('+options.schemeBasePoint.x+', '+options.schemeBasePoint.y+')')
.attr("fill", this.mainLightColor)
.attr("stroke", this.mainColor)
.attr("stroke-width", 0);
// vector arc
options.vectorArcGenerator = d3.arc()
.innerRadius(0)
.outerRadius(0.44 * options.schemeRadius)
.startAngle(90 * Math.PI / 180)
.endAngle(function(d) { return (90 - d) * Math.PI / 180; });
options.mainGroup.append('path')
.attr("id", "vector-arc")
.attr("d", options.vectorArcGenerator(0))
.attr("transform", 'translate('+options.schemeBasePoint.x+', '+options.schemeBasePoint.y+')')
.attr("fill", this.mainColor);
// vector
options.mainGroup.append("line")
.style("stroke", this.mainColor)
.attr("id", "vector")
.attr("x1", options.schemeBasePoint.x)
.attr("y1", options.schemeBasePoint.y)
.attr("x2", options.schemeBasePoint.x + options.vectorLength)
.attr("y2", options.schemeBasePoint.y)
.attr("stroke-width", 1.5);
// x axis
options.mainGroup.append("line")
.style("stroke", this.mainColor) // colour the line
.attr("x1", options.schemeBasePoint.x) // x position of the first end of the line
.attr("y1", options.schemeBasePoint.y) // y position of the first end of the line
.attr("x2", options.schemeBasePoint.x + options.schemeRadius) // x position of the second end of the line
.attr("y2", options.schemeBasePoint.y)
.attr("stroke-width", 3);
// y axis
options.mainGroup.append("line")
.style("stroke", this.mainColor) // colour the line
.attr("x1", options.schemeBasePoint.x) // x position of the first end of the line
.attr("y1", options.schemeBasePoint.y + options.schemeRadius) // y position of the first end of the line
.attr("x2", options.schemeBasePoint.x) // x position of the second end of the line
.attr("y2", options.schemeBasePoint.y - options.schemeRadius)
.attr("stroke-width", 3);
},
updateExitLaunchAngleModule: function(){
},
animateExitLaunchAngleModule: function(){
var options = this.exitLaunchAngleModuleOptions;
var data = this.data[this.activeIndex];
options.mainGroup
.select('#'+options.id+' #vector')
.datum(data)
.transition()
.duration(this.animation.duration)
.tween("animations", function(d){
var el = d3.select(this);
var angleLabel = d3.select('#'+options.id+' #angle-label');
var vectorArc = d3.select('#'+options.id+' #vector-arc');
var X2 = +el.attr("x2");
var Y2 = +el.attr("y2");
var fromAngle = 180 / Math.PI * Math.asin((options.schemeBasePoint.y - Y2) / options.vectorLength);
var toAngle = d['vAngle'];
var iAngle = d3.interpolateNumber(fromAngle, toAngle);
return function(t){
el.attr("x2", options.schemeBasePoint.x + (options.vectorLength * Math.cos(iAngle(t) * Math.PI / 180)));
el.attr("y2", options.schemeBasePoint.y - (options.vectorLength * Math.sin(iAngle(t) * Math.PI / 180)));
angleLabel.html(Math.round(iAngle(t)) + "&#176;");
vectorArc.attr("d", options.vectorArcGenerator(iAngle(t)))
}
})
},
// ============================== DISTANCE MODULE ==============================
initDistanceModule: function(){
var that = this;
var options = this.distanceModuleOptions;
var graphWidth = options.width - options.margins.left - options.margins.right,
graphHeight = options.height - options.margins.top - options.margins.bottom;
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 += '&#176;';
}
}
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();
{"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"}]}
<!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>
.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