Skip to content

Instantly share code, notes, and snippets.

@drzax
Last active December 6, 2023 16:28
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 16 You must be signed in to fork a gist
  • Save drzax/81fff35393fb65255621fd0ab8d11bd7 to your computer and use it in GitHub Desktop.
Save drzax/81fff35393fb65255621fd0ab8d11bd7 to your computer and use it in GitHub Desktop.
Narrative Charts
license: mit
scrolling: true
height: 300
// 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];
}
}
{
"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"
]
]
}
<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment