Permalink
Cannot retrieve contributors at this time
<!DOCTYPE html> | |
<html> | |
<head> | |
<script src=""></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/4.1.2/papaparse.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/vis/4.16.1/vis.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/vis/4.16.1/vis.min.css"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.5.8/es5-shim.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.35.1/es6-shim.min.js"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/themes/blitzer/jquery-ui.min.css"> | |
<script> | |
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ | |
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), | |
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) | |
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); | |
ga('create', 'UA-77691127-4', 'auto'); | |
ga('send', 'pageview'); | |
</script> | |
<script> | |
// helper function | |
function getLabel(node) { | |
label = node.name.replace(/\s(\S{3})/g,"\n$1"); | |
if (node.type == "s") label += "\n(" + node.distance/2 + ")"; | |
return label; | |
} | |
// helper function | |
function getGroup(node, toNode, fromNode) { | |
if (node == toNode) return "To"; | |
if (node == fromNode) return "From"; | |
return node.type; | |
} | |
// helper function | |
function getUrlVars() | |
{ | |
var vars = [], hash; | |
var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); | |
for(var i = 0; i < hashes.length; i++) | |
{ | |
hash = hashes[i].split('='); | |
vars.push(hash[0]); | |
vars[hash[0]] = hash[1].replace("%20", " "); | |
} | |
return vars; | |
} | |
// Global vars | |
var quartets = {}; // dictionary | |
var singers = {}; // dictionary | |
var timeOuts = []; | |
// Read the CSV file and build a network graph model | |
Papa.parse("https://raw.githubusercontent.com/liwenyip/6-degrees-of-nooj/gh-pages/BABS%20Quartet%20Singers.csv", { | |
download: true, | |
complete: function(results) { | |
// compile unique lists of quartets, singers, and relationships | |
$.each(results.data, function(i, row) { | |
q = row[0] + " (" + row[3] + ")"; // name (affiliation) | |
s = row[1]; | |
// create the quartet and singer if they don't exist | |
if (!quartets.hasOwnProperty(q)) quartets[q] = {name:q, type:"q", links:[]}; | |
if (!singers.hasOwnProperty(s)) singers[s] = {name:s, type:"s", links:[]}; | |
// create links between quartet and singer (but don't create duplicates) | |
if ($.inArray(singers[s], quartets[q].links) == -1) quartets[q].links.push(singers[s]); | |
if ($.inArray(quartets[q], singers[s].links) == -1) singers[s].links.push(quartets[q]); | |
}); | |
// Set autocomplete on inputs | |
$( "#from, #to" ).autocomplete({ | |
source: Object.keys(singers) | |
}); | |
// read to/from from URL string if available | |
vars = getUrlVars(); | |
if (vars["to"]) $("#to").val(vars["to"]); | |
if (vars["from"]) $("#from").val(vars["from"]); | |
updateTitles(); | |
// debug | |
console.log(quartets); | |
console.log(singers); | |
} | |
}); | |
function updateTitles() { | |
for (i = 1; i <= 6; i++) { | |
$("#" + i + "deg").html(i + " degrees of " + $("#from").val()); | |
$("#paths").html("Shortest paths between " + $("#from").val() + " and " + $("#to").val()); | |
$("h1, title").html("Six Degrees of " + $("#to").val()); | |
} | |
} | |
$(function() { | |
// update dropdown when fromNode changes | |
$("#from, #to").focusout(function() {updateTitles()}); | |
// Make a graph | |
$( "#go" ).click(function() { | |
// create the new graph | |
console.log("Creating graph object..."); | |
var container = document.getElementById('network'); | |
var data = {/* nodes: graphNodes, edges: graphLinks */}; | |
/* | |
// Development mode options | |
var options = { configure: { | |
filter:function (option, path) { | |
if (path.indexOf('physics') !== -1) { | |
return true; | |
} | |
if (path.indexOf('smooth') !== -1 || option === 'smooth') { | |
return true; | |
} | |
return false; | |
}, | |
container: document.getElementById('config') | |
} | |
}; | |
*/ | |
var options = {}; // override previous options | |
var network = new vis.Network(container, data, options); | |
// Clear any unexecuted timeout events | |
while (timeOuts.length > 0) clearTimeout(timeOuts.pop()); | |
// Traverse the graph model | |
console.log("Traversing model..."); | |
// initialise root node | |
fromNode = singers[$("#from").val()]; | |
fromNode.distance = 0; | |
// initialise Nooj | |
toNode = singers[$("#to").val()]; | |
toNode.distance = 0; | |
// initialise lists | |
toVisit = [fromNode]; // queue of nodes to be visited | |
visited = [] // list of nodes that have been visited | |
visitedLinks = [] // list of links, use for graphing web | |
// Create a new radial model, by doing a breadth-first search starting at fromNode | |
furtherestNode = fromNode; | |
while (toVisit.length > 0) { | |
// get the next node to visit, mark it as visited, check for max distance | |
node = toVisit.shift() | |
visited.push(node); | |
if (node.distance > furtherestNode.distance) furtherestNode = node; | |
// Loop through linked nodes, create radial links | |
node.radialLinks = []; // clear any previous links | |
$.each(node.links, function(i, node2) { | |
if ($.inArray(node2, visited) != -1) return true; // skip linked nodes we've already visited | |
if ($.inArray(node2, toVisit) != -1) return true; // skip linked nodes we're already planning vo visit (prevents non-radial links) | |
node.radialLinks.push(node2); // create radial link | |
// visitedLinks.push({from:node.name, to:node2.name}); // add link (edge) to the graph | |
toVisit.push(node2); // add linked node to list to visit | |
node2.distance = node.distance + 1; // calculate distance from fromNode | |
}); | |
} | |
console.log("Furtherest node from " + fromNode.name + " is " + furtherestNode.name + " who has distance of " + furtherestNode.distance/2); | |
// Check if we reached the target and calculate shortest path | |
console.log("Calculating shortest path..."); | |
msg = "<p>"; | |
if (toNode != fromNode && toNode.distance == 0) { | |
msg += fromNode.name + " is not connected to " + toNode.name; | |
} else { | |
// calculate distance | |
msg += fromNode.name + " has a " + toNode.name + " number of " + toNode.distance /2 + ": "; | |
permalink = "http://liwenyip.github.io/6-degrees-of-nooj/?from=" + fromNode.name + "&to=" + toNode.name; | |
msg += " (Permalink: <a href=\"" + permalink + "\">" + permalink + "</a>)"; | |
// find ONE shortest path from fromNode to toNode | |
next = toNode; | |
path = [toNode]; // list of nodes along shortest path | |
while (path[0] != fromNode) { | |
// find the linked node that is next-closest to fromNode | |
$.each(path[0].links, function(j, link) { | |
if (link.distance < next.distance) next = link; | |
}); | |
path.unshift(next); | |
} | |
msg += "</p><p>" + path.map(getLabel).join(' -> ') + "</p>"; | |
} | |
$("#result").html(msg); // display result | |
// initialise network | |
graphLinks = new vis.DataSet([]); | |
graphNodes = new vis.DataSet([]); | |
network.setData({edges:graphLinks, nodes:graphNodes}); | |
// Make some pretty graphs | |
if ( $("#graphtype").val() == "paths") { | |
// find and graph shortest multiple paths | |
console.log("Graphing paths..."); | |
// start at toNode and work way back towards fromNode | |
pathToVisit = [toNode]; | |
pathVisited = []; // list of nodes that lie along shortest path(s) | |
while (pathToVisit.length > 0) { | |
// get the next node to visit, mark it as visited, and add it to graph | |
node = pathToVisit.shift() | |
pathVisited.push(node); | |
if (node == fromNode) break; // if we've reached fromNode then we're done | |
$.each(node.links, function(i, node2) { | |
if ($.inArray(node2, pathVisited) != -1) return true; // skip linked nodes we've already visited | |
if (node2.distance >= node.distance) return true; // skip linked nodes that aren't any closer to fromNode | |
if ($.inArray(node2, pathToVisit) == -1) pathToVisit.push(node2); // add linked node to list to visit | |
graphLinks.add({from:node.name, to:node2.name}); // add link to graph | |
}); | |
} | |
// Add nodes to graph at 100ms intervals | |
$.each(pathVisited, function(i, node) { | |
timeOuts.push(setTimeout( function() { | |
graphNodes.add({ | |
id:node.name, | |
label:getLabel(node), | |
group:getGroup(node, toNode, fromNode), | |
level:node.distance}) | |
}, 100 * i)); | |
}); | |
} else { | |
// graph a radial network | |
console.log("Graphing web..."); | |
// traverse the radial network with depth-first search | |
radialToVisit = [fromNode]; | |
radialVisited = []; // list of nodes that lie along shortest path(s) | |
while (radialToVisit.length > 0) { | |
// get the next node to visit, mark it as visited, and add it to graph | |
node = radialToVisit.shift(); | |
radialVisited.push(node); | |
$.each(node.radialLinks, function(i, node2) { | |
if ($.inArray(node2, radialVisited) != -1) return true; // skip linked nodes we've already visited | |
if ($.inArray(node2, radialToVisit) != -1) return true; // skip linked nodes we're already planning to visit | |
if (node2.distance > $("#graphtype").val()*2) return true; // skip nodes that are more than 6 quartets away from fromNode | |
graphLinks.add({from:node.name, to:node2.name}); // add link to graph | |
radialToVisit.push(node2); // add linked node to list to visit | |
}); | |
} | |
// Add nodes to graph at 100ms intervals | |
$.each(radialVisited, function(i, node) { | |
timeOuts.push(setTimeout( function() { | |
graphNodes.add({ | |
id:node.name, | |
label:getLabel(node), | |
group:getGroup(node, toNode, fromNode), | |
level:node.distance}); | |
}, 100 * i)); | |
}); | |
} | |
// Do the visualisation stuff | |
console.log("Done."); | |
}); | |
}); | |
</script> | |
<style type="text/css"> | |
#network { | |
float: left; | |
width: 100%; | |
height: 600px; | |
border: 1px solid lightgray; | |
} | |
#config { | |
float: right; | |
width: 40%; | |
height: 600px; | |
border: 1px solid lightgray; | |
} | |
#inputs, #result, #header { | |
width: 100%; | |
padding: 10px; | |
} | |
#header h1, #header p { | |
margin: 2px; | |
} | |
#result p { | |
margin: 5px; | |
} | |
</style> | |
<title>Six Degrees of Nooj</title> | |
<meta name="description" content="Find out how you're connected to other barbershop quartet singers"> | |
<meta name="keywords" content="Barbershop,Quartet,Six,Degrees,Separation,Links,BABS,LABBS,UK,BinH,Mixed,Singers,Linked,Network,Nooj,Number"> | |
<meta name="author" content="Li-Wen Yip"></head> | |
<body> | |
<div class="ui-widget"> | |
<div id="header" class="ui-widget-header"> | |
<h1>Six Degrees of Nooj</h1> | |
<p>Find out how you're connected to other barbershop quartet singers</p> | |
</div> | |
<div id="inputs" class="ui-widget"> | |
<label for="from">From: </label> | |
<input id="from"> | |
<label for="to">To: </label> | |
<input id="to" value="Nooj"> | |
<label for="graphtype">Show me: </label> | |
<select id="graphtype"> | |
<option id="paths" value="paths">Shortest paths between them</option> | |
<option id="1deg" value=1>1 degrees of x</option> | |
<option id="2deg" value=2>2 degrees of x</option> | |
<option id="3deg" value=3>3 degrees of x</option> | |
<option id="4deg" value=4>4 degrees of x</option> | |
<option id="5deg" value=5>5 degrees of x</option> | |
<option id="6deg" value=6>6 degrees of x</option> | |
</select> | |
<button id="go">Go!</button> | |
<a href="about.html">About</a> | |
</div> | |
<div id="result" class="ui-widget-container"><p> </p></div> | |
<div id="network" class="ui-widget-container"></div> | |
</div> | |
<!--<div id="config"></div>--> | |
</body> | |
</html> |