A direct visualization of data from themigrantsfiles.com. The data was exported to CSV format from this Google Doc on September 7, 2015. Each row of the table is represented as a red circle on the map.
I’d like to somehow overcome the issue of overlapping circles occluding one another, but I’m not sure how. Maybe clustering or binning?
Inspired by
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Migrant Deaths Direct Visualization</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="https://unpkg.com/leaflet@0.7.5/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@0.7.5/dist/leaflet-src.js"></script>
<!-- A functional reactive model library. github.com/curran/model -->
<script src="https://curran.github.io/model/cdn/model-v0.2.4.js"></script>
<!-- Chiasm core and plugins. github.com/chiasm-project -->
<script src="https://chiasm-project.github.io/chiasm/chiasm-v0.2.0.js"></script>
<script src="https://chiasm-project.github.io/chiasm-component/chiasm-component-v0.2.0.js"></script>
<script src="https://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;
}
</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": [14.15, 41.70],
"zoom": 4,
"rColumn": "dead_and_missing",
"rMax": 30
}
},
"migrantDeaths": {
"plugin": "dataLoader",
"state": {
"path": "events"
}
},
"links": {
"plugin": "links",
"state": {
"bindings": [
"migrantDeaths.data -> map.data"
]
}
}
});
</script>
</body>
</html>
// 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,
});
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');
ctx.fillStyle = "rgba(255, 255, 250, 0.8)";
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(d3.extent(data, function (d){ return d[rColumn]; }))
.range([rMin, rMax]);
my.r = function (d){ return rScale(d[rColumn]); };
}
});
my.when(["data", "r"], function (data, r){
// TODO remove old markers.
// TODO move these to config.
var latitudeColumn = "latitude";
var longitudeColumn = "longitude";
data.forEach(function (d){
var lat = d[latitudeColumn];
var lng = d[longitudeColumn];
var markerCenter = L.latLng(lat, lng);
var circleMarker = L.circleMarker(markerCenter, {
// TODO move this to config.
color: "#FF4136",
weight: 1,
clickable: false
});
circleMarker.setRadius(r(d));
circleMarker.addTo(my.map);
});
});
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);
// 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;
}
{
"columns": [
{ "name": "dead", "type": "number" },
{ "name": "missing", "type": "number" },
{ "name": "dead_and_missing", "type": "number" },
{ "name": "cause_of_death", "type": "string" },
{ "name": "CartoDB_Cause_of_death", "type": "string" },
{ "name": "latitude", "type": "number" },
{ "name": "longitude", "type": "number" },
{ "name": "date", "type": "string" }
]
}