block by curran a479b91bba14d633487e

Migrant Deaths over Time

Full Screen

This example shows a linked bubble map and temporal histogram of migrant deaths. Brushing in the date view filters the data shown in the map. Panning and zooming in the map filters the data shown in the time view. This is a combination of the previous examples Migrant Deaths Map and Crossfilter & Chiasm.

The data is from themigrantsfiles.com, exported to CSV format from this Google Doc on September 14, 2016. Each row of the table is represented as a red circle on the map. Unfortunately, the data collection project was discontinued in July 2016.

Uses

web counter

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Chiasm Crossfilter Integration</title>
    
    <!-- A functional reactive model library. See github.com/curran/model -->
    <script src="https://curran.github.io/model/cdn/model-v0.2.4.js"></script>
    
    <!-- The common base for Chiasm components (depends on Model.js). -->
    <script src="https://chiasm-project.github.io/chiasm-component/chiasm-component-v0.2.1.js"></script>

    <!-- This script defines the BarChart component. -->
    <script src="barChart.js"> </script>
    
    <!-- Load Crossfilter and the Crossfilter Chiasm component. -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.12/crossfilter.min.js"></script>
    <script src="chiasm-crossfilter.js"></script>
    
    <!-- Chiasm.js depends on Model.js, Lodash.js, D3.js. -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
    
    <!-- Chiasm.js and plugins. See 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-layout/chiasm-layout-v0.2.2.js"></script>
    <script src="https://chiasm-project.github.io/chiasm-links/chiasm-links-v0.2.1.js"></script>
    <script src="https://chiasm-project.github.io/chiasm-dataset-loader/chiasm-dataset-loader-v0.3.1.js"></script>

    <!-- The map component. -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/leaflet.css" /> 
    <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/leaflet.js"></script>
    <script src="chiasm-leaflet.js"></script>
    <script src="bubble-map.js"></script>

    <!-- Make the Chiasm container fill the page and have a 20px black border. -->
    <style>

      #chiasm-container {
        position: fixed;
        left: 0px;
        right: 0px;
        top: 0px;
        bottom: 0px;
      }

      /* Style the brush. Draws from //bl.ocks.org/mbostock/4343214 */
      .brush .extent {
        stroke: black;
        fill-opacity: .2;
        shape-rendering: crispEdges;
      }

      /* Axis CSS from bar chart example //bl.ocks.org/mbostock/3885304 */
      .axis {
        font: 10px sans-serif;
      }

      .axis path,
      .axis line {
        fill: none;
        stroke: lightgray;
        shape-rendering: crispEdges;
      }

      /* Custom CSS for axis labels. */
      .axis-label {
        text-anchor: middle;
        font-size: 1.1em;
      }

    </style>
  </head>
  <body>

    <!-- Chiasm component DOM elements will be injected into this div. -->
    <div id="chiasm-container"></div>
    
    <!-- This is the main program that sets up the Chiasm application. -->
    <script>

      // Create a new Chiasm instance.
      var chiasm = new Chiasm();

      // Register plugins that the configuration can access.
      chiasm.plugins.layout = ChiasmLayout;
      chiasm.plugins.links = ChiasmLinks;
      chiasm.plugins.datasetLoader = ChiasmDatasetLoader;
      chiasm.plugins.barChart = BarChart;
      chiasm.plugins.bubbleMap = BubbleMap;
      chiasm.plugins.crossfilter = ChiasmCrossfilter;

      // Set the Chaism configuration.
      chiasm.setConfig({
        "layout": {
          "plugin": "layout",
          "state": {
            "containerSelector": "#chiasm-container",
            "layout": {
              "orientation": "vertical",
              "children": [
                "map",
                "date-chart"
              ]
            },
            "sizes": {
              "date-chart": {
                "size": 0.3
              }
            }
          }
        },
        "map": {
          "plugin": "bubbleMap",
          "state": {
            "center": [15, 33],
            "zoom": 4,
            "rColumn": "dead_and_missing",
            "rMax": 30
          }
        },
        "date-chart": {
          "plugin": "barChart",
          "state": {
            "fill": "#FF7F78",
            "yColumn": "value",
            "xColumn": "key",
            "margin": { left: 14, top: 1, right: 14, bottom: 20 }
          }
        },
        "data-loader": {
          "plugin": "datasetLoader",
          "state": {
            "path": "events"
          }
        },
        "crossfilter": {
          "plugin": "crossfilter",
          "state": {
            "groups": {
              "dates": {
                "dimension": "date",
                "aggregation": "month"
              },
              "locations": {
                "dimension": "latlong",
              },
              "latitudes": {
                "dimension": "latitude",
                "aggregation": "floor 500"
              },
              "longitudes": {
                "dimension": "longitude",
                "aggregation": "floor 500"
              }
            }
          }
        },
        "links": {
          "plugin": "links",
          "state": {
            "bindings": [
              "data-loader.dataset -> crossfilter.dataset",
              "crossfilter.dates -> date-chart.data",
              "crossfilter.locations-elements -> map.data",
              "data-loader.dataset -> map.datasetForScaleDomain",
              "date-chart.brushIntervalX -> crossfilter.dateFilter",
              "map.longitudeInterval -> crossfilter.longitudeFilter",
              "map.latitudeInterval -> crossfilter.latitudeFilter"
            ]
          }
        }
      });
    </script>
  </body>
</html>

LICENSE

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.

barChart.js

// This is an example Chaism plugin that uses D3 to make a bar chart. 
// Draws from this Bar Chart example http://bl.ocks.org/mbostock/3885304
function BarChart() {

  var my = ChiasmComponent({

    margin: {
      left:   20,
      top:    40,
      right:  20,
      bottom: 20
    },

    yColumn: Model.None,
    xColumn: Model.None,

    // These properties adjust spacing between bars.
    // The names correspond to the arguments passed to
    // d3.scale.ordinal.rangeRoundBands(interval[, padding[, outerPadding]])
    // https://github.com/mbostock/d3/wiki/Ordinal-Scales#ordinal_rangeRoundBands
    barPadding: 0.1,
    barOuterPadding: 0.1,

    fill: "#a3a3a3",
    stroke: "none",
    strokeWidth: "1px",

    brushEnabled: false,
    brushIntervalX: Model.None,

    title: "",
    titleSize: "1.5em",
    titleOffset: "-0.3em"
  });

  var yScale = d3.scale.linear();

  // This scale is for the brush to use.
  var xScale = d3.time.scale();

  var brush = d3.svg.brush()
    .x(xScale)
    .on("brush", onBrush);

  my.el = document.createElement("div");
  var svg = d3.select(my.el).append("svg");
  var g = svg.append("g");
  var titleText = g.append("text");
  var barsG = g.append("g");
  var brushG = g.append("g").attr("class", "brush");

  xAxis(my, g);

  function onBrush() {
    my.brushIntervalX = brush.empty() ? Model.None : brush.extent();
  }

  my.when("title", titleText.text, titleText);

  my.when("titleSize", function (titleSize){
    titleText.style("font-size", titleSize);
  });

  my.when("titleOffset", function (titleOffset){
    titleText.attr("dy", titleOffset);
  });

  // Respond to changes in size and margin.
  // Inspired by D3 margin convention from http://bl.ocks.org/mbostock/3019563
  my.when(["box", "margin"], function(box, margin){

    my.innerBox = {
      width: box.width - margin.left - margin.right,
      height: box.height - margin.top - margin.bottom
    };

    svg
      .attr("width", box.width)
      .attr("height", box.height);

    g.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
  });

  my.when(["data", "xColumn", "innerBox", "barPadding", "barOuterPadding"],
      function (data, xColumn, innerBox, barPadding, barOuterPadding){

    var xAccessor = function (d){ return d[xColumn]; };

    var interval = d3.time.month;

    var xExtent = d3.extent(data, xAccessor);

    xExtent[1] = interval.offset(xExtent[1], 1);

    xScale
      .domain(xExtent)
      .range([0, innerBox.width]);

    var numIntervals = interval.range(xScale.domain()[0], xScale.domain()[1]).length;

    my.x = function(d) { return xScale(xAccessor(d)); }

    // Add 1 so the bars run together.
    my.width = innerBox.width / numIntervals + 1;
    my.xScale = xScale;

  });

  my.when(["data", "yColumn", "innerBox"],
      function (data, yColumn, innerBox){

    var yAccessor = function (d){ return d[yColumn]; };

    yScale
      .domain([0, d3.max(data, yAccessor)])
      .range([innerBox.height, 0]);

    my.y = function(d) { return yScale(yAccessor(d)); };
    my.height = function(d) { return innerBox.height - my.y(d); };

  });

  my.when(["data", "x", "y", "width", "height", "fill", "stroke", "strokeWidth"],
    function (data, x, y, width, height, fill, stroke, strokeWidth){

    var bars = barsG.selectAll("rect").data(data);
    bars.enter().append("rect");
    bars
      .transition().duration(500)
      .attr("x", x)
      .attr("width", width)
      .attr("y", y)
      .attr("height", height)
      .attr("fill", fill)
      .attr("stroke", stroke)
      .attr("stroke-width", strokeWidth);
    bars.exit().remove();

  });

  my.when(["brushIntervalX", "innerBox", "x", "y"],
      function (brushIntervalX, innerBox){

    if(brushIntervalX !== Model.None){

      //brush.extent(parseDates(brushIntervalX));

      // Uncomment this to see what the brush interval is as you drag.
      //console.log(brushIntervalX.map(function (date){
      //  return date.toUTCString();
      //}));
    }

    brushG.call(brush);

    brushG.selectAll("rect")
      .attr("y", 0)
      .attr("height", innerBox.height - 1);
  });

  return my;
}

function xAxis(my, g){
  var axisG = g.append("g").attr("class", "x axis");
  var axis = d3.svg.axis();

  my.addPublicProperty("xAxisTickDensity", 70);
  my.addPublicProperty("xAxisTickAngle", 0);

  my.when(["xScale", "xAxisTickDensity", "xAxisTickAngle", "innerBox"], function (xScale, xAxisTickDensity, xAxisTickAngle, innerBox){
    var width = innerBox.width;
    axis.scale(xScale).ticks(width / xAxisTickDensity)
    axisG.call(axis);

    var text = axisG.selectAll("text")  
      .attr("transform", "rotate(-" + xAxisTickAngle + ")" );

    if(xAxisTickAngle > 45){
      // TODO try to find a way to have this non-hard-coded
      text
        .attr("dx", "-0.9em")
        .attr("dy", "-0.6em")
        .style("text-anchor", "end");
    } else {
      text
        .attr("dx", "0em")
        //.attr("dy", "0em")
        //.style("text-anchor", "middle");
    }
  });

  my.when("innerBox", function (innerBox){
    axisG.attr("transform", "translate(0," + innerBox.height + ")");
  });

  return axisG;
}

bubble-map.js

// This is a Chiasm component that implements a bubble map.
// Based on chiasm-leaflet.
function BubbleMap() {

  // TODO move these to config.
  var latitudeColumn = "latitude";
  var longitudeColumn = "longitude";
    

  // Extend chiasm-leaflet using composition (not inheritence).
  var my = ChiasmLeaflet();
  // my.map is the Leaflet instance.

  my.when("data", function (data){
    my.cleanData = data.filter(function (d){
      var lat = d[latitudeColumn];
      var lng = d[longitudeColumn];
      if(isNaN(+lat) || isNaN(+lng)){
        console.log("Bad data - lat = " + lat + " lng = " + lng);
        return false;
      }
      return true;
    });
  });

  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.7)";
    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(["datasetForScaleDomain", "rColumn", "rDefault", "rMin", "rMax"],
      function (dataset, rColumn, rDefault, rMin, rMax){
    var data = dataset.data;

    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]); };
    }
  });

  var oldMarkers = [];
  my.when(["cleanData", "r"], _.throttle(function (data, r){

    // TODO make this more efficient.
    // Use D3 data joins?
    oldMarkers.forEach(function (marker){
      my.map.removeLayer(marker);
    });

    oldMarkers = data.map(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 circleMarker;
    });
  }, 100));

  return my;
}

chiasm-crossfilter.js

// This function defines a Chiasm component that exposes a Crossfilter instance
// to visualizations via the Chaism configuration.
function ChiasmCrossfilter() {

  var my = new ChiasmComponent({
    groups: Model.None
  });

  var listeners = [];

  my.when(["dataset", "groups"], function (dataset, groups){
    var data = dataset.data;
    if(groups !== Model.None) {
      var cf = crossfilter(data);
      var updateFunctions = [];

      listeners.forEach(my.cancel);

      listeners = Object.keys(groups).map(function (groupName){

        var group = groups[groupName];
        var dimension = group.dimension;
        var cfDimension = cf.dimension(function (d){ return d[dimension]; });

        // Generate an aggregate function by parsing the "aggregation" config option.
        var aggregate;
        if(group.aggregation){
          if(group.aggregation === "day"){
            aggregate = d3.time.day;
          } else if(group.aggregation === "week"){
            aggregate = d3.time.week;
          } else if(group.aggregation === "month"){
            aggregate = d3.time.month;
          } else if(group.aggregation.indexOf("floor") === 0){
            var interval = parseInt(group.aggregation.substr(6));
            aggregate = function(d) {
              return Math.floor(d / interval) * interval;
            };
          }
        } else {
          aggregate = function (d){ return d; };
        }

        var cfGroup = cfDimension.group(aggregate);

        var updateMyGroup = function (){

          // This contains the aggregated values.
          my[groupName] = cfGroup.all();

          // This contains the non-aggregated values
          // with filters from other dimensions applied.
          my[groupName + "-elements"] = cfDimension.top(Infinity);
        };
        updateFunctions.push(updateMyGroup);
        updateMyGroup();

        return my.when(dimension + "Filter", function (extent){
          if(extent !== Model.None){
            cfDimension.filterRange(extent);
          } else {
            cfDimension.filterAll();
          }
          updateFunctions.forEach(function (updateFunction){
            if(updateFunction !== updateMyGroup){
              updateFunction();
            }
          });
        });
      });
    }
  });
  return my;
}

chiasm-csv-loader.js

function ChiasmCSVLoader (){

  var my = ChiasmComponent({
    path: Model.None
  });

  var parseFunctions = {
    number: parseFloat
  };

  function generateColumnParsers(metadata) {
    if("columns" in metadata){
      return metadata.columns
        .filter(function (column){
          return column.type !== "string";
        })
        .map(function (column){
          var parse = parseFunctions[column.type];
          var name = column.name;
          return function (d){
            d[name] = parse(d[name]);
          }
        });
    } else {
      return [];
    }
  }

  my.when("path", function (path){
    if(path !== Model.None){

      d3.json(path + ".json", function(error, metadata) {
        var columnParsers = generateColumnParsers(metadata);
        var numColumns = columnParsers.length;

        function type (d){
          // Old school for loop as an optimization.
          for(var i = 0; i < numColumns; i++){

            // Each column parser function mutates the row object,
            // replacing the column property string with its parsed variant.
            columnParsers[i](d);
          }
          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];
  }

  var onMove = _.throttle(function (){
    my.center = getCenter();
    my.zoom = my.map.getZoom();
    
    var bounds = my.map.getBounds();
    my.longitudeInterval = [bounds.getWest(), bounds.getEast()];
    my.latitudeInterval = [bounds.getSouth(), bounds.getNorth()];

  }, 1000);

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

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

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": "date" }
  ]
}