Skip to content

Instantly share code, notes, and snippets.

@matt-bernhardt
Last active August 29, 2015 14:15
Show Gist options
  • Save matt-bernhardt/c5a1d99aa3b8d52160bc to your computer and use it in GitHub Desktop.
Save matt-bernhardt/c5a1d99aa3b8d52160bc to your computer and use it in GitHub Desktop.
Combination of list items, v2

This is an attempt to build a visualization pattern showing combinations among items in a single list. It is inspired by a device I remember from printed maps, showing distances between cities - usually printed in an unused corner.

This pattern currently works best for combinations that are direction-agnostic. In this example, the datasete illustrates how often a group of soccer players appeared together over the course of a single season. For bi-directional combinations a different strategy may be needed, or perhaps a more nuanced design at each intersection.

{
"nodes":[
{"name":"Oshoniyi","value":13},
{"name":"Michallik","value":32},
{"name":"Clark","value":30},
{"name":"Watson","value":14},
{"name":"Battelle","value":16},
{"name":"Thompson","value":24},
{"name":"Bliss","value":19},
{"name":"Paz","value":30},
{"name":"Khumalo","value":26},
{"name":"Marino","value":32},
{"name":"McBride","value":31}
],
"combinations":[
{"source":0,"target":1,"value":12},
{"source":0,"target":2,"value":12},
{"source":0,"target":3,"value":12},
{"source":0,"target":4,"value":9},
{"source":0,"target":5,"value":13},
{"source":0,"target":6,"value":12},
{"source":0,"target":7,"value":13},
{"source":0,"target":8,"value":10},
{"source":0,"target":9,"value":11},
{"source":0,"target":10,"value":12},
{"source":1,"target":2,"value":27},
{"source":1,"target":3,"value":14},
{"source":1,"target":4,"value":15},
{"source":1,"target":5,"value":23},
{"source":1,"target":6,"value":18},
{"source":1,"target":7,"value":27},
{"source":1,"target":8,"value":25},
{"source":1,"target":9,"value":29},
{"source":1,"target":10,"value":28},
{"source":2,"target":3,"value":13},
{"source":2,"target":4,"value":15},
{"source":2,"target":5,"value":21},
{"source":2,"target":6,"value":17},
{"source":2,"target":7,"value":28},
{"source":2,"target":8,"value":23},
{"source":2,"target":9,"value":28},
{"source":2,"target":10,"value":26},
{"source":3,"target":4,"value":11},
{"source":3,"target":5,"value":14},
{"source":3,"target":6,"value":13},
{"source":3,"target":7,"value":14},
{"source":3,"target":8,"value":11},
{"source":3,"target":9,"value":12},
{"source":3,"target":10,"value":11},
{"source":4,"target":5,"value":12},
{"source":4,"target":6,"value":11},
{"source":4,"target":7,"value":15},
{"source":4,"target":8,"value":13},
{"source":4,"target":9,"value":14},
{"source":4,"target":10,"value":13},
{"source":5,"target":6,"value":19},
{"source":5,"target":7,"value":22},
{"source":5,"target":8,"value":20},
{"source":5,"target":9,"value":21},
{"source":5,"target":10,"value":21},
{"source":6,"target":7,"value":17},
{"source":6,"target":8,"value":16},
{"source":6,"target":9,"value":17},
{"source":6,"target":10,"value":16},
{"source":7,"target":8,"value":23},
{"source":7,"target":9,"value":27},
{"source":7,"target":10,"value":26},
{"source":8,"target":9,"value":23},
{"source":8,"target":10,"value":22},
{"source":9,"target":10,"value":28}
]
}
// Basic parameters
var i; // counters
var _s32 = (Math.sqrt(3)/2); // constant ratio of hexagon's internal to external radius
var nodeSize = 20; // 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 = 600; // 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: 100
};
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("crew96.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([9,29]) // games played together
.range([255,0]); // 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 "rgba(" + Math.floor(colorRange(d.value)) + "," + Math.floor(colorRange(d.value)) + "," + Math.floor(colorRange(d.value)) + ",0.75)";
/*
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 + " games played together";
});
};
buildLabels = function(data) {
labels = svgContainer.append("g")
.attr("class","labels");
labelText = labels.selectAll("g text")
.data(data)
.enter()
.append("text")
.attr("x",nodeText)
.attr("y",function(d,i){
return (i*dy*2) + dy + nodeText/2;
})
.attr("font-size",nodeText )
.text(function(d) {
return d.name;
}); // label text
label = labels.selectAll("path")
.data(data)
.enter()
.append("path") // label drawing
.style("fill", "rgba(255,226,90,0.5)")
.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 0 " + 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 0 ";
return path;
})
.append("svg:title")
.text(function(d){
return d.value + " games played";
});
};
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;
};
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>Node Combinations Graph - Color</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="hexagons.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