block by boeric 47aceae44bb5f8b63d7b

Deaths of Migrants

Full Screen

Migrant Deaths

Slippy (zoomable/draggable) map and tile management, with data layer comprised of semitransparent circles. Also demonstrates an info overlay activated when the user is hovering over a data item. The visualization does not use any other library than D3.js.

See the script in action here

index.html

<!doctype html>
<html lang="en">
  <!-- Based on Map examples by Mike Bostock, MapBox, etc. -->
<head>
  <meta charset="UTF-8">
  <title>Death of Migrants</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
  <style>
    body {
      margin: 10px;
      font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    }
    #title {
      margin-bottom: 10px;
    }
    p, a {
      font-size: 12px;
      line-height: 12px;
      margin: 0px
      margin-bottom: 10px;
    }
    a {
      margin-left: 10px;
    }
    circle {
      cursor: crosshair;
      stroke: gray;
      stroke-width: 1px;
      stroke-opacity: 0.0;
      fill-opacity: 0.4;
    }
    .cursorLocation {
      padding: 3px;
      position: absolute;
      bottom: 10px;
      left: 10px;
      min-width: 300px;
      min-height: 15px;
      opacity: 0.5;
      background-color1: white;
    }
    .cursorLocation label {
      font-size: 12px;
      font-weight1: bold;
      opacity: 1;
    }
    .attribution {
      padding: 3px;
      position: absolute;
      bottom: 0px;
      right: 0px;
      opacity: 0.5;
      background-color: white;
    }
    .attribution label {
      font-size: 10px;
      font-weight: bold;
    }
    .attribution a {
      color: #404040;
      text-decoration: none;
    }
    .attribution a:hover {
      text-decoration: underline;
    }
    .attribution a:link {
      -webkit-tap-highlight-color: rgba(0,0,0,0);
    }
    #viz {
      border: 1px solid gray;
      overflow: scroll;
    }
    label {
      font-size: 12px;
    }
    #overlay {
      padding: 5px;
      position: absolute;
      min-width: 200px;
      max-width: 400px;
      top: 10px;
      left: 10px;
      border: 1px solid gray;
      border-radius: 5px;
      background-color: white;
      opacity: 0.8;
      display: none;
      z-index: 10001;
    }
    #overlay p {
      margin: 5px;
    }
    .migrantsOnly {
      position: absolute;
      top: 10px;
      right: 10px;
    }
    #main {
      position: absolute;
      top: 10px;
      left: 10px;
      opacity: 0.8;
      background-color: white;
      border-radius: 5px;
      padding-left: 20px;
      padding-right: 20px;
      z-index: 10000;
    }
  </style>

<body>

<div style="position: relative">
  <div id="main">
    <h3 id="title">Death of Migrants</h3>
    <p><b>Source:&nbsp;</b><a href="//www.themigrantsfiles.com" target="_blank">The Migrants Files</a>&nbsp;<a href="https://docs.google.com/spreadsheets/d/1YNqIzyQfEn4i_be2GGWESnG2Q80E_fLASffsXdCOftI/edit?usp=sharing" target="_blank">Data</a>
  </div>
</div>

<div id="viz" style="position: relative"></div>

<script>
'use strict';

var width = 960,
    height = 500,
    mapCenter = [15, 40], // lng, lat
    center,
    svg,
    tile,
    projection,
    zoom,
    tileGroup,
    eventGroup,
    eventElements,
    data,
    overlay;

// get the data
d3.json("migrantDeaths.json", function(_) {
  data = _;
  console.log("data.length", data.length)

  // remove items with missing dates;
  data = data.filter(function(d) { if (d.date != "") return true; })

  // convert text to numbers
  data.forEach(function(d, i) {
    d.latitude = +d.latitude;
    d.longitude = +d.longitude;
    d.Year = +d.Year;
    d.dead = +d.dead;
    d.missing = +d.missing;
    d.dead_and_missing = +d.dead_and_missing;
    d.origOrder = i;
  })

  // append date range to DOM
  var dateRange = d3.extent(data, function(d) { return new Date(d.date) })
  var earliestDate = new Date(dateRange[0]).toISOString().substring(0, 10);
  var latestDate = new Date(dateRange[1]).toISOString().substring(0, 10);
  d3.select("#main")
    .append("p")
      .style("margin-bottom", "20px")
      .html("<b>Date Range:</b> &nbsp;" + earliestDate + " - " + latestDate + " (in this visualization)")

  // sort events in decending order (so "smaller" events are drawn on top of "larger" events)
  data.sort(function(a, b) { return b.dead_and_missing - a.dead_and_missing; })
  console.log("data", data);

  // create the map
  createMap();
}) 

// create the map
function createMap() {

  // setup tile processing
  tile = d3.geo.tile().size([width, height]);

  // define mercator projection
  projection = d3.geo.mercator()
      .scale((1 << 12) / 2 / Math.PI) 
      .translate([width / 2, height / 2]); // translate to center of svg

  // get pixel location of map center 
  center = projection(mapCenter);

  // define zoom behavior
  zoom = d3.behavior.zoom()
      .scale(projection.scale() * 2 * Math.PI)
      .translate([width - center[0], height - center[1]])
      .on("zoom", redraw);

  // set dimension of container
  d3.select("#viz")
      .style("max-width", width + "px")
      .style("max-height", height + "px")

  // create the svg
  svg = d3.select("#viz")
    .append("svg")
      .style("width", width + "px")
      .style("height", height + "px")
      //.style("border", "1px solid gray")
      .call(zoom)
      .on("mousemove", function() {
        // get current zoom scale
        var zoomScale = zoom.scale();
        // get current mouse location
        var location = projection.invert(d3.mouse(this));
        // format location and show
        var text = formatLocation(location, zoomScale);
        d3.select("#cursorLocation").html("Cursor: " + text)
      })

  // add overlay div
  overlay = d3.select("#viz")
      .append("div")
      .attr("id", "overlay");

  overlay.append("p").attr("id", "deadText").text("Dead: X")
  overlay.append("p").attr("id", "dateText").text("Date: X")
  overlay.append("p").attr("id", "descriptionText").text("Event: X")

  // add div attribution (as per Mapbox' requirements)
  d3.select("#viz")
    .append("div")
      .attr("class", "attribution")
    .append("label")
      .html("<a href='https://www.mapbox.com/about/maps/' target='_blank'>© MapBox © OpenStreetMap</a>&nbsp;<a href='https://www.mapbox.com/map-feedback/'>Improve this map</a>")

  // add div for current cursor location
  d3.select("#viz")
    .append("div")
      .attr("class", "cursorLocation")
    .append("label")
      .attr("id", "cursorLocation")
      .html("")

  // append checkbox
  d3.select("#viz")
    .append("label")
      .attr("class", "migrantsOnly")
      .text("Missing migrants")
    .append("input")
      .attr("type", "checkbox")
      .property("checked", false)
      .on("change", function(d) {
        var state = d3.select(this).property("checked")
        showMissing(state);
      })

  // append tile and event groups
  tileGroup = svg.append("g").attr("id", "tiles"); // tiles get filled in redraw function
  eventGroup = svg.append("g").attr("id", "events");

  // add events to map
  eventElements = eventGroup
      .selectAll("circle")
      .data(data)
    .enter().append("circle")
      .attr("cx", function(d) { return projection([d.longitude, d.latitude])[0] })
      .attr("cy", function(d) { return projection([d.longitude, d.latitude])[1] })
      .attr("r", function(d) { return calcRadius(d) })
      .style("fill", function(d) { return "darkred" })
      .on("mouseover", function() {
        // set overlay text items
        var item = d3.select(this).data()[0];
        var deadText = "<b>Dead: " + item.dead_and_missing + (item.missing > 0 ? " (" + item.missing + " missing) " : "") + "</b>"
        d3.select("#deadText").html(deadText);
        d3.select("#dateText").html("<b>Date:</b> " + item.date);
        d3.select("#descriptionText").html("<b>Event:</b> " + item.description);

        // get current size of overlay
        overlay.style("display", "block");
        var overlayWidth = overlay.node().offsetWidth;
        var overlayHeight = overlay.node().offsetHeight;

        // compute position of overlay
        var mouse = d3.mouse(this);
        var x = mouse[0] - overlayWidth / 2;
        x = (x < 0) ? 10 : (x + overlayWidth) > width ? width - overlayWidth - 10 : x; // adjust x position if needed
        var y = mouse[1] - 20 - overlayHeight;
        y = (y < 0) ? mouse[1] + 20  : y; // adjust y position if needed

        // set position of overlay
        overlay.style("left", x + "px")
        overlay.style("top", y + "px")
      })
      .on("mouseout", function() {
        overlay.style("display", "none")
      });

  // redraws viz after drag or zoom
  function redraw() {

    // get the tiles for this zoom level
    var tiles = tile
      .scale(zoom.scale())
      .translate(zoom.translate())();

    // scale and translate tile group and bind new tiles
    var image = tileGroup 
        .attr("transform", "scale(" + tiles.scale + ")translate(" + tiles.translate + ")")
        .selectAll("image")
        .data(tiles, function(d) { return d; });

    // remove prior tiles
    image.exit().remove();

    // append the new tiles
    image.enter().append("image")
        .attr("xlink:href", function(d) { 
          var url = "//" + ["a", "b", "c", "d"][Math.random() * 4 | 0] + ".tiles.mapbox.com/v4/boeric.naal7ngd/" + d[2] + "/" + d[0] + "/" + d[1] + ".png" + "?access_token=pk.eyJ1IjoiYm9lcmljIiwiYSI6IkZEU3BSTjQifQ.XDXwKy2vBdzFEjndnE4N7Q"; 
          return url;
        })
        .attr("width", 1)
        .attr("height", 1)
        .attr("x", function(d) { return d[0]; })
        .attr("y", function(d) { return d[1]; });
    
    // update projection
    projection
        .scale(zoom.scale() / 2 / Math.PI)
        .translate(zoom.translate());

    // update object positions
    eventElements
        .attr("cx", function(d) { return projection([d.longitude, d.latitude])[0] })
        .attr("cy", function(d) { return projection([d.longitude, d.latitude])[1] });

  }

  // initial draw
  redraw();

  // toggles between missing and found dead migrants
  // radius is set to 0 for those elements not to be shown (easier than reloading the DOM)
  function showMissing(show) {

    if (show) {
      eventElements.attr("r", function(d) {
        if (d.missing > 0) { return calcRadius(d) }
        else return 0;
      })
    }
    else eventElements.attr("r", function(d) { return calcRadius(d) });

  }

  // calculates the radius of each circle
  function calcRadius(d) {

    // scale radius to value (sqrt), then compress dynamic range (log), then scale
    var r = Math.log(Math.sqrt(Math.max(d.dead_and_missing))) * 5; 
    // ensure minumum size
    r = Math.max(3, r); 
    return r; 

  }

  // formats the geo location string (zoom-in generates more decimals)
  function formatLocation(p, k) {

    var format = d3.format("." + Math.floor(Math.log(k) / 2 - 2) + "f");
    return (p[1] < 0 ? format(-p[1]) + "°S" : format(p[1]) + " °N") + " " + (p[0] < 0 ? format(-p[0]) + "°W" : format(p[0]) + "°E");

  }

} // end createMap function

// d3 tile generator
// https://github.com/d3/d3-plugins/blob/master/geo/tile/tile.js
d3.geo.tile = function() {
  var size = [960, 500],
      scale = 256,
      translate = [size[0] / 2, size[1] / 2],
      zoomDelta = 0;

  function tile() {

    var z = Math.max(Math.log(scale) / Math.LN2 - 8, 0),
        z0 = Math.round(z + zoomDelta),
        k = Math.pow(2, z - z0 + 8),
        origin = [(translate[0] - scale / 2) / k, (translate[1] - scale / 2) / k],
        tiles = [],
        cols = d3.range(Math.max(0, Math.floor(-origin[0])), Math.max(0, Math.ceil(size[0] / k - origin[0]))),
        rows = d3.range(Math.max(0, Math.floor(-origin[1])), Math.max(0, Math.ceil(size[1] / k - origin[1])));

    rows.forEach(function(y) {
      cols.forEach(function(x) {
        tiles.push([x, y, z0]);
      });
    });

    tiles.translate = origin;
    tiles.scale = k;

    return tiles;
  }

  tile.size = function(_) {
    if (!arguments.length) return size;
    size = _;
    return tile;
  };

  tile.scale = function(_) {
    if (!arguments.length) return scale;
    scale = _;
    return tile;
  };

  tile.translate = function(_) {
    if (!arguments.length) return translate;
    translate = _;
    return tile;
  };

  tile.zoomDelta = function(_) {
    if (!arguments.length) return zoomDelta;
    zoomDelta = +_;
    return tile;
  };

  return tile;

} // end tile function

</script>