Skip to content

Instantly share code, notes, and snippets.

@anbnyc
Last active July 26, 2017 17:31
Show Gist options
  • Save anbnyc/615bab659945d323b8481a3c647e134a to your computer and use it in GitHub Desktop.
Save anbnyc/615bab659945d323b8481a3c647e134a to your computer and use it in GitHub Desktop.
Game of Thrones d3.pack layout
<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