Skip to content

Instantly share code, notes, and snippets.

@rpgove
Last active January 16, 2020 10:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save rpgove/8c8b08cc0ae1e1e969f5d2904a6a0e26 to your computer and use it in GitHub Desktop.
Save rpgove/8c8b08cc0ae1e1e969f5d2904a6a0e26 to your computer and use it in GitHub Desktop.
Force Directed Layout Quality Convergence
license: gpl-3.0
height: 600
scrolling: no
border: yes

Graph readability metrics can be used to check the convergence rate of graph layout algorithms. This example uses Greadability.js to calculate four graph layout readability metrics at each iteration of the D3's force-directed graph layout algorithm:

  • Edge crossings measures the fraction of edges that cross (intersect) out of an approximate maximum number that can cross.
  • Edge crossing angle measures the mean deviation of edge crossing angles from the ideal edge crossing angle (70 degrees).
  • Angular resolution (minimum) measures the mean deviation of adjacent incident edge angles from the ideal minimum angles (360 degrees divided by the degree of that node).
  • Angular resoluction (deviation) measures the average deviation of angles between incident edges on each vertex.
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(factory((global.greadability = global.greadability || {})));
}(this, (function (exports) { 'use strict';
var greadability = function (nodes, links, id) {
var i,
j,
n = nodes.length,
m,
degree = new Array(nodes.length),
cMax,
idealAngle = 70,
dMax;
/*
* Tracks the global graph readability metrics.
*/
var graphStats = {
crossing: 0, // Normalized link crossings
crossingAngle: 0, // Normalized average dev from 70 deg
angularResolutionMin: 0, // Normalized avg dev from ideal min angle
angularResolutionDev: 0, // Normalized avg dev from each link
};
var getSumOfArray = function (numArray) {
var i = 0, n = numArray.length, sum = 0;
for (; i < n; ++i) sum += numArray[i];
return sum;
};
var initialize = function () {
var i, j, link;
var nodeById = {};
// Filter out self loops
links = links.filter(function (l) {
return l.source !== l.target;
});
m = links.length;
if (!id) {
id = function (d) { return d.index; };
}
for (i = 0; i < n; ++i) {
nodes[i].index = i;
degree[i] = [];
nodeById[id(nodes[i], i, nodeById)] = nodes[i];
}
// Make sure source and target are nodes and not indices.
for (i = 0; i < m; ++i) {
link = links[i];
if (typeof link.source !== "object") link.source = nodeById[link.source];
if (typeof link.target !== "object") link.target = nodeById[link.target];
}
// Filter out duplicate links
var filteredLinks = [];
links.forEach(function (l) {
var s = l.source, t = l.target;
if (s.index > t.index) {
filteredLinks.push({source: t, target: s});
} else {
filteredLinks.push({source: s, target: t});
}
});
links = filteredLinks;
links.sort(function (a, b) {
if (a.source.index < b.source.index) return -1;
if (a.source.index > b.source.index) return 1;
if (a.target.index < b.target.index) return -1;
if (a.target.index > b.target.index) return 1;
return 0;
});
i = 1;
while (i < links.length) {
if (links[i-1].source.index === links[i].source.index &&
links[i-1].target.index === links[i].target.index) {
links.splice(i, 1);
}
else ++i;
}
// Update length, if a duplicate was deleted.
m = links.length;
// Calculate degree.
for (i = 0; i < m; ++i) {
link = links[i];
link.index = i;
degree[link.source.index].push(link);
degree[link.target.index].push(link);
};
}
// Assume node.x and node.y are the coordinates
function direction (pi, pj, pk) {
var p1 = [pk[0] - pi[0], pk[1] - pi[1]];
var p2 = [pj[0] - pi[0], pj[1] - pi[1]];
return p1[0] * p2[1] - p2[0] * p1[1];
}
// Is point k on the line segment formed by points i and j?
// Inclusive, so if pk == pi or pk == pj then return true.
function onSegment (pi, pj, pk) {
return Math.min(pi[0], pj[0]) <= pk[0] &&
pk[0] <= Math.max(pi[0], pj[0]) &&
Math.min(pi[1], pj[1]) <= pk[1] &&
pk[1] <= Math.max(pi[1], pj[1]);
}
function linesCross (line1, line2) {
var d1, d2, d3, d4;
// CLRS 2nd ed. pg. 937
d1 = direction(line2[0], line2[1], line1[0]);
d2 = direction(line2[0], line2[1], line1[1]);
d3 = direction(line1[0], line1[1], line2[0]);
d4 = direction(line1[0], line1[1], line2[1]);
if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
return true;
} else if (d1 === 0 && onSegment(line2[0], line2[1], line1[0])) {
return true;
} else if (d2 === 0 && onSegment(line2[0], line2[1], line1[1])) {
return true;
} else if (d3 === 0 && onSegment(line1[0], line1[1], line2[0])) {
return true;
} else if (d4 === 0 && onSegment(line1[0], line1[1], line2[1])) {
return true;
}
return false;
}
function linksCross (link1, link2) {
// Self loops are not intersections
if (link1.index === link2.index ||
link1.source === link1.target ||
link2.source === link2.target) {
return false;
}
// Links cannot intersect if they share a node
if (link1.source === link2.source ||
link1.source === link2.target ||
link1.target === link2.source ||
link1.target === link2.target) {
return false;
}
var line1 = [
[link1.source.x, link1.source.y],
[link1.target.x, link1.target.y]
];
var line2 = [
[link2.source.x, link2.source.y],
[link2.target.x, link2.target.y]
];
return linesCross(line1, line2);
}
function linkCrossings () {
var i, j, c = 0, d = 0, link1, link2, line1, line2;;
// Sum the upper diagonal of the edge crossing matrix.
for (i = 0; i < m; ++i) {
for (j = i + 1; j < m; ++j) {
link1 = links[i], link2 = links[j];
// Check if link i and link j intersect
if (linksCross(link1, link2)) {
line1 = [
[link1.source.x, link1.source.y],
[link1.target.x, link1.target.y]
];
line2 = [
[link2.source.x, link2.source.y],
[link2.target.x, link2.target.y]
];
++c;
d += Math.abs(idealAngle - acuteLinesAngle(line1, line2));
}
}
}
return {c: 2*c, d: 2*d};
}
function linesegmentsAngle (line1, line2) {
// Finds the (counterclockwise) angle from line segement line1 to
// line segment line2. Assumes the lines share one end point.
// If both endpoints are the same, or if both lines have zero
// length, then return 0 angle.
// Param order matters:
// linesegmentsAngle(line1, line2) != linesegmentsAngle(line2, line1)
var temp, len, angle1, angle2, sLine1, sLine2;
// Re-orient so that line1[0] and line2[0] are the same.
if (line1[0][0] === line2[1][0] && line1[0][1] === line2[1][1]) {
temp = line2[1];
line2[1] = line2[0];
line2[0] = temp;
} else if (line1[1][0] === line2[0][0] && line1[1][1] === line2[0][1]) {
temp = line1[1];
line1[1] = line1[0];
line1[0] = temp;
} else if (line1[1][0] === line2[1][0] && line1[1][1] === line2[1][1]) {
temp = line1[1];
line1[1] = line1[0];
line1[0] = temp;
temp = line2[1];
line2[1] = line2[0];
line2[0] = temp;
}
// Shift the line so that the first point is at (0,0).
sLine1 = [
[line1[0][0] - line1[0][0], line1[0][1] - line1[0][1]],
[line1[1][0] - line1[0][0], line1[1][1] - line1[0][1]]
];
// Normalize the line length.
len = Math.hypot(sLine1[1][0], sLine1[1][1]);
if (len === 0) return 0;
sLine1[1][0] /= len;
sLine1[1][1] /= len;
// If y < 0, angle = acos(x), otherwise angle = 360 - acos(x)
angle1 = Math.acos(sLine1[1][0]) * 180 / Math.PI;
if (sLine1[1][1] < 0) angle1 = 360 - angle1;
// Shift the line so that the first point is at (0,0).
sLine2 = [
[line2[0][0] - line2[0][0], line2[0][1] - line2[0][1]],
[line2[1][0] - line2[0][0], line2[1][1] - line2[0][1]]
];
// Normalize the line length.
len = Math.hypot(sLine2[1][0], sLine2[1][1]);
if (len === 0) return 0;
sLine2[1][0] /= len;
sLine2[1][1] /= len;
// If y < 0, angle = acos(x), otherwise angle = 360 - acos(x)
angle2 = Math.acos(sLine2[1][0]) * 180 / Math.PI;
if (sLine2[1][1] < 0) angle2 = 360 - angle2;
return angle1 <= angle2 ? angle2 - angle1 : 360 - (angle1 - angle2);
}
function acuteLinesAngle (line1, line2) {
// Acute angle of intersection, in degrees. Assumes these lines
// intersect.
var slope1 = (line1[1][1] - line1[0][1]) / (line1[1][0] - line1[0][0]);
var slope2 = (line2[1][1] - line2[0][1]) / (line2[1][0] - line2[0][0]);
// If these lines are two links incident on the same node, need
// to check if the angle is 0 or 180.
if (slope1 === slope2) {
// If line2 is not on line1 and line1 is not on line2, then
// the lines share only one point and the angle must be 180.
if (!(onSegment(line1[0], line1[1], line2[0]) && onSegment(line1[0], line1[1], line2[1])) ||
!(onSegment(line2[0], line2[1], line1[0]) && onSegment(line2[0], line2[1], line1[1])))
return 180;
else return 0;
}
var angle = Math.abs(Math.atan(slope1) - Math.atan(slope2));
return (angle > Math.PI / 2 ? Math.PI - angle : angle) * 180 / Math.PI;
}
function angularRes () {
var j,
resMin = 0,
resDev = 0,
nonZeroDeg,
node,
minAngle,
idealMinAngle,
incident,
line0,
line1,
line2,
incidentLinkAngles,
nextLink;
nonZeroDeg = degree.filter(function (d) { return d.length >= 1; }).length;
for (j = 0; j < n; ++j) {
node = nodes[j];
line0 = [[node.x, node.y], [node.x+1, node.y]];
// Links that are incident to this node (already filtered out self loops)
incident = degree[j];
if (incident.length <= 1) continue;
idealMinAngle = 360 / incident.length;
// Sort edges by the angle they make from an imaginary vector
// emerging at angle 0 on the unit circle.
// Necessary for calculating angles of incident edges correctly
incident.sort(function (a, b) {
line1 = [
[a.source.x, a.source.y],
[a.target.x, a.target.y]
];
line2 = [
[b.source.x, b.source.y],
[b.target.x, b.target.y]
];
var angleA = linesegmentsAngle(line0, line1);
var angleB = linesegmentsAngle(line0, line2);
return angleA < angleB ? -1 : angleA > angleB ? 1 : 0;
});
incidentLinkAngles = incident.map(function (l, i) {
nextLink = incident[(i + 1) % incident.length];
line1 = [
[l.source.x, l.source.y],
[l.target.x, l.target.y]
];
line2 = [
[nextLink.source.x, nextLink.source.y],
[nextLink.target.x, nextLink.target.y]
];
return linesegmentsAngle(line1, line2);
});
minAngle = Math.min.apply(null, incidentLinkAngles);
resMin += Math.abs(idealMinAngle - minAngle) / idealMinAngle;
resDev += getSumOfArray(incidentLinkAngles.map(function (angle) {
return Math.abs(idealMinAngle - angle) / idealMinAngle;
})) / (2 * incident.length - 2);
}
// Divide by number of nodes with degree != 0
resMin = resMin / nonZeroDeg;
// Divide by number of nodes with degree != 0
resDev = resDev / nonZeroDeg;
return {resMin: resMin, resDev: resDev};
}
initialize();
cMax = (m * (m - 1) / 2) - getSumOfArray(degree.map(function (d) { return d.length * (d.length - 1); })) / 2;
var crossInfo = linkCrossings();
dMax = crossInfo.c * idealAngle;
graphStats.crossing = 1 - (cMax > 0 ? crossInfo.c / cMax : 0);
graphStats.crossingAngle = 1 - (dMax > 0 ? crossInfo.d / dMax : 0);
var angularResInfo = angularRes();
graphStats.angularResolutionMin = 1 - angularResInfo.resMin;
graphStats.angularResolutionDev = 1 - angularResInfo.resDev;
return graphStats;
};
exports.greadability = greadability;
Object.defineProperty(exports, '__esModule', { value: true });
})));
<!DOCTYPE html>
<meta charset="utf-8">
<style>
html, body {
width: 960px;
height: 600px;
display: flex;
align-items: center;
justify-content: center;
}
svg {
overflow: visible;
}
.line-g path {
stroke: #d30000;
}
.links line {
stroke: #999;
stroke-opacity: 0.6;
stroke-width: 2px;
}
.nodes circle {
fill: #d30000;
stroke: #fff;
stroke-width: 1px;
}
</style>
<svg class="graph"></svg>
<svg class="convergence"></svg>
<script src="greadability.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var width = 600;
var height = 600;
var convWidth = 360;
var convHeight = 300;
var margin = {left: 40, right: 10, top: 30, bottom: 20};
var metrics = [
{name: 'Edge crossings', varName: 'crossing', data: []},
{name: 'Crossing angle', varName: 'crossingAngle', data: []},
{name: 'Angular resolution (min)', varName: 'angularResolutionMin', data: []},
{name: 'Angular resolution (dev)', varName: 'angularResolutionDev', data: []}
];
var drag = d3.drag()
.on('start', dragStart)
.on('drag', dragging)
.on('end', dragEnd);
var svg = d3.select('svg.graph')
.attr('width', width)
.attr('height', height);
var convSvg = d3.select('svg.convergence')
.attr('width', convWidth)
.attr('height', convHeight)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var x = d3.scaleLinear()
.domain([0, 300])
.range([0, (convWidth = convWidth - margin.left - margin.right)]);
var y = d3.scaleLinear()
.domain([0, 1])
.range([(convHeight = convHeight - margin.top - margin.bottom), 0]);
var line = d3.line()
.x(function (d) { return x(d[0]); })
.y(function (d) { return y(d[1]); });
var forceSim = d3.forceSimulation()
.force('link', d3.forceLink())
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width/2, height/2));
d3.json('miserables.json', function (error, graph) {
if (error) throw error;
var link = svg.append('g')
.attr('class', 'links')
.selectAll('line')
.data(graph.links)
.enter().append('line');
var node = svg.append('g')
.attr('class', 'nodes')
.selectAll('circle')
.data(graph.nodes)
.enter().append('circle')
.attr('r', 4)
.call(drag);
node.append('title').text(function (d) { return d.name; });
forceSim.nodes(graph.nodes)
.on('tick', tick)
.stop();
forceSim.force('link')
.links(graph.links);
var graphReadability = greadability.greadability(graph.nodes, graph.links);
metrics.forEach(function (m) {
var iterNum = 0;
m.data.push([iterNum, graphReadability[m.varName]]);
});
forceSim.restart();
function tick () {
link
.attr('x1', function (d) { return d.source.x; })
.attr('x2', function (d) { return d.target.x; })
.attr('y1', function (d) { return d.source.y; })
.attr('y2', function (d) { return d.target.y; });
node
.attr('cx', function (d) { return d.x; })
.attr('cy', function (d) { return d.y; });
var graphReadability = greadability.greadability(graph.nodes, graph.links);
metrics.forEach(function (m) {
var iterNum = m.data[m.data.length - 1][0] + 1;
m.data.push([iterNum, graphReadability[m.varName]]);
if (m.data.length > 301) {
m.data = m.data.slice(metrics.length - 301);
}
});
x.domain([metrics[0].data[0][0], metrics[0].data[0][0] + 300]);
convSvg.selectAll('*').remove();
convSvg.append('g')
.attr('transform', 'translate(0,' + convHeight + ')')
.call(d3.axisBottom(x).ticks(7))
.append("text")
.attr("fill", "#000")
.attr('transform', 'translate(' + convWidth + ',' + 0 + ')')
.attr("y", -10)
.attr("dy", "0.71em")
.attr("text-anchor", "end")
.text("Number of iterations");
convSvg.append('g')
.call(d3.axisLeft(y))
.append("text")
.attr("fill", "#000")
.attr("transform", "rotate(-90)")
.attr("y", -39)
.attr("dy", "0.71em")
.attr("text-anchor", "end")
.text("Readability score");
var lineG = convSvg.selectAll('g.line-g')
.data(metrics)
.enter().append('g')
.attr('class', function (d) { return 'line-g ' + d.varName; });
lineG.append('path')
.attr("fill", "none")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 1.5)
.attr("d", function (d) { return line(d.data); });
lineG.append("text")
.datum(function(d) { return {name: d.name, value: d.data[d.data.length - 1]}; })
.attr("transform", function(d) { return "translate(" + x(d.value[0]) + "," + y(d.value[1]) + ")"; })
.attr("x", 3)
.attr('y', -6)
.attr("dy", "0.35em")
.attr('text-anchor', 'end')
.style("font", "10px sans-serif")
.text(function(d) { return d.name; });
}
});
function dragStart (d) {
if (!d3.event.active) forceSim.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragging (d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragEnd (d) {
if (!d3.event.active) forceSim.alphaTarget(0);
d.fx = null;
d.fy = null;
}
</script>
{
"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