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)
<!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="//cdn.leafletjs.com/leaflet-0.7.5/leaflet.css" />
<script src="//cdn.leafletjs.com/leaflet-0.7.5/leaflet.js"></script>
<!-- A functional reactive model library. github.com/curran/model -->
<script src="//curran.github.io/model/cdn/model-v0.2.4.js"></script>
<!-- Chiasm core and plugins. github.com/chiasm-project -->
<script src="//chiasm-project.github.io/chiasm/chiasm-v0.2.0.js"></script>
<script src="//chiasm-project.github.io/chiasm-component/chiasm-component-v0.2.0.js"></script>
<script src="//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.
// 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 is a prototype for a Chiasm plugin that does data binding between Chiasm
// components. It uses a special domain specific language (DSL) to express two
// types of data binding links between two components:
//
// * Unidirectional `myComponent.myPropertyA -> myOtherComponent.myPropertyB`
// * Bidirectional (planned, not implemented) `myComponent.myPropertyA <-> myOtherComponent.myPropertyB`
function Links(chiasm) {
var model = ChiasmComponent({
bindings: []
});
var sourceListeners = [];
model.when("bindings", function (bindings){
var oldSourceListeners = sourceListeners;
sourceListeners = [];
// Clear out the listeners for the old bindings.
oldSourceListeners.forEach(function (_){
chiasm.getComponent(_.sourceAlias).then(function (sourceComponent){
sourceComponent.cancel(_.listener);
});
});
// Add listeners for the new bindings.
bindings.forEach(function(bindingExpr){
// Parse the binding expression of the form
// "sourceAlias.sourceProperty -> targetAlias.targetProperty"
var parts = bindingExpr.split("->").map(function(str){ return str.trim(); }),
source = parts[0].split("."),
sourceAlias = source[0],
sourceProperty = source[1],
target = parts[1].split("."),
targetAlias = target[0],
targetProperty = target[1];
// Retreive the source and target components.
chiasm.getComponent(sourceAlias).then(function(sourceComponent){
chiasm.getComponent(targetAlias).then(function(targetComponent){
// TODO report errors for missing components.
// Add a reactive function that binds the source to the target.
var listener = sourceComponent.when(sourceProperty, function(value){
if(targetComponent[targetProperty] !== value){
targetComponent[targetProperty] = value;
}
});
// Keep track of the added listener so it can be removed later.
sourceListeners.push({
sourceAlias: sourceAlias,
listener: listener
});
});
});
});
});
return model;
}
# 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
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" }
]
}