block by boeric 4d62de0846a2554b113b

Driver License Suspensions II

Full Screen

Driver License Suspensions II

The visualization shows driver license suspensions in California per zip code due to “failure to pay” or “failure to appear”. The viz is using the Mapbox GL API, which provides high performance rendering of complex geo features (which in this case includes over 1600 high resolution zip code boundaries).

The viz is based on the Mapbox GL API and examples by Anand Thakker here, and here, and Bobby Sudekum here.

The viz is also using the D3 and Topojson libraries.

Four data files are used by the script: a) driver license suspension data (CSV), b) zip code meta data (CSV), c) California county boundaries (Topojson) and d) Zip code boundaries (Topojson).

See the viz in action here, and fullscreen

See another implementation of the same dataset here, fullscreen. This implementation is using Mapbox tiles

index.html

<!doctype html>
<html lang="en">
<!-- Based on examples by Mike Bostock, MapBox, etc. (see README.md) -->
<!-- 
    https://www.mapbox.com/mapbox-gl-js/example/image-on-a-map/, 
    //bl.ocks.org/anandthakker/69afa4bfb0c4f5778785, 
    //bl.ocks.org/anandthakker/52d26ae7b71b7e23c279,
    //bl.ocks.org/AlanPew/ac2f7753e488d8ac66d5
 -->
<!-- Author: Bo Ericsson, Email: bo@boe.net -->
<head>
  <title>License Suspensions</title>
  <meta charset="UTF-8">
  <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />

  <script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.11.4/mapbox-gl.js'></script>
  <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.11.4/mapbox-gl.css' rel='stylesheet' />
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
  <!--<script src="../_lib/d3.min.js" charset="utf-8" type="text/JavaScript"></script>-->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.js" type="text/JavaScript"></script>

  <style>
    body {
      margin: 0px;
      font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    }
    p, a {
      font-size: 12px;
      line-height: 12px;
      margin: 0px
      margin-bottom: 10px;
    }
    a {
      margin-left: 10px;
    }
    h4 {
      margin-top: 10px;
      margin-bottom: 5px;
    }
    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: 10000;
    }
    #overlay p {
      margin: 5px;
    }
    #controls {
      padding1: 5px;
      margin-bottom: 15px;
    }
    #controls p {
      margin: 5px;
      margin-left: 0px;
    }
    #main {
      position: absolute;
      top: 10px;
      left: 10px;
      opacity: 0.85;
      background-color: white;
      border-radius: 5px;
      padding-left: 20px;
      padding-right: 20px;
      padding-bottom: 20px;
      z-index: 10000;
      width: 300px;
      border: 1px solid gray;
    }
    ul {
      font-size: 12px;
      padding-left: 15px;
      margin-top: 5px;
      margin-bottom: 5px;
    }
    li {
      font-style: italic;
    }
    #howToUse:hover {
      color: red;
    }
    #map { 
      position: absolute; 
      top: 0px; 
      bottom: 0px; 
      width: 100%; 
    }
  </style>
</head>
<body>

<div id='map'></div>

<div style="position: relative; display: block">
  <div id="main">
    <h4 style="margin-bottom: 0px">Driver License Suspensions in</h4>
    <h4 style="margin-top: 0px">California by Zip Code</h4>
    <p style="margin-top: 0px; font-weight: bold">Due to "Failure to Pay" or "Failure to Appear"</p>
    <div id="controls">
      <p>&nbsp;<p>&nbsp;<p>&nbsp;<p>&nbsp;<p>&nbsp;<p>&nbsp;<p>&nbsp;
    </div>
    <p style="margin-top: 10px; margin-bottom: 0px; font-weight: bold">Sources</p>
    <ul>
      <li>East Bay Community Law Center</li>
      <li>CA Deparment of Motor Vehicles</li>
      <li>US Census Bureau (for geo data)</li>
    </ul>

    <p style="margin-top: 10px; margin-bottom: 0px; font-weight: bold">Map tilt</p>
    <input id="tiltSlider" style="width: 100%" type="range" min="0" max="60" value="0" step="1">

    <p id="howToUse" style="margin-top: 10px; margin-bottom: 0px; font-weight: bold">How to use...</p>
    <ul id="instructions"; style="display: none">
      <li>Move the mouse to see the specifics for the zip code</li>
      <li>The zip code boundaries are shown for each county</li>
      <li>The darker the zip code areas, the higher suspension rate</li>
      <li>Zoom in/out by rolling the mouse wheel, or by using the plus/minus buttons</li>
      <li>Pan around the map by dragging the mouse</li>
      <li>To tilt the map, use the slider above</li>
      <li>Rotate the map, drag the compass icon in upper right</li>
      <li>To reset the rotation, click the compass icon</li>
    </ul>
  </div>
</div>

<script>
"use strict";

var counties,
    zipcodes,
    data,
    zipGeo,
    countyZips,
    zipData;

window.onload = function() { start(); }

function start() {

  // load zip code data 
  d3.tsv("cazipgeo.txt", function(_) {
    zipGeo = _;

    // create object for quick lookup of a county's zip codes (used by code that dynamically injects zip code geometry)
    countyZips = {};
    zipGeo.forEach(function(d) {
      var key = d.County.trim(); 
      if (countyZips[key] == undefined) countyZips[key] = [];
      countyZips[key].push(+d.ZipCode)
    })

    // create data structure used to merge zip code and suspension data
    var obj = {};
    zipGeo.forEach(function(d) {
      var key = d.ZipCode.trim(); 
      obj[key] = d;
      delete obj[key].ZipCode;
    })
    zipGeo = obj; // redefine zipGeo from array to object

    // load driver license suspension data
    d3.tsv("suspensions.txt", function(suspensions) {
      console.log("loaded driver license suspension data...");

      // merge in the zip geo data and create main data structure
      data = suspensions.map(function(d, i, a) {
        var zipData = zipGeo[d.ZipCode];
        Object.keys(zipData).forEach(function(prop) { d[prop] = zipData[prop]; })
        return d;
      })

      // convert to numbers
      data.forEach(function(d) {
        var props = Object.keys(d);
        props.forEach(function(prop) { d[prop] = (isNaN(+d[prop])) ? d[prop] : +d[prop]; })
      })

      // sort data in ascending order (so highest suspension rates are drawn last and on top of the stack)
      data.sort(function(a, b) {
        return a.FTAFTPS100 - b.FTAFTPS100;
      })

      // create data structure for quick lookup (used by zip code geometry event handler)
      zipData = {};
      data.forEach(function(d) {
        zipData[d.ZipCode] = d;
      })

      // load county geometry
      d3.json("county4.json", function(error, county) {
        counties = topojson.feature(county, county.objects.CaliforniaCounty);
        console.log("loaded county info...");

        // load zip code geometry
        d3.json("ziptopo6.json", function(error, zip) {
          zipcodes = topojson.feature(zip, zip.objects.zip);
          console.log("loaded zip code geo json file...")

          var caZipCodeMin = 90001;
          var caZipCodeMax = 96162;
          zipcodes.features = zipcodes.features.filter(function(item) {
            if (item.properties.zip >= caZipCodeMin && item.properties.zip <= caZipCodeMax) return true;
          })
          //console.log("number of zipcodes: ", zipcodes.features.length)

          var nodataZipCodes = [];
          zipcodes.features.forEach(function(d) {
            d.properties.zip = +d.properties.zip;
            var zipCode = d.properties.zip;

            if (zipData[zipCode] == undefined) {
              nodataZipCodes.push(zipCode);
              d.properties.noData = true;
            }
            else {
              d.properties.noData = false;
              d.properties.ZipCode = zipData[d.properties.zip].ZipCode;              
              d.properties.Places = zipData[d.properties.zip].Places;
              d.properties.FTAFTPS100 = zipData[d.properties.zip].FTAFTPS100;
              d.properties.City = zipData[d.properties.zip].City;
              d.properties.povrate = zipData[d.properties.zip].povrate;
              d.properties.Pop15Plus = zipData[d.properties.zip].Pop15Plus;
              d.properties.IncK = zipData[d.properties.zip].IncK;
            }

          })
          //console.log("nodataZipCodes: ", nodataZipCodes)
          console.log("Zip codes with no data: ", nodataZipCodes.length)

          zipcodes.features = zipcodes.features.filter(function(d) {
            if (d.properties.noData) return false;
            else return true;
          })
          //console.log("zipcodes.features.length: ", zipcodes.features)
          console.log("Zip codes with data: ", zipcodes.features.length)

          // establish handler for the "how to use" div
          d3.select("#howToUse").on("click", function() {
            var state = d3.select("#instructions").style("display");
            if (state == "none") state = "block";
            else state = "none";
            d3.select("#instructions").style("display", state);
            d3.select(this).text(function() {
              return state == "none" ? "How to use..." : "How to use";
            })
          })

          mapBoxInit();
        })
      })
    })
  })
}

function mapBoxInit() {

  // mapbox access token
  mapboxgl.accessToken = 'pk.eyJ1IjoiYm9lcmljIiwiYSI6IkZEU3BSTjQifQ.XDXwKy2vBdzFEjndnE4N7Q';

  // define map layers
  var layerStack0 =  [    
    {
      "id": "countiesArea",
      "interactive": true,
      "source": "counties",
      "type": "fill",
      "paint": {
        "fill-color": "white",
        "fill-opacity": 0.1//0.01
      }
    },
    {
      "id": "countiesLine",
      "source": "counties",
      "type": "line",
      "paint": {
        "line-color": "#999",
        "line-width": 1,
      }
    }
  ]

  var layerStack2 = [
    {
      "id": "zipcodesLine",
      "source": "zipcodes",
      "type": "line",
      "paint": {
        "line-color": "darkred",
        "line-width": 0 //0.5,
      }
    }
  ]

  // define 9 map layers and bin the zip codes into layer based on driver licens suspension rate
  var zipLayers = []
  var levels = d3.range(9); // the ramp generated here is used to drive the fill opacity

  for (var p = 0; p < levels.length; p++) {
    var filters;
    if (p < levels.length - 1) {
      filters = [ 'all',
        [ '>=', 'FTAFTPS100', levels[p] ],
        [ '<', 'FTAFTPS100', levels[p + 1] ]
      ]
    } 
    else {
      filters = [ 'all',
        [ '>=', 'FTAFTPS100', levels[p] ]
      ]
    }

    // add the layer (and filters) to the zipLayers array
    zipLayers.push({
      id: 'cat' + p,
      interactive: true,
      type: 'fill',
      source: 'zipcodes', 
      paint: {
        'fill-color': "darkred",
        'fill-opacity': (p + 0) / levels.length // < 1% suspension rate == transparent
      },
      filter: filters
    })
  }

  var layers = [];
  layerStack0.forEach(function(d) { layers.push(d) })
  zipLayers.forEach(function(d)   { layers.push(d) })
  layerStack2.forEach(function(d) { layers.push(d) })


  // define the map
  var map = new mapboxgl.Map({
      container: 'map',
      maxZoom: 14, //13 
      minZoom: 4,
      zoom: 6,
      center: [-119, 37],
      style: 'mapbox://styles/mapbox/bright-v8',
      hash: false
  });

  // Add zoom and rotation controls to the map.
  map.addControl(new mapboxgl.Navigation());

  // load the counties and zipcode layers at style.load event
  map.on("style.load", function() {

    // add the two data sources before adding the layers
    // note: extreme performance penalty if adding the data source repeatedly for each layer
    map.addSource("counties", {
        "type": "geojson",
        "data": counties
    });

    map.addSource("zipcodes", {
        "type": "geojson",
        "data": zipcodes
    });

    // add the zip code layers
    layers.forEach(function(d, i) {
      map.addLayer(d)
    })
  })

  d3.select(".mapboxgl-ctrl-compass").on("click", function() {
    d3.select("#tiltSlider").property("value", 0)
  })

  // map pitch handlers
  d3.select("#tiltSlider").on("change", function() { tiltSlider.call(this) });
  d3.select("#tiltSlider").on("input",  function() { tiltSlider.call(this) });
  function tiltSlider() {
    var elem = d3.select(this)
    var value = +elem.property("value");
    map.setPitch(value)
  }

  // remove zip code area border when zoomed out
  map.on('zoom', function() {
      var layer = map.getLayer("zipcodesLine");
      var zoom = map.getZoom();
      //console.log("zoom", zoom)
      if (zoom < 9) map.setPaintProperty("zipcodesLine", "line-width", 0)
      else map.setPaintProperty("zipcodesLine", "line-width", 0.5);
  });

  // number formats for mouse handler below
  var fmtPct = d3.format(",.1%");
  var fmtInt = d3.format(",d");
  var fmtFloat = d3.format(".1f");

  // mouse handler
  map.on('mousemove', function (e) {
    map.featuresAt(e.point, {radius: 5}, function (error, features) {
      if (error) throw error;
      if (features.length == 0) return;

      // separate county and zip code entries in the features array
      var countyInfo = features.filter(function(d) { if (d.properties.ALAND != undefined) return true });
      var zipInfo = features.filter(function(d) { if (d.properties.City != undefined) return true });

      // clear properties
      var item = {
        County: "",
        ZipCode: "",
        Places: "",
        FTAFTPS100: "",
        City: "",
        povrate: "",
        Pop15Plus: "",
        IncK: ""
      };

      // obtain county name from first item in county array
      if (countyInfo.length > 0) {
        item.County = countyInfo[0].properties.NAME
      }

      // obtain zip code info from first item in zip code array
      if (zipInfo.length > 0) {
        item.ZipCode =    zipInfo[0].properties.zip;
        item.Places =     zipInfo[0].properties.Places;
        item.FTAFTPS100 = zipInfo[0].properties.FTAFTPS100;
        item.City =       zipInfo[0].properties.City;
        item.povrate =    zipInfo[0].properties.povrate;
        item.Pop15Plus =  zipInfo[0].properties.Pop15Plus;
        item.IncK =       zipInfo[0].properties.IncK;
      }

      // set the text in the overlay panel
      var text = [
        "Zip Code: <b>" + item.ZipCode + "</b>",
        "Place: <b>" + item.Places + "</b>",
        "County: <b>" + item.County + "</b>",
        "Suspensions: <b>" + fmtPct(item.FTAFTPS100 / 100) + "</b>",
        "Poverty Rate: <b>" + fmtPct(item.povrate / 100) + "</b>",
        "Population 15y+: <b>" + fmtInt(item.Pop15Plus) + "</b>",
        "Avg Income: <b>" + fmtFloat(""+item.IncK) + "K</b>"
      ];

      // update panel
      manageSidePanel(text);
    });
  });


  // manages the side panel
  function manageSidePanel(data) {
    var controls = d3.select("#controls");
    
    // remove current elements
    controls.selectAll("p").remove();
    
    // add new p elements
    controls.selectAll("p")
        .data(data)
      .enter().append("p")
        .html(function(d) { return d; })
  }

}

</script>