block by pjsier 3d53350e7939239461473d4d0fcda527

Chicago Ward Shape Transitions

Full Screen

Chicago Ward Shape Transitions

Example representing Chicago wards as a cartogram, boundaries, and bar chart with path tween transitions. Using a modified version of the Wall Street Journal’s Squaire library and MinnPost’s aRanger to produce the cartogram layout.

script.js

// Pulled from cartograms/chi_ward_cartogram_layout.json
// Created with http://code.minnpost.com/aranger/ and Squaire WSJ library
var chi_ward_layout = [[0,0,"41"],[3,0,"50"],[4,0,"49"],[1,1,"45"],[2,1,"39"],[3,1,"40"],[4,1,"48"],[0,2,"38"],[1,2,"30"],[2,2,"35"],[3,2,"33"],[4,2,"47"],[5,2,"46"],[1,3,"29"],[2,3,"36"],[3,3,"31"],[4,3,"32"],[5,3,"44"],[2,4,"37"],[3,4,"26"],[4,4,"1"],[5,4,"2"],[6,4,"43"],[3,5,"24"],[4,5,"28"],[5,5,"27"],[6,5,"42"],[3,6,"22"],[4,6,"12"],[5,6,"25"],[6,6,"11"],[1,7,"23"],[2,7,"14"],[3,7,"16"],[4,7,"15"],[5,7,"20"],[6,7,"3"],[7,7,"4"],[2,8,"13"],[3,8,"18"],[4,8,"17"],[5,8,"21"],[6,8,"6"],[7,8,"5"],[4,9,"19"],[5,9,"34"],[6,9,"8"],[7,9,"7"],[6,10,"9"],[7,10,"10"]];
var SQ_SIZE = 50;
var ease = d3.easeQuadInOut;
var transitionTime = 500;
var transitionDelay = 15;
var projection, path, geoData, centered, maxData, svg;
var active = d3.select(null);

// Margin convention from https://bl.ocks.org/mbostock/3019563
var margin = {top: 20, right: 10, bottom: 20, left: 10};
var width = 550 - margin.left - margin.right;
var height = 650 - margin.top - margin.bottom;
var svg = d3.select("#map-container")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

function convertRectPath(x, y, w, h) {
  return "M" + [[x,y], [x+w,y], [x+w, y+h], [x, y+h], [x,y]].join("L");
}

// Pulled from Mike Bostock example here: https://bl.ocks.org/mbostock/3916621
// Example without continuous update: http://bl.ocks.org/sarahob/707cb381d48f57abba57
function pathTween(d1, precision) {
  return function() {
    var path0 = this,
        path1 = path0.cloneNode(),
        n0 = path0.getTotalLength(),
        n1 = (path1.setAttribute("d", d1), path1).getTotalLength();

    // Uniform sampling of distance based on specified precision.
    var distances = [0], i = 0, dt = precision / Math.max(n0, n1);
    while ((i += dt) < 1) distances.push(i);
    distances.push(1);

    // Compute point-interpolators at each distance.
    var points = distances.map(function(t) {
      var p0 = path0.getPointAtLength(t * n0),
          p1 = path1.getPointAtLength(t * n1);
      return d3.interpolate([p0.x, p0.y], [p1.x, p1.y]);
    });

    return function(t) {
      return t < 1 ? "M" + points.map(function(p) { return p(t); }).join("L") : d1;
    };
  };
}

// Zoom to ward on click, pulled from https://bl.ocks.org/mbostock/4699541
function reset() {
  active.classed("active", false);
  active = d3.select(null);

  svg.transition()
    .duration(transitionTime)
    .style("stroke-width", "1.5px")
    .attr("transform", "");
}

function clicked(d) {
  if (active.node() === this) return reset();
  active.classed("active", false);
  active = d3.select(this).classed("active", true);

  var bounds = path.bounds(d),
      dx = bounds[1][0] - bounds[0][0],
      dy = bounds[1][1] - bounds[0][1],
      x = (bounds[0][0] + bounds[1][0]) / 2,
      y = (bounds[0][1] + bounds[1][1]) / 2,
      scale = .9 / Math.max(dx / width, dy / height),
      translate = [width / 2 - scale * x, height / 2 - scale * y];

  svg.transition()
    .duration(transitionTime)
    .style("stroke-width", 1.5 / scale + "px")
    .attr("transform", "translate(" + translate + ")scale(" + scale + ")");
}

function transitionShapes(el, idx, tween){
	 d3.select(el)
		.transition()
    .delay(idx*transitionDelay)
		.duration(transitionTime)
    .ease(ease)
		.attrTween('d', pathTween(tween, 5));
}

function drawCartogram(el, data, w, h){
  var color = d3.scaleQuantize().domain([0,maxData]).range(colorbrewer['OrRd'][5]);
  var g = el.append("g");
    g.selectAll("path")
      .data(data)
      .enter().append("path")
        .attr("d", function(d){return convertRectPath(d.x*w, d.y*h, w, h);})
        .attr("fill", function(d){return color(d.val);});

  var textG = el.append("g");
  textG.selectAll("text")
    .data(data)
    .enter().append("text")
    .attr("x", function(d) { return (d.x*w)+(w/2); })
    .attr("y", function(d) { return (d.y*h)+(h/2); })
    .text(function(d){ return d.properties.ward; });
};

function toWards() {
  if (centered) {
    clicked();
  }

  svg.selectAll("g path")
    .style("opacity", "1")
    .each(function(d, i) { transitionShapes(this, i, path(d)); })
    .on("click", clicked);

  svg.selectAll("g text")
    .transition()
    .delay(function(d, i) { return i*transitionDelay; })
    .duration(transitionTime)
    .ease(ease)
    .attr("x", function(d) { return path.centroid(d)[0]; })
    .attr("y", function(d) { return path.centroid(d)[1]; });
}

function toSquares() {
  if (centered) {
    clicked();
  }

  svg.selectAll("g path")
    .each(function(d, i) { transitionShapes(this, i, convertRectPath(d.x*SQ_SIZE, d.y*SQ_SIZE, SQ_SIZE, SQ_SIZE)); })
    .on("click", null);

  svg.selectAll("g text")
    .transition()
    .delay(function(d, i) { return i*transitionDelay; })
    .duration(transitionTime)
    .ease(ease)
    .attr("x", function(d) { return (d.x*SQ_SIZE)+(SQ_SIZE/2); })
    .attr("y", function(d) { return (d.y*SQ_SIZE)+(SQ_SIZE/2); });
}

function toBars() {
  if (centered) {
    clicked();
  }

  var y = d3.scaleLinear().range([width-50, 0]);
  var x = d3.scaleBand().rangeRound([0, height], .1);
  y.domain([0, maxData]);
  x.domain(geoData.map(function(d) { return d.properties.ward; }));

  svg.selectAll("g path")
    .each(function(d, i) {
      transitionShapes(this, i, convertRectPath(25, x(d.properties.ward), width-y(d.val), x.bandwidth()));
    })
    .on("click", null);

  svg.selectAll("g text")
    .transition()
    .delay(function(d, i) { return i*transitionDelay; })
    .duration(transitionTime)
    .ease(ease)
    .attr("x", 10)
    .attr("y", function(d) { return x(d.properties.ward)+(x.bandwidth()/2); });
}

(function() {
  projection = d3.geoMercator().scale(1).translate([0,0]);
  path = d3.geoPath().projection(projection);

  d3.queue()
    .defer(d3.json, "chi_wards_calls.geojson")
    .await(function(error, json) {
      var bounds = path.bounds(json);
      var s = 0.95 / Math.max((bounds[1][0] - bounds[0][0]) / width, (bounds[1][1] - bounds[0][1]) / height);
      var t = [(width - s * (bounds[1][0] + bounds[0][0])) / 2, (height - s * (bounds[1][1] + bounds[0][1])) / 2];
      projection.scale(s).translate(t);

      maxData = d3.max(json.features, function(d) { return d.properties.wib_calls; });
      geoData = json.features.map(function(d) {
        var wardRow = chi_ward_layout.filter(function(w) { return w[2] === d.properties.ward; });
        if (wardRow.length) {
          d.x = wardRow[0][0];
          d.y = wardRow[0][1];
          d.val = d.properties.wib_calls;
        }
        return d;
      });
      // Sort on ward strings
      geoData = geoData.sort(function(a, b) {
        a = parseInt(a.properties.ward);
        b = parseInt(b.properties.ward);
        if (a < b) {
          return -1;
        }
        else if (a > b) {
          return 1;
        }
        return 0;
      });

      drawCartogram(svg, geoData, SQ_SIZE, SQ_SIZE);
    });
})()

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>D3 Maps</title>
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
    <meta charset='utf-8' />
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <script src="https://d3js.org/d3-queue.v3.min.js"></script>
    <script src="//d3js.org/colorbrewer.v1.min.js"></script>
    <style>
    * {
      font-family: "Verdana";
      font-size: 10;
    }
    #container {
      margin-left: 15px;
      margin-top: 15px;
    }
    #map-container {
      display: block;
    }
    path {
      stroke: black;
      stroke-width: 0.3;
    }
    g text {
      alignment-baseline: central;
      text-anchor: middle;
      font-size: 10px;
    }
    </style>
  </head>
  <body>
    <div id="container">
      <input name="toWards"
             type="button"
             value="To Wards"
             onclick="toWards()" />
      <input name="toSquares"
             type="button"
             value="To Squares"
             onclick="toSquares()" />
     <input name="toBars"
            type="button"
            value="To Bars"
            onclick="toBars()" />
      <svg id="map-container"></svg>
    </div>
    <script src="script.js"></script>
  </body>
</html>

_block

license: mit