Source: Global Terrorism Database (GTD)
Data and data wrangling process via this gist: https://gist.github.com/chris-creditdesign/e07ff4ab3d879396f2a1
Build with d3.js
Source: Global Terrorism Database (GTD)
Data and data wrangling process via this gist: https://gist.github.com/chris-creditdesign/e07ff4ab3d879396f2a1
Build with d3.js
BuildWidget.prototype.buildAxes = function () { | |
var self = this; | |
this.yAxis = d3.svg.axis() | |
.scale(this.yScale) | |
.tickSize(3,0) | |
.orient("left"); | |
this.yBrushAxis = d3.svg.axis() | |
.scale(this.yBrushScale) | |
.tickSize(3,0) | |
.orient("left"); | |
this.xAxis = d3.svg.axis() | |
.scale(this.xScale) | |
.tickSize(3,0) | |
.ticks(6) | |
.orient("bottom"); | |
this.xBrushAxis = d3.svg.axis() | |
.scale(this.xBrushScale) | |
.tickSize(3,0) | |
.orient("bottom"); | |
/* Prepare the y axis */ | |
this.svg.append("g") | |
.attr("class", "y axis") | |
.attr("transform", "translate(" + (this.params.margin.left + this.params.brushThickness + this.params.margin.mid) + "," + this.params.margin.top + ")") | |
.call(this.yAxis); | |
this.svg.append("g") | |
.attr("class", "y-brush axis") | |
.attr("transform", "translate(" + this.params.margin.left + "," + this.params.margin.top + ")") | |
.call(this.yBrushAxis) | |
.append("g") | |
.attr("class", "axisLabel") | |
.append("text") | |
.attr("transform", "translate(" + -(this.params.margin.left * 0.8) + "," + (this.params.height / 2) + "), rotate(-90)") | |
.style("text-anchor", "middle") | |
.text(this.params.key.yAxisLabel); | |
/* Prepare the x axis */ | |
this.svg.append("g") | |
.attr("class", "x axis") | |
.attr("transform", "translate(" + (this.params.margin.left + this.params.brushThickness + this.params.margin.mid) + "," + (this.params.margin.top + this.params.height) + ")") | |
.call(this.xAxis) | |
this.svg.append("g") | |
.attr("class", "x-brush axis") | |
.attr("transform", "translate(" + (this.params.margin.left + this.params.brushThickness + this.params.margin.mid) + "," + (this.params.margin.top + this.params.height + this.params.margin.mid + this.params.brushThickness) + ")") | |
.call(this.xBrushAxis) | |
.append("g") | |
.attr("class","axisLabel") | |
.append("text") | |
.attr("transform", "translate(" + (this.params.width / 2) + "," + (this.params.margin.bottom * 0.7) + ")") | |
.style("text-anchor","middle") | |
.text(self.params.key.xAxisLabel); | |
}; |
BuildWidget.prototype.buildBrush = function () { | |
var self = this; | |
var xDisplay = function () { | |
var extent = self.xBrush.extent(); | |
self.xScale.domain(extent); | |
self.updateView(); | |
}; | |
var xBrushend = function () { | |
if (self.xBrush.extent()[0] === self.xBrush.extent()[1]) { | |
d3.select(this).call(self.xBrush.extent(self.xExtent)); | |
self.xScale.domain(self.xExtent); | |
self.updateView(); | |
} | |
}; | |
var yDisplay = function () { | |
var extent = self.yBrush.extent(); | |
self.yScale.domain(extent); | |
self.updateView(); | |
}; | |
var yBrushend = function () { | |
if (self.yBrush.extent()[0] === self.yBrush.extent()[1]) { | |
d3.select(this).call(self.yBrush.extent(self.yExtent)); | |
self.yScale.domain(self.yExtent); | |
self.updateView(); | |
} | |
}; | |
var buildHandles = function (target, horizontal) { | |
var handleGroup = target.selectAll(".resize").append("g") | |
.attr("transform", function(d, i) { | |
if (horizontal) { | |
return i ? "translate(" + -(self.params.handleWidth) + ",0)" : "translate(0,0)"; | |
} else { | |
return i ? "translate(0,0)" : "translate(0," + -(self.params.handleWidth) + ")"; | |
} | |
}); | |
handleGroup.append("rect") | |
.attr("x", 0) | |
.attr("y", 0) | |
.attr("fill", self.params.uiColour.grey) | |
.attr("width", horizontal ? self.params.handleWidth : self.params.brushThickness) | |
.attr("height", horizontal ? self.params.brushThickness : self.params.handleWidth); | |
handleGroup.append("line") | |
.attr("x1", horizontal ? (self.params.handleWidth * 0.65) : (self.params.brushThickness * 0.2)) | |
.attr("y1", horizontal ? (self.params.brushThickness * 0.2) : (self.params.handleWidth * 0.65)) | |
.attr("x2", horizontal ? (self.params.handleWidth * 0.65) : (self.params.brushThickness * 0.8)) | |
.attr("y2", horizontal ? (self.params.brushThickness * 0.8): (self.params.handleWidth * 0.65)) | |
.attr("stroke", self.params.uiColour.veryLightGrey) | |
.attr("stroke-width", 1); | |
handleGroup.append("line") | |
.attr("x1", horizontal ? (self.params.handleWidth * 0.35) : (self.params.brushThickness * 0.2)) | |
.attr("y1", horizontal ? (self.params.brushThickness * 0.2) : (self.params.handleWidth * 0.35)) | |
.attr("x2", horizontal ? (self.params.handleWidth * 0.35) : (self.params.brushThickness * 0.8)) | |
.attr("y2", horizontal ? (self.params.brushThickness * 0.8) : (self.params.handleWidth * 0.35)) | |
.attr("stroke", self.params.uiColour.veryLightGrey) | |
.attr("stroke-width", 1); | |
}; | |
this.xBrush = d3.svg.brush() | |
.x(this.xBrushScale) | |
.extent(this.xExtent) | |
.on("brush", xDisplay) | |
.on("brushend", xBrushend); | |
this.yBrush = d3.svg.brush() | |
.y(this.yBrushScale) | |
.extent(this.yExtent) | |
.on("brush", yDisplay) | |
.on("brushend", yBrushend); | |
this.xBrushGroup.append("g") | |
.attr("class", "brush") | |
.call(this.xBrush) | |
.selectAll("rect") | |
.attr("fill",this.params.uiColour.veryLightGrey) | |
.attr("height", this.params.brushThickness ); | |
buildHandles(this.xBrushGroup, true); | |
this.yBrushGroup.append("g") | |
.attr("class", "brush") | |
.call(this.yBrush) | |
.selectAll("rect") | |
.attr("fill", this.params.uiColour.veryLightGrey) | |
.attr("width", this.params.brushThickness); | |
buildHandles(this.yBrushGroup, false); | |
}; |
function buildData (data) { | |
data.forEach(function(d) { | |
d.id = d.gname; | |
d.x = +d.count; | |
d.y = +d["sum(nkill)"]; | |
}); | |
return data; | |
} |
BuildWidget.prototype.buildGraphic = function () { | |
this.svg = d3.select(this.target).append("svg") | |
.attr("width", this.params.width + this.params.margin.left + this.params.brushThickness + this.params.margin.mid + this.params.margin.right) | |
.attr("height", this.params.height + this.params.margin.top + this.params.brushThickness + this.params.margin.mid + this.params.margin.bottom); | |
var clip = this.svg.append("defs").append("svg:clipPath") | |
.attr("id", "clip") | |
.append("svg:rect") | |
.attr("x", 0) | |
.attr("y", 0) | |
.attr("width", this.params.width) | |
.attr("height", this.params.height); | |
var miniClip = this.svg.append("defs").append("svg:clipPath") | |
.attr("id", "mini-clip") | |
.append("svg:rect") | |
.attr("x", 0) | |
.attr("y", 0) | |
.attr("width", this.params.miniMapThickness) | |
.attr("height", this.params.miniMapThickness); | |
this.scatterGroup = this.svg.append("g") | |
.attr("class","scatterGroup") | |
.attr("clip-path", "url(#clip)") | |
.attr("transform","translate(" + (this.params.margin.left + this.params.brushThickness + this.params.margin.mid) + "," + this.params.margin.top + ")"); | |
this.scatterGroup.append("rect") | |
.attr("x", 0) | |
.attr("y", 0) | |
.attr("width", this.params.width) | |
.attr("height", this.params.height) | |
.attr("fill", this.params.uiColour.offWhite); | |
this.keyGroup = this.svg.append("g") | |
.attr("class","keyGroup") | |
.attr("transform","translate(" + (this.params.margin.left + this.params.brushThickness + this.params.margin.mid) + "," + this.params.margin.top + ")"); | |
this.xBrushGroup = this.svg.append("g") | |
.attr("class","xBrushGroup") | |
.attr("transform","translate(" + (this.params.margin.left + this.params.brushThickness + this.params.margin.mid) + "," + (this.params.margin.top + this.params.height + this.params.margin.mid)+ ")"); | |
this.yBrushGroup = this.svg.append("g") | |
.attr("class","yBrushGroup") | |
.attr("transform","translate(" + this.params.margin.left + "," + this.params.margin.top + ")"); | |
this.miniMapGroup = this.svg.append("g") | |
.attr("class","miniMapGroup") | |
.attr("clip-path", "url(#mini-clip)") | |
.attr("transform","translate(" + this.params.miniMapMargin + "," + (this.params.margin.top + this.params.height + this.params.margin.mid) + ")"); | |
this.mapperGroup = this.svg.append("g") | |
.attr("class","miniMapGroup") | |
.attr("transform","translate(" + this.params.miniMapMargin + "," + (this.params.margin.top + this.params.height + this.params.margin.mid) + ")"); | |
}; |
BuildWidget.prototype.buildMiniMap = function () { | |
var self = this; | |
this.miniMapGroup.append("rect") | |
.attr("x", 0) | |
.attr("y", 0) | |
.attr("width", this.params.miniMapThickness) | |
.attr("height", this.params.miniMapThickness) | |
.attr("fill", this.params.uiColour.veryLightGrey) | |
.attr("stroke","none"); | |
this.mapper = this.mapperGroup.append("rect") | |
.attr("x", function () { | |
return self.xMiniMapScale(self.xExtent[0]) | |
}) | |
.attr("y", function () { | |
var top = self.yExtent[1] - self.yExtent[1]; | |
return self.yMiniMapInvertScale(top); | |
}) | |
.attr("width", function () { | |
var width = self.xExtent[1] - self.xExtent[0]; | |
return self.xMiniMapScale(width); | |
}) | |
.attr("height", function () { | |
var height = self.yExtent[1] - self.yExtent[0]; | |
return self.yMiniMapInvertScale(height); | |
}) | |
.attr("fill","none") | |
.attr("stroke", this.params.uiColour.darkGrey) | |
.attr("storke-width","1px"); | |
}; | |
BuildWidget.prototype.updateMiniMap = function(x, y) { | |
var self = this; | |
this.mapper | |
.attr("x", function() { | |
return self.xMiniMapScale(x[0]); | |
}) | |
.attr("y", function() { | |
var top = self.yExtent[1] - y[1]; | |
return self.yMiniMapInvertScale(top); | |
}) | |
.attr("width", function () { | |
var width = x[1] - x[0]; | |
return self.xMiniMapScale(width); | |
}) | |
.attr("height", function () { | |
var height = y[1] - y[0]; | |
return self.yMiniMapInvertScale(height); | |
}); | |
}; |
function buildParams () { | |
var params = {}; | |
params.uiColour = { | |
offWhite: "#efefef", | |
veryLightGrey: "#ddd", | |
lightGrey: "#999", | |
grey: "#666", | |
darkGrey: "#333" | |
}; | |
params.key = { | |
xAxisLabel: "Number of attacks", | |
yAxisLabel: "Number of fatalities" | |
}; | |
/* Margin, Width and height */ | |
params.margin = {top: 10, right: 10, mid: 50, bottom: 60, left: 60}; | |
params.brushThickness = 30; | |
params.handleWidth = 10; | |
params.miniMapMargin = 10; | |
params.radius = 3; | |
params.radiusSmall = 1; | |
params.fill = "darkred"; | |
params.opacity = 0.6; | |
params.miniMapThickness = params.margin.left + params.brushThickness - params.miniMapMargin; | |
params.width = 500 - params.margin.left - params.brushThickness - params.margin.mid - params.margin.right; | |
params.height = 500 - params.margin.top - params.margin.mid - params.brushThickness - params.margin.bottom; | |
params.format = d3.format("0,000"); | |
return params; | |
} |
BuildWidget.prototype.buildScales = function () { | |
this.xExtent = d3.extent(this.data, function (d) { | |
return d.x; | |
}); | |
this.xExtent[1] *= 1.1; | |
this.yExtent = d3.extent(this.data, function (d) { | |
return d.y; | |
}); | |
this.yExtent[1] *= 1.1; | |
this.yExtentInvert = d3.extent(this.data, function (d) { | |
return d.y; | |
}).reverse(); | |
this.yExtentInvert[0] *= 1.1; | |
this.yScale = d3.scale.linear() | |
.range([this.params.height, 0]) | |
.domain(this.yExtent); | |
this.yBrushScale = d3.scale.linear() | |
.range([this.params.height, 0]) | |
.domain(this.yExtent); | |
this.xScale = d3.scale.linear() | |
.range([0, this.params.width]) | |
.domain(this.xExtent); | |
this.xBrushScale = d3.scale.linear() | |
.range([0, this.params.width]) | |
.domain(this.xExtent); | |
this.yMiniMapScale = d3.scale.linear() | |
.range([this.params.miniMapThickness, 0]) | |
.domain(this.yExtent); | |
this.yMiniMapInvertScale = d3.scale.linear() | |
.range([this.params.miniMapThickness, 0]) | |
.domain(this.yExtentInvert); | |
// Reverse this one | |
this.xMiniMapScale = d3.scale.linear() | |
.range([0, this.params.miniMapThickness]) | |
.domain(this.xExtent); | |
}; |
BuildWidget.prototype.enterScatterPlot = function (target, main) { | |
var self = this; | |
target.selectAll("circle") | |
.data(this.data, function (d) { | |
return d.id; | |
}) | |
.enter() | |
.append("circle") | |
.attr("cx", function (d) { | |
return main ? self.xScale(d.x) : self.xMiniMapScale(d.x); | |
}) | |
.attr("cy", function (d) { | |
return main ? self.yScale(d.y) : self.yMiniMapScale(d.y); | |
}) | |
.attr("r", 0) | |
.attr("opacity", this.params.opacity) | |
.attr("fill", this.params.fill) | |
.attr("stroke", self.params.uiColour.darkGrey) | |
.attr("stroke-width", 0) | |
.attr("r", function () { | |
return main ? self.params.radius : self.params.radiusSmall; | |
}); | |
}; | |
BuildWidget.prototype.updateScatterPlot = function () { | |
var self = this; | |
this.scatterGroup.selectAll("circle") | |
.data(this.data, function (d) { | |
return d.id; | |
}) | |
.attr("cx", function (d) { | |
return self.xScale(d.x); | |
}) | |
.attr("cy", function (d) { | |
return self.yScale(d.y); | |
}); | |
}; |
BuildWidget.prototype.buildTooltip = function () { | |
var self = this; | |
this.scatterGroup.selectAll("circle") | |
.on("mouseover", function (d) { | |
var myCircle = d3.select(this); | |
myCircle.attr("stroke-width", 3); | |
var tooltipWidth = parseInt(d3.select("#widget-tooltip").style("padding-left"),10) + parseInt(d3.select("#widget-tooltip").style("width"),10) + parseInt(d3.select("#widget-tooltip").style("padding-right"),10); | |
var top = (parseFloat(myCircle.attr("cy")) + self.params.margin.top); | |
var left = (parseFloat(myCircle.attr("cx")) + self.params.margin.left + self.params.brushThickness + self.params.margin.mid + 30); | |
d3.select("#widget-tooltip") | |
.style("top", top + "px") | |
.style("left", left + "px") | |
.classed("hidden", false); | |
d3.select("#id").text(d.id); | |
d3.select("#y").text(self.params.format(d.y)); | |
d3.select("#x").text(self.params.format(d.x)); | |
}).on("mouseout", function () { | |
d3.select(this).attr("stroke-width", 0); | |
self.hideTooltip(); | |
}); | |
}; | |
BuildWidget.prototype.hideTooltip = function () { | |
d3.select("#widget-tooltip").classed("hidden", true); | |
}; |
function BuildWidget (target, params, data) { | |
this.target = target; | |
this.params = params; | |
this.data = data; | |
} |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Global Terrorism Database</title> | |
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script> | |
<script src="./buildWidget.js" charset="utf-8"></script> | |
<script src="./buildTooltip.js" charset="utf-8"></script> | |
<script src="./updateView.js" charset="utf-8"></script> | |
<script src="./buildScatterPlot.js" charset="utf-8"></script> | |
<script src="./buildMiniMap.js" charset="utf-8"></script> | |
<script src="./buildBrush.js" charset="utf-8"></script> | |
<script src="./buildAxes.js" charset="utf-8"></script> | |
<script src="./buildScales.js" charset="utf-8"></script> | |
<script src="./buildGraphic.js" charset="utf-8"></script> | |
<script src="./buildData.js" charset="utf-8"></script> | |
<script src="./buildParams.js" charset="utf-8"></script> | |
<script src="./index.js" charset="utf-8"></script> | |
<style> | |
body { | |
color: #333; | |
font-family: helvetica; | |
} | |
h1 { | |
margin-bottom: 0; | |
} | |
.outer-wrapper { | |
width: 940px; | |
height: 500px; | |
padding: 5px 10px; | |
position: relative; | |
} | |
.axis { | |
font-size: 12px; | |
} | |
.axis path, | |
.axis line { | |
fill: none; | |
stroke: #000; | |
shape-rendering: crispEdges; | |
} | |
.widget-tooltip { | |
position: absolute; | |
height: auto; | |
padding: 6px 10px; | |
color: #ccc; | |
background-color: #333; | |
pointer-events: none; | |
font-size: 0.8em; | |
line-height: 1.4em; | |
opacity: 1; | |
transition: opacity 0.3s ease; | |
} | |
.widget-tooltip:before { | |
content: ""; | |
position:absolute; | |
top: 5px; | |
left: -20px; | |
border-right: 0; | |
border-top: 10px solid #333; | |
border-bottom: 0; | |
border-left: 20px solid transparent; | |
} | |
p { | |
padding: 0; | |
margin: 0; | |
} | |
span { | |
font-weight: bold; | |
color: #fff; | |
} | |
.hidden { | |
opacity: 0; | |
pointer-events: none; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="outer-wrapper"> | |
<div id="scatterplot"> | |
<div class="widget-tooltip hidden" id="widget-tooltip"> | |
<p id="id">Hello Mum!</p> | |
<p>Fatalities: <span id="y"></span></p> | |
<p>Attacks: <span id="x"></span></p> | |
</div> | |
</div> | |
</div> |
function init () { | |
d3.csv("/chris-creditdesign/raw/e07ff4ab3d879396f2a1/gtd_gname_count.csv", function(d) { | |
drawScatterPlot(d); | |
}); | |
} | |
function drawScatterPlot (d) { | |
var params = buildParams(); | |
var data = buildData(d); | |
var scatterplot = new BuildWidget("#scatterplot", params, data); | |
scatterplot.buildGraphic(); | |
scatterplot.buildScales(); | |
scatterplot.buildAxes(); | |
scatterplot.buildBrush(); | |
scatterplot.buildMiniMap(); | |
scatterplot.enterScatterPlot(scatterplot.scatterGroup, true); | |
scatterplot.buildTooltip(); | |
scatterplot.enterScatterPlot(scatterplot.miniMapGroup, false); | |
} | |
window.onload = init; |
BuildWidget.prototype.updateView = function () { | |
this.updateScatterPlot(); | |
this.svg.selectAll(".x.axis").call(this.xAxis); | |
this.svg.selectAll(".y.axis").call(this.yAxis); | |
this.updateMiniMap(this.xBrush.extent(), this.yBrush.extent()); | |
}; |