Skip to content

Instantly share code, notes, and snippets.

@dhoboy
Last active October 1, 2017 18:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dhoboy/fe4f34b6debb7021105b5d8a1e514676 to your computer and use it in GitHub Desktop.
Save dhoboy/fe4f34b6debb7021105b5d8a1e514676 to your computer and use it in GitHub Desktop.
Verlander Brush

Using crossfilter on Justin Verlander pitching data.

Open in new window to see full block.

I scraped data from Retrosheet with Python's BeautifulSoup. Scraping repo here.

Retrosheet data use statement: 'The information used here was obtained free of charge from and is copyrighted by Retrosheet. Interested parties may contact Retrosheet at "www.retrosheet.org".'

<!DOCTYPE html>
<meta charset="utf-8">
<title>Verlander</title>
<style>
#title, .subtitle {
font-family: sans-serif;
}
#title {
font-size: 35px;
padding: 10px;
}
.subtitle {
font-size: 18px;
padding-left: 10px;
}
#charts {
margin: 25px;
display: flex;
flex-direction: row;
}
.secondaryChart {
margin: 20px;
}
#secondaryCharts {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-top: -100px;
}
.tooltip {
background-color: #f7f7f7;
padding: 3px 12px;
font-family: sans-serif;
border: 1px solid #bbbbbb;
box-shadow: 1px 1px 4px #bbbbbb;
display: flex;
flex-direction: column;
align-items: center;
}
pre {
font-weight: normal;
margin: 5px 0;
text-align: center;
}
rect.selection {
fill: steelblue;
}
.line {
stroke: steelblue;
fill: none;
stroke-width: 3px;
}
.point {
fill: steelblue;
stroke: none;
}
.existing {
stroke: red;
stroke-width: 2px;
}
.bar, .bar.selected {
fill: steelblue;
}
.bar.unselected {
fill: #ddd;
}
.axis path,
.axis line {
fill: none;
stroke: #666666;
shape-rendering: crispEdges;
}
.axis text {
fill: #666666;
font-family: sans-serif;
font-size: 14px;
}
text.axis_title {
font-size: 15px;
fill: black;
font-weight: bold;
}
</style>
<body>
<div id="title">Justin Verlander</div>
<div class="subtitle">Brush on the bar graph to filter scatter plot data</div>
<div id="charts">
<div id="mainChart"></div>
<div id="secondaryCharts"></div>
</div>
<script src="https://d3js.org/d3.v4.js"></script>
<script src='//alexmacy.github.io/crossfilter/crossfilter.v1.min.js'></script>
<script>
/* chart sizes */
var margin = {
main: { // year v. innings pitched
top: 10, right: 40, bottom: 80, left: 80
},
secondary: { // strikeouts, walks ...
top: 10, right: 20, bottom: 40, left: 80
}
};
var mainWidth = 625 - margin.main.left - margin.main.right;
var mainHeight = 400 - margin.main.top - margin.main.bottom;
var secondaryWidth = 250 - margin.secondary.left - margin.secondary.right;
var secondaryHeight = 200 - margin.secondary.top - margin.secondary.bottom;
// set this when you brush on the main graph
var filterDates = [];
/* load Verlander data */
d3.csv("http://dhoboy.github.io/baseball/Justin_Verlander.csv", function(err, raw) {
var data = raw.slice(0, raw.length - 1);
// set up crossfilter
var pitchingData = crossfilter(data);
var years = pitchingData.dimension(function(d) {
return +d.Year;
});
var metrics = {
'BB': "Walks",
'SO': "Strikeouts",
'H': "Hits allowed",
'ER': "Runs allowed",
'W': "Wins",
'L': "Loses",
'ERA': "Earned Run Average",
'CG': "Complete Games",
//'SHO': "Shutouts"
};
/* make charts */
var mainChart = d3.select("#mainChart")
.append("svg")
.attr("height", mainHeight + margin.main.top + margin.main.bottom)
.attr("width", mainWidth + margin.main.left + margin.main.right)
.append("g")
.attr("transform", "translate(" + margin.main.left + "," + margin.main.top + ")");
var secondaryCharts = Object.keys(metrics).map(function(metric) {
var x = d3.scaleLinear()
.domain(d3.extent(data, function(d) { return +d.Year; }))
.range([0, secondaryWidth]);
var y = d3.scaleLinear()
.domain(d3.extent(data, function(d) { return +d[metric]; }))
.range([secondaryHeight, 0]);
return (
{ metric: metric,
svg: d3.select("#secondaryCharts")
.append("svg")
.attr("class", "secondaryChart " + metric)
.attr("height", secondaryHeight + margin.secondary.top + margin.secondary.bottom)
.attr("width", secondaryWidth + margin.secondary.left + margin.secondary.right)
.append("g")
.attr("transform", "translate(" + margin.secondary.left + "," + margin.secondary.top + ")"),
x: x,
y: y,
xAxis: d3.axisBottom(x).ticks(2).tickFormat(d3.format("")),
yAxis: d3.axisLeft(y).ticks(5)
});
});
/* for hovering on secondary charts */
var tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "hidden");
/* setup main chart */
var mainX = d3.scaleBand()
.domain(data.map(function(d) {
return d.Year;
}))
.range([0, mainWidth])
.padding(0.2);
var mainY = d3.scaleLinear()
.domain([0, d3.max(data, function(d) { return +d.IP; })])
.range([mainHeight, 0]);
// for getting dates out of brush on main graph
var reverseMainX = d3.scaleLinear()
.domain([0, mainWidth])
.range(d3.extent(data, function(d) {
return +d.Year;
}));
/* setup main axes */
var mainXAxis = d3.axisBottom(mainX);
var mainYAxis = d3.axisLeft(mainY);
/* draw main graph */
(function drawMainGraph() {
mainChart.selectAll(".bar")
.data(data)
.enter()
.append("rect")
.attr("class", function(d) {
return "bar yr" + d.Year;
})
.attr("x", function(d) {
return mainX(d.Year);
})
.attr("y", function(d) {
return mainY(+d.IP);
})
.attr("width", mainX.bandwidth())
.attr("height", function(d) {
return mainHeight - mainY(+d.IP);
});
/* draw axes */
var mainXAxisG = mainChart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + mainHeight + ")")
.call(mainXAxis);
mainXAxisG.append("text")
.attr("transform", "translate(490,45)")
.attr("class", "axis_title")
.text("Year");
mainChart.append("g")
.attr("class", "y axis")
.attr("transform", "translate(0" + ",0)")
.call(mainYAxis)
.append("text")
.attr("transform", "translate(-50,0) rotate(-90)")
.attr("class", "axis_title")
.text("Innings Pitched");
})();
/* setup brush on main graph */
var brush = d3.brushX()
.extent([[0,-10],[mainWidth, mainHeight]])
var mainBrush = mainChart.append("g")
.attr("class", "brush")
.call(brush);
brush.on("brush end", function() {
var brushSection = d3.brushSelection(this);
if (brushSection === null) { // clear the brush
d3.selectAll(".bar")
.attr("class", function(d) {
return "bar yr" + d.Year;
});
redrawSecondaryCharts(data, true);
} else { // filter data for secondary graphs
var f = d3.timeFormat("%Y");
// set filterDates for secondary graphs
filterDates = [
Math.round(reverseMainX(brushSection[0])),
Math.round(reverseMainX(brushSection[1])) + 1
];
// clear all bar highlighting
d3.selectAll(".bar")
.attr("class", function(d) {
return "bar yr" + d.Year + " unselected";
});
// highlight selected bars
d3.range(filterDates[0], filterDates[1]).map(function(yr) {
d3.select(".bar.yr" + yr)
.attr("class", "bar yr" + yr + " selected");
});
// filter the data on the years selected
years.filterRange(filterDates);
redrawSecondaryCharts(years.bottom(pitchingData.size()));
}
});
// draw secondary charts
(function initializeSecondaryCharts() {
secondaryCharts.forEach(function(chart) {
var metric = chart.metric;
var svg = chart.svg;
var x = chart.x;
var y = chart.y;
var xAxis = chart.xAxis;
var yAxis = chart.yAxis;
// draw the points
svg.selectAll(".point")
.data(data, function(d) { return d.Year; })
.enter()
.append("circle")
.attr("class", "point")
.attr("cx", function(d) {
return x(+d.Year);
})
.attr("cy", function(d) {
return y(+d[metric]);
})
.attr("r", 5);
// draw the axes
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(0" + ",0)")
.call(yAxis)
.append("text")
.attr("transform", "translate(-40,0) rotate(-90)")
.attr("class", "axis_title")
.text(metrics[metric]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + secondaryHeight + ")")
.call(xAxis)
.append("text")
.attr("transform", "translate(150,40)")
.attr("class", "axis_title")
.text("Year");
});
})();
function redrawSecondaryCharts(data, reset) {
console.log("data in draw secondary: ", data);
secondaryCharts.forEach(function(chart) {
var metric = chart.metric;
var svg = chart.svg;
var x = chart.x;
var y = chart.y;
var xAxis = chart.xAxis;
var yAxis = chart.yAxis;
x.domain(d3.extent(data, function(d) { return +d.Year; }));
y.domain(d3.extent(data, function(d) { return +d[metric]; }));
svg.select(".x.axis")
.transition()
.duration(reset ? 0 : 750)
.call(xAxis);
svg.select(".y.axis")
.transition()
.duration(reset ? 0 : 750)
.call(yAxis);
var points = svg.selectAll(".point")
.data(data, function(d) { return d.Year; })
points
.exit()
.transition()
.duration(reset ? 0 : 400)
.remove();
points
.transition()
.delay(reset ? 0 : 400)
.duration(reset ? 0 : 500)
.attr("cx", function(d) {
return x(d.Year);
});
points
.transition()
.delay(reset ? 0 : 900)
.duration(reset ? 0 : 500)
.attr("cy", function(d) {
return y(+d[metric]);
});
points
.enter()
.append("circle")
.attr("class", "point")
.attr("r", 5)
.attr("cx", function(d) {
return x(d.Year);
})
.attr("cy", function(d) {
return y(+d[metric]);
})
.attr("fill-opacity", reset ? 1 : 0);
// issues with the enter selection executing the
// fill-opacity transition only when 'reseting' graph
// graph resets on brush end where brush selection is null
points
.transition()
.delay(reset ? 0 : 1400)
.duration(reset ? 0 : 400)
.attr("fill-opacity", 1);
svg.selectAll(".point")
.on("mouseover", function(d) {
tooltip.html("");
tooltip.append("pre")
.text(d.Year + ": " + d[metric] + " " + metrics[metric]);
return tooltip.style("visibility", "visible");
})
.on("mousemove", function(d) {
return tooltip.style("top", (d3.event.pageY-20) + "px").style("left", (d3.event.pageX+10) + "px");
})
.on("mouseout", function(d) {
return tooltip.style("visibility", "hidden");
});
});
}
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment