d3-soccer
Version:
A d3 plugin to visualize soccer data.
1,391 lines (1,300 loc) • 69.9 kB
JavaScript
// https://github.com/probberechts/d3-soccer v0.2.0 Copyright 2024 undefined
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-selection'), require('d3-shape'), require('d3-color'), require('d3-scale'), require('d3-scale-chromatic')) :
typeof define === 'function' && define.amd ? define(['exports', 'd3-selection', 'd3-shape', 'd3-color', 'd3-scale', 'd3-scale-chromatic'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.d3 = global.d3 || {}, global.d3, global.d3, global.d3, global.d3, global.d3));
})(this, (function (exports, d3Selection, d3Shape, d3Color, d3Scale, colorScale) { 'use strict';
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var colorScale__namespace = /*#__PURE__*/_interopNamespaceDefault(colorScale);
var pitchLenght = 105;
var pitchWidth = 68;
var spadlResults = [{
label: "Fail"
}, {
label: "Success"
}, {
label: "Offside"
}, {
label: "Own goal"
}, {
label: "Yellow card"
}, {
label: "Red card"
}];
var spadlActionTypes = [{
label: "Pass"
}, {
label: "Cross"
}, {
label: "Throw in"
}, {
label: "Freekick (cross)"
}, {
label: "Freekick (short)"
}, {
label: "Corner (cross)"
}, {
label: "Corner (short)"
}, {
label: "Dribble"
}, {
label: "Foul"
}, {
label: "Tackle"
}, {
label: "Interception"
}, {
label: "Shot"
}, {
label: "Penalty"
}, {
label: "Freekick (shot)"
}, {
label: "Save"
}, {
label: "Claim"
}, {
label: "Punch"
}, {
label: "Pick up"
}, {
label: "Clearance"
}, {
label: "Bad touch"
}, {
label: "-"
}, {
label: "Carry"
}, {
label: "Goal kick"
}];
var spadlBodyparts = [{
label: "Foot"
}, {
label: "Head"
}, {
label: "Other"
}];
function pitch () {
var clip = {
top: 0,
right: pitchLenght,
bottom: pitchWidth,
left: 0
};
var height = 300;
var rotated = false;
var width = (-clip.left + clip.right) / (-clip.top + clip.bottom) * height;
var pitchstrokewidth = 0.5;
var dirOfPlay = false;
var shadeMiddleThird = false;
var drawGoalsFn = drawGoalsAsLine;
function drawGoalsAsBox(lines) {
lines.append("rect").attr("stroke-width", pitchstrokewidth).attr("fill", "none").attr("x", -2).attr("y", pitchWidth / 2 - 3.66).attr("width", 2).attr("height", 7.32);
lines.append("rect").attr("stroke-width", pitchstrokewidth).attr("fill", "none").attr("x", pitchLenght).attr("y", pitchWidth / 2 - 3.66).attr("width", 2).attr("height", 7.32);
}
function drawGoalsAsLine(lines) {
lines.append("rect").attr("stroke-width", 0).attr("x", -pitchstrokewidth * 1.5).attr("y", pitchWidth / 2 - 3.66).attr("width", pitchstrokewidth * 3).attr("height", 7.32);
lines.append("rect").attr("stroke-width", 0).attr("x", pitchLenght - pitchstrokewidth * 1.5).attr("y", pitchWidth / 2 - 3.66).attr("width", pitchstrokewidth * 3).attr("height", 7.32);
}
function chart(selection) {
selection.each(function () {
var pitch = d3Selection.select(this).append("svg").attr("width", width).attr("height", height).attr("viewBox", function () {
var width = clip.right - clip.left;
var height = clip.bottom - clip.top;
var xdim, ydim, xpad, ypad;
if (rotated) {
xpad = height === pitchWidth ? 4 : 2;
ypad = width === pitchLenght ? 4 : 2;
xdim = -clip.left + clip.right + ypad;
ydim = -clip.top + clip.bottom + xpad;
} else {
xpad = height === pitchWidth ? 4 : 2;
ypad = width === pitchLenght ? 4 : 2;
ydim = -clip.top + clip.bottom + xpad;
xdim = -clip.left + clip.right + ypad;
}
return "-2 -2 ".concat(xdim, " ").concat(ydim);
}).append("g").attr("class", "pitch").attr("transform", "translate(".concat(-clip.left, ", ").concat(-clip.top, ")rotate(").concat(rotated ? -90 : 0, " 0 0)translate(").concat(rotated ? -105 : 0, " 0)"));
pitch.append("g").attr("class", "below");
var lines = pitch.append("g").attr("class", "lines").attr("stroke", "#000").attr("fill", "#000").attr("pointer-events", "none");
// Halfway line
lines.append("line").attr("stroke-width", pitchstrokewidth).attr("x1", pitchLenght / 2).attr("y1", 0).attr("x2", pitchLenght / 2).attr("y2", pitchWidth);
// Centre circle
lines.append("circle").attr("stroke-width", pitchstrokewidth).attr("fill", "none").attr("cx", pitchLenght / 2).attr("cy", pitchWidth / 2).attr("r", 9.15);
lines.append("circle").attr("stroke-width", 0).attr("cx", pitchLenght / 2).attr("cy", pitchWidth / 2).attr("r", pitchstrokewidth);
// Penalty arcs
var arc1 = d3Shape.arc().innerRadius(9.15).outerRadius(9.15).startAngle(38 * (Math.PI / 180)) //converting from degs to radians
.endAngle(142 * (Math.PI / 180)); //just radians
lines.append("path").attr("stroke-width", pitchstrokewidth).attr("d", arc1).attr("transform", "translate(11," + pitchWidth / 2 + ")");
var arc2 = d3Shape.arc().innerRadius(9.15).outerRadius(9.15).startAngle(218 * (Math.PI / 180)) //converting from degs to radians
.endAngle(322 * (Math.PI / 180)); //just radians
lines.append("path").attr("stroke-width", pitchstrokewidth).attr("d", arc2).attr("transform", "translate(" + (pitchLenght - 11) + "," + pitchWidth / 2 + ")");
// Goal areas
lines.append("rect").attr("stroke-width", pitchstrokewidth).attr("fill", "none").attr("x", 0).attr("y", pitchWidth / 2 - 9.16).attr("width", 5.5).attr("height", 18.32);
lines.append("rect").attr("stroke-width", pitchstrokewidth).attr("fill", "none").attr("x", pitchLenght - 5.5).attr("y", pitchWidth / 2 - 9.16).attr("width", 5.5).attr("height", 18.32);
// Penalty areas
lines.append("rect").attr("stroke-width", pitchstrokewidth).attr("fill", "none").attr("x", 0).attr("y", pitchWidth / 2 - 20.16).attr("width", 16.5).attr("height", 40.32);
lines.append("rect").attr("stroke-width", pitchstrokewidth).attr("fill", "none").attr("x", pitchLenght - 16.5).attr("y", pitchWidth / 2 - 20.16).attr("width", 16.5).attr("height", 40.32);
// Penalty marks
lines.append("circle").attr("stroke-width", 0).attr("cx", 11).attr("cy", pitchWidth / 2).attr("r", pitchstrokewidth);
lines.append("circle").attr("stroke-width", 0).attr("cx", pitchLenght - 11).attr("cy", pitchWidth / 2).attr("r", pitchstrokewidth);
// Direction of play
if (dirOfPlay) {
lines.append("polygon").attr("opacity", 0.1).attr("stroke-width", 0).attr("class", "shaded").attr("points", "\n 25,".concat(pitchWidth / 2 - 2, " \n 35,").concat(pitchWidth / 2 - 2, " \n 35,").concat(pitchWidth / 2 - 5, " \n 40,").concat(pitchWidth / 2, " \n 35,").concat(pitchWidth / 2 + 5, " \n 35,").concat(pitchWidth / 2 + 2, " \n 25,").concat(pitchWidth / 2 + 2, " \n 25,").concat(pitchWidth / 2 - 2, "\n "));
}
// Pitch boundaries
lines.append("rect").attr("stroke-width", pitchstrokewidth).attr("fill", "none").attr("x", 0).attr("y", 0).attr("width", pitchLenght).attr("height", pitchWidth);
// Goals
drawGoalsFn(lines);
pitch.append("g").attr("class", "above");
// Middle third
if (shadeMiddleThird) {
lines.append("rect").attr("opacity", 0.1).attr("class", "shaded").attr("x", 35).attr("y", 0).attr("width", 35).attr("height", pitchWidth);
}
});
}
chart.height = function (_) {
if (!arguments.length) return height;
height = +_;
width = (-clip.left + clip.right) / (-clip.top + clip.bottom) * height;
return chart;
};
chart.width = function () {
return width;
};
chart.rotate = function (_) {
if (!arguments.length) return rotated;
rotated = Boolean(_);
return chart;
};
chart.showDirOfPlay = function (_) {
if (!arguments.length) return dirOfPlay;
dirOfPlay = Boolean(_);
return chart;
};
chart.shadeMiddleThird = function (_) {
if (!arguments.length) return shadeMiddleThird;
shadeMiddleThird = Boolean(_);
return chart;
};
chart.pitchStrokeWidth = function (_) {
if (!arguments.length) return pitchstrokewidth;
pitchstrokewidth = +_;
return chart;
};
chart.goals = function (_) {
if (!arguments.length) return drawGoalsFn;
if (_ === "box") drawGoalsFn = drawGoalsAsBox;else if (_ === "line") drawGoalsFn = drawGoalsAsLine;else drawGoalsFn = _;
return chart;
};
chart.clip = function (_) {
if (!arguments.length) return [[clip.left, clip.top], [clip.right, clip.bottom]];
clip = {
top: _[0][1],
bottom: _[1][1],
left: _[0][0],
right: _[1][0]
};
width = (-clip.left + clip.right) / (-clip.top + clip.bottom) * height;
return chart;
};
return chart;
}
function extent(values, valueof) {
let min;
let max;
if (valueof === undefined) {
for (const value of values) {
if (value != null) {
if (min === undefined) {
if (value >= value) min = max = value;
} else {
if (min > value) min = value;
if (max < value) max = value;
}
}
}
} else {
let index = -1;
for (let value of values) {
if ((value = valueof(value, ++index, values)) != null) {
if (min === undefined) {
if (value >= value) min = max = value;
} else {
if (min > value) min = value;
if (max < value) max = value;
}
}
}
}
return [min, max];
}
function max(values, valueof) {
let max;
if (valueof === undefined) {
for (const value of values) {
if (value != null
&& (max < value || (max === undefined && value >= value))) {
max = value;
}
}
} else {
let index = -1;
for (let value of values) {
if ((value = valueof(value, ++index, values)) != null
&& (max < value || (max === undefined && value >= value))) {
max = value;
}
}
}
return max;
}
var bicubicInterpolate;
var hasRequiredBicubicInterpolate;
function requireBicubicInterpolate () {
if (hasRequiredBicubicInterpolate) return bicubicInterpolate;
hasRequiredBicubicInterpolate = 1;
// Interpolate values for a function of the form `f(x, y) = z` within a 1x1 square (from (x, y) = (0, 0) to (1, 1)) by providing 16 points at it's edges and around it ((-1, -1), (0, -1), (1, -1), (2, -1), (0, -1), (0, 0), ..., (3, 3)).
// Takes in the 16 values for z as a 2D array and an options object. Make sure the first selector corresponds to x position (e.g. points[x][y]). This is the rotated from the visual layout if you declare the array in javascript manually. Also, keep in mind that values[0][0] actually corresponds to the value for (-1, -1).
// You can set `options.scaleX` and `options.scaleY` to multiply the positions input to the interpolator by before interpolating. defaults = 1
// You can set `options.translateX` and `options.translateY` to add to the positions input to the interpolator before interpolating (but after scaling). defaults = 0
// Returns a function that takes two arguments (x, y) and returns the interpolated value.
function createInterpolator(values, options={}) {
options = Object.assign({
extrapolate: false,
scaleX: 1,
scaleY: 1,
translateX: 0,
translateY: 0
}, options);
//Coefficients: first number corresponds to x's exponent in the polynomial, the second to y's.
const a00 = values[1][1],
a01 = (-1/2)*values[1][0] + (1/2)*values[1][2],
a02 = values[1][0] + (-5/2)*values[1][1] + 2*values[1][2] + (-1/2)*values[1][3],
a03 = (-1/2)*values[1][0] + (3/2)*values[1][1] + (-3/2)*values[1][2] + (1/2)*values[1][3],
a10 = (-1/2)*values[0][1] + (1/2)*values[2][1],
a11 = (1/4)*values[0][0] + (-1/4)*values[0][2] + (-1/4)*values[2][0] + (1/4)*values[2][2],
a12 = (-1/2)*values[0][0] + (5/4)*values[0][1] + (-1)*values[0][2] + (1/4)*values[0][3] + (1/2)*values[2][0] + (-5/4)*values[2][1] + values[2][2] + (-1/4)*values[2][3],
a13 = (1/4)*values[0][0] + (-3/4)*values[0][1] + (3/4)*values[0][2] + (-1/4)*values[0][3] + (-1/4)*values[2][0] + (3/4)*values[2][1] + (-3/4)*values[2][2] + (1/4)*values[2][3],
a20 = values[0][1] + (-5/2)*values[1][1] + 2*values[2][1] + (-1/2)*values[3][1],
a21 = (-1/2)*values[0][0] + (1/2)*values[0][2] + (5/4)*values[1][0] + (-5/4)*values[1][2] + (-1)*values[2][0] + values[2][2] + (1/4)*values[3][0] + (-1/4)*values[3][2],
a22 = values[0][0] + (-5/2)*values[0][1] + 2*values[0][2] + (-1/2)*values[0][3] + (-5/2)*values[1][0] + (25/4)*values[1][1] + (-5)*values[1][2] + (5/4)*values[1][3] + 2*values[2][0] + (-5)*values[2][1] + 4*values[2][2] + (-1)*values[2][3] + (-1/2)*values[3][0] + (5/4)*values[3][1] + (-1)*values[3][2] + (1/4)*values[3][3],
a23 = (-1/2)*values[0][0] + (3/2)*values[0][1] + (-3/2)*values[0][2] + (1/2)*values[0][3] + (5/4)*values[1][0] + (-15/4)*values[1][1] + (15/4)*values[1][2] + (-5/4)*values[1][3] + (-1)*values[2][0] + 3*values[2][1] + (-3)*values[2][2] + values[2][3] + (1/4)*values[3][0] + (-3/4)*values[3][1] + (3/4)*values[3][2] + (-1/4)*values[3][3],
a30 = (-1/2)*values[0][1] + (3/2)*values[1][1] + (-3/2)*values[2][1] + (1/2)*values[3][1],
a31 = (1/4)*values[0][0] + (-1/4)*values[0][2] + (-3/4)*values[1][0] + (3/4)*values[1][2] + (3/4)*values[2][0] + (-3/4)*values[2][2] + (-1/4)*values[3][0] + (1/4)*values[3][2],
a32 = (-1/2)*values[0][0] + (5/4)*values[0][1] + (-1)*values[0][2] + (1/4)*values[0][3] + (3/2)*values[1][0] + (-15/4)*values[1][1] + 3*values[1][2] + (-3/4)*values[1][3] + (-3/2)*values[2][0] + (15/4)*values[2][1] + (-3)*values[2][2] + (3/4)*values[2][3] + (1/2)*values[3][0] + (-5/4)*values[3][1] + values[3][2] + (-1/4)*values[3][3],
a33 = (1/4)*values[0][0] + (-3/4)*values[0][1] + (3/4)*values[0][2] + (-1/4)*values[0][3] + (-3/4)*values[1][0] + (9/4)*values[1][1] + (-9/4)*values[1][2] + (3/4)*values[1][3] + (3/4)*values[2][0] + (-9/4)*values[2][1] + (9/4)*values[2][2] + (-3/4)*values[2][3] + (-1/4)*values[3][0] + (3/4)*values[3][1] + (-3/4)*values[3][2] + (1/4)*values[3][3];
return (x, y) => {
x = (x * options.scaleX) + options.translateX;
y = (y * options.scaleY) + options.translateY;
if(x < 0 || y < 0 || x > 1 || y > 1) throw 'cannot interpolate outside the square from (0, 0) to (1, 1): (' + x + ', ' + y + ')';
const x2 = x*x,
x3 = x*x2,
y2 = y*y,
y3 = y*y2;
return (a00 + a01*y + a02*y2 + a03*y3) +
(a10 + a11*y + a12*y2 + a13*y3) * x +
(a20 + a21*y + a22*y2 + a23*y3) * x2 +
(a30 + a31*y + a32*y2 + a33*y3) * x3;
}
}
// Interpolate values for a function of the form `f(x, y) = z` within an (m-3)x(n-3) rectangle (from (x, y) = (1, 1) to (m-2, n-2)) by providing m x n samples for z.
// Takes in the m x n values for z as a 2D array and an options object. If `options.extrapolate` is true, this will get modified. Make sure the first selector corresponds to x position (e.g. points[x][y]). This is the rotated from the visual layout if you declare the array in javascript manually. Also, keep in mind that for this function, values[0][0] actually does corresponds to the value for (0, 0).
// If `options.extrapolate` is true, the `values` arrays will be modified to allow for interpolating from (-1, -1) to (m, n) by linearly estimating a margin of 2 more values on each side. This is useful, for example, to interpolate values of an image all the way to the edge of the pixels on the edge (corresponding to -0.5 < x < m-0.5 and -0.5 < y < n-0.5). default = false
// You can set `options.scaleX` and `options.scaleY` to multiply the positions input to the interpolators by before interpolating. defaults = 1
// You can set `options.translateX` and `options.translateY` to add to the positions input to the interpolators before interpolating (but after scaling). defaults = 0
// Returns a function that takes two arguments (x, y) and returns the interpolated value.
function createGridInterpolator(values, options={}) {
options = Object.assign({
extrapolate: false,
scaleX: 1,
scaleY: 1,
translateX: 0,
translateY: 0
}, options);
const m = values.length;
const n = values[0].length;
const interpolators = [];
if(options.extrapolate) {
//Extrapolate X
values[-2] = [];
values[-1] = [];
values[m] = [];
values[m+1] = [];
for(var y = 0; y < n; y++) {
const leftDelta = values[0][y] - values[1][y];
const rightDelta = values[m-1][y] - values[m-2][y];
values[-2][y] = values[0][y] + 2*leftDelta;
values[-1][y] = values[0][y] + leftDelta;
values[m][y] = values[m-1][y] + rightDelta;
values[m+1][y] = values[m-1][y] + 2*rightDelta;
}
//Extrapolate Y
for(var x = -2; x < m+2; x++) {
const bottomDelta = values[x][0] - values[x][1];
const topDelta = values[x][n-1] - values[x][n-2];
values[x][-2] = values[x][0] + 2*bottomDelta;
values[x][-1] = values[x][0] + bottomDelta;
values[x][n] = values[x][n-1] + topDelta;
values[x][n+1] = values[x][n-1] + 2*topDelta;
}
//Populate interpolator arrays
for(var x = -1; x < m; x++) interpolators[x] = [];
}else {
//Populate interpolator arrays
for(var x = 1; x < m-2; x++) interpolators[x] = [];
}
return (x, y) => {
x = (x * options.scaleX) + options.translateX;
y = (y * options.scaleY) + options.translateY;
if(options.extrapolate) {
if(x < -1 || y < -1 || x > m || y > n) throw 'cannot interpolate outside the rectangle from (-1, -1) to (' + m + ', ' + n + ') even when extrapolating: (' + x + ', ' + y + ')';
}else {
if(x < 1 || y < 1 || x > m-2 || y > n-2) throw 'cannot interpolate outside the rectangle from (1, 1) to (' + (m-2) + ', ' + (n-2) + '): (' + x + ', ' + y + '), you might want to enable extrapolating';
}
var blX = Math.floor(x);// The position of interpolator's (0, 0) for this point
var blY = Math.floor(y);
if(options.extrapolate) {//If you're trying to interpolate on the top or right edges of what can be interpolated, you have to interpolate in the region to the left or bottom respectively.
if(x === m) blX--;
if(y === n) blY--;
}else {
if(x === m-2) blX--;
if(y === n-2) blY--;
}
if(!interpolators[blX][blY]) {
interpolators[blX][blY] = createInterpolator([
[values[blX-1][blY-1], values[blX-1][blY], values[blX-1][blY+1], values[blX-1][blY+2]],
[values[blX+0][blY-1], values[blX+0][blY], values[blX][blY+1], values[blX][blY+2]],
[values[blX+1][blY-1], values[blX+1][blY], values[blX+1][blY+1], values[blX+1][blY+2]],
[values[blX+2][blY-1], values[blX+2][blY], values[blX+2][blY+1], values[blX+2][blY+2]]
], {
translateX: -blX,
translateY: -blY
});
}
const interpolator = interpolators[blX][blY];
return interpolator(x, y);
}
}
// These are just helper functions for when you need to interpolate multiple sets of values (e.g. components of color in an image).
// Instead of taking in 2D value arrays they expect 3D arrays, where the last index corresponds to the set/ surface (e.g. values[x][y][s])
function createMultiInterpolator(values, options={}) {
const s = values[0][0].length;
const interpolators = [];
for(var i = 0; i < s; i++) interpolators[i] = createInterpolator(values.map(col => col.map(vals => vals[i])), options);
return (x, y) => {
return interpolators.map(interpolator => interpolator(x, y));
}
}
function createMultiGridInterpolator(values, options={}) {
const s = values[0][0].length;
const interpolators = [];
for(var i = 0; i < s; i++) interpolators[i] = createGridInterpolator(values.map(col => col.map(vals => vals[i])), options);
return (x, y) => {
return interpolators.map(interpolator => interpolator(x, y));
}
}
bicubicInterpolate = {
createInterpolator: createInterpolator,
createGridInterpolator: createGridInterpolator,
createMultiInterpolator: createMultiInterpolator,
createMultiGridInterpolator: createMultiGridInterpolator
};
return bicubicInterpolate;
}
var bicubicInterpolateExports = requireBicubicInterpolate();
function position(el, parent) {
var elPos = el.node().getBoundingClientRect();
var vpPos = parent.node().getBoundingClientRect();
return {
top: elPos.top - vpPos.top,
left: elPos.left - vpPos.left,
width: elPos.width,
bottom: elPos.bottom - vpPos.top,
height: elPos.height,
right: elPos.right - vpPos.left
};
}
var grid = function grid() {
function grid(d) {
var data = [];
var gx = d.length;
var gy = d[0].length;
var incx = pitchLenght / gx;
var incy = pitchWidth / gy;
var max_v = 0;
for (var x = 0; x < gx; x++) {
for (var y = 0; y < gy; y++) {
if (d[x][y] > max_v) {
max_v = d[x][y];
}
data.push({
i: x,
j: y,
x: x * incx,
y: y * incy,
width: incx,
height: incy,
value: d[x][y]
});
}
}
return data;
}
return grid;
};
var rectbin = function rectbin() {
var dx = 0.1,
dy = 0.1,
x = function x(d) {
return d[0];
},
y = function y(d) {
return d[1];
};
function trunc(x) {
return x < 0 ? Math.ceil(x) : Math.floor(x);
}
function rectbin(points) {
var binsById = {};
var xExtent = [0, 105];
var yExtent = [0, 68];
//var xExtent = d3.extent(points, function(d, i){ return x.call(rectbin, d, i) ;});
//var yExtent = d3.extent(points, function(d, i){ return y.call(rectbin, d, i) ;});
for (var Y = yExtent[0], pj = 0; Y < yExtent[1] - 0.0001; Y += dy, pj++) {
for (var X = xExtent[0], pi = 0; X < xExtent[1] - 0.0001; X += dx, pi++) {
var id = pi + "-" + pj;
var bin = binsById[id] = [];
bin.i = pi;
bin.j = pj;
bin.x = X;
bin.y = Y;
bin.width = dx;
bin.height = dy;
bin.value = 0;
}
}
points.forEach(function (point, i) {
var py = y.call(rectbin, point, i) / dy;
var pj = trunc(py);
var px = x.call(rectbin, point, i) / dx;
var pi = trunc(px);
var id = pi + "-" + pj;
var bin = binsById[id];
if (bin) {
bin.push(point);
bin.value += 1;
}
});
return Object.values(binsById);
}
rectbin.x = function (_) {
if (!arguments.length) return x;
x = _;
return rectbin;
};
rectbin.y = function (_) {
if (!arguments.length) return y;
y = _;
return rectbin;
};
rectbin.dx = function (_) {
if (!arguments.length) return dx;
dx = _;
return rectbin;
};
rectbin.dy = function (_) {
if (!arguments.length) return dy;
dy = _;
return rectbin;
};
return rectbin;
};
function heatmap (pitch) {
var enableInteraction = false,
selected = [undefined, undefined],
onSelect = function onSelect() {
return;
},
onDeselect = function onDeselect() {
return;
},
color = d3Scale.scaleSequential(colorScale__namespace.interpolateGreens).domain([undefined, undefined]),
stroke = "#00000011",
selStroke = "#FF6600",
strokewidth = 0.0,
interpolated = false,
showValues = false,
valueFormatter = function valueFormatter(v) {
return v;
},
valueFontSize = function valueFontSize(h) {
return h * 0.2 + "px";
},
parent_el = "body",
selStrokewidth = 0.5;
function chart(g) {
g.each(function (data) {
var selx = selected[0];
var sely = selected[1];
if (color.domain().some(isNaN)) {
color.domain(extent(data, function (d) {
return d.value;
}));
}
var draw = d3Selection.select(this).call(pitch).select(".below");
var join = draw.selectAll("rect.cell") // these
.data(data);
var enterSel = join.enter().append("g").attr("class", "cell").attr("id", function (d) {
return "cell(".concat(d.i, ",").concat(d.j, ")");
});
enterSel.append("rect").attr("x", function (d) {
return d.x;
}).attr("y", function (d) {
return d.y;
}).attr("width", function (d) {
return d.width;
}).attr("height", function (d) {
return d.height;
}).attr("data", function (d) {
return d.value;
}).style("stroke", function (d) {
return selx === d.x && sely === d.y ? selStroke : stroke;
}).style("stroke-width", function (d) {
return selx === d.x && sely === d.y ? selStrokewidth : strokewidth;
}).style("fill", function (d) {
return interpolated ? "transparent" : color(+d.value);
}).style("cursor", enableInteraction ? "crosshair" : "default").on("mouseover", function (_, d) {
if (enableInteraction) d3Selection.select(this).style("stroke", selStroke).style("stroke-width", selStrokewidth);
onSelect(d.x, d.y, d.value);
}).on("mouseout", function (_, d) {
if (enableInteraction) d3Selection.select(this).style("stroke", stroke).style("stroke-width", strokewidth);
onDeselect(d.x, d.y, d.value);
});
join.merge(enterSel).select("rect")
//.transition()
.attr("x", function (d) {
return d.x;
}).attr("y", function (d) {
return d.y;
}).attr("width", function (d) {
return d.width;
}).attr("height", function (d) {
return d.height;
}).attr("data", function (d) {
return d.value;
}).style("fill", function (d) {
return interpolated ? "transparent" : color(+d.value);
});
join.exit().select("rect")
//.transition()
.attr("width", 0).attr("height", 0).remove();
if (showValues) {
enterSel.append("text").attr("x", function (d) {
return d.x + d.width / 2;
}).attr("y", function (d) {
return d.y + d.height / 2;
}).attr("dy", ".35em").attr("text-anchor", "middle").text(function (d) {
return valueFormatter(d.value);
}).style("fill", "white").style("mix-blend-mode", "difference").style("font-size", function (d) {
return valueFontSize(d.height, d.width, d.value);
}).style("pointer-events", "none");
join.merge(enterSel).select("text").attr("x", function (d) {
return d.x + d.width / 2;
}).attr("y", function (d) {
return d.y + d.height / 2;
}).text(function (d) {
return valueFormatter(d.value);
});
join.exit().select("text")
//.transition()
.style("font-size", 0).remove();
}
if (interpolated) {
var gx = max(data, function (d) {
return d.i;
}) + 1;
var gy = max(data, function (d) {
return d.j;
}) + 1;
var grid = [];
for (var x = 0; x < gx; x++) {
grid.push([]);
for (var y = 0; y < gy; y++) {
grid[x].push(data.find(function (d) {
return d.i === x && d.j === y;
}).value);
}
}
var canvas = d3Selection.select(parent_el).style("position", "relative").append("canvas").style("z-index", -1).style("position", "absolute").style("pointer-events", "none");
var bbox = position(draw, d3Selection.select(parent_el));
var n = parseInt(bbox.height);
var m = parseInt(bbox.width);
var scaleX = (pitch.clip()[1][0] - pitch.clip()[0][0]) / 105;
var scaleY = (pitch.clip()[1][1] - pitch.clip()[0][1]) / 68;
if (pitch.rotate()) {
scaleY = (pitch.clip()[1][1] - pitch.clip()[0][1]) / 105;
scaleX = (pitch.clip()[1][0] - pitch.clip()[0][0]) / 68;
}
canvas.attr("width", m * scaleX).attr("height", n * scaleY).style("left", bbox.left + "px").style("top", bbox.top + "px");
var gridInterpolator = bicubicInterpolateExports.createGridInterpolator(grid, {
extrapolate: true,
scaleX: pitch.rotate() ? gx / n : gx / m,
scaleY: pitch.rotate() ? gy / m : gy / n,
translateX: -0.5,
translateY: -0.5
});
var context = canvas.node().getContext("2d"),
image = context.createImageData(m, n);
var l = 0;
for (var j = 0; j < n; ++j) {
for (var i = 0; i < m; ++i) {
var v;
if (pitch.rotate()) v = gridInterpolator(n - j, i);else v = gridInterpolator(i, j);
var c = d3Color.rgb(color(v || 0));
image.data[l + 0] = c.r;
image.data[l + 1] = c.g;
image.data[l + 2] = c.b;
image.data[l + 3] = 255;
l += 4;
}
}
context.putImageData(image, 0, 0);
}
});
}
chart.colorScale = function (_) {
if (!arguments.length) return color;
color = _;
return chart;
};
chart.selected = function (_) {
if (!arguments.length) return selected;
selected = _;
return chart;
};
chart.enableInteraction = function (_) {
if (!arguments.length) return enableInteraction;
enableInteraction = Boolean(_);
return chart;
};
chart.showValues = function (_) {
if (!arguments.length) return showValues;
showValues = Boolean(_);
return chart;
};
chart.valueFormatter = function (_) {
if (!arguments.length) return valueFormatter;
valueFormatter = _;
return chart;
};
chart.valueFontSize = function (_) {
if (!arguments.length) return valueFontSize;
valueFontSize = _;
return chart;
};
chart.interpolate = function (_) {
if (!arguments.length) return interpolated;
interpolated = Boolean(_);
return chart;
};
chart.parent_el = function (_) {
if (!arguments.length) return parent_el;
parent_el = _;
return chart;
};
chart.onSelect = function (f) {
onSelect = f;
return chart;
};
chart.onDeselect = function (f) {
onDeselect = f;
return chart;
};
return chart;
}
function _arrayLikeToArray(r, a) {
(null == a || a > r.length) && (a = r.length);
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
return n;
}
function _arrayWithHoles(r) {
if (Array.isArray(r)) return r;
}
function _createForOfIteratorHelper(r, e) {
var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
if (!t) {
if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e) {
t && (r = t);
var n = 0,
F = function () {};
return {
s: F,
n: function () {
return n >= r.length ? {
done: !0
} : {
done: !1,
value: r[n++]
};
},
e: function (r) {
throw r;
},
f: F
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
var o,
a = !0,
u = !1;
return {
s: function () {
t = t.call(r);
},
n: function () {
var r = t.next();
return a = r.done, r;
},
e: function (r) {
u = !0, o = r;
},
f: function () {
try {
a || null == t.return || t.return();
} finally {
if (u) throw o;
}
}
};
}
function _iterableToArrayLimit(r, l) {
var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
if (null != t) {
var e,
n,
i,
u,
a = [],
f = !0,
o = !1;
try {
if (i = (t = t.call(r)).next, 0 === l) ; else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
} catch (r) {
o = !0, n = r;
} finally {
try {
if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
} finally {
if (o) throw n;
}
}
return a;
}
}
function _nonIterableRest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _slicedToArray(r, e) {
return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
}
function _unsupportedIterableToArray(r, a) {
if (r) {
if ("string" == typeof r) return _arrayLikeToArray(r, a);
var t = {}.toString.call(r).slice(8, -1);
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
}
}
var noop = {value: () => {}};
function dispatch() {
for (var i = 0, n = arguments.length, _ = {}, t; i < n; ++i) {
if (!(t = arguments[i] + "") || (t in _) || /[\s.]/.test(t)) throw new Error("illegal type: " + t);
_[t] = [];
}
return new Dispatch(_);
}
function Dispatch(_) {
this._ = _;
}
function parseTypenames(typenames, types) {
return typenames.trim().split(/^|\s+/).map(function(t) {
var name = "", i = t.indexOf(".");
if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);
if (t && !types.hasOwnProperty(t)) throw new Error("unknown type: " + t);
return {type: t, name: name};
});
}
Dispatch.prototype = dispatch.prototype = {
constructor: Dispatch,
on: function(typename, callback) {
var _ = this._,
T = parseTypenames(typename + "", _),
t,
i = -1,
n = T.length;
// If no callback was specified, return the callback of the given type and name.
if (arguments.length < 2) {
while (++i < n) if ((t = (typename = T[i]).type) && (t = get(_[t], typename.name))) return t;
return;
}
// If a type was specified, set the callback for the given type and name.
// Otherwise, if a null callback was specified, remove callbacks of the given name.
if (callback != null && typeof callback !== "function") throw new Error("invalid callback: " + callback);
while (++i < n) {
if (t = (typename = T[i]).type) _[t] = set(_[t], typename.name, callback);
else if (callback == null) for (t in _) _[t] = set(_[t], typename.name, null);
}
return this;
},
copy: function() {
var copy = {}, _ = this._;
for (var t in _) copy[t] = _[t].slice();
return new Dispatch(copy);
},
call: function(type, that) {
if ((n = arguments.length - 2) > 0) for (var args = new Array(n), i = 0, n, t; i < n; ++i) args[i] = arguments[i + 2];
if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
for (t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
},
apply: function(type, that, args) {
if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
for (var t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
}
};
function get(type, name) {
for (var i = 0, n = type.length, c; i < n; ++i) {
if ((c = type[i]).name === name) {
return c.value;
}
}
}
function set(type, name, callback) {
for (var i = 0, n = type.length; i < n; ++i) {
if (type[i].name === name) {
type[i] = noop, type = type.slice(0, i).concat(type.slice(i + 1));
break;
}
}
if (callback != null) type.push({name: name, value: callback});
return type;
}
// These are typically used in conjunction with noevent to ensure that we can
// preventDefault on the event.
const nonpassive = {passive: false};
const nonpassivecapture = {capture: true, passive: false};
function nopropagation(event) {
event.stopImmediatePropagation();
}
function noevent(event) {
event.preventDefault();
event.stopImmediatePropagation();
}
function nodrag(view) {
var root = view.document.documentElement,
selection = d3Selection.select(view).on("dragstart.drag", noevent, nonpassivecapture);
if ("onselectstart" in root) {
selection.on("selectstart.drag", noevent, nonpassivecapture);
} else {
root.__noselect = root.style.MozUserSelect;
root.style.MozUserSelect = "none";
}
}
function yesdrag(view, noclick) {
var root = view.document.documentElement,
selection = d3Selection.select(view).on("dragstart.drag", null);
if (noclick) {
selection.on("click.drag", noevent, nonpassivecapture);
setTimeout(function() { selection.on("click.drag", null); }, 0);
}
if ("onselectstart" in root) {
selection.on("selectstart.drag", null);
} else {
root.style.MozUserSelect = root.__noselect;
delete root.__noselect;
}
}
var constant = x => () => x;
function DragEvent(type, {
sourceEvent,
subject,
target,
identifier,
active,
x, y, dx, dy,
dispatch
}) {
Object.defineProperties(this, {
type: {value: type, enumerable: true, configurable: true},
sourceEvent: {value: sourceEvent, enumerable: true, configurable: true},
subject: {value: subject, enumerable: true, configurable: true},
target: {value: target, enumerable: true, configurable: true},
identifier: {value: identifier, enumerable: true, configurable: true},
active: {value: active, enumerable: true, configurable: true},
x: {value: x, enumerable: true, configurable: true},
y: {value: y, enumerable: true, configurable: true},
dx: {value: dx, enumerable: true, configurable: true},
dy: {value: dy, enumerable: true, configurable: true},
_: {value: dispatch}
});
}
DragEvent.prototype.on = function() {
var value = this._.on.apply(this._, arguments);
return value === this._ ? this : value;
};
// Ignore right-click, since that should open the context menu.
function defaultFilter(event) {
return !event.ctrlKey && !event.button;
}
function defaultContainer() {
return this.parentNode;
}
function defaultSubject(event, d) {
return d == null ? {x: event.x, y: event.y} : d;
}
function defaultTouchable() {
return navigator.maxTouchPoints || ("ontouchstart" in this);
}
function d3Drag() {
var filter = defaultFilter,
container = defaultContainer,
subject = defaultSubject,
touchable = defaultTouchable,
gestures = {},
listeners = dispatch("start", "drag", "end"),
active = 0,
mousedownx,
mousedowny,
mousemoving,
touchending,
clickDistance2 = 0;
function drag(selection) {
selection
.on("mousedown.drag", mousedowned)
.filter(touchable)
.on("touchstart.drag", touchstarted)
.on("touchmove.drag", touchmoved, nonpassive)
.on("touchend.drag touchcancel.drag", touchended)
.style("touch-action", "none")
.style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
}
function mousedowned(event, d) {
if (touchending || !filter.call(this, event, d)) return;
var gesture = beforestart(this, container.call(this, event, d), event, d, "mouse");
if (!gesture) return;
d3Selection.select(event.view)
.on("mousemove.drag", mousemoved, nonpassivecapture)
.on("mouseup.drag", mouseupped, nonpassivecapture);
nodrag(event.view);
nopropagation(event);
mousemoving = false;
mousedownx = event.clientX;
mousedowny = event.clientY;
gesture("start", event);
}
function mousemoved(event) {
noevent(event);
if (!mousemoving) {
var dx = event.clientX - mousedownx, dy = event.clientY - mousedowny;
mousemoving = dx * dx + dy * dy > clickDistance2;
}
gestures.mouse("drag", event);
}
function mouseupped(event) {
d3Selection.select(event.view).on("mousemove.drag mouseup.drag", null);
yesdrag(event.view, mousemoving);
noevent(event);
gestures.mouse("end", event);
}
function touchstarted(event, d) {
if (!filter.call(this, event, d)) return;
var touches = event.changedTouches,
c = container.call(this, event, d),
n = touches.length, i, gesture;
for (i = 0; i < n; ++i) {
if (gesture = beforestart(this, c, event, d, touches[i].identifier, touches[i])) {
nopropagation(event);
gesture("start", event, touches[i]);
}
}
}
function touchmoved(event) {
var touches = event.changedTouches,
n = touches.length, i, gesture;
for (i = 0; i < n; ++i) {
if (gesture = gestures[touches[i].identifier]) {
noevent(event);
gesture("drag", event, touches[i]);
}
}
}
function touchended(event) {
var touches = event.changedTouches,
n = touches.length, i, gesture;
if (touchending) clearTimeout(touchending);
touchending = setTimeout(function() { touchending = null; }, 500); // Ghost clicks are delayed!
for (i = 0; i < n; ++i) {
if (gesture = gestures[touches[i].identifier]) {
nopropagation(event);
gesture("end", event, touches[i]);
}
}
}
function beforestart(that, container, event, d, identifier, touch) {
var dispatch = listeners.copy(),
p = d3Selection.pointer(touch || event, container), dx, dy,
s;
if ((s = subject.call(that, new DragEvent("beforestart", {
sourceEvent: event,
target: drag,
identifier,
active,
x: p[0],
y: p[1],
dx: 0,
dy: 0,
dispatch
}), d)) == null) return;
dx = s.x - p[0] || 0;
dy = s.y - p[1] || 0;
return function gesture(type, event, touch) {
var p0 = p, n;
switch (type) {
case "start": gestures[identifier] = gesture, n = active++; break;
case "end": delete gestures[identifier], --active; // falls through
case "drag": p = d3Selection.pointer(touch || event, container), n = active; break;
}
dispatch.call(
type,
that,
new DragEvent(type, {
sourceEvent: event,
subject: s,
target: drag,
identifier,
active: n,
x: p[0] + dx,
y: p[1] + dy,
dx: p[0] - p0[0],
dy: p[1] - p0[1],
dispatch
}),
d
);
};
}
drag.filter = function(_) {
return arguments.length ? (filter = typeof _ === "function" ? _ : constant(!!_), drag) : filter;
};
drag.container = function(_) {
return arguments.length ? (container = typeof _ === "function" ? _ : constant(_), drag) : container;
};
drag.subject = function(_) {
return arguments.length ? (subject = typeof _ === "function" ? _ : constant(_), drag) : subject;
};
drag.touchable = function(_) {
return arguments.length ? (touchable = typeof _ === "function" ? _ : constant(!!_), drag) : touchable;
};
drag.on = function() {
var value = listeners.on.apply(listeners, arguments);
return value === listeners ? drag : value;
};
drag.clickDistance = function(_) {
return arguments.length ? (clickDistance2 = (_ = +_) * _, drag) : Math.sqrt(clickDistance2);
};
return drag;
}
/**
* actions - draws a sequence of SPADL actions
* on a pitch.
*
*/
function actions (pitch) {
var scale = 4;
var showTooltip = false;
var draggable = false;
var teamColors = {};
var onUpdate = function onUpdate() {
return undefined;
};
/**
* chart - constructor function.
* Appends the plot to the g selection.
*
* @param g - d3 selection of elements to which the plot will be appended.
*/
function chart(g) {
g.each(function (data) {
// Create the soccer pitch
var actionsLayer = d3Selection.select(this).call(pitch).select(".above");
// Create an arrow symbol
actionsLayer.append("svg:defs").append("svg:marker").attr("id", "triangle").attr("refX", 11).attr("refY", 6).attr("dy", 6).attr("markerWidth", 12).attr("markerHeight", 12).attr("orient", "auto").append("path").attr("d", "M 0 0 12 6 0 12 3 6");
var dragHandler = d3Drag().on("start", function () {
d3Selection.select(this).classed("active", true);
}).on("drag", function (d) {
var _d3Pointer = d3Selection.pointer(actionsLayer.node()),
_d3Pointer2 = _slicedToArray(_d3Pointer, 2),
x = _d3Pointer2[0],
y = _d3Pointer2[1];
var marker = d3Selection.select(this).attr("marker");
d3Selection.select(this).attr("transform", "translate(" + x + "," + y + ")");
var action = d3Selection.select("#action-".concat(d.action_id));
if (marker === "marker-start") {
action.select("line").attr("x1", x).attr("y1", y);
var prev_action = d3Selection.select("#action-".concat(d.action_id - 1));
prev_action.select("line").attr("x2", x).attr("y2", y);
} else {
action.select("line").attr("x2", x).attr("y2", y);
}
}).on("end", function (d) {
var _d3Pointer3 = d3Selection.pointer(actionsLayer.node()),
_d3Pointer4 = _slicedToArray(_d3Pointer3, 2),
x = _d3Pointer4[0],
y = _d3Pointer4[1];
var marker = d3Selection.select(this).attr("marker");
d3Selection.select(this).classed("active", false);
var newData = data.slice();
if (marker === "marker-start") {
newData.find(function (a) {
return a.action_id === d.action_id;
})["start_x"] = x;
newData.find(function (a) {
return a.action_id === d.action_id;
})["start_y"] = 68 - y;
var prevAction = newData.find(function (a) {
return a.action_id === d.action_id - 1;
});
if (prevAction) {
prevAction["end_x"] = x;
prevAction["end_y"] = 68 - y;
}
} else {
newData.find(function (a) {
return a.action_id === d.action_id;
})["end_x"] = x;
newData.find(function (a) {
return a.action_id === d.action_id;
})["end_y"] = 68 - y;
}
onUpdate(newData);
});
// Select a unique color for each team
var defaultColors = ["#FDB913", "#87CEEB"];
var _iterator = _createForOfIteratorHelper(data),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var action = _step.value;
if (action.team_id in teamColors === false) {
if (Object.keys(teamColors).length >= 2) throw "You specified ids of teams that do not occur in the data!";else teamColors[action.team_id] = defaultColors[Object.keys(teamColors).length];
}
}
// Draw the actions
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
var update = actionsLayer.selectAll("g.action").data(data);
update.each(function (d, i) {
d3Selection.select(this).selectAll("*").remove();
var action = d3Selection.select(this).attr("class", "action").attr("id", "action-".concat(d.action_id)).call(actiontypePlotFn[d.type_id]["plot"], d, i + 1, teamColors[d.team_id], scale);
action.selectAll("g.symbol").on("mouseover", function (e, d) {
return showTooltip && showTooltip.show(e, d);
}).on("mouseout", function () {
return showTooltip && showTooltip.hide();
});
});
update.enter().append("g").attr("class", "action").each(function (d, i) {
var action = d3Selection.select(this).attr("class", "action").attr("id", "action-".concat(d.action_id)).call(actiontypePlotFn[d.type_id]["plot"], d, i + 1, teamColors[d.team_id], scale);
action.selectAll("g.symbol").on("mouseover", function (e, d) {
return showTooltip && showTooltip.show(e, d);
}).on("mouseout", function () {
return showTooltip && showTooltip.hide();
});
});
var last_action = actionsLayer.selectAll(".action:last-of-type");
var mend = last_action.selectAll("g.marker-end").data(function (d) {
return [d];
}).enter().append("g").attr("class", "marker marker-end").attr("marker", "marker-end").attr("transform", function (d) {
return "translate(" + d.end_x + "," + (68 - d.end_y) + ")";
});
mend.append("circle").attr("stroke-width", 0).attr("fill", "#000").attr("opacity", 0).attr("r", 2);
update.exit().remove();
if (draggable) dragHandler(actionsLayer.selectAll("g.marker"));
});
}
chart.showTooltip = function (_) {
if (!arguments.length) return showTooltip;
showTooltip = _;
return chart;
};
chart.scale = function (_) {
if (!arguments.length) return scale;
scale = _;
return chart;
};
chart.draggable = function (_) {
if (!arguments.length) return draggable;
draggable = Boolean(_);
return chart;
};
chart.teamColors = function (_) {
if (!arguments.length) return teamColors;
teamColors = _;
return chart;
};
chart.onUpdate = function (f) {
if (!arguments.length) return onUpdate;
onUpdate = f;
return chart;
};
return chart;
}
var actiontypePlotFn = [{
label: "Pass",
plot: plotPass,
symbol: symbolPass
}, {
label: "Cross",
plot: plotPass,
symbol: symbolPass
}, {
label: "Throw in",
plot: plotPass,
symbol: symbolPass
}, {
label: "Freekick (cross)",
plot: plotPass,
symbol: symbolPass
}, {
label: "Freekick (short)",
plot: plotPass,
symbol: symbolPass
}, {
label: "Corner (cross)",
plot: plotPass,
symbol: symbolPass
}, {
label: "Corner (short)",
plot: plotPass,
symbol: symbolPass
}, {
label: "Dribble",
plot: plotDribble