Skip to content

Instantly share code, notes, and snippets.

@tomshanley
Last active July 1, 2018 20:24
Show Gist options
  • Save tomshanley/52229cba0fc4fd82279fd3839b3c131c to your computer and use it in GitHub Desktop.
Save tomshanley/52229cba0fc4fd82279fd3839b3c131c to your computer and use it in GitHub Desktop.
Recreating the NY Times immigration flows chart
license: mit
height: 1500
border: no
scrolling: no

Trying to recreate a reusable version of this type, without the D3 chord diagram business

https://www.nytimes.com/interactive/2018/06/20/business/economy/immigration-economic-impact.html

The labels are harder to programatically position, so I've opted for coordinates positioned on the link itself, inset from from end.

The chordNetwork function could be called with any set of nodes and links if you wish.

I've added greadability.js to review the global graph readability metrics, which for this graph is:

  • crossing: 0.33333333333333337
  • crossingAngle: 0.8
  • angularResolutionMin: 0.4166666666666664
  • angularResolutionDev: 0.41666666666666663

Built with blockbuilder.org

forked from tomshanley's block: Recreating the NY Times immigration flows chart

function appendArrow(d, i) {
let arrowHeadSize = strokeWidth(d.value) * 3;
let thisPath = d3.select(this).node();
let pathLength = thisPath.getTotalLength();
let point = thisPath.getPointAtLength(pathLength);
let previousPoint = thisPath.getPointAtLength(pathLength - 1);
let rotation = 0;
if (point.y == previousPoint.y) {
rotation = point.x < previousPoint.x ? 180 : 0;
} else if (point.x == previousPoint.x) {
rotation = point.y < previousPoint.y ? -90 : 90;
} else {
let adj = Math.abs(point.x - previousPoint.x);
let opp = Math.abs(point.y - previousPoint.y);
let angle = Math.atan(opp / adj) * (180 / Math.PI);
if (point.x < previousPoint.x) {
angle = angle + (90 - angle) * 2;
}
if (point.y < previousPoint.y) {
rotation = -angle;
} else {
rotation = angle;
}
}
let dString =
"M" +
point.x +
"," +
(point.y - arrowHeadSize / 2) +
" L" +
(point.x + arrowHeadSize / 2) +
"," +
point.y +
" L" +
point.x +
"," +
(point.y + arrowHeadSize / 2);
let rotationString =
"rotate(" + rotation + "," + point.x + "," + point.y + ")";
g.append("path")
.attr("d", dString)
.attr("class", "arrow-head")
.attr("transform", rotationString)
.style("stroke", colour(d.source))
.style("fill", colour(d.source));
}
function createChordData(_nodes, _links, _width, _height) {
let graph = { nodes: [], links: [] };
let n = _nodes.length;
let maxI = n - 1;
const rotate = 0.5
const chartRadius = (Math.min(_width, _height) / 2) - 20
const pointRadius = 20;
const selfLinkRadius = 55;
const selfLinkOffset = 30;
const clockwiseLinksOffset = 40;
const antiClockwiseLinksOffset = 20;
const angleDegrees = 360 / n;
const angleRadians = angleDegrees * (Math.PI / 180);
const nodeAngleDegrees = 25;
const nodeAngleRadians = nodeAngleDegrees * (Math.PI / 180);
const cLinkGap = nodeAngleDegrees / ((n - 3) * 2 - 1);
_nodes.forEach(function(d, i) {
let node = {};
node.id = d.id;
node.index = i
node.position = i + rotate
node.coord = d3.pointRadial(angleRadians * node.position, chartRadius);
node.labelCoord = d3.pointRadial(angleRadians * node.position, chartRadius + selfLinkOffset + selfLinkRadius );
node.x = node.coord[0];
node.y = node.coord[1];
node.labelX = node.labelCoord[0];
node.labelY = node.labelCoord[1];
graph.nodes.push(node);
});
_links.forEach(function(d) {
let link = {};
link.source = d.source;
link.sourceIndex = linkIndex(link.source)
link.sourcePosition = linkPosition(link.source)
link.target = d.target;
link.targetIndex = linkIndex(link.target)
link.targetPosition = linkPosition(link.target)
link.value = d.value;
if (isCentreLink(link.sourceIndex, link.targetIndex)) {
let aS = 0;
let sourceOffset = 0;
let tS = 0;
let targetOffset = 0;
if (link.targetPosition < link.sourcePosition) {
let i = (link.sourcePosition - link.targetPosition - 2) * 2;
let start = angleDegrees * link.sourcePosition - nodeAngleDegrees / 2;
sourceOffset = start + i * cLinkGap;
let end = angleDegrees * link.targetPosition + nodeAngleDegrees / 2;
targetOffset = end - i * cLinkGap;
link.inner = Math.abs(link.sourcePosition - link.targetPosition) > n / 2 ? false : true;
} else {
let i = (link.targetPosition - link.sourcePosition - 2) * 2 + 1;
let start = angleDegrees * link.sourcePosition + nodeAngleDegrees / 2;
sourceOffset = start - i * cLinkGap;
let end = angleDegrees * link.targetPosition - nodeAngleDegrees / 2;
targetOffset = end + i * cLinkGap;
link.inner = Math.abs(link.sourcePosition - link.targetPosition) > n / 2 ? true : false;
}
sourceOffset = sourceOffset < 0 ? sourceOffset + 360 : sourceOffset;
aS = sourceOffset * (Math.PI / 180);
link.sourceCoord = d3.pointRadial(aS, chartRadius);
targetOffset = targetOffset < 0 ? targetOffset + 360 : targetOffset;
tS = targetOffset * (Math.PI / 180);
link.targetCoord = d3.pointRadial(tS, chartRadius);
link.sourceX = link.sourceCoord[0];
link.sourceY = link.sourceCoord[1];
link.targetX = link.targetCoord[0];
link.targetY = link.targetCoord[1];
}
graph.links.push(link);
});
createPathData(graph.links);
return graph;
function createPathData(links) {
links.forEach(function(link) {
let x1 = 0
let y1 = 0
let x2 = 0
let y2 = 0
let path = ""
//self links
if (link.sourcePosition == link.targetPosition) {
link.type = "selfLink"
let i = link.sourcePosition + n / 2;
let offset = 1;
let centre = d3.pointRadial(
angleRadians * link.sourcePosition,
chartRadius + selfLinkRadius + selfLinkOffset
);
let start = d3.pointRadial(angleRadians * i - offset, selfLinkRadius);
let end = d3.pointRadial(angleRadians * i + offset, selfLinkRadius);
x1 = centre[0] + start[0];
y1 = centre[1] + start[1];
x2 = centre[0] + end[0];
y2 = centre[1] + end[1];
path =
"M " +
x1 +
" " +
y1 +
" A " +
selfLinkRadius +
" " +
selfLinkRadius +
" 0 1 0 " +
x2 +
" " +
y2;
}
//anti-clockwise outer links
else if (
link.sourceIndex - link.targetIndex === 1 ||
(link.sourceIndex === 0 && link.targetIndex === maxI)
) {
link.type = "ac-outer"
let r = chartRadius + antiClockwiseLinksOffset;
let offset = nodeAngleRadians / 2 + 0.05;
let start = d3.pointRadial(angleRadians * link.sourcePosition - offset, r);
let end = d3.pointRadial(angleRadians * link.targetPosition + offset, r);
x1 = start[0];
x2 = end[0];
y1 = start[1];
y2 = end[1];
let sweep =
link.targetPosition * angleDegrees < link.sourcePosition * angleDegrees ? "0" : "1";
sweep = link.targetIndex === maxI && link.sourceIndex == 0 ? "0" : sweep;
path =
"M" +
x1 +
"," +
y1 +
" " +
"A" +
chartRadius +
" " +
chartRadius +
" 0 0 " +
sweep +
" " +
x2 +
" " +
y2;
}
//clockwiseLinks
else if (
link.targetIndex - link.sourceIndex === 1 ||
(link.sourceIndex === maxI && link.targetIndex === 0)
) {
link.type = "c-outer"
let r = chartRadius + clockwiseLinksOffset;
let offset = nodeAngleRadians / 2 + 0.05;
let start = d3.pointRadial(angleRadians * link.sourcePosition + offset, r);
let end = d3.pointRadial(angleRadians * link.targetPosition - offset, r);
x1 = start[0];
x2 = end[0];
y1 = start[1];
y2 = end[1];
let sweep =
link.targetPosition * angleDegrees < link.sourcePosition * angleDegrees ? "0" : "1";
sweep = link.targetIndex === 0 && link.sourceIndex == maxI ? "1" : sweep;
path =
"M" +
x1 +
"," +
y1 +
" " +
"A" +
chartRadius +
" " +
chartRadius +
" 0 0 " +
sweep +
" " +
x2 +
" " +
y2;
} else {
x1 = link.sourceX;
y1 = link.sourceY;
x2 = link.targetX;
y2 = link.targetY;
if (isOpposite(n, link.sourcePosition, link.targetPosition)) {
link.type = "inner-opposite"
path = "M" + x1 + " " + y1 + " L" + x2 + " " + y2;
} else {
link.type = "inner-curve"
let distance = Math.abs(link.sourcePosition - link.targetPosition);
let mid = 0;
if (distance < n / 2) {
mid = Math.min(link.sourcePosition, link.targetPosition) + distance / 2;
} else {
distance =
n -
Math.max(link.sourcePosition, link.targetPosition) +
Math.min(link.sourcePosition, link.targetPosition);
mid = Math.max(link.sourcePosition, link.targetPosition) + distance / 2;
mid = mid == n ? 0 : mid;
}
let midAngleRadians = (mid * angleDegrees) * (Math.PI / 180);
let opp = Math.abs(x1 - x2);
let adj = Math.abs(y1 - y2);
let hyp = triangleHypotenuse(opp, adj);
let ratio = 1 - hyp / (chartRadius * 2);
let r = ratio * chartRadius;
r = link.inner ? r : r - (cLinkGap + 0);
let cCoords = d3.pointRadial(midAngleRadians, r);
let c = cCoords[0] + " " + cCoords[1];
path = "M" + x1 + " " + y1 + " Q " + c + " " + x2 + " " + y2;
}
}
link.x1 = x1;
link.x2 = x2;
link.y1 = y1;
link.y2 = y2;
link.path = path;
});
}
function isOpposite(n, source, target) {
if (n % 2 !== 0) {
return false;
} else {
return Math.abs(source - target) == n / 2 ? true : false;
}
}
function linkPosition(id) {
let p = 0
graph.nodes.forEach(function(node){
if (id == node.id){
p = node.position
}
})
return p
}
function linkIndex(id) {
let index = 0
graph.nodes.forEach(function(node){
if (id == node.id){
index = node.index
}
})
return index
}
function isCentreLink(source, target) {
if (Math.abs(source - target) == 1) {
return false;
} else if (source == maxI && target == 0) {
return false;
} else if (target == maxI && source == 0) {
return false;
} else if (target == source) {
return false;
} else {
return true;
}
}
}
(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>
<head>
<meta charset="utf-8">
<script src="links.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="greadability.js"></script>
<script src="chordNetwork.js"></script>
<script src="appendArrow.js"></script>
<script src="triangleEquations.js"></script>
<style>
body {
font-family: sans-serif
}
text {
text-anchor: middle
}
path {
fill: none;
stroke-linecap: square;
stroke-opacity: 1
}
</style>
</head>
<body>
<div class="header">
<p>A D3.js re-creation of the New York Times' chart from the June 20 2018 article <a href="https://www.nytimes.com/interactive/2018/06/20/business/economy/immigration-economic-impact.html https://www.nytimes.com/interactive/2018/06/20/business/economy/immigration-economic-impact.html">Migrants Are on the Rise Around the World, and Myths About Them Are Shaping Attitudes"</a>.</p>
<h1>Migration in 2017 in millions</h1>
</div>
<div id="chart"></div>
<div class="footer">
<p>Data source: United Nations Department of Economic and Social Affairs, Population Division, <a href="http://www.un.org/en/development/desa/population/migration/publications/migrationreport/docs/MigrationReport2017.pdf">Migration Report 2017</a>.</p>
</div>
<script>
console.clear();
let nodes = [
{"id": "Europe"},
{"id": "Asia"},
{"id": "Oceania"},
{"id": "Africa"},
{"id": "Latin America"},
{"id": "North America"},
];
let links = migration2017
//Generate dummy data
//let n = 6;
//var s = 0;
//var t = 0;
/*nodes = d3.range(n).map(function(d) {
return { id: d };
});
for (s = 0; s < n; s++) {
target = 0;
for (t = 0; t < n; t++) {
let link = {};
link.source = s;
link.target = t;
link.value = s == t ? Math.random() * 100 : Math.random() * 20;
links.push(link);
}
}*/
let colour = d3
.scaleOrdinal()
.domain(nodes.map(d => d.id)) //.range(['#1b9e77','#d95f02','#7570b3','#e7298a','#66a61e','#e6ab02'])
.range(["#eb978f"]);
const width = 600;
const height = width;
const m = 150;
const margin = { top: m, bottom: m, left: m, right: m };
let strokeWidth = d3.scaleLinear()
.domain(d3.extent(links, d => d.value))
.range([1, 20]);
let chartData = createChordData(nodes, links, width, height);
let svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
let chart = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
let g = chart
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
let paths = g
.selectAll(".link")
.data(chartData.links)
.enter()
.append("g")
path = paths.append("path")
.attr("d", function(d) {
return d.path;
})
.style("stroke-width", d => strokeWidth(d.value))
.style("stroke", d => colour(d.source))
path.each(appendArrow)
path.each(appendLinkLabel)
console.log(greadability.greadability(chartData.nodes, chartData.links, function(d){ return d.id }));
let nodeLabels = g
.selectAll(".overlay")
.data(chartData.nodes)
.enter()
.append("g")
.append("text")
.text(d => d.id)
.style("fill", "black")
.attr("x", d => d.labelX)
.attr("y", d => (d.labelY + 6));
function roundValue(n) {
n = Math.round(n * 10)/10
return n
}
function appendLinkLabel(d) {
let thisPath = d3.select(this).node();
let pathLength = thisPath.getTotalLength();
let point = thisPath.getPointAtLength(pathLength - 20);
let parentG = d3.select(this.parentNode)
let label = parentG.append("g")
.attr("transform", "translate(" + point.x + "," + point.y + ")")
label.append("text")
.style("fill", "white")
.style("stroke", "white")
.style("stroke-width", "5px")
label.append("text")
.style("fill", colour(d.source))
.style("stroke", "none")
let text = d.value == 0 ? "" : roundValue(d.value)
label.selectAll("text")
.text(text)
.attr("dy", "0.35em")
}
</script>
</body>
//returns the length of the opposite side to the angle, using the adjacent side's length
function oppositeTan(angle, adjacent) {
return Math.tan(angle) * adjacent;
};
//returns the length of the adjacent side to the angle, using the hypotenuse's length
function adjacentCos(angle, hypotenuse) {
return Math.cos(angle) * hypotenuse;
}
//returns the length of the opposite side to the angle, using the adjacent's length
function oppositeTan(angle, adjacent) {
return Math.tan(angle) * adjacent;
}
//returns the length of the adjacent side to the angle, using the opposite's length
function adjacentTan(angle, opposite) {
return opposite / Math.tan(angle);
}
//returns the length of the opposite side to the angle, using the hypotenuse's length
function oppositeSin(angle, hypotenuse) {
return Math.sin(angle) * hypotenuse;
}
//returns the angle using the opposite and adjacent
function angleTan(opposite, adjacent) {
return Math.atan(opposite/adjacent);
}
//returns the length of the unknown side of a triangle, using the other two sides' lengths
function triangleSide(sideA, sideB) {
var hypothenuse, shorterSide;
if (sideA > sideB) {
hypothenuse = sideA;
shorterSide = sideB;
} else {
hypothenuse = sideB;
shorterSide = sideA;
};
return Math.sqrt(Math.pow(hypothenuse, 2) - Math.pow(shorterSide, 2))
};
//returns the length of the hypotenuse, using the other two sides' lengths
function triangleHypotenuse(sideA, sideB) {
return Math.sqrt(Math.pow(sideA, 2) + Math.pow(sideB, 2))
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment