|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<title>Determining Baseball Fan Loyalty: MLB Attendance vs. Wins</title> |
|
<meta http-equiv="content-type" content="application/xhtml+xml; charset=utf-8" /> |
|
<script src="http://d3js.org/d3.v3.min.js"></script> |
|
<script src="colors.js"></script> |
|
<style type="text/css"> |
|
html, body { |
|
font: 12px Helvetica; |
|
margin: 0; padding: 0; |
|
} |
|
circle { |
|
fill: #000; |
|
stroke-width: 0; |
|
fill-opacity: 0.5; |
|
} |
|
circle:hover, circle.highlight { |
|
fill-opacity: 1; |
|
stroke-width: 2; |
|
stroke-opacity: 1; |
|
} |
|
circle.hide { |
|
fill-opacity: 0.1; |
|
} |
|
#tooltip { |
|
background: #111; |
|
color: #fff; |
|
padding: 5px; |
|
text-align: right; |
|
} |
|
#tooltip span { |
|
font: 11px Helvetica; |
|
color: #ddd; |
|
} |
|
text { |
|
font-size: 10px; |
|
text-transform: uppercase; |
|
} |
|
text.team { |
|
text-transform: uppercase; |
|
fill: #ccc; |
|
cursor: pointer; |
|
} |
|
text.teamlowlight { |
|
fill: #666 !important; |
|
} |
|
text.team:hover { |
|
fill: #ccc; |
|
cursor: pointer; |
|
font-weight: bold; |
|
} |
|
rect.avg { |
|
fill: black; |
|
stroke-width: 2; |
|
} |
|
path.range { |
|
fill: none; |
|
stroke: #333; |
|
stroke-width: 3; |
|
stroke-opacity: 0.2; |
|
} |
|
div.toggle { |
|
cursor: pointer; |
|
color: #999; |
|
background: #ddd; |
|
padding: 5px; |
|
position: absolute; |
|
font-size: 10px; |
|
text-transform: uppercase; |
|
} |
|
div.toggle:hover { |
|
background: #666; |
|
color: #fff; |
|
} |
|
path.shame { |
|
fill: #ddd; |
|
stroke: none; |
|
} |
|
path.guidelines { |
|
stroke: #ccc; |
|
stroke-width: 1; |
|
/*fill-opacity*/ |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<svg id="svg"></svg> |
|
<script type="text/javascript"> |
|
|
|
var ttw = 125; |
|
var tooltip = d3.select("body") |
|
.append("div") |
|
.attr("id", "tooltip") |
|
.style("position", "absolute") |
|
.style("z-index", "10") |
|
.style("visibility", "hidden") |
|
.text("a simple tooltip"); |
|
|
|
var slug = function (str) { return str.replace(/[^a-z]/ig, '_'); }; |
|
|
|
var draw = function(errors, years) { |
|
var h = 500, w = 960; |
|
var gh = 420, gw = 760; |
|
var pad = (h - gh)/2; |
|
var wdiff = w - gw; |
|
var hdiff = h - gh; |
|
var r = 6; |
|
|
|
var maxAtt = d3.max(years, function(d) { return d.attendance; }); |
|
var maxWins = d3.max(years, function(d) { return d.win; }); |
|
var minAtt = d3.min(years, function(d) { return d.attendance; }); |
|
var minWins = d3.min(years, function(d) { return d.win; }); |
|
var round = function(x) { return Math.round(x * 1000) / 1000; }; |
|
|
|
// todo normalize attendance and wins for these "scores" |
|
var aggregates = d3.nest() |
|
.key(function(d, i) { return d.team; }) |
|
.rollup(function(d, i) { |
|
var result = { |
|
attendance: d3.mean(d, function(d2){ return parseFloat(d2.attendance); }), |
|
attendanceMin: d3.min(d, function(d2){ return parseFloat(d2.attendance); }), |
|
attendanceMax: d3.max(d, function(d2){ return parseFloat(d2.attendance); }), |
|
win: d3.mean(d, function(d2){ return parseFloat(d2.win); }), |
|
winMin: d3.min(d, function(d2){ return parseFloat(d2.win); }), |
|
winMax: d3.max(d, function(d2){ return parseFloat(d2.win); }) |
|
}; |
|
result.score = round(result.attendance / result.win); |
|
return result; |
|
}) |
|
.entries(years); |
|
|
|
var teams = aggregates; |
|
teams.sort(function(a, b) { return d3.descending(a.values.score, b.values.score)}); |
|
|
|
var ascale = d3.scale.linear().domain([minAtt, maxAtt]).range([2 * r, gw - pad - 2 * r]); |
|
var wscale = d3.scale.linear().domain([minWins, maxWins]).range([gh - 2 * r, 2 * r]); |
|
var tscale = d3.scale.linear().domain([0, teams.length]).range([pad, gh + pad]); |
|
|
|
var svg = d3.select("#svg") |
|
.attr("width", w) |
|
.attr("height", h) |
|
.style("background", "#eee"); |
|
|
|
var g = svg.append("g") |
|
.attr("transform", "translate(" + (w - gw) + " " + pad + ")"); |
|
|
|
var line = d3.svg.line(); |
|
// Background |
|
svg.append("rect").attr("fill", "#333").attr("width", w - gw - pad).attr("height", h); |
|
// The shame zone + guidelines |
|
g.append("path").classed("shame", true) |
|
.attr("d", function () { |
|
return line([ |
|
[ascale(minAtt) - r, wscale(maxWins) - r], |
|
[ascale(minAtt) - r, wscale(minWins) + r], |
|
[ascale(minWins), wscale(minWins) + r], |
|
[ascale(maxWins), wscale(maxWins) - r], |
|
[ascale(minAtt) - r, wscale(maxWins) - r] |
|
]); |
|
}); |
|
g.append("text").style("fill", "#999").text("Fan Dead Zone").attr("x", 50).attr("y", 50); |
|
// 0.500 |
|
g.append("path").style("stroke", "#ccc").attr("d", function() { return line([[ascale(minAtt), wscale(0.5)],[ascale(maxAtt), wscale(0.5)]]); }) |
|
g.append("text").style("fill", "#999").text("0.500").attr("y", wscale(0.5) - 5).attr("x", ascale(maxAtt)).style("text-anchor","end"); |
|
|
|
// Axis |
|
g.append("text") |
|
.attr("transform", "translate(0 " + (gh / 2) + ") rotate (-90)") |
|
.style("text-anchor", "middle") |
|
.text("Wins"); |
|
g.append("text") |
|
.style("text-anchor", "start").style("fill", "#999") |
|
.attr("transform", "translate(0 " + wscale(minWins) + ") rotate (-90)") |
|
.text(minWins); |
|
g.append("text") |
|
.style("text-anchor", "end").style("fill", "#999") |
|
.attr("transform", "translate(0 " + wscale(maxWins) + ") rotate (-90)") |
|
.text(maxWins); |
|
g.append("text") |
|
.attr("transform", "translate(" + (gw / 2) + " " + (gh+10) + ")") |
|
.style("text-anchor", "middle") |
|
.text("Attendance"); |
|
g.append("text") |
|
.style("text-anchor", "start").style("fill", "#999") |
|
.attr("transform", "translate(" + ascale(minAtt) + " " + (gh+10) + ")") |
|
.text(minAtt); |
|
g.append("text") |
|
.style("text-anchor", "end").style("fill", "#999") |
|
.attr("transform", "translate(" + ascale(maxAtt) + " " + (gh+10) + ")") |
|
.text(maxAtt); |
|
|
|
// Show Average Toggle |
|
d3.select("body") |
|
.append("div") |
|
.attr("class", "toggle") |
|
.text("Show Only Averages") |
|
.style("left", w - 170 + "px") |
|
.style("top", pad/2 + "px") |
|
.on("mouseover", function(d) { |
|
toggleMode("aggregate"); |
|
}) |
|
.on("mouseout", function(d) { |
|
toggleMode("yearly"); |
|
}) |
|
; |
|
|
|
// Team Side Bar |
|
svg |
|
.append("text") |
|
.style("fill", "#fff") |
|
.text("Team • Score") |
|
.attr("transform", "translate(" + pad/2 + " " + tscale(0) + ")"); |
|
svg.selectAll("text.team") |
|
.data(teams) |
|
.enter() |
|
.append("text") |
|
.attr("class", function(d) { return slug(d.key) + " team"; }) |
|
.text(function(d) { return d.key + " • " + (d.values.score); }) |
|
.attr("transform", function(d, i) { return "translate(" + (pad/2) + " " + tscale(i+1) + ")"; }) |
|
.on("mouseover", function(d) {highlight(d.key);}) |
|
.on("mouseout", unhighlight) |
|
; |
|
|
|
// Team Balls |
|
g.selectAll("g.team") |
|
.data(years) |
|
.enter() |
|
.append("g") |
|
.attr("class", "team") |
|
.attr("transform", function (d) { return "translate(" + ascale(d.attendance) + " " + wscale(d.win) + ")"; }) |
|
.on("mouseover", function(d) { |
|
tooltip.style("visibility", "visible"); |
|
tooltip.html(d.year + " " + d.team + "<br /><span>Win: " + d.win + "</span><br /><span>Att: " + d.attendance + "</span>"); |
|
highlight(d.team); |
|
}) |
|
.on("mousemove", function(d) { tooltip.style("top", (event.pageY - 10)+"px").style("left",(event.pageX - 30 - parseInt(tooltip.style("width"), 10)) + "px");}) |
|
.on("mouseout", function(d) { |
|
tooltip.style("visibility", "hidden"); |
|
unhighlight(); |
|
}) |
|
.append("circle") |
|
.style("fill", function(d) { return colors[d.team][0]; }) |
|
.style("stroke", function(d) { return colors[d.team][1]; }) |
|
.attr("class", function(d) { return slug(d.team) + " " + d.year; }) |
|
.attr("id", function(d) { return d.team + ":" + d.year; }) |
|
.attr("title", function(d) { return d.team + ": " + d.year; }) |
|
.attr("r", 0.01) |
|
.transition() |
|
.delay(function(d, i) { return (d.year - 2004) * 300 + 100; }) |
|
.duration(300) |
|
.attr("r", r) |
|
; |
|
|
|
// Averages |
|
var rw = 12; |
|
var avgs = g.selectAll("g.avg") |
|
.data(aggregates) |
|
.enter() |
|
.append("g") |
|
.attr("class", function(d) { return "avg " + slug(d.key); }) |
|
.style("visibility", "hidden") |
|
.attr("transform", function (d) { return "translate(" + (ascale(d.values.attendance) - rw/2) + " " + (wscale(d.values.win) - rw/2) + ")"; }) |
|
; |
|
avgs |
|
.append("path") |
|
.attr("class", "range") |
|
.style("stroke", function(d) { return colors[d.key][0]; }) |
|
.attr("d", function(d, i) { |
|
return line([ |
|
[ascale(d.values.attendanceMin) - ascale(d.values.attendance) + rw/2, rw / 2], |
|
[ascale(d.values.attendanceMax) - ascale(d.values.attendance) + rw/2, rw / 2] |
|
]); |
|
}) |
|
; |
|
avgs |
|
.append("rect") |
|
.attr("class", "avg") |
|
.style("fill", function(d) { return colors[d.key][0]; }) |
|
.style("stroke", function(d) { return colors[d.key][1]; }) |
|
.attr("width", rw) |
|
.attr("height", rw) |
|
; |
|
avgs |
|
.append("text") |
|
.text(function(d) { return d.key; /*+ " " + round(d.values.attendance) + " / " + round(d.values.win);*/ }) |
|
.attr("transform", function(d) { |
|
return "translate(" + rw*1.5 + " " + (rw/2-rw/6+5) + ")"; |
|
}) |
|
.attr("class", "avg") |
|
; |
|
|
|
}; |
|
|
|
var highlight = function(team) { |
|
var team = slug(team); |
|
d3.selectAll("circle").classed("hide", true); |
|
d3.selectAll("circle." + team).classed("highlight", true).classed("hide", false); |
|
d3.selectAll("text.team").classed("teamlowlight", true); |
|
d3.selectAll("text." + team).classed("teamlowlight", false); |
|
d3.selectAll("g." + team).style("visibility", "visible"); |
|
}; |
|
var unhighlight = function() { |
|
d3.selectAll("text.team").classed("teamlowlight", false) |
|
d3.selectAll("circle").classed("hide", false).classed("highlight", false); |
|
d3.selectAll("g.avg").style("visibility", "hidden"); |
|
}; |
|
|
|
var toggleMode = function(mode) { |
|
if (mode === "yearly") { |
|
d3.selectAll("g.team").style("visibility", "visible"); |
|
d3.selectAll("g.avg").style("visibility", "hidden"); |
|
} else if (mode === "aggregate") { |
|
d3.selectAll("g.team").style("visibility", "hidden"); |
|
d3.selectAll("g.avg").style("visibility", "visible"); |
|
} |
|
}; |
|
|
|
|
|
d3.csv("attendance-vs-wins-by-year.csv").get(draw); |
|
|
|
</script> |
|
</body> |
|
</html> |