|
// 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; |
|
}); |
|
}); |
|
var tooltip_svg = d3.select("body").append('svg') |
|
.attr('width', width) |
|
.attr('height', height); |
|
|
|
tooltip_svg.append("foreignObject") |
|
.attr("width", 600) |
|
.attr("height", 300) |
|
.append("xhtml:p") |
|
.attr("id", "scene_tooltip") |
|
.attr("transform", "translate(0, 20)") |
|
.attr("width", 600) |
|
.attr("height", 300) |
|
.attr("fill", "black") |
|
|
|
// Remove all the temporary labels. |
|
svg.selectAll('text.temp').remove(); |
|
|
|
// Do the layout |
|
narrative = d3.layout.narrative() |
|
.scenes(scenes) |
|
.size([width,height]) |
|
.pathSpace(20) |
|
.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] + 300); |
|
|
|
// Draw links |
|
svg.selectAll('.link').data(narrative.links()).enter() |
|
.append('path') |
|
.attr('id', function(d, i){return "link_" + d.character.id + i}) |
|
.attr('class', function(d) { |
|
return 'link ' + d.character.affiliation.toLowerCase() + ' link_character_' + d.character.id; |
|
}) |
|
.attr('d', narrative.link()) |
|
.on("mousemove", function(d, i){ |
|
highlight_character(d.character.id, true) |
|
}) |
|
.on("mouseout", function(d, i){ |
|
highlight_character(d.character.id, false) |
|
}) |
|
|
|
// Draw the scenes |
|
var g_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]+')'; |
|
}) |
|
g_scenes.append('rect') |
|
.attr('id', function(d, i){return "scene_" + i}) |
|
.attr('width', sceneWidth) |
|
|
|
.attr('height', function(d){ |
|
return d.height; |
|
}) |
|
.attr('y', 0) |
|
.attr('x', 0) |
|
.attr('rx', 3) |
|
.attr('ry', 3); |
|
|
|
g_scenes.on("mousemove", function(d, i){ |
|
//console.log(d.characters) |
|
d3.selectAll("#scene_tooltip").html(d.description) |
|
d3.selectAll("#scene_" + i).style('fill', '#ffd0a8') |
|
|
|
for (var c in d.characters) { |
|
highlight_character(d.characters[c].id, true) |
|
} |
|
}) |
|
g_scenes.on("mouseout", function(d, i){ |
|
//console.log(d.characters) |
|
d3.selectAll("#scene_tooltip").html("") |
|
d3.selectAll("#scene_" + i).style('fill', 'white') |
|
|
|
for (var c in d.characters) { |
|
highlight_character(d.characters[c].id, false) |
|
} |
|
}) |
|
|
|
// 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('fill', function(d){return d.character.affiliation}) |
|
.attr('class', function(d){ |
|
return 'appearance ' + d.character.affiliation + ' appearance_character_' + d.character.id; |
|
}); |
|
|
|
// 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') |
|
.attr('id', function(d){return "character_" + d.character.id}) |
|
.text(function(d){ return d.character.name; }) |
|
.on('mousemove', function(d, i){ |
|
highlight_character(d.character.id, true) |
|
}) |
|
.on('mouseout', function(d, i){ |
|
highlight_character(d.character.id, false) |
|
}) |
|
|
|
g.select('.color') |
|
.attr('class', function(d){ |
|
return 'color ' + d.character.affiliation; |
|
}); |
|
|
|
g.select('rect') |
|
.attr('class', function(d){ |
|
return d.character.affiliation; |
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
function highlight_character(character_id, turn_on) { |
|
if (turn_on) { |
|
d3.selectAll(".link_character_" + character_id) |
|
.style('stroke-width', '3') |
|
d3.selectAll(".appearance_character_" + character_id) |
|
.attr('r', 3) |
|
d3.selectAll("#character_" + character_id) |
|
.style("font-weight", "bold") |
|
} else { |
|
d3.selectAll(".link_character_" + character_id) |
|
.style('stroke-width', '2') |
|
d3.selectAll(".appearance_character_" + character_id) |
|
.attr('r', 2) |
|
d3.selectAll("#character_" + character_id) |
|
.style("font-weight", "normal") |
|
} |
|
} |
|
|
|
function wrangle(data) { |
|
|
|
var charactersMap = {}; |
|
|
|
return data.scenes.map(function(scene){ |
|
return {characters: scene.characters.map(function(id){ |
|
return characterById(id); |
|
}).filter(function(d) { return (d); }), description: scene.description}; |
|
}); |
|
|
|
// 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]; |
|
} |
|
|
|
} |