Skip to content

Instantly share code, notes, and snippets.

@nbremer
Last active August 11, 2017 09:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nbremer/43be164e9533c7e11336f363b9bd6c2f to your computer and use it in GitHub Desktop.
Save nbremer/43be164e9533c7e11336f363b9bd6c2f to your computer and use it in GitHub Desktop.
LotR words - d3.unconf badge
license: gpl-3.0
height: 1500
scrolling: no
border: no
(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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment