Skip to content

Instantly share code, notes, and snippets.

@sdjacobs
Forked from mbostock/.block
Last active August 29, 2015 14:13
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 sdjacobs/cff1db0bd1ac17fbd20f to your computer and use it in GitHub Desktop.
Save sdjacobs/cff1db0bd1ac17fbd20f to your computer and use it in GitHub Desktop.
Finding the Key Players in a graph

This example shows a D3 implementation of the Key Players algorithm, as laid out in Borghatti 2006. We focus on KP-Pos - finding a set of size k that is maximally connected to the other nodes in the graph. We use the greedy optimization algorithm outlined in the paper, and equation (14) as our metric.

Usage: Set a number of key players to find and press "Go!". This will generate a random starting set of size k and then run the optimization algorithm. Alternatively, click nodes to select them, allowing you to view their fit with the KP metric. Then, click the list of selected nodes to run Key Player with your selected nodes as the starting set. Compare sets in the "Found KP-sets" list by hovering over the grey links.

The force-layout was adapted from Mike Bostock's force-directed graph example. As in that example, the data is 'based on character coappearence in Victor Hugo's Les Misérables, compiled by Donald Knuth.' It looks like this graph has unique local minima that the optimization algorithm finds every time. This is actually a little disappointing, at least for demonstration purposes: the algorithm starts with a random guess, and iteratively improves it. We'd like to be able to see and compare different subsets gotten from different starting guesses, but our Les Misérables characters are too well-behaved!

'keyplayer.js' contains the Key Player implementation. It relies on 'matrix.js', a extremely tiny, in-development JS library I wrote for this example, to allow some higher-level slicing and indexing than provided by Javascript arrays. In my informal, qualitative assessment, it was faster than math.js or Sylvester (my script did not finish executing when I used those libraries). But my library has pretty much NO features besides matrix creation, indexing, and slicing.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
stroke-opacity: .6;
}
.highlight {
fill: red;
}
.selected {
fill: green;
}
a {
color: grey;
}
</style>
<body>
<div id="menu">
Key Players:
<input type="number" id="kpk" style="width:50px" min="1" max="100"></input><button id="gokp">Go!</button><br>
Found KP-sets:
<ul id="kpsets"></ul>
</div>
<div id="selection">
</div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="matrix.js"></script>
<script src="keyplayer.js"></script>
<script>
var width = 960,
height = 500;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("miserables.json", function(error, graph) {
var keyPlayer = newKP()
.nodes(graph.nodes)
.links(graph.links);
keyPlayer.distanceMatrix(); // gotta do this before we start force layout
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 5)
.call(force.drag);
node.append("title")
.text(function(d) { return d.name; });
/* Selection feature */
var selectedNodesByNumber = {},
metric = keyPlayer.metric();
node.on("click", function(d, i) {
var that = d3.select(this),
selected = selectedNodesByNumber[i];
if (selected) {
that.classed("selected", false);
delete (selectedNodesByNumber[i]);
}
else {
that.classed("selected", true);
selectedNodesByNumber[i] = d;
}
var nodes = d3.keys(selectedNodesByNumber).map(function(x) { return parseInt(x); }), // appears sorted by string, which is good enough
names = d3.values(selectedNodesByNumber).map(function(d) { return d.name; }),
fit = metric(nodes);
d3.select("#selection")
.html("Selected: <a href='#'>" + JSON.stringify(names) + "</a> (" + fit + ")")
.select("a")
.datum(nodes)
.on("click", runKeyPlayer);
});
/* Force layout */
force.on("tick", function() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
var kpsets = d3.select("#kpsets");
d3.select("#gokp").on("click", function() {
var k = d3.select("#kpk").node().value;
keyPlayer.k(k);
runKeyPlayer();
});
function runKeyPlayer(s) {
var kset = keyPlayer(s);
highlightSet(kset.nodes);
kpsets.append("li")
.datum(kset.nodes)
.html("<a href='#'>" + JSON.stringify(kset.nodes.map(function(d) { return d.name })) + "</a> (" + kset.fit + ")")
.select("a")
.on("mouseover", highlightSet);
}
function highlightSet(kpset) {
graph.nodes.forEach(function(d) {
d.group = 0;
});
kpset.forEach(function(d) {
d.group = 1;
});
node.classed("highlight", function(d) { return d.group; });
/* remove selection from nodes we just highlighted... ugh */
node.each(function(d) {
if(d.group == 1)
d3.select(this).classed("selected", false);
});
}
});
</script>
/* Javascript implementation of Key Players algorithm.
* Requires matrix.js */
function newKP() {
var nodes = [],
links = [],
k = 10,
D = null,
tolerance = 0.001,
logging = true,
metric = null;
function keyPlayer(s) {
if (!D)
D = calculateDistances(nodes, links);
var n = D.size()[0];
if (!s)
s = getSample(k, n)
else {
k = s.length;
}
var fit = metric(s);
console.log("fit: " + fit + " " + s);
var i = 0;
while(true) { // TODO: add d3 tick functionality
i += 1;
Dfit = 0;
bests = [];
var t = D.rnot(s);
s.forEach(function(u, ui) {
t.forEach(function(v, vi) {
var s_ = s.slice(0);
// insert v, remove u
s_ = switchlist(s_, u, v); /* this is a terrible function */
if (s_.length != s.length) {
console.log(v)
console.log(u)
console.log("oh no! " + s_ + " " + s)
thunk();
}
var fit_ = metric(s_);
var d = fit_ - fit;
if ((d >= 0) && (d > Dfit)) {
Dfit = d
bests = s_
}
});
});
if (Dfit < tolerance)
break;
if (logging)
console.log("iteration: " + i + " : " + fit + " => " + (fit+Dfit) + " " + s);
s = bests;
fit = fit + Dfit;
}
return {"nodes": s.map(function(i) { return nodes[i]; }), "fit": fit};
}
/* TODO: make this not terrible. (Bin search) */
function switchlist(sl, x, y) {
for(var i = 0; i < sl.length; i++) {
if (sl[i] == x)
sl.splice(i, 1);
}
sl.push(y)
sl.sort(function(a, b) { return a-b });
return sl;
}
function calculateDistances(nodes, links) {
var n = nodes.length;
var m = matrix(n, n, Infinity);
links.forEach(function(d) {
m(d.source, d.target, d.value);
m(d.target, d.source, d.value);
});
for(var i = 0; i < n; i++) {
var tmp = matsquare(m);
if (allequal(tmp, m))
return m;
m = tmp;
}
return m;
}
function matsquare(m0) {
var m1 = m0.clone(),
n = m1.size()[0];
for (var i = 0; i < n; i++)
for(var j = 0; j < n; j++)
for(var k = 0; k < n; k++)
m1(i, j, Math.min(m1(i,j), m0(i,k) + m0(k, j)));
return m1;
}
function allequal(m0, m1) {
var a0 = m0.data(),
a1 = m1.data();
if (a0.length != a1.length)
return false;
for (var i = 0; i < a0.length; i++)
if (a0[i] != a1[i])
return false
return true;
}
// insert into sorted list. TODO: make this efficient (take advantage of sortedness)
function insertsorted(xl, x) {
//for (var i = 0; i < xl.length; i++)
// if (x > xl[i])
xl.push(x)
xl.sort(function(a, b) { return a-b });
}
function range(size) {
var arr = new Array(size);
for (var i = 0; i < size; i++)
arr[i] = i;
return arr;
}
// adapted from http://stackoverflow.com/questions/11935175/sampling-a-random-subset-from-an-array
// get sample of size k from range 1...size
function getSample(k, size) {
var copy = range(size), rand = [];
for (var i = 0; i < k && i < size; i++) {
var index = Math.floor(Math.random() * copy.length);
rand.push(copy.splice(index, 1)[0]);
}
return rand;
}
keyPlayer.distanceMatrix = function() {
if (!D) {
D = calculateDistances(nodes, links);
var n = nodes.length;
metric = function(s) {
var H = D.slice(D.rnot(s), s);
var Hd = H.min().data(), sm = 0;
for (var i = 0; i < Hd.length; i++) {
sm += 1/Hd[i];
}
return sm/n;
}
}
return D;
}
keyPlayer.nodes = function (_) {
if (!arguments.length)
return nodes;
else {
nodes = _;
return keyPlayer;
}
};
keyPlayer.links = function (_) {
if (!arguments.length)
return links;
else {
links = _;
return keyPlayer;
}
};
keyPlayer.k = function (_) {
if (!arguments.length)
return k;
else {
k = _;
return keyPlayer;
}
};
keyPlayer.tolerance = function (_) {
if (!arguments.length)
return tolerance;
else {
tolerance = _;
return keyPlayer;
}
};
keyPlayer.logging = function (_) {
if (!arguments.length)
return logging;
else {
logging = _;
return keyPlayer;
}
};
keyPlayer.metric = function (_) {
if (!arguments.length)
return metric;
else {
metric = _;
return keyPlayer;
}
};
return keyPlayer;
}
// create matrix of size n filled with v
var matrix = function (n, m, v) {
if (v instanceof Array)
var _data = v.slice(0);
else {
var _data = new Array(n*m);
for (var i = 0; i < n; i++)
for (var j = 0; j < m; j++)
_data[i*m+j] = v;
}
function value(i, j, v) {
if (v)
_data[i * m + j] = v;
else
return _data[i * m + j];
}
value.data = (function(_) {
if(_)
_data = _;
else
return _data;
})
value.clone = function() {
return matrix(n, m, _data);
}
value.size = function() {
return [n,m];
}
/* Return a new matrix with the minimum in each row */
value.min = function() {
var mm = new Array(n);
for (var i = 0; i < n; i++) {
var min = Infinity;
for (var j = 0; j < m; j++) {
if (_data[i*m+j] < min)
min = _data[i*m+j];
}
mm[i] = min;
}
return matrix(n, 1, mm);
}
/*
* Perhaps make this polymorphic.
* The following functions are intended to mimic R slicing.
* Goal: use a matrix like [1, 4, 6...] to index.
*/
value.slice = function(I, J) {
var ni = I.length;
var nj = J.length;
var data = new Array(ni * nj);
I.forEach(function(i, ii) {
J.forEach(function(j, jj) {
data[ii*nj+jj] = _data[i*m+j];
});
});
return matrix(ni, nj, data);
}
value.rnot = function(I) {
var I_ = new Array(n - I.length);
var i_ = 0, j = 0;
/* i is in the index in the logical row, i_ is the index in the new row mask.
* j is the index in the argument row mask. */
for (var i = 0; i < n; i++) {
if (I[j] == i) {
j++
} else {
I_[i_] = i;
i_++;
}
}
return I_;
}
return value;
}
{
"nodes":[
{"name":"Myriel","group":1},
{"name":"Napoleon","group":1},
{"name":"Mlle.Baptistine","group":1},
{"name":"Mme.Magloire","group":1},
{"name":"CountessdeLo","group":1},
{"name":"Geborand","group":1},
{"name":"Champtercier","group":1},
{"name":"Cravatte","group":1},
{"name":"Count","group":1},
{"name":"OldMan","group":1},
{"name":"Labarre","group":2},
{"name":"Valjean","group":2},
{"name":"Marguerite","group":3},
{"name":"Mme.deR","group":2},
{"name":"Isabeau","group":2},
{"name":"Gervais","group":2},
{"name":"Tholomyes","group":3},
{"name":"Listolier","group":3},
{"name":"Fameuil","group":3},
{"name":"Blacheville","group":3},
{"name":"Favourite","group":3},
{"name":"Dahlia","group":3},
{"name":"Zephine","group":3},
{"name":"Fantine","group":3},
{"name":"Mme.Thenardier","group":4},
{"name":"Thenardier","group":4},
{"name":"Cosette","group":5},
{"name":"Javert","group":4},
{"name":"Fauchelevent","group":0},
{"name":"Bamatabois","group":2},
{"name":"Perpetue","group":3},
{"name":"Simplice","group":2},
{"name":"Scaufflaire","group":2},
{"name":"Woman1","group":2},
{"name":"Judge","group":2},
{"name":"Champmathieu","group":2},
{"name":"Brevet","group":2},
{"name":"Chenildieu","group":2},
{"name":"Cochepaille","group":2},
{"name":"Pontmercy","group":4},
{"name":"Boulatruelle","group":6},
{"name":"Eponine","group":4},
{"name":"Anzelma","group":4},
{"name":"Woman2","group":5},
{"name":"MotherInnocent","group":0},
{"name":"Gribier","group":0},
{"name":"Jondrette","group":7},
{"name":"Mme.Burgon","group":7},
{"name":"Gavroche","group":8},
{"name":"Gillenormand","group":5},
{"name":"Magnon","group":5},
{"name":"Mlle.Gillenormand","group":5},
{"name":"Mme.Pontmercy","group":5},
{"name":"Mlle.Vaubois","group":5},
{"name":"Lt.Gillenormand","group":5},
{"name":"Marius","group":8},
{"name":"BaronessT","group":5},
{"name":"Mabeuf","group":8},
{"name":"Enjolras","group":8},
{"name":"Combeferre","group":8},
{"name":"Prouvaire","group":8},
{"name":"Feuilly","group":8},
{"name":"Courfeyrac","group":8},
{"name":"Bahorel","group":8},
{"name":"Bossuet","group":8},
{"name":"Joly","group":8},
{"name":"Grantaire","group":8},
{"name":"MotherPlutarch","group":9},
{"name":"Gueulemer","group":4},
{"name":"Babet","group":4},
{"name":"Claquesous","group":4},
{"name":"Montparnasse","group":4},
{"name":"Toussaint","group":5},
{"name":"Child1","group":10},
{"name":"Child2","group":10},
{"name":"Brujon","group":4},
{"name":"Mme.Hucheloup","group":8}
],
"links":[
{"source":1,"target":0,"value":1},
{"source":2,"target":0,"value":8},
{"source":3,"target":0,"value":10},
{"source":3,"target":2,"value":6},
{"source":4,"target":0,"value":1},
{"source":5,"target":0,"value":1},
{"source":6,"target":0,"value":1},
{"source":7,"target":0,"value":1},
{"source":8,"target":0,"value":2},
{"source":9,"target":0,"value":1},
{"source":11,"target":10,"value":1},
{"source":11,"target":3,"value":3},
{"source":11,"target":2,"value":3},
{"source":11,"target":0,"value":5},
{"source":12,"target":11,"value":1},
{"source":13,"target":11,"value":1},
{"source":14,"target":11,"value":1},
{"source":15,"target":11,"value":1},
{"source":17,"target":16,"value":4},
{"source":18,"target":16,"value":4},
{"source":18,"target":17,"value":4},
{"source":19,"target":16,"value":4},
{"source":19,"target":17,"value":4},
{"source":19,"target":18,"value":4},
{"source":20,"target":16,"value":3},
{"source":20,"target":17,"value":3},
{"source":20,"target":18,"value":3},
{"source":20,"target":19,"value":4},
{"source":21,"target":16,"value":3},
{"source":21,"target":17,"value":3},
{"source":21,"target":18,"value":3},
{"source":21,"target":19,"value":3},
{"source":21,"target":20,"value":5},
{"source":22,"target":16,"value":3},
{"source":22,"target":17,"value":3},
{"source":22,"target":18,"value":3},
{"source":22,"target":19,"value":3},
{"source":22,"target":20,"value":4},
{"source":22,"target":21,"value":4},
{"source":23,"target":16,"value":3},
{"source":23,"target":17,"value":3},
{"source":23,"target":18,"value":3},
{"source":23,"target":19,"value":3},
{"source":23,"target":20,"value":4},
{"source":23,"target":21,"value":4},
{"source":23,"target":22,"value":4},
{"source":23,"target":12,"value":2},
{"source":23,"target":11,"value":9},
{"source":24,"target":23,"value":2},
{"source":24,"target":11,"value":7},
{"source":25,"target":24,"value":13},
{"source":25,"target":23,"value":1},
{"source":25,"target":11,"value":12},
{"source":26,"target":24,"value":4},
{"source":26,"target":11,"value":31},
{"source":26,"target":16,"value":1},
{"source":26,"target":25,"value":1},
{"source":27,"target":11,"value":17},
{"source":27,"target":23,"value":5},
{"source":27,"target":25,"value":5},
{"source":27,"target":24,"value":1},
{"source":27,"target":26,"value":1},
{"source":28,"target":11,"value":8},
{"source":28,"target":27,"value":1},
{"source":29,"target":23,"value":1},
{"source":29,"target":27,"value":1},
{"source":29,"target":11,"value":2},
{"source":30,"target":23,"value":1},
{"source":31,"target":30,"value":2},
{"source":31,"target":11,"value":3},
{"source":31,"target":23,"value":2},
{"source":31,"target":27,"value":1},
{"source":32,"target":11,"value":1},
{"source":33,"target":11,"value":2},
{"source":33,"target":27,"value":1},
{"source":34,"target":11,"value":3},
{"source":34,"target":29,"value":2},
{"source":35,"target":11,"value":3},
{"source":35,"target":34,"value":3},
{"source":35,"target":29,"value":2},
{"source":36,"target":34,"value":2},
{"source":36,"target":35,"value":2},
{"source":36,"target":11,"value":2},
{"source":36,"target":29,"value":1},
{"source":37,"target":34,"value":2},
{"source":37,"target":35,"value":2},
{"source":37,"target":36,"value":2},
{"source":37,"target":11,"value":2},
{"source":37,"target":29,"value":1},
{"source":38,"target":34,"value":2},
{"source":38,"target":35,"value":2},
{"source":38,"target":36,"value":2},
{"source":38,"target":37,"value":2},
{"source":38,"target":11,"value":2},
{"source":38,"target":29,"value":1},
{"source":39,"target":25,"value":1},
{"source":40,"target":25,"value":1},
{"source":41,"target":24,"value":2},
{"source":41,"target":25,"value":3},
{"source":42,"target":41,"value":2},
{"source":42,"target":25,"value":2},
{"source":42,"target":24,"value":1},
{"source":43,"target":11,"value":3},
{"source":43,"target":26,"value":1},
{"source":43,"target":27,"value":1},
{"source":44,"target":28,"value":3},
{"source":44,"target":11,"value":1},
{"source":45,"target":28,"value":2},
{"source":47,"target":46,"value":1},
{"source":48,"target":47,"value":2},
{"source":48,"target":25,"value":1},
{"source":48,"target":27,"value":1},
{"source":48,"target":11,"value":1},
{"source":49,"target":26,"value":3},
{"source":49,"target":11,"value":2},
{"source":50,"target":49,"value":1},
{"source":50,"target":24,"value":1},
{"source":51,"target":49,"value":9},
{"source":51,"target":26,"value":2},
{"source":51,"target":11,"value":2},
{"source":52,"target":51,"value":1},
{"source":52,"target":39,"value":1},
{"source":53,"target":51,"value":1},
{"source":54,"target":51,"value":2},
{"source":54,"target":49,"value":1},
{"source":54,"target":26,"value":1},
{"source":55,"target":51,"value":6},
{"source":55,"target":49,"value":12},
{"source":55,"target":39,"value":1},
{"source":55,"target":54,"value":1},
{"source":55,"target":26,"value":21},
{"source":55,"target":11,"value":19},
{"source":55,"target":16,"value":1},
{"source":55,"target":25,"value":2},
{"source":55,"target":41,"value":5},
{"source":55,"target":48,"value":4},
{"source":56,"target":49,"value":1},
{"source":56,"target":55,"value":1},
{"source":57,"target":55,"value":1},
{"source":57,"target":41,"value":1},
{"source":57,"target":48,"value":1},
{"source":58,"target":55,"value":7},
{"source":58,"target":48,"value":7},
{"source":58,"target":27,"value":6},
{"source":58,"target":57,"value":1},
{"source":58,"target":11,"value":4},
{"source":59,"target":58,"value":15},
{"source":59,"target":55,"value":5},
{"source":59,"target":48,"value":6},
{"source":59,"target":57,"value":2},
{"source":60,"target":48,"value":1},
{"source":60,"target":58,"value":4},
{"source":60,"target":59,"value":2},
{"source":61,"target":48,"value":2},
{"source":61,"target":58,"value":6},
{"source":61,"target":60,"value":2},
{"source":61,"target":59,"value":5},
{"source":61,"target":57,"value":1},
{"source":61,"target":55,"value":1},
{"source":62,"target":55,"value":9},
{"source":62,"target":58,"value":17},
{"source":62,"target":59,"value":13},
{"source":62,"target":48,"value":7},
{"source":62,"target":57,"value":2},
{"source":62,"target":41,"value":1},
{"source":62,"target":61,"value":6},
{"source":62,"target":60,"value":3},
{"source":63,"target":59,"value":5},
{"source":63,"target":48,"value":5},
{"source":63,"target":62,"value":6},
{"source":63,"target":57,"value":2},
{"source":63,"target":58,"value":4},
{"source":63,"target":61,"value":3},
{"source":63,"target":60,"value":2},
{"source":63,"target":55,"value":1},
{"source":64,"target":55,"value":5},
{"source":64,"target":62,"value":12},
{"source":64,"target":48,"value":5},
{"source":64,"target":63,"value":4},
{"source":64,"target":58,"value":10},
{"source":64,"target":61,"value":6},
{"source":64,"target":60,"value":2},
{"source":64,"target":59,"value":9},
{"source":64,"target":57,"value":1},
{"source":64,"target":11,"value":1},
{"source":65,"target":63,"value":5},
{"source":65,"target":64,"value":7},
{"source":65,"target":48,"value":3},
{"source":65,"target":62,"value":5},
{"source":65,"target":58,"value":5},
{"source":65,"target":61,"value":5},
{"source":65,"target":60,"value":2},
{"source":65,"target":59,"value":5},
{"source":65,"target":57,"value":1},
{"source":65,"target":55,"value":2},
{"source":66,"target":64,"value":3},
{"source":66,"target":58,"value":3},
{"source":66,"target":59,"value":1},
{"source":66,"target":62,"value":2},
{"source":66,"target":65,"value":2},
{"source":66,"target":48,"value":1},
{"source":66,"target":63,"value":1},
{"source":66,"target":61,"value":1},
{"source":66,"target":60,"value":1},
{"source":67,"target":57,"value":3},
{"source":68,"target":25,"value":5},
{"source":68,"target":11,"value":1},
{"source":68,"target":24,"value":1},
{"source":68,"target":27,"value":1},
{"source":68,"target":48,"value":1},
{"source":68,"target":41,"value":1},
{"source":69,"target":25,"value":6},
{"source":69,"target":68,"value":6},
{"source":69,"target":11,"value":1},
{"source":69,"target":24,"value":1},
{"source":69,"target":27,"value":2},
{"source":69,"target":48,"value":1},
{"source":69,"target":41,"value":1},
{"source":70,"target":25,"value":4},
{"source":70,"target":69,"value":4},
{"source":70,"target":68,"value":4},
{"source":70,"target":11,"value":1},
{"source":70,"target":24,"value":1},
{"source":70,"target":27,"value":1},
{"source":70,"target":41,"value":1},
{"source":70,"target":58,"value":1},
{"source":71,"target":27,"value":1},
{"source":71,"target":69,"value":2},
{"source":71,"target":68,"value":2},
{"source":71,"target":70,"value":2},
{"source":71,"target":11,"value":1},
{"source":71,"target":48,"value":1},
{"source":71,"target":41,"value":1},
{"source":71,"target":25,"value":1},
{"source":72,"target":26,"value":2},
{"source":72,"target":27,"value":1},
{"source":72,"target":11,"value":1},
{"source":73,"target":48,"value":2},
{"source":74,"target":48,"value":2},
{"source":74,"target":73,"value":3},
{"source":75,"target":69,"value":3},
{"source":75,"target":68,"value":3},
{"source":75,"target":25,"value":3},
{"source":75,"target":48,"value":1},
{"source":75,"target":41,"value":1},
{"source":75,"target":70,"value":1},
{"source":75,"target":71,"value":1},
{"source":76,"target":64,"value":1},
{"source":76,"target":65,"value":1},
{"source":76,"target":66,"value":1},
{"source":76,"target":63,"value":1},
{"source":76,"target":62,"value":1},
{"source":76,"target":48,"value":1},
{"source":76,"target":58,"value":1}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment