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.
license: mit | |
scrolling: true | |
height: 300 |
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.
// 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> |