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; | |
| } |