Skip to content

Instantly share code, notes, and snippets.

@matt-bernhardt
Last active February 15, 2018 03:05
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 matt-bernhardt/2d9dd021832afb5d1f7091c6cc200964 to your computer and use it in GitHub Desktop.
Save matt-bernhardt/2d9dd021832afb5d1f7091c6cc200964 to your computer and use it in GitHub Desktop.
Columbus Attendance Changes In Context

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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment