Last active
January 3, 2018 00:11
-
-
Save trvrb/cdcfcf5d4ce86c64d8252b0e6404fe39 to your computer and use it in GitHub Desktop.
VEG Network
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function (global, factory) { | |
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-array'), require('d3-path')) : | |
typeof define === 'function' && define.amd ? define(['exports', 'd3-array', 'd3-path'], factory) : | |
(factory((global.d3 = global.d3 || {}),global.d3,global.d3)); | |
}(this, function (exports,d3Array,d3Path) { 'use strict'; | |
var cos = Math.cos; | |
var sin = Math.sin; | |
var pi = Math.PI; | |
var halfPi = pi / 2; | |
var tau = pi * 2; | |
var max = Math.max; | |
function compareValue(compare) { | |
return function(a, b) { | |
return compare( | |
a.source.value + a.target.value, | |
b.source.value + b.target.value | |
); | |
}; | |
} | |
function multichord() { | |
var padAngle = 0, | |
sortGroups = null, | |
sortSubgroups = null, | |
sortChords = null; | |
function multichord(matrix) { | |
var n = matrix.length, | |
nCategories = matrix[0][0].length, | |
groupSums = {}, | |
groupIndex = d3Array.range(n), | |
subgroupIndex = [], | |
chords = [], | |
groups = chords.groups = new Array(n), | |
subgroups = chords.subgroups = new Array(n * n), | |
z, | |
k, | |
x, | |
x0, | |
dx, | |
i, | |
j; | |
// Compute the sum. | |
z = 0, i = -1; while (++i < n) { | |
if (!groupSums[i]){ | |
groupSums[i] = {} | |
} | |
x = 0, j = -1; while (++j < n) { | |
if (!groupSums[j]){ | |
groupSums[j] = {} | |
} | |
x += d3Array.sum(matrix[i][j]) | |
if (!groupSums[i].in){ | |
groupSums[i].in = d3Array.sum(matrix[i][j]) | |
} else { | |
groupSums[i].in += d3Array.sum(matrix[i][j]) | |
} | |
if (!groupSums[j].out){ | |
groupSums[j].out = d3Array.sum(matrix[i][j]) | |
} else { | |
groupSums[j].out += d3Array.sum(matrix[i][j]) | |
} | |
} | |
subgroupIndex.push(d3Array.range(n)); | |
z += x; | |
} | |
// Sort groups… | |
if (sortGroups) groupIndex.sort(function(a, b) { | |
return sortGroups(groupSums[a].in, groupSums[b].in); | |
}); | |
// Sort subgroups… | |
if (sortSubgroups) subgroupIndex.forEach(function(d, i) { | |
d.sort(function(a, b) { | |
return sortSubgroups(d3Array.sum(matrix[i][a]), d3Array.sum(matrix[i][b])); | |
}); | |
}); | |
// Convert the sum to scaling factor for [0, 2pi]. | |
// TODO Allow start and end angle to be specified? | |
// TODO Allow padding to be specified as percentage? | |
z = max(0, tau - padAngle * n) / z; | |
dx = z ? padAngle : tau / n; | |
// Compute the start and end angle for each group and subgroup. | |
// Note: Opera has a bug reordering object literal properties! | |
x = 0, i = -1; while (++i < n) { | |
x0 = x, j = -1; while (++j < n) { | |
var di = groupIndex[i], | |
dj = subgroupIndex[di][j], | |
v = d3Array.sum(matrix[di][dj]), | |
a0 = x; | |
x += v * z; | |
subgroups[dj * n + di] = new Array(nCategories), k = -1; while (++k < nCategories) { | |
v = matrix[di][dj][k]; | |
var b0 = a0, | |
b1 = a0 += v * z; | |
subgroups[dj * n + di][k] = { | |
index: di, | |
subindex: dj, | |
startAngle: b0, | |
endAngle: b1, | |
value: v, | |
category: k, | |
}; | |
}; | |
} | |
groups[di] = { | |
index: di, | |
startAngle: x0, | |
endAngle: x, | |
value: {in: groupSums[di].in, | |
out: groupSums[di].out} | |
}; | |
x += dx; | |
} | |
// Generate chords for each (non-empty) subgroup-subgroup link. | |
i = -1; while (++i < n) { | |
j = i - 1; while (++j < n) { | |
k = -1; while (++k < nCategories) { | |
var source = subgroups[j * n + i][k], | |
target = subgroups[i * n + j][k]; | |
if (source.value || target.value) { | |
chords.push(source.value < target.value | |
? {source: target, target: source} | |
: {source: source, target: target}); | |
} | |
} | |
} | |
} | |
return sortChords ? chords.sort(sortChords) : chords; | |
} | |
multichord.padAngle = function(_) { | |
return arguments.length ? (padAngle = max(0, _), multichord) : padAngle; | |
}; | |
multichord.sortGroups = function(_) { | |
return arguments.length ? (sortGroups = _, multichord) : sortGroups; | |
}; | |
multichord.sortSubgroups = function(_) { | |
return arguments.length ? (sortSubgroups = _, multichord) : sortSubgroups; | |
}; | |
multichord.sortChords = function(_) { | |
return arguments.length ? (_ == null ? sortChords = null : (sortChords = compareValue(_))._ = _, multichord) : sortChords && sortChords._; | |
}; | |
return multichord; | |
} | |
var slice = Array.prototype.slice; | |
function constant(x) { | |
return function() { | |
return x; | |
}; | |
} | |
function defaultSource(d) { | |
return d.source; | |
} | |
function defaultTarget(d) { | |
return d.target; | |
} | |
function defaultRadius(d) { | |
return d.radius; | |
} | |
function defaultStartAngle(d) { | |
return d.startAngle; | |
} | |
function defaultEndAngle(d) { | |
return d.endAngle; | |
} | |
function ribbon() { | |
var source = defaultSource, | |
target = defaultTarget, | |
radius = defaultRadius, | |
startAngle = defaultStartAngle, | |
endAngle = defaultEndAngle, | |
context = null; | |
function ribbon() { | |
var buffer, | |
argv = slice.call(arguments), | |
s = source.apply(this, argv), | |
t = target.apply(this, argv), | |
sr = +radius.apply(this, (argv[0] = s, argv)), | |
sa0 = startAngle.apply(this, argv) - halfPi, | |
sa1 = endAngle.apply(this, argv) - halfPi, | |
sx0 = sr * cos(sa0), | |
sy0 = sr * sin(sa0), | |
tr = +radius.apply(this, (argv[0] = t, argv)), | |
ta0 = startAngle.apply(this, argv) - halfPi, | |
ta1 = endAngle.apply(this, argv) - halfPi; | |
if (!context) context = buffer = d3Path.path(); | |
context.moveTo(sx0, sy0); | |
context.arc(0, 0, sr, sa0, sa1); | |
if (sa0 !== ta0 || sa1 !== ta1) { // TODO sr !== tr? | |
context.quadraticCurveTo(0, 0, tr * cos(ta0), tr * sin(ta0)); | |
context.arc(0, 0, tr, ta0, ta1); | |
} | |
context.quadraticCurveTo(0, 0, sx0, sy0); | |
context.closePath(); | |
if (buffer) return context = null, buffer + "" || null; | |
} | |
ribbon.radius = function(_) { | |
return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), ribbon) : radius; | |
}; | |
ribbon.startAngle = function(_) { | |
return arguments.length ? (startAngle = typeof _ === "function" ? _ : constant(+_), ribbon) : startAngle; | |
}; | |
ribbon.endAngle = function(_) { | |
return arguments.length ? (endAngle = typeof _ === "function" ? _ : constant(+_), ribbon) : endAngle; | |
}; | |
ribbon.source = function(_) { | |
return arguments.length ? (source = _, ribbon) : source; | |
}; | |
ribbon.target = function(_) { | |
return arguments.length ? (target = _, ribbon) : target; | |
}; | |
ribbon.context = function(_) { | |
return arguments.length ? ((context = _ == null ? null : _), ribbon) : context; | |
}; | |
return ribbon; | |
} | |
exports.multichord = multichord; | |
exports.ribbon = ribbon; | |
Object.defineProperty(exports, '__esModule', { value: true }); | |
})); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<title>VEG</title> | |
<meta name="author" content="Trevor Bedford"> | |
<!-- HTML5 shim, for IE6-8 support of HTML5 elements --> | |
<!--[if lt IE 9]> | |
<script src="https://html5shim.googlecode.com/svn/trunk/html5.js"></script> | |
<![endif]--> | |
</head> | |
<body> | |
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script> | |
<script src="https://d3js.org/d3-format.v1.min.js"></script> | |
<script src="https://d3js.org/d3-queue.v3.min.js"></script> | |
<script src="https://d3js.org/d3-selection.v1.min.js"></script> | |
<script src="d3-multichord.js"></script> | |
<style> | |
#circle circle { | |
fill: none; | |
pointer-events: all; | |
} | |
.group path { | |
stroke: #000; | |
stroke-width: .25px; | |
fill-opacity: 0.9; | |
} | |
path.chord { | |
stroke: #000; | |
stroke-width: .25px; | |
fill-opacity: 0.9; | |
} | |
path.fade { | |
display: none; | |
} | |
.legend { | |
font-size: 12px; | |
} | |
rect { | |
stroke-width: 1; | |
} | |
</style> | |
<div id="vis"></div> | |
<script> | |
// Adapted from Mike Bostock's UberData Chord diagram example | |
// https://bost.ocks.org/mike/uberdata/ | |
// Overall page margins | |
var HEIGHT = 500, | |
WIDTH = 580; | |
outerRadius = Math.min(WIDTH, HEIGHT) / 2 - 40 | |
innerRadius = outerRadius - 15; | |
// Formatting functions | |
var formatPercent = d3.format(".1%"); | |
var formatNumber = function (x){ | |
if (Math.abs(x) >= 1e9) { | |
return d3.format(",.2f")(x / 1e9) + " Billion" | |
} | |
else if (Math.abs(x) >= 1e6) { | |
return d3.format(",.2f")(x / 1e6) + " Million" | |
} | |
else { | |
return d3.format(",.1f")(x) | |
} | |
} | |
// Chord chart elements | |
var arc = d3.arc() | |
.innerRadius(innerRadius) | |
.outerRadius(outerRadius); | |
var layout = d3.multichord() | |
.padAngle(.05) | |
.sortSubgroups(d3.descending) | |
.sortChords(d3.descending); | |
var path = d3.ribbon() | |
.radius(innerRadius); | |
var svg = d3.select("#vis").append("svg") | |
.attr("width", WIDTH) | |
.attr("height", HEIGHT) | |
// .attr("x", CHORD_VIS.X) | |
// .attr("y", CHORD_VIS.Y) | |
d3.queue() | |
.defer(d3.json, "veg-data.json") | |
.await(ready); | |
function move(array, from, to) { | |
array.splice(to, 0, array.splice(from, 1)[0]); | |
}; | |
function fullSwap(nodes, links, from, to) { | |
move(nodes, from, to); | |
move(links, from, to) | |
links.forEach(function(row) { | |
move(row, from, to); | |
}); | |
} | |
function ready(error, data) { | |
if (error) throw error; | |
var nodes = data.nodes, | |
categories = data.categories; | |
links = data.links; | |
fullSwap(nodes, links, 6, 7); | |
fullSwap(nodes, links, 4, 5); | |
fullSwap(nodes, links, 3, 4); | |
var chords = layout(links) | |
// Compute the chord layout. | |
var g = svg.append("g") | |
.attr("id", "circle") | |
.attr("transform", "translate(" + (WIDTH / 2) + "," + (HEIGHT / 2) + ")") | |
.datum(chords); | |
g.append("circle") | |
.attr("r", outerRadius) | |
g.append("g").attr("id", "groups"); | |
g.append("g").attr("id", "chords"); | |
var group, groupPath, groupText, chord; | |
// Add a group per neighborhood. | |
group = g.select("#groups") | |
.selectAll("g") | |
.data(function(chords){ return chords.groups}) | |
.enter().append("g") | |
.attr("class", "group") | |
.on("mouseover", mouseover) | |
.on("mouseout", mouseover_restore); | |
// Add the group arc. | |
groupPath = group.append("path") | |
.attr("id", function(d, i) { return "group" + i; }) | |
.attr("d", arc) | |
.style("fill", "#AAA"); | |
// .style("fill", function(d, i) { return nodes[i].color; }); | |
// Add a text label. | |
groupText = group.append("text") | |
.attr("x", 1) | |
.attr("dy", -6) | |
.append("textPath") | |
.attr("xlink:href", function(d, i) { return "#group" + i; }) | |
.text(function(d, i) { return nodes[i].name; }) | |
.attr("font-family", "Helvetica") | |
.attr("opacity", function(d, i) { | |
// Hide labels that don't fit | |
if (groupPath._groups[0][i].getTotalLength() / 2 - 25 < this.getComputedTextLength()) { | |
return 1; | |
} else { | |
return 1; | |
}; | |
}) | |
// Add a mouseover title. | |
group.append("title").text(function(d, i) { | |
return nodes[i].name | |
+ "\n" + "In: " + formatNumber(chords.groups[i].value.in) | |
+ "\n" + "Out: " + formatNumber(chords.groups[i].value.out); | |
}); | |
// Add the chords. | |
chord = g.select("#chords").selectAll("g") | |
.data(function(chords) { return chords;}) | |
.enter().append("g") | |
.attr("class", "chord"); | |
chord.append("path") | |
.attr("class", "chord") | |
.style("fill", function(d) { return categories[d.source.category].color; }) | |
.attr("d", path) | |
.on("mouseover", mouseover_types) | |
.on("mouseout", mouseover_restore); | |
// Add a mouseover title for each chord. | |
chord.append("title").text(function(d) { | |
var source = nodes[d.source.index]; | |
var target = nodes[d.target.index]; | |
var category = categories[d.source.category]; | |
return category.name | |
+ "\n" + source.name | |
+ " → " + target.name | |
+ ": " + formatNumber(d.source.value * category.weight) | |
+ "\n" + target.name | |
+ " → " + source.name | |
+ ": " + formatNumber(d.target.value * category.weight); | |
}); | |
function mouseover(d) { | |
g.select("#chords").selectAll("path") | |
.classed("fade", function(p) { | |
return p.source.index != d.index | |
&& p.target.index != d.index; | |
}); | |
} | |
function mouseover_types(d) { | |
g.select("#chords").selectAll("path") | |
.classed("fade", function(p) { | |
return p.source.category != d.source.category | |
&& p.target.category != d.target.category; | |
}); | |
} | |
function mouseover_restore(d) { | |
g.select("#chords").selectAll("path") | |
.classed("fade", function(p) { | |
return false; | |
}); | |
} | |
// legend | |
var legendRectSize = 24; | |
var legendSpacing = 6; | |
var legendLeftPadding = 60; | |
var legendTopPadding = 60; | |
var legend = svg.selectAll('.legend') | |
.data(categories) | |
.enter() | |
.append('g') | |
.attr('class', 'legend') | |
.attr('transform', function(d, i) { | |
var height = legendRectSize + legendSpacing; | |
var offset = height * categories.length / 2; | |
var horz = legendLeftPadding + -2 * legendRectSize; | |
var vert = legendTopPadding + i * height - offset; | |
return 'translate(' + horz + ',' + vert + ')'; | |
}); | |
legend.append('rect') | |
.attr('width', legendRectSize) | |
.attr('height', legendRectSize) | |
.style('fill', function(d) { return d.color; }) | |
.style('stroke', function(d) { return "#555" }); | |
legend.append('text') | |
.attr('x', legendRectSize + legendSpacing) | |
.attr('y', legendRectSize - legendSpacing) | |
.attr("font-family", "Helvetica") | |
.attr("font-size", "14px") | |
.text(function(d) { return d.name; }); | |
} | |
</script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"nodes": [ | |
{"name": "Bedford"}, | |
{"name": "Bloom"}, | |
{"name": "Emerman"}, | |
{"name": "Geballe"}, | |
{"name": "Lehman"}, | |
{"name": "Malik"}, | |
{"name": "Matsen"}, | |
{"name": "Overbaugh"} | |
], | |
"categories": [ | |
{"name": "Grants", "color": "#476091", "weight": 0.317}, | |
{"name": "Trainees", "color": "#BD698B", "weight": 0.338}, | |
{"name": "Papers", "color": "#FFC300", "weight": 1} | |
], | |
"links": [ | |
[ [0,0,0], [6.3,4.4,1], [0,3,0], [0,0,0], [0,0,0], [0,0,1], [9.5,0,1], [0,0,0] ], | |
[ [6.3,3,1], [0,0,0], [0,3,0], [0,0,0], [0,0,0], [0,4.4,0], [9.5,0,0], [6.3,8.9,1] ], | |
[ [0,3,0], [0,1.5,0], [0,0,0], [0,0,0], [0,0,0], [3.2,8.9,22],[0,0,3], [0,5.9,3] ], | |
[ [0,1.5,0], [0,3,0], [0,1.5,0], [0,0,0], [0,1.5,0], [3.2,3,3], [0,0,0], [0,1.5,0] ], | |
[ [0,0,0], [0,0,0], [0,0,0], [0,0,0], [0,0,0], [0,0,0], [3.2,0,3], [6.3,3,17] ], | |
[ [0,0,1], [0,4.4,0], [3.2,8.9,22],[3.2,3,3],[0,0,0], [0,0,0], [0,0,2], [0,3,0] ], | |
[ [9.5,1.5,1],[9.5,1.5,0], [0,0,3], [0,0,0], [3.2,0,3], [0,0,2], [0,0,0], [12.6,5.9,3]], | |
[ [0,1.5,0], [6.3,11.9,1],[0,5.9,3], [0,0,0], [6.3,3,17],[0,3,0], [12.6,5.9,3],[0,0,0] ] | |
] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment