Skip to content

Instantly share code, notes, and snippets.

@curran
Last active November 17, 2015 19:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save curran/38aad0e939c685023a17 to your computer and use it in GitHub Desktop.
Save curran/38aad0e939c685023a17 to your computer and use it in GitHub Desktop.
Fusillade à Paris

This is a visualization of the attacks in Paris on November 13, 2015. The data is collected from news sources and entered into this spreadsheet by hand: Fusillade à Paris (Google Doc). This data should not be considered reliable, as the situation is still changing and new facts are coming in.

Click on each circle for more information on each attack site. The area of each circle corresponds to the number of deaths at each site.

Uses Leaflet and Chiasm to create the visualization.

Locations (latitude, longitude) were derived from addresses using http://www.latlong.net/ Locations (latitude, longitude) were derived from addresses using latlong.net. Times in the data were derived from this block in The Guardian.

forked from curran's block: Migrant Deaths Map (direct)

web counter
// This is a Chiasm component that implements a bubble map.
// Based on chiasm-leaflet.
function BubbleMap() {
// Extend chiasm-leaflet using composition (not inheritence).
var my = ChiasmLeaflet();
// my.map is the Leaflet instance.
// TODO move this into chiasm-component.
my.addPublicProperties = function (publicProperties){
Object.keys(publicProperties).forEach(function (property){
my.addPublicProperty(property, publicProperties[property]);
});
};
my.addPublicProperties({
// This is the data column that maps to bubble size.
// "r" stands for radius.
rColumn: Model.None,
// The circle radius used if rColumn is not specified.
rDefault: 3,
// The range of the radius scale if rColumn is specified.
rMin: 0,
rMax: 10,
fillColor: "black"
});
var rScale = d3.scale.sqrt();
// Add a semi-transparent white layer to fade the
// black & white base map to the background.
var canvasTiles = L.tileLayer.canvas();
canvasTiles.drawTile = function(canvas, tilePoint, zoom) {
var ctx = canvas.getContext('2d');
// TODO move opacity to config.
ctx.fillStyle = "rgba(255, 255, 250, 0.85)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
canvasTiles.addTo(my.map);
// Generate a function or constant for circle radius,
// depending on whether or not rColumn is defined.
my.when(["data", "rColumn", "rDefault", "rMin", "rMax"],
function (data, rColumn, rDefault, rMin, rMax){
if(rColumn === Model.None){
my.r = function (){ return rDefault};
} else {
rScale
.domain([0, d3.max(data, function (d){ return d[rColumn]; })])
.range([rMin, rMax]);
my.r = function (d){ return rScale(d[rColumn]); };
}
});
var oldMarkers = [];
my.when(["data", "r", "fillColor"], _.throttle(function (data, r, fillColor){
oldMarkers.forEach(function (marker){
my.map.removeLayer(marker);
});
// TODO move these to config.
var latitudeColumn = "latitude";
var longitudeColumn = "longitude";
oldMarkers = data.map(function (d){
var lat = d[latitudeColumn];
var lng = d[longitudeColumn];
var markerCenter = L.latLng(lat, lng);
var circleMarker = L.circleMarker(markerCenter, {
color: fillColor,
weight: 1,
// TODO move this to config.
fillOpacity: 1,
opacity: 0,
weight: 0,
clickable: true
});
circleMarker.setRadius(r(d));
circleMarker.addTo(my.map)
.bindPopup([
d.name + " - " + d.deaths + " death" + (d.deaths > 1 ? "s" : ""),
d.note,
"source: " + "<a href=\"" + d.source_url + "\">" + d.source_name + "</a>"
].join("<br>"));
//if(d.name === "Le Petit Cambodge"){
// circleMarker.openPopup();
//}
return circleMarker;
});
}, 100));
return my;
}
// A Chiasm plugin for loading DSV data sets.
function DataLoader (){
var my = ChiasmComponent({
path: Model.None
});
my.when("path", function (path){
d3.json(path + ".json", function(error, schema) {
var numericColumns = schema.columns.filter(function (column){
return column.type === "number";
});
var type = function (d){
numericColumns.forEach(function (column){
d[column.name] = +d[column.name];
});
return d;
}
d3.csv(path + ".csv", type, function(error, data) {
my.data = data;
});
});
});
return my;
}
// This is an example Chaism plugin that uses Leaflet.js.
function ChiasmLeaflet() {
var my = ChiasmComponent({
center: [0, 0],
zoom: 2
});
// This line of code lets you see what the center value is when you pan in the map.
//my.when("center", console.log, console);
//my.when("zoom", console.log, console);
// Expose a div element that will be added to the Chiasm container.
// This is a special property that Chiasm looks for after components are constructed.
my.el = document.createElement("div");
// When you zoom out all the way, this line makes the background black
// (by default it is gray).
d3.select(my.el).style("background-color", "black");
// Instantiate the Leaflet map, see docs at
// http://leafletjs.com/reference.html#map-constructor
my.map = L.map(my.el, {
// Turn off the "Leaflet" link in the lower right corner.
// Leaflet is properly attributed in the README.
attributionControl: false
}).setView(my.center, my.zoom);
// Add the black & white style map layer.
// Found by browsing http://leaflet-extras.github.io/leaflet-providers/preview/
// TODO move this to configuration.
L.tileLayer("http://stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}.png").addTo(my.map);
// Also try this http://{s}.tiles.earthatlas.info/natural-earth/{z}/{x}/{y}.png
// Returns the current Leaflet map center
// in a format that D3 understands: [longitude, latitude]
function getCenter(){
var center = my.map.getCenter();
return [center.lng, center.lat];
}
// Sets the Leaflet map center to be the given center.
// Note that Leaflet will immediately trigger a "move"
// event
function setCenter(center){
my.map.off("move", onMove);
my.map.panTo(L.latLng(center[1], center[0]), {
animate: false
});
my.map.on("move", onMove);
}
my.map.on("move", onMove);
function onMove(){
my.center = getCenter();
my.zoom = my.map.getZoom();
}
// If the center was set externally, pan the map to that center.
my.when(["center", "zoom"], function (center, zoom){
// This comparison logic is necessary to avoid an infinite loop
// in bidirectional data binding.
// TODO move this to chiasm-links under "A <-> B" DSL syntax
if(!equal(center, getCenter())){
setCenter(center);
}
my.map.setZoom(zoom);
});
function equal(a, b){
return JSON.stringify(a) === JSON.stringify(b);
}
my.when("box", function (box) {
// Move to chiasm-layout?
d3.select(my.el)
.style("width", box.width + "px")
.style("height", box.height + "px");
// Tell Leaflet that the size has changed so it updates.
my.map.invalidateSize();
});
return my;
}
# This shell script fetches the data from the Google Doc
# and outputs the latest data to the file paris_attacks.csv .
curl -o paris_attacks.csv https://docs.google.com/spreadsheets/d/1x1voWlNozm-FGOADP50Pl31jzhmv92TOpgmx7UIrg2s/pub?output=csv
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Fusillade à Paris</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js"></script>
<!-- Leaflet.js, a geographic mapping library. -->
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.5/leaflet.css" />
<script src="http://cdn.leafletjs.com/leaflet-0.7.5/leaflet.js"></script>
<!-- A functional reactive model library. github.com/curran/model -->
<script src="http://curran.github.io/model/cdn/model-v0.2.4.js"></script>
<!-- Chiasm core and plugins. github.com/chiasm-project -->
<script src="http://chiasm-project.github.io/chiasm/chiasm-v0.2.0.js"></script>
<script src="http://chiasm-project.github.io/chiasm-component/chiasm-component-v0.2.0.js"></script>
<script src="http://chiasm-project.github.io/chiasm-layout/chiasm-layout-v0.2.1.js"></script>
<!-- Custom Chiasm plugins for this example. -->
<script src="chiasm-links.js"></script>
<script src="chiasm-data-loader.js"></script>
<script src="chiasm-leaflet.js"></script>
<script src="bubble-map.js"></script>
<style>
body {
background-color: black;
}
/* Make the chart container fill the page using CSS. */
#chiasm-container {
position: fixed;
left: 20px;
right: 20px;
top: 20px;
bottom: 20px;
}
.leaflet-popup-content {
font-size: 1.3em;
}
</style>
</head>
<body>
<div id="chiasm-container"></div>
<script>
var chiasm = Chiasm();
chiasm.plugins.layout = ChiasmLayout;
chiasm.plugins.links = Links;
chiasm.plugins.dataLoader = DataLoader;
chiasm.plugins.bubbleMap = BubbleMap;
chiasm.setConfig({
"layout": {
"plugin": "layout",
"state": {
"containerSelector": "#chiasm-container",
"layout": "map"
}
},
"map": {
"plugin": "bubbleMap",
"state": {
"center": [2.361202239990234, 48.887746278609676],
"zoom": 12,
"rColumn": "deaths",
"rMax": 20,
"fillColor": "#E00000"
}
},
"paris_attacks": {
"plugin": "dataLoader",
"state": {
"path": "paris_attacks"
}
},
"links": {
"plugin": "links",
"state": {
"bindings": [
"paris_attacks.data -> map.data"
]
}
}
});
</script>
</body>
</html>
The MIT License (MIT)
Copyright (c) 2015 Curran Kelleher
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
name deaths time address latitude longitude note source_name source_url
Stade de France 3 21:20 93216 Saint-Denis, France 48.9244 2.3600 Two suicide bombers detonated outside the stadium. BBC http://www.bbc.com/news/world-europe-34814203
Bataclan 89 21:49 50 Boulevard Voltaire, 75011 Paris, France 48.863005 2.370648 Hostage takers executed concert goers inside the venue. Washington Post https://www.washingtonpost.com/graphics/world/paris-attacks/
Le Petit Cambodge and Le Carillon 14 21:25 20 Rue Alibert, 75010 Paris, France 48.871724 2.368174 Two gunmen opened fire in the Cambodian restaurant. Washington Post https://www.washingtonpost.com/graphics/world/paris-attacks/
La Casa Nostra 5 21:32 2 Rue de la Fontaine au Roi, 75011 Paris, France 48.868576 2.368436 Gunmen open fire on the street near a bar. BBC http://www.bbc.com/news/world-europe-34814206
La Belle Equipe 19 21:38 92 Rue de Charonne, 75011 Paris, France 48.853763 2.381989 Gunmen open fire towards people on the sidewalk. BBC http://www.bbc.com/news/world-europe-34814207
Comptoir Voltaire 1 21:43 253 Boulevard Voltaire, 75011 Paris, France 48.85037 2.393098 Suicide bomber on the street. dailymail.co.uk http://www.dailymail.co.uk/news/article-3318086/11-dead-terrorists-open-fire-Paris-restaurant.html
{
"columns": [
{ "name": "name", "type": "string" },
{ "name": "deaths", "type": "number" },
{ "name": "time", "type": "string" },
{ "name": "address", "type": "string" },
{ "name": "latitude", "type": "number" },
{ "name": "longitude", "type": "number" },
{ "name": "note", "type": "string" },
{ "name": "source_name", "type": "string" },
{ "name": "source_url", "type": "string" }
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment