block by eglassman 9fdeb1aef6fc2d91944d96f38038b253

Narrative Charts

Full Screen

This is a bare-bones example for using the d3-layout-narrative module. It’s a cut-down version of the Episode IV chart in Star Wars: every scene from I-VI charted at ABC News.

View the annotated source for more on the API.

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<style>

	text {
		font-family: "ProximaNova",Helvetica,Arial,sans-serif;
		font-size: 12px;
	}

	rect {
		fill: none;
		stroke: #000;
	}
	path {
		fill: none;
		stroke-width: 2;
		stroke: #333;
	}

	path.light {
		stroke: #3c6da8;
	}

	path.dark {
		stroke: #df2929;
	}

	.intro text:first-child {
		fill: #fff;
	    stroke: #f9f9f9;
	    stroke-width: 3;
	}

	.intro text+text {
	    fill: #333;
	}

	.intro text+text.dark {
	    fill: #df2929;
	}

	.intro text+text.light {
	    fill: #3c6da8;
	}
</style>

<body>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.5.7/es5-shim.js"></script>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.35.0/es6-shim.js"></script>
	<script src="https://d3js.org/d3.v3.min.js"></script>
	<script src="https://cdn.rawgit.com/abcnews/d3-layout-narrative/1.0.0/narrative.js"></script>
	<script src="chart.js"></script>

chart.js

// Request the data
d3.json('data.json', function(err, response){

	var svg, scenes, charactersMap, width, height, sceneWidth;

	// Get the data in the format we need to feed to d3.layout.narrative().scenes
	scenes = wrangle(response);

	// Some defaults
	sceneWidth = 10;
	width = scenes.length * sceneWidth * 4;
	height = 600;
	labelSize = [150,15];

	// The container element (this is the HTML fragment);
	svg = d3.select("body").append('svg')
		.attr('id', 'narrative-chart')
		.attr('width', width)
		.attr('height', height);

	// Calculate the actual width of every character label.
	scenes.forEach(function(scene){
		scene.characters.forEach(function(character) {
			character.width = svg.append('text')
				.attr('opacity',0)
				.attr('class', 'temp')
				.text(character.name)
					.node().getComputedTextLength()+10;
		});
	});

	// Remove all the temporary labels.
	svg.selectAll('text.temp').remove();

	// Do the layout
	narrative = d3.layout.narrative()
		.scenes(scenes)
		.size([width,height])
		.pathSpace(10)
		.groupMargin(10)
		.labelSize([250,15])
		.scenePadding([5,sceneWidth/2,5,sceneWidth/2])
		.labelPosition('left')
		.layout();

	// Get the extent so we can re-size the SVG appropriately.
	svg.attr('height', narrative.extent()[1]);

	// Draw the scenes
	svg.selectAll('.scene').data(narrative.scenes()).enter()
		.append('g').attr('class', 'scene')
			.attr('transform', function(d){
					var x,y;
					x = Math.round(d.x)+0.5;
					y = Math.round(d.y)+0.5;
					return 'translate('+[x,y]+')';
				})
			.append('rect')
				.attr('width', sceneWidth)
				.attr('height', function(d){
					return d.height;
				})
				.attr('y', 0)
				.attr('x', 0)
				.attr('rx', 3)
				.attr('ry', 3);

	// Draw appearances
	svg.selectAll('.scene').selectAll('.appearance').data(function(d){
		return d.appearances;
	}).enter().append('circle')
		.attr('cx', function(d){
			return d.x;
		})
		.attr('cy', function(d){
			return d.y;
		})
		.attr('r', function(){
			return 2;
		})
		.attr('class', function(d){
			return 'appearance ' + d.character.affiliation;
		});

	// Draw links
	svg.selectAll('.link').data(narrative.links()).enter()
		.append('path')
		.attr('class', function(d) {
			return 'link ' + d.character.affiliation.toLowerCase();
		})
		.attr('d', narrative.link());

	// Draw intro nodes
	svg.selectAll('.intro').data(narrative.introductions())
		.enter().call(function(s){
			var g, text;

			g = s.append('g').attr('class', 'intro');

			g.append('rect')
				.attr('y', -4)
				.attr('x', -4)
				.attr('width', 4)
				.attr('height', 8);

			text = g.append('g').attr('class','text');

			// Apppend two actual 'text' nodes to fake an 'outside' outline.
			text.append('text');
			text.append('text').attr('class', 'color');

			g.attr('transform', function(d){
					var x,y;
					x = Math.round(d.x);
					y = Math.round(d.y);
					return 'translate(' + [x,y] + ')';
				});

			g.selectAll('text')
				.attr('text-anchor', 'end')
				.attr('y', '4px')
				.attr('x', '-8px')
				.text(function(d){ return d.character.name; });

			g.select('.color')
				.attr('class', function(d){
					return 'color ' + d.character.affiliation;
				});

			g.select('rect')
				.attr('class', function(d){
					return d.character.affiliation;
				});

		});

});

function wrangle(data) {

	var charactersMap = {};

	return data.scenes.map(function(scene){
		return {characters: scene.map(function(id){
			return characterById(id);
		}).filter(function(d) { return (d); })};
	});

	// Helper to get characters by ID from the raw data
	function characterById(id) {
		charactersMap = charactersMap || {};
		charactersMap[id] = charactersMap[id] || data.characters.find(function(character){
			return character.id === id;
		});
		return charactersMap[id];
	}

}

data.json

{
  "characters": [
      {
        "id": "R2D",
        "name": "R2-D2",
        "affiliation": "light"
      },
      {
        "id": "C3P",
        "name": "C-3PO",
        "affiliation": "light"
      },
      {
        "id": "RO1",
        "name": "Rebel Officers",
        "affiliation": "light"
      },
      {
        "id": "ST1",
        "name": "Stormtroopers",
        "affiliation": "dark"
      },
      {
        "id": "DV1",
        "name": "Anakin Skywalker / Darth Vader",
        "affiliation": "vader"
      },
      {
        "id": "PL1",
        "name": "Princess Leia Organa",
        "affiliation": "light"
      },
      {
        "id": "JW1",
        "name": "Jawas",
        "affiliation": "other"
      },
      {
        "id": "LS1",
        "name": "Luke Skywalker",
        "affiliation": "light"
      },
      {
        "id": "OL1",
        "name": "Owen Lars",
        "affiliation": "other"
      },
      {
        "id": "BL1",
        "name": "Beru Lars",
        "affiliation": "other"
      },
      {
        "id": "TR1",
        "name": "Tusken Raiders",
        "affiliation": "other"
      },
      {
        "id": "OB1",
        "name": "Obi-Wan Kenobi",
        "affiliation": "light"
      },
      {
        "id": "GT1",
        "name": "General Tagge",
        "affiliation": "dark"
      },
      {
        "id": "AM1",
        "name": "Admiral Motti",
        "affiliation": "dark"
      },
      {
        "id": "GMT",
        "name": "Grand Moff Tarkin",
        "affiliation": "dark"
      },
      {
        "id": "CB1",
        "name": "Chewbacca",
        "affiliation": "light"
      },
      {
        "id": "HS1",
        "name": "Han Solo",
        "affiliation": "light"
      },
      {
        "id": "GR1",
        "name": "Greedo",
        "affiliation": "other"
      },
      {
        "id": "JTH",
        "name": "Jabba The Hutt",
        "affiliation": "other"
      },
      {
        "id": "GW1",
        "name": "General Willard",
        "affiliation": "light"
      },
      {
        "id": "GJD",
        "name": "General Jan Dodonna",
        "affiliation": "light"
      },
      {
        "id": "JV1",
        "name": "Jon 'Dutch' Vander",
        "affiliation": "light"
      },
      {
        "id": "WA1",
        "name": "Wedge Antilles",
        "affiliation": "light"
      },
      {
        "id": "BD2",
        "name": "Biggs Darklighter",
        "affiliation": "light"
      },
      {
        "id": "GD1",
        "name": "Garven Dreis",
        "affiliation": "light"
      },
      {
        "id": "JP1",
        "name": "Jek Porkins",
        "affiliation": "light"
      },
      {
        "id": "DT1",
        "name": "Dex Tiree",
        "affiliation": "light"
      },
      {
        "id": "DK1",
        "name": "Davish Krail",
        "affiliation": "light"
      },
      {
        "id": "TN1",
        "name": "Theron Nett",
        "affiliation": "light"
      },
      {
        "id": "PN1",
        "name": "Puck Naeco",
        "affiliation": "light"
      }
    ],
  "scenes": [
    [
      "R2D",
      "C3P",
      "DV1",
      "ST1",
      "RO1"
    ],
    [
      "R2D",
      "C3P",
      "DV1",
      "PL1"
    ],
    [
      "DV1",
      "PL1"
    ],
    [
      "R2D",
      "C3P"
    ],
    [
      "R2D",
      "C3P",
      "ST1",
      "JW1"
    ],
    [
      "R2D",
      "C3P",
      "LS1",
      "OL1",
      "BL1",
      "JW1"
    ],
    [
      "R2D",
      "C3P",
      "LS1"
    ],
    [
      "LS1",
      "OL1",
      "BL1"
    ],
    [
      "LS1",
      "C3P",
      "OL1",
      "BL1",
      ""
    ],
    [
      "LS1",
      "C3P",
      "R2D",
      "TR1"
    ],
    [
      "LS1",
      "OB1",
      "R2D",
      "C3P",
      "TR1"
    ],
    [
      "LS1",
      "OB1",
      "R2D",
      "C3P"
    ],
    [
      "LS1",
      "OB1",
      "R2D",
      "C3P"
    ],
    [
      "GT1",
      "AM1",
      "DV1",
      "GMT"
    ],
    [
      "LS1",
      "OB1",
      "R2D",
      "C3P",
      "OL1",
      "BL1"
    ],
    [
      "DV1",
      "PL1"
    ],
    [
      "LS1",
      "OB1",
      "R2D",
      "C3P"
    ],
    [
      "LS1",
      "OB1",
      "R2D",
      "C3P"
    ],
    [
      "LS1",
      "OB1",
      "R2D",
      "C3P",
      "CB1"
    ],
    [
      "LS1",
      "OB1",
      "CB1",
      "HS1"
    ],
    [
      "HS1",
      "GR1"
    ],
    [
      "DV1",
      "GMT",
      "GT1",
      "AM1",
      "R2D",
      "LS1",
      "OB1",
      "C3P"
    ],
    [
      "HS1",
      "CB1",
      "JTH"
    ],
    [
      "LS1",
      "OB1",
      "R2D",
      "C3P",
      "HS1",
      "CB1",
      "ST1"
    ],
    [
      "GMT",
      "DV1",
      "PL1",
      "AMI"
    ],
    [
      "LS1",
      "OB1",
      "R2D",
      "C3P",
      "HS1",
      "CB1",
      "GMT",
      "DV1",
      ""
    ],
    [
      "HS1",
      "CB1",
      "LS1",
      "OB1",
      "ST1"
    ],
    [
      "DV1",
      "GMT"
    ],
    [
      "DV1",
      "ST1",
      "LS1",
      "HS1",
      "OB1",
      "CB1",
      "R2D",
      "C3P"
    ],
    [
      "LS1",
      "HS1",
      "OB1",
      "CB1",
      "R2D",
      "C3P",
      "ST1"
    ],
    [
      "LS1",
      "HS1",
      "OB1",
      "CB1",
      "DV1"
    ],
    [
      "LS1",
      "HS1",
      "CB1",
      "PL1",
      "ST1"
    ],
    [
      "DV1",
      "GMT"
    ],
    [
      "HS1",
      "LS1",
      "PL1",
      "CB1",
      "C3P",
      "R2D"
    ],
    [
      "LS1",
      "HS1",
      "PL1",
      "CB1"
    ],
    [
      "LS1",
      "HS1",
      "PL1",
      "CB1",
      "C3P",
      "R2D",
      "ST1"
    ],
    [
      "OB1",
      "LS1",
      "HS1",
      "PL1",
      "CB1",
      "ST1"
    ],
    [
      "LS1",
      "PL1",
      "HS1",
      "CB1",
      "R2D",
      "C3P",
      "OB1",
      "ST1"
    ],
    [
      "LS1",
      "PL1"
    ],
    [
      "DV1",
      "LS1",
      "PL1",
      "HS1",
      "CB1",
      "R2D",
      "C3P",
      "OB1",
      "ST1"
    ],
    [
      "DV1",
      "LS1",
      "PL1",
      "HS1",
      "CB1",
      "R2D",
      "C3P",
      "OB1",
      "ST1"
    ],
    [
      "LS1",
      "HS1",
      "PL1",
      "CB1",
      "C3P",
      "R2D"
    ],
    [
      "DV1",
      "GMT"
    ],
    [
      "DV1",
      "GMT",
      "HS1",
      "LS1",
      "PL1",
      "CB1"
    ],
    [
      "LS1",
      "PL1",
      "HS1",
      "CB1",
      "R2D",
      "C3P",
      "RO1",
      "GW1"
    ],
    [
      "DV1",
      "GMT"
    ],
    [
      "GJD",
      "PL1",
      "LS1",
      "HS1",
      "CB1",
      "RO1",
      "JV1",
      "WA1"
    ],
    [
      "DV1",
      "GMT"
    ],
    [
      "HS1",
      "CB1",
      "LS1",
      "C3P",
      "RO1"
    ],
    [
      "LS1",
      "PL1",
      "R2D",
      "C3P",
      "BD2",
      "RO1",
      "GD1"
    ],
    [
      "PL1",
      "C3P",
      "LS1",
      "BD2",
      "JP1",
      "GJD",
      "WA1",
      "R2D",
      "GD1"
    ],
    [
      "DV1"
    ],
    [
      "LS1",
      "GJD",
      "WA1",
      "BD2",
      "PL1",
      "C3P",
      "PN1",
      "TN1",
      "DK1",
      "JV1",
      "DT1",
      "GD1"
    ],
    [
      "LS1",
      "HS1",
      "DV1",
      "CB1",
      "PL1",
      "C3P",
      "GJD"
    ],
    [
      "PL1",
      "HS1",
      "LS1",
      "C3P",
      "CB1",
      "R2D",
      "RO1"
    ],
    [
      "PL1",
      "HS1",
      "LS1",
      "C3P",
      "CB1",
      "R2D",
      "RO1",
      "GJD"
    ]
  ]
}