Skip to content

Instantly share code, notes, and snippets.

@dudash
Last active November 27, 2016 23:23
Show Gist options
  • Save dudash/f5d60582a1fb486a7075 to your computer and use it in GitHub Desktop.
Save dudash/f5d60582a1fb486a7075 to your computer and use it in GitHub Desktop.
Star Wars Quotes Edge Bundling Viz

StarWars Movie Quotes

an experiment in visualizing movie quotes to characters along with movies

See output here: http://bl.ocks.org/dudash/f5d60582a1fb486a7075

This is an example of hierarchial edge bundling, it was based on the work done here: http://bl.ocks.org/mbostock/7607999

The data set was hand built by me - a more advanced example would parse movie scripts to pull in all characters.

  1. What is the data that you chose? Why? I choose star wars because it is enjoyable to me. Also, because a new movie is coming out soon, and it was fresh in my mind.

  2. Did you use a subset of the data? If so, what was it? Yes, I had to hand build my data set because I didn’t have time to search for scripts and parse + cleanse the data. Additionally, the data set is meant to be a curated list of popular quotes, not all movie lines.

  3. Are there any particular aspects of your visualization to which you would like to bring attention? The visualization is interactive, hover over any of the nodes and see the relationships. If you hover over a quote it will show what movie the quote appeared in as well as what character(s) said it. If you hover over a character it will highlight what quotes they’ve said as well as what movies the character has appeared in. If you hover over a movie it will show what characters and quotes appear within it.

  4. What do you think the data, and your visualization, shows? It is a fun way of showing movie character appearances paired with memorable quotes from those characters. You start to get a feel from this small data set how the visualization would work. Additional quotes whenever a character mentions "may force be with you" would map many times to multiple characters and movies.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.node {
font: 300 11px "Helvetica Neue", Helvetica, Arial, sans-serif;
fill: #bbb;
}
.node:hover {
fill: #000;
}
.link {
stroke: steelblue;
stroke-opacity: .4;
fill: none;
pointer-events: none;
}
.node:hover,
.node--source,
.node--target {
font-weight: 700;
}
.node--source {
fill: #2ca02c;
}
.node--target {
fill: #d62728;
}
.link--source,
.link--target {
stroke-opacity: 1;
stroke-width: 2px;
}
.link--source {
stroke: #d62728;
}
.link--target {
stroke: #2ca02c;
}
</style>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script>
var diameter = 960,
radius = diameter / 2,
innerRadius = radius - 120;
var cluster = d3.layout.cluster()
.size([360, innerRadius])
.sort(null)
.value(function(d) { return d.size; });
var bundle = d3.layout.bundle();
var line = d3.svg.line.radial()
.interpolate("bundle")
.tension(.85)
.radius(function(d) { return d.y; })
.angle(function(d) { return d.x / 180 * Math.PI; });
var svg = d3.select("body").append("svg")
.attr("width", diameter)
.attr("height", diameter)
.append("g")
.attr("transform", "translate(" + radius + "," + radius + ")");
var link = svg.append("g").selectAll(".link"),
node = svg.append("g").selectAll(".node");
d3.json("starwars-xs.json", function(error, classes) {
if (error) throw error;
var nodes = cluster.nodes(packageHierarchy(classes)),
links = packageImports(nodes);
link = link
.data(bundle(links))
.enter().append("path")
.each(function(d) { d.source = d[0], d.target = d[d.length - 1]; })
.attr("class", "link")
.attr("d", line);
node = node
.data(nodes.filter(function(n) { return !n.children; }))
.enter().append("text")
.attr("class", "node")
.attr("dy", ".31em")
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + (d.y + 8) + ",0)" + (d.x < 180 ? "" : "rotate(180)"); })
.style("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
.text(function(d) { return d.key; })
.on("mouseover", mouseovered)
.on("mouseout", mouseouted);
});
function mouseovered(d) {
node
.each(function(n) { n.target = n.source = false; });
link
.classed("link--target", function(l) { if (l.target === d) return l.source.source = true; })
.classed("link--source", function(l) { if (l.source === d) return l.target.target = true; })
.filter(function(l) { return l.target === d || l.source === d; })
.each(function() { this.parentNode.appendChild(this); });
node
.classed("node--target", function(n) { return n.target; })
.classed("node--source", function(n) { return n.source; });
}
function mouseouted(d) {
link
.classed("link--target", false)
.classed("link--source", false);
node
.classed("node--target", false)
.classed("node--source", false);
}
d3.select(self.frameElement).style("height", diameter + "px");
// Lazily construct the package hierarchy from class names.
function packageHierarchy(classes) {
var map = {};
function find(name, data) {
var node = map[name], i;
if (!node) {
node = map[name] = data || {name: name, children: []};
if (name.length) {
node.parent = find(name.substring(0, i = name.lastIndexOf(".")));
node.parent.children.push(node);
node.key = name.substring(i + 1);
}
}
return node;
}
classes.forEach(function(d) {
find(d.name, d);
});
return map[""];
}
// Return a list of imports for the given array of nodes.
function packageImports(nodes) {
var map = {},
imports = [];
// Compute a map from name to node.
nodes.forEach(function(d) {
map[d.name] = d;
});
// For each import, construct a link from the source to target node.
nodes.forEach(function(d) {
if (d.imports) d.imports.forEach(function(i) {
imports.push({source: map[d.name], target: map[i]});
});
});
return imports;
}
</script>
[
{"name":"Quote.Bad Feeling About This",
"size": 100,
"imports": [
"Movie.Episode IV A New Hope",
"Movie.Episode V Empire Strikes Back",
"Movie.Episode VI Return of the Jedi",
"Movie.Episode I The Phantom Menace",
"Movie.Episode II Attack of the Clones",
"Movie.Episode III Revenge of the Sith"
]
},
{"name":"Quote.It's a Trap!",
"size": 50,
"imports": [
"Movie.Episode VI Return of the Jedi"
]
},
{"name":"Quote.What an incredible smell you've discovered!",
"size": 50,
"imports": [
"Movie.Episode IV A New Hope"
]
},
{"name":"Quote.Do, or do not, there is no try",
"size": 50,
"imports": [
"Movie.Episode V Empire Strikes Back"
]
},
{
"name":"Movie.Episode IV A New Hope",
"size": 2,
"imports": [
"Character.Luke Skywalker",
"Character.Hans Solo",
"Character.Princess Leia",
"Character.C3PO",
"Character.Obi-Wan Kenobi",
"Character.Darth Vader",
"Character.R2D2",
"Character.Chewbacca"
]
},
{
"name":"Movie.Episode V Empire Strikes Back",
"size": 2,
"imports": [
"Character.Luke Skywalker",
"Character.Hans Solo",
"Character.Princess Leia",
"Character.C3PO",
"Character.Darth Vader",
"Character.Yoda",
"Character.R2D2",
"Character.Chewbacca",
"Character.Palpatine",
"Character.Spirit of Obi-Wan Kenobi"
]
},
{
"name":"Movie.Episode VI Return of the Jedi",
"size": 2,
"imports": [
"Character.Luke Skywalker",
"Character.Hans Solo",
"Character.Princess Leia",
"Character.C3PO",
"Character.Darth Vader",
"Character.Yoda",
"Character.R2D2",
"Character.Jabba the Hutt",
"Character.Chewbacca",
"Character.Admiral Ackbar",
"Character.Palpatine",
"Character.Spirit of Obi-Wan Kenobi"
]
},
{
"name":"Movie.Episode I The Phantom Menace",
"size": 2,
"imports": [
"Character.C3PO",
"Character.Obi-Wan Kenobi",
"Character.Anakin Skywalker",
"Character.Yoda",
"Character.R2D2",
"Character.Jabba the Hutt",
"Character.Qui-Gon Jinn",
"Character.Palpatine",
"Character.Padme Amidala"
]
},
{
"name":"Movie.Episode II Attack of the Clones",
"size": 2,
"imports": [
"Character.C3PO",
"Character.Obi-Wan Kenobi",
"Character.Anakin Skywalker",
"Character.Yoda",
"Character.R2D2",
"Character.Palpatine",
"Character.Padme Amidala"
]
},
{
"name":"Movie.Episode III Revenge of the Sith",
"size": 2,
"imports": [
"Character.C3PO",
"Character.Obi-Wan Kenobi",
"Character.Anakin Skywalker",
"Character.Darth Vader",
"Character.Yoda",
"Character.R2D2",
"Character.Chewbacca",
"Character.Palpatine",
"Character.Padme Amidala"
]
},
{"name":"Character.Luke Skywalker",
"size": 1,
"imports": [
"Quote.Bad Feeling About This"
]
},
{
"name":"Character.Hans Solo",
"size": 1,
"imports": [
"Quote.Bad Feeling About This",
"Quote.What an incredible smell you've discovered!"
]
},
{
"name":"Character.Princess Leia",
"size": 1,
"imports": [
"Quote.Bad Feeling About This"
]
},
{
"name":"Character.C3PO",
"size": 1,
"imports": [
"Quote.Bad Feeling About This"
]
},
{
"name":"Character.Spirit of Obi-Wan Kenobi",
"size": 1,
"imports": [
]
},
{
"name":"Character.Obi-Wan Kenobi",
"size": 1,
"imports": [
"Quote.Bad Feeling About This"
]
},
{
"name":"Character.Anakin Skywalker",
"size": 1,
"imports": [
"Quote.Bad Feeling About This"
]
},
{
"name":"Character.Darth Vader",
"size": 1,
"imports": [
]
},
{
"name":"Character.Admiral Ackbar",
"size": 1,
"imports": [
"Quote.It's a Trap!"
]
},
{
"name":"Character.Qui-Gon Jinn",
"size": 1,
"imports": [
]
},
{
"name":"Character.R2D2",
"size": 1,
"imports": [
]
},
{
"name":"Character.Palpatine",
"size": 1,
"imports": [
]
},
{
"name":"Character.Padme Amidala",
"size": 1,
"imports": [
]
},
{
"name":"Character.Chewbacca",
"size": 1,
"imports": [
]
},
{
"name":"Character.Jabba the Hutt",
"size": 1,
"imports": [
]
},
{
"name":"Character.Yoda",
"size": 1,
"imports": [
"Quote.Do, or do not, there is no try"
]
}
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment