block by curran 81271937fa94fdbdd854

Migrant Deaths Map (direct)

Full Screen

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.

Uses Leaflet and Chiasm.

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

web counter

index.html

<!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>

bubble-map.js

// 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;
}

chiasm-data-loader.js

// 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;
}

chiasm-leaflet.js

// 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;
}

chiasm-links.js

// 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;
}

events.json

{
  "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" }
  ]
}