Last active
July 26, 2017 17:31
-
-
Save anbnyc/615bab659945d323b8481a3c647e134a to your computer and use it in GitHub Desktop.
Game of Thrones d3.pack layout
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.9.1/d3.min.js"></script> | |
<style> | |
h1{ | |
text-align: center; | |
} | |
h1, p, text{ | |
font-family: Palatino; | |
-webkit-user-select: none; | |
-moz-user-select: none; | |
-ms-user-select: none; | |
} | |
/****/ | |
circle{ | |
stroke: #444; | |
stroke-width: 1px; | |
} | |
.node text{ | |
font-size: 10px; | |
text-anchor: middle; | |
} | |
.node text.header { | |
font-style: italic; | |
} | |
/****/ | |
.book text.title{ | |
font-weight: 800; | |
font-size: 12px; | |
text-anchor: end; | |
} | |
.char text{ | |
font-size: 10px; | |
opacity: 0; | |
text-anchor: end; | |
} | |
.char.hovered text{ | |
opacity: 1; | |
} | |
.char rect{ | |
stroke: #888; | |
stroke-width: 1px; | |
opacity: .7; | |
} | |
.char:hover rect{ | |
opacity: .9; | |
} | |
.char.dimmed rect{ | |
opacity: .2; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>POV Characters from Game of Thrones Books</h1> | |
<svg></svg> | |
<p>Data from <a href="anapioficeandfire.com">An API of Ice and Fire</a>. Click to interact.</p> | |
</body> | |
<script> | |
const apiUrl = "https://www.anapioficeandfire.com/api/" | |
const getCharId = o => o.replace("https://www.anapioficeandfire.com/api/characters/","") | |
const h = 800, w = 1200; | |
var color = d3.scaleOrdinal().range([ | |
"#938F46", | |
"#582919", | |
"#22162C", | |
"#3F3D28", | |
"#B39A61", | |
"#8F6882", | |
"#DB8678", | |
"#99584E", | |
"#D0957C", | |
"#F2CFA3", | |
"#BCCBE8", | |
"#7286A1", | |
"#1C2134", | |
"#908C8D", | |
]) | |
var packColor = { | |
0: d => 'gray', | |
1: d => color(d.data.name), | |
2: d => color(d.parent.data.name) | |
} | |
var svg = d3.select("svg") | |
.attr("height", h) | |
.attr("width", w); | |
function getBooks(houses){ | |
var books = {} | |
fetch(apiUrl+'books/') | |
.then(function(r){ | |
return r.json() | |
}) | |
.then(function(data){ | |
data | |
.filter(o => o.povCharacters.length > 1) | |
.map((e,i) => books[e.name] = { | |
povCharacters: e.povCharacters.map(o => getCharId(o)), | |
order: i, | |
title: e.name | |
}) | |
getCharacters(books) | |
}) | |
.catch(function(error){ | |
console.error(error) | |
}) | |
} | |
function getCharacters(books){ | |
var rawCharacters = []; | |
Object.values(books).map(e => { | |
e.povCharacters.map(c => { | |
rawCharacters.push(fetch(apiUrl+'characters/'+c).then(r => r.json())); | |
}) | |
}) | |
var characters = {} | |
Promise.all(rawCharacters).then(function(data){ | |
data.map(function(o){ | |
characters[getCharId(o.url)] = o; | |
}); | |
drawGraph(books, characters); | |
}) | |
} | |
function drawGraph(bookData, charData){ | |
var hKeys = {} | |
var hData = { | |
name: 'root', | |
children: [] | |
}; | |
for(var k in charData){ | |
let culture = charData[k].culture; | |
if(hKeys[culture] === undefined){ | |
hKeys[culture] = hData.children.length; | |
hData.children.push({ | |
name: culture, | |
children: [] | |
}) | |
} | |
hData.children[hKeys[culture]].children.push({ | |
name: charData[k].name, | |
value: charData[k].povBooks.length | |
}) | |
} | |
var root = d3.hierarchy(hData); | |
var packLayout = d3.pack(); | |
packLayout.size([w*2/3, w*2/3]) | |
root.sum(d => d.value) | |
packLayout(root); | |
const opaciter = f => .3 + .1*f | |
var nodes = svg.selectAll('g.node') | |
.data(root.descendants()) | |
.enter().append("g") | |
.attr("transform", d => "translate("+d.x+","+d.y+")") | |
.attr("class","node") | |
.on('mouseenter', function(d){ | |
if(d.data.name !== "root"){ | |
d3.select(this) | |
.select("circle") | |
.style("opacity", d => opaciter(d.depth) + .2) | |
} | |
}) | |
.on('mouseout', function(d){ | |
d3.select(this) | |
.select("circle") | |
.style("opacity", d => opaciter(d.depth)) | |
}) | |
.on('click', function(d){ | |
d3.selectAll("g.char") | |
.filter(c => c.culture !== d.data.name && c.name !== d.data.name) | |
.classed("dimmed",true) | |
d3.selectAll(".node circle") | |
.filter(c => c.data.name !== d.data.name && c.parent && c.parent.data.name !== d.data.name) | |
.style('opacity', d => .1) | |
}) | |
.on('dblclick', function(){ | |
d3.selectAll("g.char.dimmed") | |
.classed("dimmed",false); | |
d3.selectAll(".node circle") | |
.style('opacity', d => opaciter(d.depth)) | |
}); | |
nodes.append("circle") | |
.style('opacity', d => opaciter(d.depth)) | |
.attr('fill', d => d3.color(packColor[d.depth](d)).brighter(Math.random()) ) | |
.attr('r', d => d.r); | |
nodes.append("text") | |
.attr("y", d => !d.children ? 0 : 15 - d.r ) | |
.classed("header", d => !!d.children) | |
.text(d => d.data.name !== "root" ? d.data.name : ""); | |
/************/ | |
var books = svg.selectAll("g.book") | |
.data(Object.values(bookData)) | |
.enter().append("g") | |
.attr("class","book") | |
.attr("transform",(d,i) => "translate("+(125 + w*2/3)+","+(h/4+(i*80))+")"); | |
books.append("text") | |
.attr("class","title") | |
.attr("x", -5) | |
.attr("y", 20) | |
.text(d => d.title) | |
var chars = books.selectAll("g.char") | |
.data(d => d.povCharacters.map(o => charData[o])) | |
.enter().append("g") | |
.attr("class","char") | |
.attr("transform",(d,i) => "translate("+i*15+",0)") | |
.on('mouseenter', function(d){ | |
d3.selectAll("g.char") | |
.filter(c => c.name === d.name) | |
.classed("hovered",true); | |
}) | |
.on('mouseout', function(){ | |
d3.selectAll("g.char.hovered") | |
.classed("hovered",false) | |
}) | |
.on('click', function(d){ | |
d3.selectAll("g.char") | |
.filter(c => c.name !== d.name) | |
.classed("dimmed",true) | |
d3.selectAll(".node circle") | |
.filter(c => c.data.name !== d.name) | |
.style('opacity', d => .1) | |
}) | |
.on('dblclick', function(){ | |
d3.selectAll("g.char.dimmed") | |
.classed("dimmed",false); | |
d3.selectAll(".node circle") | |
.style('opacity', d => opaciter(d.depth)) | |
}); | |
chars.append("rect") | |
.attr("height",30) | |
.attr("width",15) | |
.attr("fill",d => color(d.culture)); | |
chars.append("text") | |
.attr("transform","rotate(-30)") | |
.attr("x",-8) | |
.attr("y",40) | |
.text(d => d.name); | |
} | |
getBooks(); | |
</script> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment