This is a hijacked version made specifically to create a badge for d3.unconf
forked from nbremer's block: LotR words - Who's speaking in Middle Earth
license: gpl-3.0 | |
height: 1500 | |
scrolling: no | |
border: no |
This is a hijacked version made specifically to create a badge for d3.unconf
forked from nbremer's block: LotR words - Who's speaking in Middle Earth
(function (global, factory) { | |
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : | |
typeof define === 'function' && define.amd ? define(['exports'], factory) : | |
(factory((global.d3 = global.d3 || {}))); | |
}(this, (function (exports) { 'use strict'; | |
function compareValue(compare) { | |
return function (a, b) { | |
return compare(a.outer.value, b.outer.value); | |
}; | |
} | |
function constant(x) { | |
return function () { | |
return x; | |
}; | |
} | |
/* Based on the d3v4 d3.chord() function by Mike Bostock | |
** Adjusted by Nadieh Bremer - July 2016 */ | |
/* global d3 */ | |
function loom() { | |
var tau = Math.PI * 2; | |
var padAngle = 0; | |
var sortGroups = null; | |
var sortSubgroups = null; | |
var sortLooms = null; | |
var emptyPerc = 0.2; | |
var heightInner = 20; | |
var widthInner = function widthInner() { | |
return 30; | |
}; | |
var value = function value(d) { | |
return d.value; | |
}; | |
var inner = function inner(d) { | |
return d.inner; | |
}; | |
var outer = function outer(d) { | |
return d.outer; | |
}; | |
function loomLayout(layoutData) { | |
// Nest the data on the outer variable | |
var data = d3.nest().key(outer).entries(layoutData); | |
var n = data.length; | |
// Loop over the outer groups and sum the values | |
var groupSums = []; | |
var groupIndex = d3.range(n); | |
var subgroupIndex = []; | |
var looms = []; | |
looms.groups = new Array(n); | |
var groups = looms.groups; | |
var numSubGroups = void 0; | |
looms.innergroups = []; | |
var uniqueInner = looms.innergroups; | |
var uniqueCheck = []; | |
var k = void 0; | |
var x = void 0; | |
var x0 = void 0; | |
var j = void 0; | |
var l = void 0; | |
var s = void 0; | |
var v = void 0; | |
var sum = void 0; | |
var section = void 0; | |
var remain = void 0; | |
var counter = void 0; | |
var reverseOrder = false; | |
var approxCenter = void 0; | |
k = 0; | |
numSubGroups = 0; | |
for (var i = 0; i < n; i += 1) { | |
v = data[i].values.length; | |
sum = 0; | |
for (j = 0; j < v; j += 1) { | |
sum += value(data[i].values[j]); | |
} // for j | |
groupSums.push(sum); | |
subgroupIndex.push(d3.range(v)); | |
numSubGroups += v; | |
k += sum; | |
} // for i | |
// Sort the groups… | |
if (sortGroups) { | |
groupIndex.sort(function (a, b) { | |
return sortGroups(groupSums[a], groupSums[b]); | |
}); | |
} | |
// Sort subgroups… | |
if (sortSubgroups) { | |
subgroupIndex.forEach(function (d, i) { | |
d.sort(function (a, b) { | |
return sortSubgroups(inner(data[i].values[a]), inner(data[i].values[b])); | |
}); | |
}); | |
} | |
// After which group are we past the center, taking into account the padding | |
// TODO: make something for if there is no "nice" split in two... | |
var padk = k * (padAngle / tau); | |
l = 0; | |
for (var _i = 0; _i < n; _i += 1) { | |
section = groupSums[groupIndex[_i]] + padk; | |
l += section; | |
if (l > (k + n * padk) / 2) { | |
// Check if the group should be added to left or right | |
remain = k + n * padk - (l - section); | |
approxCenter = remain / section < 0.5 ? groupIndex[_i] : groupIndex[_i - 1]; | |
break; | |
} // if | |
} // for i | |
// How much should be added to k to make the empty part emptyPerc big of the total | |
var emptyk = k * emptyPerc / (1 - emptyPerc); | |
k += emptyk; | |
// Convert the sum to scaling factor for [0, 2pi]. | |
k = Math.max(0, tau - padAngle * n) / k; | |
var dx = k ? padAngle : tau / n; | |
// Compute the start and end angle for each group and subgroup. | |
// Note: Opera has a bug reordering object literal properties! | |
var subgroups = new Array(numSubGroups); | |
x = emptyk * 0.25 * k; // starting with quarter of the empty part to the side; | |
counter = 0; | |
for (var _i2 = 0; _i2 < n; _i2 += 1) { | |
var di = groupIndex[_i2]; | |
var outername = data[di].key; | |
x0 = x; | |
s = subgroupIndex[di].length; | |
for (j = 0; j < s; j += 1) { | |
var dj = reverseOrder ? subgroupIndex[di][s - 1 - j] : subgroupIndex[di][j]; | |
v = value(data[di].values[dj]); | |
var innername = inner(data[di].values[dj]); | |
var a0 = x; | |
x += v * k; | |
var a1 = x; | |
subgroups[counter] = { | |
index: di, | |
subindex: dj, | |
startAngle: a0, | |
endAngle: a1, | |
value: v, | |
outername: outername, | |
innername: innername, | |
groupStartAngle: x0 | |
}; | |
// Check and save the unique inner names | |
if (!uniqueCheck[innername]) { | |
uniqueCheck[innername] = true; | |
uniqueInner.push({ name: innername }); | |
} // if | |
counter += 1; | |
} // for j | |
groups[di] = { | |
index: di, | |
startAngle: x0, | |
endAngle: x, | |
value: groupSums[di], | |
outername: outername | |
}; | |
x += dx; | |
// If this is the approximate center, add half of the empty piece for the bottom | |
if (approxCenter === di) x += emptyk * 0.5 * k; | |
// If you've crossed the bottom, reverse the order of the inner strings | |
if (x > Math.PI) reverseOrder = true; | |
} // for i | |
// Sort the inner groups in the same way as the strings | |
if (sortSubgroups) { | |
uniqueInner.sort(function (a, b) { | |
return sortSubgroups(a.name, b.name); | |
}); | |
} | |
// Find x and y locations of the inner categories | |
var m = uniqueInner.length; | |
for (var _i3 = 0; _i3 < m; _i3 += 1) { | |
uniqueInner[_i3].x = 0; | |
uniqueInner[_i3].y = -m * heightInner / 2 + _i3 * heightInner; | |
uniqueInner[_i3].offset = widthInner(uniqueInner[_i3].name, _i3); | |
} // for i | |
// Generate bands for each (non-empty) subgroup-subgroup link | |
counter = 0; | |
for (var _i4 = 0; _i4 < n; _i4 += 1) { | |
var _di = groupIndex[_i4]; | |
s = subgroupIndex[_di].length; | |
for (j = 0; j < s; j += 1) { | |
var outerGroup = subgroups[counter]; | |
var innerTerm = outerGroup.innername; | |
// Find the correct inner object based on the name | |
var innerGroup = searchTerm(innerTerm, 'name', uniqueInner); | |
if (outerGroup.value) { | |
looms.push({ inner: innerGroup, outer: outerGroup }); | |
} // if | |
counter += 1; | |
} // for j | |
} // for i | |
return sortLooms ? looms.sort(sortLooms) : looms; | |
} // loomLayout | |
function searchTerm(term, property, arrayToSearch) { | |
for (var i = 0; i < arrayToSearch.length; i += 1) { | |
if (arrayToSearch[i][property] === term) { | |
return arrayToSearch[i]; | |
} // if | |
} // for i | |
return null; | |
} // searchTerm | |
loomLayout.padAngle = function (_) { | |
return arguments.length ? (padAngle = Math.max(0, _), loomLayout) : padAngle; | |
}; | |
loomLayout.inner = function (_) { | |
return arguments.length ? (inner = _, loomLayout) : inner; | |
}; | |
loomLayout.outer = function (_) { | |
return arguments.length ? (outer = _, loomLayout) : outer; | |
}; | |
loomLayout.value = function (_) { | |
return arguments.length ? (value = _, loomLayout) : value; | |
}; | |
loomLayout.heightInner = function (_) { | |
return arguments.length ? (heightInner = _, loomLayout) : heightInner; | |
}; | |
loomLayout.widthInner = function (_) { | |
return arguments.length ? (widthInner = typeof _ === 'function' ? _ : constant(+_), loomLayout) : widthInner; | |
}; | |
loomLayout.emptyPerc = function (_) { | |
return arguments.length ? (emptyPerc = _ < 1 ? Math.max(0, _) : Math.max(0, _ * 0.01), loomLayout) : emptyPerc; | |
}; | |
loomLayout.sortGroups = function (_) { | |
return arguments.length ? (sortGroups = _, loomLayout) : sortGroups; | |
}; | |
loomLayout.sortSubgroups = function (_) { | |
return arguments.length ? (sortSubgroups = _, loomLayout) : sortSubgroups; | |
}; | |
loomLayout.sortLooms = function (_) { | |
return arguments.length ? (_ == null ? sortLooms = null : (sortLooms = compareValue(_))._ = _, loomLayout) : sortLooms && sortLooms._; | |
}; | |
return loomLayout; | |
} // loom | |
/* global d3 */ | |
function string() { | |
var slice = Array.prototype.slice; | |
var cos = Math.cos; | |
var sin = Math.sin; | |
var halfPi = Math.PI / 2; | |
var tau = Math.PI * 2; | |
var inner = function inner(d) { | |
return d.inner; | |
}; | |
var outer = function outer(d) { | |
return d.outer; | |
}; | |
var radius = function radius() { | |
return 100; | |
}; | |
var groupStartAngle = function groupStartAngle(d) { | |
return d.groupStartAngle; | |
}; | |
var startAngle = function startAngle(d) { | |
return d.startAngle; | |
}; | |
var endAngle = function endAngle(d) { | |
return d.endAngle; | |
}; | |
var x = function x(d) { | |
return d.x; | |
}; | |
var y = function y(d) { | |
return d.y; | |
}; | |
var offset = function offset(d) { | |
return d.offset; | |
}; | |
var pullout = 50; | |
var thicknessInner = 0; | |
var context = null; | |
function stringLayout() { | |
var buffer = void 0; | |
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { | |
args[_key] = arguments[_key]; | |
} | |
var argv = slice.call(args); | |
var out = outer.apply(this, argv); | |
var inn = inner.apply(this, argv); | |
argv[0] = out; | |
var sr = +radius.apply(this, argv); | |
var sa0 = startAngle.apply(this, argv) - halfPi; | |
var sga0 = groupStartAngle.apply(this, argv) - halfPi; | |
var sa1 = endAngle.apply(this, argv) - halfPi; | |
var sx0 = sr * cos(sa0); | |
var sy0 = sr * sin(sa0); | |
var sx1 = sr * cos(sa1); | |
var sy1 = sr * sin(sa1); | |
argv[0] = inn; | |
// 'tr' is assigned a value but never used | |
// const tr = +radius.apply(this, (argv)); | |
var tx = x.apply(this, argv); | |
var ty = y.apply(this, argv); | |
var toffset = offset.apply(this, argv); | |
var xco = void 0; | |
var yco = void 0; | |
var xci = void 0; | |
var yci = void 0; | |
// Does the group lie on the left side; | |
var leftHalf = sga0 + halfPi > Math.PI && sga0 + halfPi < tau; | |
// If the group lies on the other side, switch the inner point offset | |
if (leftHalf) toffset = -toffset; | |
tx += toffset; | |
// And the height of the end point | |
var theight = leftHalf ? -thicknessInner : thicknessInner; | |
if (!context) { | |
buffer = d3.path(); | |
context = buffer; | |
} | |
// Change the pullout based on where the stringLayout is | |
var pulloutContext = (leftHalf ? -1 : 1) * pullout; | |
sx0 += pulloutContext; | |
sx1 += pulloutContext; | |
// Start at smallest angle of outer arc | |
context.moveTo(sx0, sy0); | |
// Circular part along the outer arc | |
context.arc(pulloutContext, 0, sr, sa0, sa1); | |
// From end outer arc to center (taking into account the pullout) | |
xco = d3.interpolateNumber(pulloutContext, sx1)(0.5); | |
yco = d3.interpolateNumber(0, sy1)(0.5); | |
if (!leftHalf && sx1 < tx || leftHalf && sx1 > tx) { | |
// If the outer point lies closer to the center than the inner point | |
xci = tx + (tx - sx1) / 2; | |
yci = d3.interpolateNumber(ty + theight / 2, sy1)(0.5); | |
} else { | |
xci = d3.interpolateNumber(tx, sx1)(0.25); | |
yci = ty + theight / 2; | |
} // else | |
context.bezierCurveTo(xco, yco, xci, yci, tx, ty + theight / 2); | |
// Draw a straight line up/down (depending on the side of the circle) | |
context.lineTo(tx, ty - theight / 2); | |
// From center (taking into account the pullout) to start of outer arc | |
xco = d3.interpolateNumber(pulloutContext, sx0)(0.5); | |
yco = d3.interpolateNumber(0, sy0)(0.5); | |
if (!leftHalf && sx0 < tx || leftHalf && sx0 > tx) { | |
// If the outer point lies closer to the center than the inner point | |
xci = tx + (tx - sx0) / 2; | |
yci = d3.interpolateNumber(ty - theight / 2, sy0)(0.5); | |
} else { | |
xci = d3.interpolateNumber(tx, sx0)(0.25); | |
yci = ty - theight / 2; | |
} // else | |
context.bezierCurveTo(xci, yci, xco, yco, sx0, sy0); | |
// Close path | |
context.closePath(); | |
if (buffer) { | |
context = null; | |
return '' + buffer || null; | |
} | |
return null; | |
} | |
stringLayout.radius = function (_) { | |
return arguments.length ? (radius = typeof _ === 'function' ? _ : constant(+_), stringLayout) : radius; | |
}; | |
stringLayout.groupStartAngle = function (_) { | |
return arguments.length ? (groupStartAngle = typeof _ === 'function' ? _ : constant(+_), stringLayout) : groupStartAngle; | |
}; | |
stringLayout.startAngle = function (_) { | |
return arguments.length ? (startAngle = typeof _ === 'function' ? _ : constant(+_), stringLayout) : startAngle; | |
}; | |
stringLayout.endAngle = function (_) { | |
return arguments.length ? (endAngle = typeof _ === 'function' ? _ : constant(+_), stringLayout) : endAngle; | |
}; | |
stringLayout.x = function (_) { | |
return arguments.length ? (x = _, stringLayout) : x; | |
}; | |
stringLayout.y = function (_) { | |
return arguments.length ? (y = _, stringLayout) : y; | |
}; | |
stringLayout.offset = function (_) { | |
return arguments.length ? (offset = _, stringLayout) : offset; | |
}; | |
stringLayout.thicknessInner = function (_) { | |
return arguments.length ? (thicknessInner = _, stringLayout) : thicknessInner; | |
}; | |
stringLayout.inner = function (_) { | |
return arguments.length ? (inner = _, stringLayout) : inner; | |
}; | |
stringLayout.outer = function (_) { | |
return arguments.length ? (outer = _, stringLayout) : outer; | |
}; | |
stringLayout.pullout = function (_) { | |
return arguments.length ? (pullout = _, stringLayout) : pullout; | |
}; | |
stringLayout.context = function (_) { | |
return arguments.length ? (context = _ == null ? null : _, stringLayout) : context; | |
}; | |
return stringLayout; | |
} | |
exports.loom = loom; | |
exports.string = string; | |
Object.defineProperty(exports, '__esModule', { value: true }); | |
}))); | |
//# sourceMappingURL=d3-loom.js.map |
<!DOCTYPE html> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<title>The words of LotR</title> | |
<meta name="author" content="Nadieh Bremer"> | |
<meta name="description" content="Data Sketches - July - Movies - Nadieh - The words in LotR"> | |
<meta name="keywords" content="data, visualization, visualisation, data visualization, data visualisation, information, information visualization, information visualisation, dataviz, datavis, infoviz, infovis, collaboration, data art"> | |
<!-- Google fonts --> | |
<link href="https://fonts.googleapis.com/css?family=Macondo+Swash+Caps|Macondo" rel="stylesheet"> | |
<link href="https://fonts.googleapis.com/css?family=Cormorant:300,400" rel="stylesheet"> | |
<!-- Styling --> | |
<link href="style.css" rel="stylesheet"> | |
<!-- D3 v4 --> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<!-- Custom "chord" and "ribbon" functions --> | |
<script src="d3-loom.js"></script> | |
</head> | |
<body> | |
<div id="lotr-chart"></div> | |
<script src="script.js"></script> | |
</body> | |
</html> |
[ | |
{ | |
"location": "The Shire", | |
"character": "Frodo", | |
"words": 679 | |
}, | |
{ | |
"location": "The Shire", | |
"character": "Pippin", | |
"words": 124 | |
}, | |
{ | |
"location": "The Shire", | |
"character": "Sam", | |
"words": 239 | |
}, | |
{ | |
"location": "The Shire", | |
"character": "Gandalf", | |
"words": 1064 | |
}, | |
{ | |
"location": "The Shire", | |
"character": "Merry", | |
"words": 173 | |
}, | |
{ | |
"location": "Bree", | |
"character": "Aragorn", | |
"words": 258 | |
}, | |
{ | |
"location": "Bree", | |
"character": "Frodo", | |
"words": 125 | |
}, | |
{ | |
"location": "Bree", | |
"character": "Merry", | |
"words": 56 | |
}, | |
{ | |
"location": "Bree", | |
"character": "Pippin", | |
"words": 76 | |
}, | |
{ | |
"location": "Bree", | |
"character": "Sam", | |
"words": 71 | |
}, | |
{ | |
"location": "Isengard", | |
"character": "Aragorn", | |
"words": 3 | |
}, | |
{ | |
"location": "Isengard", | |
"character": "Pippin", | |
"words": 108 | |
}, | |
{ | |
"location": "Isengard", | |
"character": "Gimli", | |
"words": 45 | |
}, | |
{ | |
"location": "Isengard", | |
"character": "Gandalf", | |
"words": 224 | |
}, | |
{ | |
"location": "Isengard", | |
"character": "Merry", | |
"words": 116 | |
}, | |
{ | |
"location": "Rivendell", | |
"character": "Frodo", | |
"words": 153 | |
}, | |
{ | |
"location": "Rivendell", | |
"character": "Boromir", | |
"words": 259 | |
}, | |
{ | |
"location": "Rivendell", | |
"character": "Gimli", | |
"words": 38 | |
}, | |
{ | |
"location": "Rivendell", | |
"character": "Legolas", | |
"words": 34 | |
}, | |
{ | |
"location": "Rivendell", | |
"character": "Sam", | |
"words": 105 | |
}, | |
{ | |
"location": "Rivendell", | |
"character": "Gandalf", | |
"words": 276 | |
}, | |
{ | |
"location": "Rivendell", | |
"character": "Aragorn", | |
"words": 232 | |
}, | |
{ | |
"location": "Rivendell", | |
"character": "Merry", | |
"words": 29 | |
}, | |
{ | |
"location": "Rivendell", | |
"character": "Pippin", | |
"words": 27 | |
}, | |
{ | |
"location": "Misty Mountains", | |
"character": "Legolas", | |
"words": 11 | |
}, | |
{ | |
"location": "Misty Mountains", | |
"character": "Merry", | |
"words": 17 | |
}, | |
{ | |
"location": "Misty Mountains", | |
"character": "Pippin", | |
"words": 10 | |
}, | |
{ | |
"location": "Misty Mountains", | |
"character": "Sam", | |
"words": 3 | |
}, | |
{ | |
"location": "Misty Mountains", | |
"character": "Aragorn", | |
"words": 42 | |
}, | |
{ | |
"location": "Misty Mountains", | |
"character": "Boromir", | |
"words": 76 | |
}, | |
{ | |
"location": "Misty Mountains", | |
"character": "Gandalf", | |
"words": 86 | |
}, | |
{ | |
"location": "Misty Mountains", | |
"character": "Gimli", | |
"words": 66 | |
}, | |
{ | |
"location": "Misty Mountains", | |
"character": "Frodo", | |
"words": 6 | |
}, | |
{ | |
"location": "Moria", | |
"character": "Gandalf", | |
"words": 762 | |
}, | |
{ | |
"location": "Moria", | |
"character": "Gimli", | |
"words": 102 | |
}, | |
{ | |
"location": "Moria", | |
"character": "Legolas", | |
"words": 19 | |
}, | |
{ | |
"location": "Moria", | |
"character": "Merry", | |
"words": 17 | |
}, | |
{ | |
"location": "Moria", | |
"character": "Pippin", | |
"words": 21 | |
}, | |
{ | |
"location": "Moria", | |
"character": "Sam", | |
"words": 32 | |
}, | |
{ | |
"location": "Moria", | |
"character": "Frodo", | |
"words": 90 | |
}, | |
{ | |
"location": "Moria", | |
"character": "Boromir", | |
"words": 55 | |
}, | |
{ | |
"location": "Moria", | |
"character": "Aragorn", | |
"words": 98 | |
}, | |
{ | |
"location": "Lothlorien", | |
"character": "Legolas", | |
"words": 68 | |
}, | |
{ | |
"location": "Lothlorien", | |
"character": "Sam", | |
"words": 64 | |
}, | |
{ | |
"location": "Lothlorien", | |
"character": "Merry", | |
"words": 11 | |
}, | |
{ | |
"location": "Lothlorien", | |
"character": "Frodo", | |
"words": 36 | |
}, | |
{ | |
"location": "Lothlorien", | |
"character": "Pippin", | |
"words": 1 | |
}, | |
{ | |
"location": "Lothlorien", | |
"character": "Aragorn", | |
"words": 55 | |
}, | |
{ | |
"location": "Lothlorien", | |
"character": "Boromir", | |
"words": 176 | |
}, | |
{ | |
"location": "Lothlorien", | |
"character": "Gimli", | |
"words": 165 | |
}, | |
{ | |
"location": "Parth Galen", | |
"character": "Sam", | |
"words": 89 | |
}, | |
{ | |
"location": "Parth Galen", | |
"character": "Frodo", | |
"words": 129 | |
}, | |
{ | |
"location": "Parth Galen", | |
"character": "Pippin", | |
"words": 17 | |
}, | |
{ | |
"location": "Parth Galen", | |
"character": "Boromir", | |
"words": 398 | |
}, | |
{ | |
"location": "Parth Galen", | |
"character": "Aragorn", | |
"words": 319 | |
}, | |
{ | |
"location": "Parth Galen", | |
"character": "Gimli", | |
"words": 60 | |
}, | |
{ | |
"location": "Parth Galen", | |
"character": "Legolas", | |
"words": 52 | |
}, | |
{ | |
"location": "Parth Galen", | |
"character": "Merry", | |
"words": 20 | |
}, | |
{ | |
"location": "Emyn Muil", | |
"character": "Sam", | |
"words": 347 | |
}, | |
{ | |
"location": "Emyn Muil", | |
"character": "Frodo", | |
"words": 223 | |
}, | |
{ | |
"location": "Rohan", | |
"character": "Aragorn", | |
"words": 907 | |
}, | |
{ | |
"location": "Rohan", | |
"character": "Legolas", | |
"words": 407 | |
}, | |
{ | |
"location": "Rohan", | |
"character": "Pippin", | |
"words": 203 | |
}, | |
{ | |
"location": "Rohan", | |
"character": "Merry", | |
"words": 281 | |
}, | |
{ | |
"location": "Rohan", | |
"character": "Gandalf", | |
"words": 671 | |
}, | |
{ | |
"location": "Rohan", | |
"character": "Gimli", | |
"words": 607 | |
}, | |
{ | |
"location": "Fangorn", | |
"character": "Gandalf", | |
"words": 524 | |
}, | |
{ | |
"location": "Fangorn", | |
"character": "Legolas", | |
"words": 73 | |
}, | |
{ | |
"location": "Fangorn", | |
"character": "Merry", | |
"words": 297 | |
}, | |
{ | |
"location": "Fangorn", | |
"character": "Pippin", | |
"words": 276 | |
}, | |
{ | |
"location": "Fangorn", | |
"character": "Aragorn", | |
"words": 108 | |
}, | |
{ | |
"location": "Fangorn", | |
"character": "Gimli", | |
"words": 89 | |
}, | |
{ | |
"location": "Gondor", | |
"character": "Boromir", | |
"words": 132 | |
}, | |
{ | |
"location": "Gondor", | |
"character": "Sam", | |
"words": 822 | |
}, | |
{ | |
"location": "Gondor", | |
"character": "Frodo", | |
"words": 491 | |
}, | |
{ | |
"location": "Gondor", | |
"character": "Gandalf", | |
"words": 1155 | |
}, | |
{ | |
"location": "Gondor", | |
"character": "Pippin", | |
"words": 386 | |
}, | |
{ | |
"location": "Gondor", | |
"character": "Aragorn", | |
"words": 175 | |
}, | |
{ | |
"location": "Gondor", | |
"character": "Gimli", | |
"words": 72 | |
}, | |
{ | |
"location": "Gondor", | |
"character": "Merry", | |
"words": 97 | |
}, | |
{ | |
"location": "Gondor", | |
"character": "Legolas", | |
"words": 8 | |
}, | |
{ | |
"location": "Mordor", | |
"character": "Legolas", | |
"words": 8 | |
}, | |
{ | |
"location": "Mordor", | |
"character": "Frodo", | |
"words": 361 | |
}, | |
{ | |
"location": "Mordor", | |
"character": "Aragorn", | |
"words": 128 | |
}, | |
{ | |
"location": "Mordor", | |
"character": "Gandalf", | |
"words": 32 | |
}, | |
{ | |
"location": "Mordor", | |
"character": "Gimli", | |
"words": 21 | |
}, | |
{ | |
"location": "Mordor", | |
"character": "Merry", | |
"words": 3 | |
}, | |
{ | |
"location": "Mordor", | |
"character": "Pippin", | |
"words": 12 | |
}, | |
{ | |
"location": "Mordor", | |
"character": "Sam", | |
"words": 753 | |
} | |
] |
var margin = {left:120, top:40, right:170, bottom:50}, | |
width = 1050 - margin.left - margin.right, | |
height = 1500 - margin.top - margin.bottom, | |
innerRadius = Math.min(width * 0.33, height * .45), | |
outerRadius = innerRadius * 1.05; | |
//Reset the overall font size | |
var newFontSize = Math.min(70, Math.max(40, innerRadius * 62.5 / 250)); | |
d3.select("html").style("font-size", newFontSize + "%"); | |
//////////////////////////////////////////////////////////// | |
////////////////// Set-up Chord parameters ///////////////// | |
//////////////////////////////////////////////////////////// | |
var pullOutSize = 20 + 30/135 * innerRadius; | |
var numFormat = d3.format(",.0f"); | |
var defaultOpacity = 0.85, | |
fadeOpacity = 0.075; | |
var loom = d3.loom() | |
.padAngle(0.05) | |
.emptyPerc(0.2) | |
.widthInner(30) | |
.value(function(d) { return d.words; }) | |
.inner(function(d) { return d.character; }) | |
.outer(function(d) { return d.location; }); | |
var arc = d3.arc() | |
.innerRadius(innerRadius*1.01) | |
.outerRadius(outerRadius); | |
var string = d3.string() | |
.radius(innerRadius) | |
.pullout(pullOutSize); | |
//////////////////////////////////////////////////////////// | |
////////////////////// Create SVG ////////////////////////// | |
//////////////////////////////////////////////////////////// | |
var svg = d3.select("#lotr-chart").append("svg") | |
.attr("width", width + margin.left + margin.right) | |
.attr("height", height + margin.top + margin.bottom); | |
//////////////////////////////////////////////////////////// | |
///////////////////// Read in data ///////////////////////// | |
//////////////////////////////////////////////////////////// | |
d3.json('lotr_words_location.json', function (error, dataAgg) { | |
//////////////////////////////////////////////////////////// | |
///////////////////// Prepare the data ///////////////////// | |
//////////////////////////////////////////////////////////// | |
//Sort the inner characters based on the total number of words spoken | |
//Find the total number of words per character | |
var dataChar = d3.nest() | |
.key(function(d) { return d.character; }) | |
.rollup(function(leaves) { return d3.sum(leaves, function(d) { return d.words; }); }) | |
.entries(dataAgg) | |
.sort(function(a, b){ return d3.descending(a.value, b.value); }); | |
//Unflatten the result | |
var characterOrder = dataChar.map(function(d) { return d.key; }); | |
//Sort the characters on a specific order | |
function sortCharacter(a, b) { | |
return characterOrder.indexOf(a) - characterOrder.indexOf(b); | |
}//sortCharacter | |
//Set more loom functions | |
loom | |
.sortSubgroups(sortCharacter) | |
.heightInner(innerRadius*0.75/characterOrder.length); | |
//////////////////////////////////////////////////////////// | |
///////////////////////// Colors /////////////////////////// | |
//////////////////////////////////////////////////////////// | |
//Color for the unique locations | |
var locations = ["Bree", "Emyn Muil", "Fangorn", "Gondor", "Isengard", "Lothlorien", "Misty Mountains", "Mordor", "Moria", "Parth Galen", "Rivendell", "Rohan", "The Shire"]; | |
var colors = ["#5a3511", "#47635f", "#223e15", "#C6CAC9", "#0d1e25", "#53821a", "#4387AA", "#770000", "#373F41", "#602317", "#8D9413", "#c17924", "#3C7E16"]; | |
var color = d3.scaleOrdinal() | |
.domain(locations) | |
.range(colors); | |
//Create a group that already holds the data | |
var g = svg.append("g") | |
.attr("transform", "translate(" + (width/2 + margin.left) + "," + (height/2 + margin.top) + ")") | |
.datum(loom(dataAgg)); | |
/////////////////////////////////////////////////////////////////////////// | |
//////////////////////////// Create the filter //////////////////////////// | |
/////////////////////////////////////////////////////////////////////////// | |
//Container for the gradients | |
var defs = svg.append("defs"); | |
//Filter for the outside glow | |
var filter = defs.append("filter").attr("id","glow"); | |
filter.append("feGaussianBlur") | |
.attr("class", "blur") | |
.attr("stdDeviation","2") | |
.attr("result","coloredBlur"); | |
var feMerge = filter.append("feMerge"); | |
feMerge.append("feMergeNode").attr("in","coloredBlur"); | |
feMerge.append("feMergeNode").attr("in","SourceGraphic"); | |
//////////////////////////////////////////////////////////// | |
//////////////// Draw the ring inscription ///////////////// | |
//////////////////////////////////////////////////////////// | |
var ringWrapper = g.append("g").attr("class", "ring-wrapper"); | |
var ringR = innerRadius*0.65; | |
ringWrapper.append("path") | |
.attr("id", "ring-path-top") | |
.attr("class", "ring-path") | |
.style("fill", "none") | |
.attr("d", "M" + -ringR + "," + 0 + " A" + ringR + "," + ringR + " 0 0,1 " + ringR + "," + 0); | |
ringWrapper.append("text") | |
.attr("class", "ring-text") | |
.append("textPath") | |
.attr("startOffset", "50%") | |
.style("filter", "url(#glow)") | |
.attr("xlink:href", "#ring-path-top") | |
.text("AE5,Ex26Yw1EjYzH= AE5,Exx:w%P1Dj^"); | |
ringWrapper.append("path") | |
.attr("id", "ring-path-bottom") | |
.attr("class", "ring-path") | |
.style("fill", "none") | |
.attr("d", "M" + -ringR + "," + 0 + " A" + ringR + "," + ringR + " 0 0,0 " + ringR + "," + 0); | |
ringWrapper.append("text") | |
.attr("class", "ring-text") | |
.append("textPath") | |
.attr("startOffset", "50%") | |
.style("filter", "url(#glow)") | |
.attr("xlink:href", "#ring-path-bottom") | |
.text("AE5,Ex37zD1EjYzH= X#w6Ykt^AT`Bz7qTp1EjY"); | |
//////////////////////////////////////////////////////////// | |
///////////////////// Set-up title ///////////////////////// | |
//////////////////////////////////////////////////////////// | |
var titles = g.append("g") | |
.attr("class", "texts") | |
.style("opacity", 0); | |
titles.append("text") | |
.attr("class", "name-title") | |
.attr("x", 0) | |
.attr("y", -innerRadius*5/6); | |
titles.append("text") | |
.attr("class", "value-title") | |
.attr("x", 0) | |
.attr("y", -innerRadius*5/6 + 25); | |
//////////////////////////////////////////////////////////// | |
////////////////////// Draw outer arcs ///////////////////// | |
//////////////////////////////////////////////////////////// | |
var arcs = g.append("g") | |
.attr("class", "arcs") | |
.selectAll("g") | |
.data(function(s) { return s.groups; }) | |
.enter().append("g") | |
.attr("class", "arc-wrapper") | |
.each(function(d) { d.pullOutSize = (pullOutSize * ( d.startAngle > Math.PI + 1e-2 ? -1 : 1)); }); | |
//////////////////////////////////////////////////////////// | |
//////////////////// Draw outer labels ///////////////////// | |
//////////////////////////////////////////////////////////// | |
//The text needs to be rotated with the offset in the clockwise direction | |
var outerLabels = arcs.append("g") | |
.each(function(d) { d.angle = ((d.startAngle + d.endAngle) / 2); }) | |
.attr("class", "outer-labels") | |
.attr("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; }) | |
.attr("transform", function(d,i) { | |
var c = arc.centroid(d); | |
return "translate(" + (c[0] + d.pullOutSize) + "," + c[1] + ")" | |
+ "rotate(" + (d.angle * 180 / Math.PI - 90) + ")" | |
+ "translate(" + 26 + ",0)" | |
+ (d.angle > Math.PI ? "rotate(180)" : "") | |
}) | |
var elvishName = ["175{#","7R`B4#6Y","x{#75$iY1","t%j4#7iT","93GlExj6T", | |
"KiAZADDÚMU","j3Hj~N7`B5$","q7E3 xj#5$","t$I5 thUj", | |
"79N5#","ex{#7Y5","x2{^6Y","t7Y46Y"]; | |
//The outer name in Elvish | |
outerLabels.append("text") | |
.attr("class", function(d,i) { return d.outername === "Moria" ? "dwarfish-outer-label" : "elvish-outer-label"; }) | |
.attr("dy", ".15em") | |
.text(function(d,i){ return elvishName[i]; }); | |
//The outer name | |
outerLabels.append("text") | |
.attr("class", "outer-label") | |
.attr("dy", ".35em") | |
.text(function(d,i){ return d.outername; }); | |
//The value below it | |
outerLabels.append("text") | |
.attr("class", "outer-label-value") | |
.attr("dy", "1.5em") | |
.text(function(d,i){ return numFormat(d.value) + " words"; }); | |
//////////////////////////////////////////////////////////// | |
//////////////////// Draw outer arcs /////////////////////// | |
//////////////////////////////////////////////////////////// | |
var outerArcs = arcs.append("path") | |
.attr("class", "arc") | |
.style("fill", function(d) { return color(d.outername); }) | |
.attr("d", arc) | |
.attr("transform", function(d, i) { //Pull the two slices apart | |
return "translate(" + d.pullOutSize + ',' + 0 + ")"; | |
}); | |
//////////////////////////////////////////////////////////// | |
////////////////// Draw inner strings ////////////////////// | |
//////////////////////////////////////////////////////////// | |
var strings = g.append("g") | |
.attr("class", "stringWrapper") | |
.style("isolation", "isolate") | |
.selectAll("path") | |
.data(function(strings) { return strings; }) | |
.enter().append("path") | |
.attr("class", "string") | |
.style("mix-blend-mode", "multiply") | |
.attr("d", string) | |
.style("fill", function(d) { return d3.rgb( color(d.outer.outername) ).brighter(0.2) ; }) | |
.style("opacity", defaultOpacity); | |
//////////////////////////////////////////////////////////// | |
//////////////////// Draw inner labels ///////////////////// | |
//////////////////////////////////////////////////////////// | |
//The text also needs to be displaced in the horizontal directions | |
//And also rotated with the offset in the clockwise direction | |
var innerLabels = g.append("g") | |
.attr("class","inner-labels") | |
.selectAll("text") | |
.data(function(s) { | |
return s.innergroups; | |
}) | |
.enter().append("text") | |
.attr("class", "inner-label") | |
.attr("x", function(d,i) { return d.x; }) | |
.attr("y", function(d,i) { return d.y; }) | |
.style("text-anchor", "middle") | |
.attr("dy", ".35em") | |
.text(function(d,i) { return d.name; }) | |
.on("mouseover", mouseOverInner) | |
.on("mouseout", mouseOutInner); | |
function mouseOverInner(d,i) { | |
setTimeout(function() { | |
//Show all the strings of the highlighted character and hide all else | |
d3.selectAll(".string") | |
.style("opacity", function(s) { | |
return s.outer.innername !== d.name ? fadeOpacity : 1; | |
}); | |
//Update the word count of the outer labels | |
var characterData = loom(dataAgg).filter(function(s) { return s.outer.innername === d.name; }); | |
d3.selectAll(".outer-label-value") | |
.text(function(s,i){ | |
//Find which characterData is the correct one based on location | |
var loc = characterData.filter(function(c) { return c.outer.outername === s.outername; }); | |
if(loc.length === 0) { | |
var value = 0; | |
} else { | |
var value = loc[0].outer.value; | |
} | |
return numFormat(value) + (value === 1 ? " word" : " words"); | |
}); | |
//Hide the arc where the character hasn't said a thing | |
d3.selectAll(".arc-wrapper") | |
.style("opacity", function(s) { | |
//Find which characterData is the correct one based on location | |
var loc = characterData.filter(function(c) { return c.outer.outername === s.outername; }); | |
return loc.length === 0 ? 0.1 : 1; | |
}); | |
//Update the title to show the total word count of the character | |
d3.selectAll(".texts") | |
.style("opacity", 1); | |
d3.select(".name-title") | |
.text(d.name); | |
d3.select(".value-title") | |
.text(function() { | |
var words = dataChar.filter(function(s) { return s.key === d.name; }); | |
return numFormat(words[0].value); | |
}); | |
//Hide ring text | |
d3.selectAll(".ring-wrapper") | |
.style("opacity", fadeOpacity); | |
}, i*1000); | |
}//function mouseOverInner | |
function mouseOutInner(d) { | |
//Put the string opacity back to normal | |
d3.selectAll(".string") | |
.style("opacity", defaultOpacity); | |
//Return the word count to what it was | |
d3.selectAll(".outer-label-value") | |
.text(function(s,i){ return numFormat(s.value) + " words"; }); | |
//Show all arcs again | |
d3.selectAll(".arc-wrapper") | |
.style("opacity", 1); | |
//Hide the title | |
d3.selectAll(".texts") | |
.style("opacity", 0); | |
//Show ring text | |
d3.selectAll(".ring-wrapper") | |
.style("opacity", 1); | |
}//function mouseOutInner | |
//////////////////////////////////////////////////////////// | |
////////////////// Create Animation Loop /////////////////// | |
//////////////////////////////////////////////////////////// | |
setTimeout( function() { innerLabels.dispatch("mouseover"); },2000); | |
setTimeout( function() { innerLabels.dispatch("mouseout"); },11000); | |
});//d3.csv | |
//////////////////////////////////////////////////////////// | |
///////////////////// Extra functions ////////////////////// | |
//////////////////////////////////////////////////////////// |
@font-face { | |
font-family: "Aniron"; | |
src: url("Aniron.ttf") format('truetype'); | |
} | |
@font-face { | |
font-family: "Bilbo"; | |
src: url("Bilbo.ttf") format('truetype'); | |
} | |
@font-face { | |
font-family: "Elvish"; | |
src: url("Elvish.ttf") format('truetype'); | |
} | |
@font-face { | |
font-family: "Dwarfish"; | |
src: url("Dwarfish.ttf") format('truetype'); | |
} | |
html { font-size: 62.5%; } | |
body { | |
font-family: 'Cormorant', serif; | |
font-size: 1.2rem; | |
fill: #b9b9b9; | |
text-align: center; | |
} | |
::-webkit-scrollbar { | |
display: none; | |
} | |
/*--- chart ---*/ | |
.name-title { | |
font-family: 'Aniron', cursive; | |
font-size: 1.8rem; | |
fill: #232323; | |
cursor: default; | |
text-anchor: middle; | |
} | |
.value-title { | |
font-family: 'Bilbo', serif; | |
text-anchor: middle; | |
font-size: 2.1rem; | |
fill: #b9b9b9; | |
} | |
.character-note { | |
text-anchor: middle; | |
font-size: 1.4rem; | |
fill: #232323; | |
/* font-weight: 300;*/ | |
} | |
.inner-label { | |
font-family: 'Aniron', cursive; | |
font-size: 1.0rem; | |
fill: #232323; | |
cursor: default; | |
} | |
.elvish-outer-label { | |
font-family: 'Elvish', cursive; | |
font-size: 4rem; | |
fill: #e2e2e2; | |
cursor: default; | |
} | |
.dwarfish-outer-label { | |
font-family: 'Dwarfish', cursive; | |
font-size: 4rem; | |
fill: #e2e2e2; | |
cursor: default; | |
} | |
.outer-label { | |
font-family: 'Aniron', cursive; | |
font-size: 1.1rem; | |
fill: #5f5f5f; | |
cursor: default; | |
} | |
.outer-label-value { | |
font-family: 'Bilbo', serif; | |
font-size: 1.3rem; | |
fill: #878787; | |
} | |
.ring-text { | |
font-family: 'Elvish', cursive; | |
font-size: 2rem; | |
fill: #da6f0b; | |
opacity: 0.3; | |
text-anchor: middle; | |
} |