Skip to content

Instantly share code, notes, and snippets.

@standarderror
Last active July 7, 2016 09:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save standarderror/1d5d573022b4fb7cc60d to your computer and use it in GitHub Desktop.
Save standarderror/1d5d573022b4fb7cc60d to your computer and use it in GitHub Desktop.
D3.js network of murders in The Wire

A force-directed graph of murders in TV show The Wire, discussed at blog subsubroutine.com. Created with D3.js.

Credits:

<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
background-color: #FFF;
}
text {
fill: #000;
font: 10px sans-serif;
pointer-events: none;
}
circle {
fill: #ccc;
stroke: #fff;
stroke-width: 1.5px;
}
#network {
min-height:500px;
position:absolute;
margin:auto;
top: 0; left: 0; bottom: 0; right: 0;
float:left;
}
#network rect:hover{
fill: black;
}
.link {
stroke: #999;
stroke-opacity: .6;
stroke-width: 1.5px;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.js"></script>
<svg id='network'></svg>
<script>
// note that nodes must be listed in id order corresponding to links
// so first node corresponds to 0, second to 1, etc.
var nodes = [
{node_name:'9_year_old_TT', name:'9 year old T.T.', group:2,value:0}
,{node_name:'Anton_Stinkum_Artis', name:'Anton "Stinkum" Artis', group:2,value:2}
,{node_name:'Avon_Barksdale', name:'Avon Barksdale', group:8,value:5}
,{node_name:'Bodie_Broadus', name:'Bodie Broadus', group:2,value:1}
,{node_name:'Bodies_mother', name:"Bodie's mother", group:1,value:0}
,{node_name:'Boo', name:'Boo', group:2,value:0}
,{node_name:'Boy_in_a_Vacant_lot', name:'Boy in a Vacant lot', group:2,value:0}
,{node_name:'Brandon_Wright', name:'Brandon Wright', group:3,value:0}
,{node_name:'Brother_Mouzone', name:'Brother Mouzone', group:8,value:1}
,{node_name:'Bubbles', name:'Bubbles', group:8,value:1}
,{node_name:'Butchie', name:'Butchie', group:2,value:0}
,{node_name:'Butchies_bodyguard', name:"Butchie's bodyguard", group:2,value:0}
,{node_name:'Cheese_Wagstaff', name:'Cheese Wagstaff', group:2,value:1}
,{node_name:'Chipper', name:'Chipper', group:2,value:0}
,{node_name:'Chris_Partlow', name:'Chris Partlow', group:8,value:36}
,{node_name:'Country', name:'Country', group:2,value:0}
,{node_name:'Curtis_Lex_Anderson', name:'Curtis "Lex" Anderson', group:2,value:1}
,{node_name:'DAngelo_Barksdale', name:"D'Angelo Barksdale", group:4,value:1}
,{node_name:'Dante', name:'Dante', group:8,value:1}
,{node_name:'Dennis_Cutty_Wise', name:'Dennis "Cutty" Wise', group:8,value:1}
,{node_name:'Devar_Manigault', name:'Devar Manigault', group:0,value:0}
,{node_name:'Devonne', name:'Devonne', group:2,value:0}
,{node_name:'Dierdre_Kresson', name:'Dierdre Kresson', group:2,value:0}
,{node_name:'Donnie', name:'Donnie', group:2,value:0}
,{node_name:'Elijah_Davis', name:'Elijah Davis', group:2,value:0}
,{node_name:'Frank_Sobotka', name:'Frank Sobotka', group:3,value:0}
,{node_name:'Fredo_Braddock', name:'Fredo Braddock', group:2,value:0}
,{node_name:'Fruit', name:'Fruit', group:2,value:0}
,{node_name:'George_Double_G_Glekas', name:'George "Double G" Glekas', group:2,value:0}
,{node_name:'Hungry_Man', name:'Hungry Man', group:2,value:0}
,{node_name:'Illegal_immigrant_female_1', name:'Illegal immigrant female 1', group:0,value:0}
,{node_name:'Illegal_immigrant_female_10', name:'Illegal immigrant female 10', group:5,value:0}
,{node_name:'Illegal_immigrant_female_11', name:'Illegal immigrant female 11', group:5,value:0}
,{node_name:'Illegal_immigrant_female_12', name:'Illegal immigrant female 12', group:5,value:0}
,{node_name:'Illegal_immigrant_female_13', name:'Illegal immigrant female 13', group:5,value:0}
,{node_name:'Illegal_immigrant_female_14', name:'Illegal immigrant female 14', group:5,value:0}
,{node_name:'Illegal_immigrant_female_15', name:'Illegal immigrant female 15', group:5,value:0}
,{node_name:'Illegal_immigrant_female_2', name:'Illegal immigrant female 2', group:5,value:0}
,{node_name:'Illegal_immigrant_female_3', name:'Illegal immigrant female 3', group:5,value:0}
,{node_name:'Illegal_immigrant_female_4', name:'Illegal immigrant female 4', group:5,value:0}
,{node_name:'Illegal_immigrant_female_5', name:'Illegal immigrant female 5', group:5,value:0}
,{node_name:'Illegal_immigrant_female_6', name:'Illegal immigrant female 6', group:5,value:0}
,{node_name:'Illegal_immigrant_female_7', name:'Illegal immigrant female 7', group:5,value:0}
,{node_name:'Illegal_immigrant_female_8', name:'Illegal immigrant female 8', group:5,value:0}
,{node_name:'Illegal_immigrant_female_9', name:'Illegal immigrant female 9', group:5,value:0}
,{node_name:'Jelly', name:'Jelly', group:2,value:0}
,{node_name:'Jessup_Correctional_Facility_inmate_1', name:'Jessup Correctional Facility inmate 1', group:1,value:0}
,{node_name:'Jessup_Correctional_Facility_inmate_2', name:'Jessup Correctional Facility inmate 2', group:1,value:0}
,{node_name:'Jessup_Correctional_Facility_inmate_3', name:'Jessup Correctional Facility inmate 3', group:1,value:0}
,{node_name:'Jessup_Correctional_Facility_inmate_4', name:'Jessup Correctional Facility inmate 4', group:1,value:0}
,{node_name:'Jessup_Correctional_Facility_inmate_5', name:'Jessup Correctional Facility inmate 5', group:1,value:0}
,{node_name:'John_Bailey', name:'John Bailey', group:2,value:0}
,{node_name:'Junebug', name:'Junebug', group:2,value:0}
,{node_name:'Kenard', name:'Kenard', group:8,value:1}
,{node_name:'Kiesha', name:'Kiesha', group:1,value:0}
,{node_name:'Kimmy', name:'Kimmy', group:8,value:1}
,{node_name:'LaTroy', name:'LaTroy', group:2,value:0}
,{node_name:'Little_Kevin', name:'Little Kevin', group:2,value:0}
,{node_name:'Little_Man', name:'Little Man', group:2,value:1}
,{node_name:'Manny', name:'Manny', group:2,value:0}
,{node_name:'Marlo_Stanfield', name:'Marlo Stanfield', group:8,value:1}
,{node_name:'Marquis_Bird_Hilton', name:'Marquis "Bird" Hilton', group:8,value:4}
,{node_name:'Maurice_Scroggins', name:'Maurice Scroggins', group:2,value:0}
,{node_name:'Michael_Lee', name:'Michael Lee', group:8,value:2}
,{node_name:'Nakeesha_Lyles', name:'Nakeesha Lyles', group:2,value:0}
,{node_name:'New_York_Dealer_1', name:'New York Dealer 1', group:2,value:0}
,{node_name:'New_York_Dealer_2', name:'New York Dealer 2', group:2,value:0}
,{node_name:'New_York_Dealer_3', name:'New York Dealer 3', group:7,value:0}
,{node_name:'New_York_Dealer_4', name:'New York Dealer 4', group:7,value:0}
,{node_name:'O-Dog', name:'O-Dog', group:8,value:1}
,{node_name:'Officer Derrick_Waggoner', name:'Officer Derrick Waggoner', group:2,value:0}
,{node_name:'Old_Face_Andre', name:'Old Face Andre', group:2,value:0}
,{node_name:'Omar_Little', name:'Omar Little', group:2,value:5}
,{node_name:'Pooh_Blanchard', name:'Pooh Blanchard', group:2,value:0}
,{node_name:'Poot_Carr', name:'Poot Carr', group:8,value:1}
,{node_name:'Proposition_Joe', name:'Proposition Joe', group:2,value:0}
,{node_name:'Random_in_vacant_1', name:'Random in vacant 1', group:2,value:0}
,{node_name:'Random_in_vacant_10', name:'Random in vacant 10', group:2,value:0}
,{node_name:'Random_in_vacant_11', name:'Random in vacant 11', group:2,value:0}
,{node_name:'Random_in_vacant_12', name:'Random in vacant 12', group:2,value:0}
,{node_name:'Random_in_vacant_13', name:'Random in vacant 13', group:2,value:0}
,{node_name:'Random_in_vacant_14', name:'Random in vacant 14', group:2,value:0}
,{node_name:'Random_in_vacant_15', name:'Random in vacant 15', group:2,value:0}
,{node_name:'Random_in_vacant_16', name:'Random in vacant 16', group:2,value:0}
,{node_name:'Random_in_vacant_17', name:'Random in vacant 17', group:2,value:0}
,{node_name:'Random_in_vacant_2', name:'Random in vacant 2', group:2,value:0}
,{node_name:'Random_in_vacant_3', name:'Random in vacant 3', group:2,value:0}
,{node_name:'Random_in_vacant_4', name:'Random in vacant 4', group:2,value:0}
,{node_name:'Random_in_vacant_5', name:'Random in vacant 5', group:2,value:0}
,{node_name:'Random_in_vacant_6', name:'Random in vacant 6', group:2,value:0}
,{node_name:'Random_in_vacant_7', name:'Random in vacant 7', group:2,value:0}
,{node_name:'Random_in_vacant_8', name:'Random in vacant 8', group:2,value:0}
,{node_name:'Random_in_vacant_9', name:'Random in vacant 9', group:2,value:0}
,{node_name:'Rico', name:'Rico', group:2,value:0}
,{node_name:'Roland_Prez_Pryzbylewski', name:'Roland "Prez" Pryzbylewski', group:8,value:1}
,{node_name:'Roland_Wee-Bey_Brice', name:'Roland "Wee-Bey" Brice', group:8,value:8}
,{node_name:'Roland_Legget', name:'Roland Legget', group:2,value:0}
,{node_name:'Sam_The_Atlantic_Light_Crewman', name:'Sam, The Atlantic Light Crewman', group:6,value:15}
,{node_name:'Savino_Bratton', name:'Savino Bratton', group:2,value:0}
,{node_name:'Sherrod', name:'Sherrod', group:1,value:0}
,{node_name:'Slim_Charles', name:'Slim Charles', group:8,value:2}
,{node_name:'Snoop', name:'Snoop', group:2,value:0}
,{node_name:'Snoop_Pearson', name:'Snoop Pearson', group:8,value:30}
,{node_name:'Snot_Boogie', name:'Snot Boogie', group:2,value:0}
,{node_name:'Stringer_Bell', name:'Stringer Bell', group:2,value:0}
,{node_name:'Stringer_Bells_bodyguard', name:"Stringer Bell's bodyguard", group:2,value:0}
,{node_name:'Tank', name:'Tank', group:2,value:0}
,{node_name:'Tatar', name:'Tatar', group:2,value:0}
,{node_name:'Toreen_Boyd', name:'Toreen Boyd', group:2,value:0}
,{node_name:'Tosha_Mitchell', name:'Tosha Mitchell', group:2,value:0}
,{node_name:'Tree', name:'Tree', group:8,value:1}
,{node_name:'Unnamed_dealer_1', name:'Unnamed dealer 1', group:2,value:0}
,{node_name:'Unnamed_dealer_2', name:'Unnamed dealer 2', group:2,value:0}
,{node_name:'Unnamed_delivery_woman', name:'Unnamed delivery woman', group:2,value:0}
,{node_name:'Unnamed_gang_member_1', name:'Unnamed gang member 1', group:2,value:0}
,{node_name:'Unnamed_security_guard', name:'Unnamed security guard', group:2,value:0}
,{node_name:'Wallace', name:'Wallace', group:2,value:0}
,{node_name:'Wendell_Orlando_Blocker', name:'Wendell "Orlando" Blocker', group:2,value:0}
,{node_name:'William_Gant', name:'William Gant', group:2,value:0}
,{node_name:'Ziggy_Sobotka', name:'Ziggy Sobotka', group:8,value:1}
,{node_name:'Unnamed_corner_boy', name:'Unnamed corner boy', group:2,value:0}
,{node_name:'Spiros_Vondas', name:'Spiros Vondas', group:8,value:1}
];
var links = [
{source:61, target:118}
,{source:95, target:51}
,{source:61, target:51}
,{source:95, target:7}
,{source:61, target:7}
,{source:1, target:7}
,{source:95, target:54}
,{source:61, target:54}
,{source:1, target:54}
,{source:72, target:1}
,{source:95, target:117}
,{source:58, target:117}
,{source:95, target:58}
,{source:3, target:116}
,{source:74, target:116}
,{source:121, target:97}
,{source:119, target:28}
,{source:110, target:45}
,{source:18, target:109}
,{source:55, target:106}
,{source:100, target:5}
,{source:102, target:93}
,{source:14, target:107}
,{source:94, target:70}
,{source:60, target:21}
,{source:72, target:105}
,{source:72, target:104}
,{source:8, target:104}
,{source:102, target:120}
,{source:14, target:52}
,{source:102, target:52}
,{source:14, target:114}
,{source:102, target:114}
,{source:102, target:11}
,{source:14, target:10}
,{source:12, target:29}
,{source:14, target:75}
,{source:14, target:23}
,{source:72, target:59}
,{source:72, target:98}
,{source:53, target:72}
,{source:63, target:101}
,{source:100, target:12}
,{source:17, target:73}
,{source:95, target:96}
,{source:95, target:108}
,{source:95, target:22}
,{source:19, target:24}
,{source:14, target:76}
,{source:102, target:76}
,{source:14, target:85}
,{source:102, target:85}
,{source:14, target:86}
,{source:102, target:86}
,{source:14, target:87}
,{source:102, target:87}
,{source:14, target:88}
,{source:102, target:88}
,{source:14, target:89}
,{source:102, target:89}
,{source:14, target:90}
,{source:102, target:90}
,{source:14, target:91}
,{source:102, target:91}
,{source:14, target:92}
,{source:102, target:92}
,{source:14, target:77}
,{source:102, target:77}
,{source:14, target:78}
,{source:102, target:78}
,{source:14, target:79}
,{source:102, target:79}
,{source:14, target:80}
,{source:102, target:80}
,{source:14, target:81}
,{source:102, target:81}
,{source:14, target:82}
,{source:102, target:82}
,{source:14, target:83}
,{source:102, target:83}
,{source:14, target:84}
,{source:102, target:84}
,{source:2, target:46}
,{source:2, target:47}
,{source:2, target:48}
,{source:2, target:49}
,{source:2, target:50}
,{source:97, target:30}
,{source:97, target:37}
,{source:97, target:38}
,{source:97, target:39}
,{source:97, target:40}
,{source:97, target:41}
,{source:97, target:42}
,{source:97, target:43}
,{source:97, target:44}
,{source:97, target:31}
,{source:97, target:32}
,{source:97, target:33}
,{source:97, target:34}
,{source:97, target:35}
,{source:97, target:36}
,{source:14, target:111}
,{source:16, target:27}
,{source:14, target:16}
,{source:102, target:16}
,{source:14, target:115}
,{source:102, target:115}
,{source:14, target:113}
,{source:14, target:6}
,{source:102, target:6}
,{source:14, target:65}
,{source:14, target:66}
,{source:102, target:66}
,{source:14, target:67}
,{source:102, target:67}
,{source:14, target:68}
,{source:102, target:68}
,{source:14, target:71}
,{source:102, target:71}
,{source:14, target:57}
,{source:14, target:20}
,{source:9, target:99}
,{source:69, target:3}
,{source:63, target:112}
];
// dimensions of plot
var width = 800,
height = 500;
// Colour scheme for nodes by cause of death
var color = d3.scale.category10().domain(['8','1','2','3','4','5','6','7','0']);
var grayscale = d3.scale.linear()
.domain([0, 10])
.range(["white", "black"]);
// opacityscale = scale min-max link values between 0 and 1
var opacityscale = d3.scale.linear()
.domain([d3.min(links, function(d) { return d.value; }),
d3.max(links, function(d) { return d.value; })])
.range([0.2,1]);
// for scaling node sizes based on # of murders
var hitsscale = d3.scale.linear()
.domain([d3.min(nodes, function(d) { return d.value; }),
d3.max(nodes, function(d) { return d.value; })])
.range([5,10]);
//// create network plot ////
var force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(30)
.linkStrength(1)
.charge(-300)
.friction(0.7)
.gravity(0.5)
.on("tick", tick)
.start();
var svg = d3.select("#network")
.attr("width", width)
.attr("height", height);
// build the arrow
// Thanks to Mike Bostock for this code (and much more below)
svg.append("svg:defs").selectAll("marker")
.data(["end"])
.enter()
.append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 20)
.attr("refY", -0.5)
.attr("markerWidth", 4)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5")
;
var path = svg.selectAll(".link")
.data(force.links())
.enter().append("line")
.attr("class", "link")
.attr("marker-end", "url(#end)")
.style("stroke", function(d) { return color(d.target.group); })
;
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.call(force.drag)
.on('dblclick', connectedNodes)
;
node.append("circle")
.style("fill", function(d) { return color(d.group); })
.attr("r", function(d) { return hitsscale(d.value); })
.attr("opacity", 1.0)
.attr('class', function(d) { return 'node' + d.node_name;} )
;
node.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name; })
.attr('class', function(d) { return 'label' + d.node_name;} )
.style({opacity:'0.0'})
;
function tick() {
path.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"; });
}
// INTERACTIVITY functions for network
node.on("mouseover", function(d){
d3.select(this.parentNode.appendChild(this))
.select("text")
.transition()
.duration(400)
.style('opacity',1);
d3.selectAll("." + d.node_name)
.attr('fill', '#000000');
d3.selectAll("." + d.node_name)
.transition()
.duration(400)
.attr("x", function(d) { return (width-x(d.value))/2 ; })
.transition()
.duration(400)
.attr("x", function(d) { return width - x(d.value) ; })
;
});
node.on("mouseout", function(d){
d3.select(this).select("text").transition()
.duration(750)
.style('opacity',0);
d3.selectAll("." + d.node_name)
.attr('fill', function(d) { return color(d.group); });
});
/// CODE TO ALLOW DOUBLE-CLICK TO FOCUS ON LOCAL NETWORK ///
//Toggle stores whether the highlighting is on
var toggle = 0;
//Create an array logging what is connected to what
var linkedByIndex = {};
for (i = 0; i < nodes.length; i++) {
linkedByIndex[i + "," + i] = 1;
};
links.forEach(function (d) {
linkedByIndex[d.source.index + "," + d.target.index] = 1;
});
//This function looks up whether a pair are neighbours
function neighboring(a, b) {
return linkedByIndex[a.index + "," + b.index];
}
function connectedNodes() {
if (toggle == 0) {
console.log(toggle);
//Reduce the op
toggle = 1;
//Reduce the opacity of all but the neighbouring nodes
d = d3.select(this).node().__data__;
node.style("opacity", function (o) {
return neighboring(d, o) | neighboring(o, d) ? 1 : 0.1;
});
path.style("opacity", function (o) {
return d.index==o.source.index | d.index==o.target.index ? 1 : 0.1;
});
} else {
console.log(toggle);
toggle = 0;
//Put them back to opacity=1
node.style("opacity", 1);
path.style("opacity", 1);
}
}
/// Add legend to LHS ///
var legend = [
{group:0, target:'Beaten to death'}
, {group:1, target:'Overdose'}
, {group:2, target:'Shot'}
, {group:3, target:'Stabbed'}
, {group:4, target:'Strangled'}
, {group:5, target:'Suffocated'}
, {group:6, target:'Tortured'}
, {group:7, target:'Unknown'}
, {group:8, target:'Survived'}
];
var circle = svg.selectAll("circle2")
.data(legend)
;
svg.selectAll("text2")
.data([0], function(d) { return d; })
.enter()
.append("text")
.text("Legend")
.attr("x", 5).attr("y", 50)
.style("font-weight","bold");
circle.enter().append("circle")
.attr("cy", function(d) { return 22 * d.group + 65; })
.attr("cx", 15)
.attr("r", 9)
.style("fill", function(d) { return color(d.group); })
;
circle.enter().append("text")
.attr("dy", function(d) { return 22 * d.group + 68; })
.attr("dx", 30)
.text(function(d) { return d.target; })
;
svg.selectAll("text2")
.data([0], function(d) { return d; })
.enter()
.append("text")
.text("Click to drag")
.attr("x", 5).attr("y", 272)
;
svg.selectAll("text2")
.data([0], function(d) { return d; })
.enter()
.append("text")
.text("Double click to focus")
.attr("x", 5).attr("y", 285)
;
/// ADD BAR CHART TO RHS ///
var width = 150,
barHeight = 9;
svg.selectAll("text2")
.data([0], function(d) { return d; })
.enter()
.append("text")
.text("Murders (square root scale)")
.attr("x", 665).attr("y", 50)
.style("font-weight","bold");
var x = d3.scale.sqrt()
.domain([0.1, d3.max(nodes, function(d) { return d.value; })])
.range([2, width]);
var chart = d3.select("#barchart").append("svg")
.attr("class", "chart")
.attr("width", width)
.attr("height", barHeight * nodes.length);
var bar = svg.selectAll("rect")
.data(d3.values(nodes)
.sort(function(a, b){return b.value - a.value;}))
;
bar.enter().append("rect")
.filter(function(d) { return d.value > 0 })
.attr("transform", function(d, i) { return "translate(650," + i * barHeight + ")"; })
.attr("x", function(d) { return width - x(d.value) ; })
.attr("y",56)
.attr("width", function(d) { return x(d.value); })
.attr("height", barHeight - 1)
.attr("fill", function(d) { return color(d.group); })
.attr('class', function(d) { return d.node_name;} );
;
bar.on("mouseover", function(d) {
d3.select(".label" + d.node_name)
.transition()
.duration(400)
.style('opacity',1);
d3.selectAll(".node" + d.node_name)
.transition()
.duration(300)
.attr('r',20)
.transition()
.duration(300)
.attr("r", function(d) { return hitsscale(d.value); });
});
bar.on("mouseout", function(d) {
d3.select(".label" + d.node_name)
.transition()
.duration(400)
.style('opacity',0);
});
bar.enter().append("text")
.filter(function(d) { return d.value > 0 })
.attr("transform", function(d, i) { return "translate(650," + i * barHeight + ")"; })
.attr("x", function(d) { return width - x(d.value) + 2; })
.attr("y", 56 + barHeight / 2)
.attr("dy", ".35em")
.text(function(d) { return d.value; })
.style('fill','white')
.style('font-size','8')
.style('font','sans-serif')
;
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment