block by nbremer 43be164e9533c7e11336f363b9bd6c2f

LotR words - d3.unconf badge

Full Screen

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

script.js

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 //////////////////////
////////////////////////////////////////////////////////////

index.html

<!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>

d3-loom.js

(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

lotr_words_location.json

[
  {
    "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
  }
]

style.css

@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;
}