This is an attempt to build a visualization of organic attendance growth by the Columbus Crew over various time periods during their 21-year history.
For more information about this plot, please reach out to me on Twitter at @BernhardtSoccer.
This is an attempt to build a visualization of organic attendance growth by the Columbus Crew over various time periods during their 21-year history.
For more information about this plot, please reach out to me on Twitter at @BernhardtSoccer.
// Basic parameters | |
var i; // counters | |
var _s32 = (Math.sqrt(3)/2); // constant ratio of hexagon's internal to external radius | |
var nodeSize = 15; // size of an individual node - this may become auto calculated | |
var nodeText = 12; | |
// need to add a label size bsaed on data label lengths | |
var dx = nodeSize * 1.5; // column spacing value | |
var dy = nodeSize * _s32; // row spacing value | |
var w = 600; // total plot width | |
var h = 700; // total plot height | |
// need to refactor the margin calculations... | |
var margin = { // this is meant to provide a margin around the plot | |
top: nodeSize, | |
right: nodeSize * 1.5, | |
bottom: nodeSize * 1.5, | |
left: 150 | |
}; | |
var svgContainer; // overall plot container | |
var label, labels, labelText; // plot labels | |
var hexagon, hexagons; // plot data appears in hexagons | |
// create container | |
svgContainer = d3.select("#combinations") | |
.append("svg") | |
.attr("width", w) | |
.attr("height", h); | |
// load data, build visualization | |
d3.json("data.json", function(error, data) { | |
if(error) { | |
alert("Error loading json file:\n" + error.statusText); | |
console.log(error); | |
} else { | |
// build node labels | |
buildLabels(data.nodes); | |
// draw hexagon grid | |
buildHexagons(data.combinations); | |
buildListeners(); | |
} | |
}); | |
// Functions | |
buildHexagons = function(data) { | |
var colorRange = d3.scale.linear() | |
.domain([0,0.11,0.22,0.33,0.44,0.55,0.66,0.77,0.8,1]) // percentile from 0 - 1 | |
.interpolate(d3.interpolateHcl) | |
.range(['#000000','#2166ac','#4393c3','#92c5de','#d1e5f0','#f7f7f7','#fddbc7','#f4a582','#d6604d','#b2182b']); // color being mapped | |
hexagons = svgContainer.append("g") | |
.attr("class","hexagons"); | |
hexagon = hexagons.selectAll("path") | |
.data(data) | |
.enter() | |
.append("path") | |
.attr("data-combination",function(d,i) { | |
return d.source+" "+d.target; | |
}) | |
.style("fill", function(d) { | |
console.log(d.value + " => " + (colorRange(d.value))); | |
return colorRange(d.value); | |
/* | |
if(d.value === 10) { | |
return "rgba(192,192,128,0.5)"; | |
} else { | |
return "rgba(255,192,192,0.5)"; | |
} | |
*/ | |
}) | |
.attr("stroke","rgb(255,255,255)") | |
.attr("stroke-width","0") | |
.attr("d", function(d,i) { | |
col = margin.left + ( Math.abs(d.source - d.target) * dx ); | |
row = ((Math.abs(d.source-d.target)/2) + Math.min(d.source,d.target)) * Math.sqrt(3) * nodeSize + margin.top; | |
return setHexagonPoints(col,row,nodeSize); | |
}) | |
.attr("class","cell") | |
.append("svg:title") | |
.text(function(d){ | |
return d.value.toLocaleString(undefined, {style: 'percent'}) + " percentile in attendance growth"; | |
}); | |
}; | |
buildLabels = function(data) { | |
var baseScale = d3.scale.linear() | |
.domain([0,20000]) | |
.range([0,margin.left]); | |
labels = svgContainer.append("g") | |
.attr("class","labels"); | |
labelText = labels.selectAll("g text") | |
.data(data) | |
.enter() | |
.append("text") | |
.attr("x",75) | |
.attr("y",function(d,i){ | |
return (i*dy*2) + dy + nodeText/2; | |
}) | |
.attr("font-size",nodeText ) | |
.text(function(d) { | |
return d.name + ": " + d.value.toLocaleString(); | |
}); // label text | |
label = labels.selectAll("path") | |
.data(data) | |
.enter() | |
.append("path") // label drawing | |
.style("fill", "rgba(255,226,90,0.75)") | |
.attr("stroke","rgb(255,255,255)") | |
.attr("class","label") | |
.attr("data-node",function(d,i){ | |
return i; | |
}) | |
.attr("d",function(d,i) { | |
var startY = i * nodeSize*_s32*2 + (margin.top - nodeSize*_s32); | |
var path = "M " + (margin.left - baseScale(d.value)) + " " + startY + " "; | |
path += "H " + (margin.left + dx - nodeSize) + " "; | |
path += "L " + (margin.left + dx - nodeSize/2) + " " + (startY + ( _s32*nodeSize ) ) + " "; | |
path += "L " + (margin.left + dx - nodeSize) + " " + (startY + ( _s32*nodeSize * 2) ) + " "; | |
path += "H " + (margin.left - baseScale(d.value)) + " "; | |
path += "V " + startY; | |
return path; | |
}) | |
.append("svg:title") | |
.text(function(d){ | |
return d.value + " average attendance"; | |
}); | |
}; | |
buildListeners = function() { | |
$("g.hexagons path").mouseout(function() { | |
// reset all labels | |
$("g.labels").children("path").each(function() { | |
$(this).attr("class","label"); | |
}); | |
}); | |
$("g.labels path").mouseout(function() { | |
// reset all labels | |
$("g.hexagons").children("path").each(function() { | |
$(this).attr("class","cell"); | |
}); | |
}); | |
$("g.hexagons path").mouseover(function() { | |
// highlight the labels for the relevant combined node | |
var cl = $(this).data("combination").split(" ").sort(); | |
for(var x in cl){ | |
// cl[x] = +cl[x]; | |
var needle = $("g.labels").children("path")[+cl[x]]; | |
$(needle).attr("class","label active"); | |
} | |
}); | |
$("g.labels path").mouseover(function() { | |
// highlight the hexagons for the relevant label | |
var needle = $(this).data("node"); | |
var haystack = $("g.hexagons path"); | |
console.log(haystack); | |
for (var x = 0; x < haystack.length; x++) { | |
var candidate = haystack[x].attributes["data-combination"].value.split(" ").map(Number); | |
console.log(needle); | |
console.log(candidate); | |
console.log(typeof(candidate)); | |
// need to be able to read the candidate's data-combination attribute | |
if(candidate.indexOf(needle) >= 0) { | |
console.log("found"); | |
haystack[x].attributes["class"].value = "cell active"; | |
} else { | |
console.log("not here"); | |
haystack[x].attributes["class"].value = "cell"; | |
} | |
console.log(""); | |
} | |
}); | |
}; | |
setHexagonPoints = function(x,y,size) { | |
var hexPoints = ""; | |
hexPoints += "M " + (size + x ) + " " + (0 + y ) + " "; | |
hexPoints += "L " + (size/2 + x ) + " " + (size*_s32 + y ) + " " ; | |
hexPoints += "L " + (-size/2 + x) + " " + (size*_s32 + y ) + " " ; | |
hexPoints += "L " + (-size + x ) + " " + (0 + y ) + " " ; | |
hexPoints += "L " + (-size/2 + x) + " " + (-size*_s32 + y) + " " ; | |
hexPoints += "L " + (size/2 + x ) + " " + (-size*_s32 + y) + " " ; | |
hexPoints += "L " + (size + x ) + " " + (0 + y ) + " " ; | |
return hexPoints; | |
}; |
{ | |
"nodes":[ | |
{"name":"1996","value":18950}, | |
{"name":"1997","value":15043}, | |
{"name":"1998","value":12274}, | |
{"name":"1999","value":17696}, | |
{"name":"2000","value":15451}, | |
{"name":"2001","value":17511}, | |
{"name":"2002","value":17429}, | |
{"name":"2003","value":16250}, | |
{"name":"2004","value":16872}, | |
{"name":"2005","value":12916}, | |
{"name":"2006","value":13294}, | |
{"name":"2007","value":15230}, | |
{"name":"2008","value":14662}, | |
{"name":"2009","value":14175}, | |
{"name":"2010","value":14642}, | |
{"name":"2011","value":12185}, | |
{"name":"2012","value":14397}, | |
{"name":"2013","value":16080}, | |
{"name":"2014","value":16881}, | |
{"name":"2015","value":16985}, | |
{"name":"2016","value":17125}, | |
{"name":"2017","value":15439} | |
], | |
"combinations":[ | |
{"source":0,"target":1,"value":0.33}, | |
{"source":0,"target":2,"value":0.22}, | |
{"source":1,"target":2,"value":0.00}, | |
{"source":3,"target":4,"value":0.27}, | |
{"source":3,"target":5,"value":0.36}, | |
{"source":3,"target":6,"value":0.33}, | |
{"source":3,"target":7,"value":0.63}, | |
{"source":3,"target":8,"value":0.38}, | |
{"source":3,"target":9,"value":0.00}, | |
{"source":4,"target":5,"value":0.55}, | |
{"source":4,"target":6,"value":0.78}, | |
{"source":4,"target":7,"value":0.75}, | |
{"source":4,"target":8,"value":0.63}, | |
{"source":4,"target":9,"value":0.29}, | |
{"source":4,"target":10,"value":0.40}, | |
{"source":5,"target":6,"value":0.33}, | |
{"source":5,"target":7,"value":0.50}, | |
{"source":5,"target":8,"value":0.63}, | |
{"source":5,"target":9,"value":0.29}, | |
{"source":5,"target":10,"value":0.40}, | |
{"source":5,"target":11,"value":0.25}, | |
{"source":6,"target":7,"value":0.50}, | |
{"source":6,"target":8,"value":0.50}, | |
{"source":6,"target":9,"value":0.00}, | |
{"source":6,"target":10,"value":0.40}, | |
{"source":6,"target":11,"value":0.00}, | |
{"source":6,"target":12,"value":0.00}, | |
{"source":7,"target":8,"value":0.33}, | |
{"source":7,"target":9,"value":0.13}, | |
{"source":7,"target":10,"value":0.33}, | |
{"source":7,"target":11,"value":0.20}, | |
{"source":7,"target":12,"value":0.20}, | |
{"source":7,"target":13,"value":0.60}, | |
{"source":8,"target":9,"value":0.13}, | |
{"source":8,"target":10,"value":0.17}, | |
{"source":8,"target":11,"value":0.20}, | |
{"source":8,"target":12,"value":0.20}, | |
{"source":8,"target":13,"value":0.60}, | |
{"source":8,"target":14,"value":0.75}, | |
{"source":9,"target":10,"value":0.56}, | |
{"source":9,"target":11,"value":0.63}, | |
{"source":9,"target":12,"value":0.43}, | |
{"source":9,"target":13,"value":1.00}, | |
{"source":9,"target":14,"value":1.00}, | |
{"source":9,"target":15,"value":0.60}, | |
{"source":10,"target":11,"value":0.50}, | |
{"source":10,"target":12,"value":0.56}, | |
{"source":10,"target":13,"value":0.89}, | |
{"source":10,"target":14,"value":0.88}, | |
{"source":10,"target":15,"value":0.57}, | |
{"source":10,"target":16,"value":0.50}, | |
{"source":11,"target":12,"value":0.45}, | |
{"source":11,"target":13,"value":0.64}, | |
{"source":11,"target":14,"value":0.60}, | |
{"source":11,"target":15,"value":0.22}, | |
{"source":11,"target":16,"value":0.75}, | |
{"source":11,"target":17,"value":1.00}, | |
{"source":12,"target":13,"value":0.69}, | |
{"source":12,"target":14,"value":0.67}, | |
{"source":12,"target":15,"value":0.36}, | |
{"source":12,"target":16,"value":0.70}, | |
{"source":12,"target":17,"value":0.70}, | |
{"source":12,"target":18,"value":0.80}, | |
{"source":13,"target":14,"value":0.69}, | |
{"source":13,"target":15,"value":0.00}, | |
{"source":13,"target":16,"value":0.27}, | |
{"source":13,"target":17,"value":0.55}, | |
{"source":13,"target":18,"value":0.64}, | |
{"source":13,"target":19,"value":0.56}, | |
{"source":14,"target":15,"value":0.00}, | |
{"source":14,"target":16,"value":0.31}, | |
{"source":14,"target":17,"value":0.62}, | |
{"source":14,"target":18,"value":0.54}, | |
{"source":14,"target":19,"value":0.55}, | |
{"source":14,"target":20,"value":0.36}, | |
{"source":15,"target":16,"value":0.94}, | |
{"source":15,"target":17,"value":0.94}, | |
{"source":15,"target":18,"value":0.94}, | |
{"source":15,"target":19,"value":0.86}, | |
{"source":15,"target":20,"value":0.86}, | |
{"source":15,"target":21,"value":0.79}, | |
{"source":16,"target":17,"value":1.00}, | |
{"source":16,"target":18,"value":0.78}, | |
{"source":16,"target":19,"value":0.88}, | |
{"source":16,"target":20,"value":0.75}, | |
{"source":16,"target":21,"value":0.69}, | |
{"source":17,"target":18,"value":0.61}, | |
{"source":17,"target":19,"value":0.69}, | |
{"source":17,"target":20,"value":0.63}, | |
{"source":17,"target":21,"value":0.13}, | |
{"source":18,"target":19,"value":0.44}, | |
{"source":18,"target":20,"value":0.50}, | |
{"source":18,"target":21,"value":0.19}, | |
{"source":19,"target":20,"value":0.58}, | |
{"source":19,"target":21,"value":0.11}, | |
{"source":20,"target":21,"value":0.11} | |
] | |
} |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta http-equiv="content-type" content="text/html;charset=utf-8"> | |
<title>Columbus Attendance Growth in Context</title> | |
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js" charset="utf-8"></script> | |
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script> | |
<link href="style.css" rel="stylesheet" type='text/css' /> | |
</head> | |
<body> | |
<div id="combinations"></div> | |
<script src="attendance.js" charset="utf-8"></script> | |
</body> | |
</html> |
svg { | |
font: 10px sans-serif; | |
} | |
.cell, | |
.label { | |
/* opacity: 0.5; */ | |
} | |
.label { | |
opacity: 0.5; | |
} | |
.cell:hover, | |
.label:hover { | |
opacity: 1; | |
stroke: red; | |
stroke-width: 2px; | |
} | |
.active { | |
opacity: 1; | |
stroke: red; | |
stroke-width: 2px; | |
} |