block by curran 6284affc05bdeb7dfc9e

Mortality

Full Screen

This example is a revived piece of old code from February 2014.

It shows causes of death in an interactive set of linked visualizations, a tree navigator and a stacked area chart.

web counter

index.html

<!-- This page is an example visualization of Cause of Death data
     from the Centers for Disease Control.

     Details of the data can be found here:
     https://github.com/curran/data/tree/gh-pages/cdc/mortality

     The visualization draws from this D3 example:
     //bl.ocks.org/mbostock/3885211

     Curran Kelleher
     2/13/2014
-->
<!DOCTYPE html>
<html>
  <head>
    <meta charset=utf-8 />
    <title>Causes of Death</title>
    <script src="//d3js.org/d3.v3.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
    <style>
      body {
        font: 12px sans-serif;
      }
      .title {
        text-anchor: middle;
        font-size: 2em;
      }

      /* Begin style for stacked area visualization. */
      .axis path,
      .axis line {
        fill: none;
        stroke: #000;
        shape-rendering: crispEdges;
      }
      /* End style for stacked area visualization. */

      /* Begin style for tree visualization. */
      .node circle {
        stroke: #000;
        stroke-width: 1.5px;
      }

      .node .with-children {
        fill: #000;
      }

      .node .without-children {
        fill: #FFF;
      }

      .node {
        font: 12px sans-serif;
      }

      .link {
        fill: none;
        stroke: #ccc;
        stroke-width: 1.5px;
      }

      /* End style for tree visualization. */

    </style>
  </head>
  <body>
    <script data-main="main" src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.10/require.min.js"></script>
  </body>
</html>

getShortName.js

// This module provides short names for causes of death,
// for use as labels for concise presentation.
//
// Curran Kelleher 2/18/2014

define([], function () {
  var shortNames = {
    'Major cardiovascular diseases': 'Cardiovascular diseases',
    'Symptoms, signs, and abnormal clinical and laboratory findings, not elsewhere classified': 'Unclassified conditions',
    'Chronic lower respiratory diseases': 'Respiratory diseases',
    'Pneumonitis due to solids and liquids': 'Pneumonitis',
    'Chronic liver disease and cirrhosis': 'Liver disease',
    'Complications of medical and surgical care': 'Complications of care',
    'Benign/other neoplasms': 'Neoplasms'
  };

  // Gets a short version of a given cause of disease
  // for use as labels.
  return function getShortName(name) {
    var shortName = shortNames[name];
    return shortName ? shortName : name;
  };
});

main.js

// A visualization of Cause of Death data from the Centers for Disease Control.
//
// Details of the data can be found here:
// https://github.com/curran/data/tree/gh-pages/cdc/mortality
//
// Curran Kelleher
// 2/18/2014
require(['tree', 'stackedArea'], function (tree, stackedArea) {

  // Fetch the data as an AMD module.
  var tableURL = 'http://curran.github.io/data/cdc/mortality/mortality_full.js',
      hierarchyURL =  'http://curran.github.io/data/cdc/mortality/hierarchy/hierarchy.js';
  require([tableURL, hierarchyURL], function(table, hierarchy){

    // The dimensionsions of the SVG shared by both visualizations.
    var outerWidth = 960,
        outerHeight = 500,

        // The number of pixels from the left where the
        // two visualizations meet.
        horizontalSplit = 350,

        // The margins for each visualization.
        margins = {
          tree: {
            top: 20,
            right: outerWidth - horizontalSplit + 160,
            bottom: 27,
            left: 8
          },
          stackedArea: {
            top: 27,
            right: 170,
            bottom: 30,
            left: horizontalSplit
          }
        },

        // Create the SVG element that will contain both visualizations.
        svg = d3.select('body').append('svg')
          .attr('width', outerWidth)
          .attr('height', outerHeight),
          
        // Add the title of the plot.
        title = svg.append('text')
          .attr('x', outerWidth / 2 )
          .attr('y', 20)
          .attr('class', 'title');

    // Initialize the tree visualization.
    tree.init(svg, outerWidth, outerHeight, margins.tree, hierarchy);

    // Initialize the stacked area visualizaiton.
    stackedArea.init(svg, outerWidth, outerHeight, margins.stackedArea, table);

    // Set up the stacked area to respond to tree navigations.
    // Called with the cause of death names to show.
    tree.onNavigate(function (newRoot, names){
      stackedArea.update(names);
      title.text('Causes of Death in the US: ' + newRoot.name);
    });
    
  }, function(err){
    // If we are here, the data failed to load.
    console.log(err);
  });
});

stackedArea.js

// This module implements the stacked area visualization.
//
// Curran Kelleher 2/18/2014
define(['getShortName'], function (getShortName) {
  var width,
      height,
      g,
      data,
      x = d3.time.scale(),
      y = d3.scale.linear(),
      color = d3.scale.ordinal()
        // Colors hand-picked from http://www.w3schools.com/tags/ref_colorpicker.asp
        .range(['#006699','#00CC99','#009933','#CC6699','#99CC00','#CC9900','#CC3300','#FFCC00','#FF0000','#990033','#FF6699','#CC3399','#9900FF','#6666FF','#3333CC','#66CCFF','#0000FF','#00FFFF','#99FFCC','#CCFF99','#FFCC99','#FF99CC','#CC99FF','#CCCCFF','#333300']),
      xAxis = d3.svg.axis()
        .scale(x)
        .orient('bottom'),
      xAxisG,
      area = d3.svg.area()
        .x(function(d) { return x(d.date); })
        .y0(function(d) { return y(d.y0); })
        .y1(function(d) { return y(d.y0 + d.y); }),
      stack = d3.layout.stack()
        .values(function(d) { return d.values; })
        .offset('expand');

  // This function should be called once to set up the visualization.
  function init(svg, outerWidth, outerHeight, margin, _data){
    data = _data;

    // Parse years into Date objects for use with D3 time scale.
    data.forEach(function(d) {
      d.date = new Date(d.year, 0);
    });

    width = outerWidth - margin.left - margin.right;
    height = outerHeight - margin.top - margin.bottom;
    x.range([0, width]);
    y.range([height, 0]);
    g = svg.append('g')
      .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
    xAxisG = g.append('g')
      .attr('class', 'x axis')
      .attr('transform', 'translate(0,' + height + ')');
  }

  function update(names){
    var causes,
        namesIndex = {};

    x.domain(d3.extent(data, function(d) { return d.date; }));

    names.forEach(function(name){
      namesIndex[name] = true;
    });

    // Transform the data for D3's stack layout.
    // see https://github.com/mbostock/d3/wiki/Stack-Layout
    causes = names.map(function(name) {
      return {
        name: name,
        values: data.map(function(d) {
          return {date: d.date, y: clean(d[name])};
        })
      };
    })

    // Sort the layers by the most recent value.
    causes = _.sortBy(causes, function(cause) {
      return cause.values[cause.values.length - 1].y;
    });

    // Set the color domain so each color is a cause of death.
    // Use sorted values.
    color.domain(_.pluck(causes, 'name').reverse());

    // Add the stacked areas.
    var cause = g.selectAll('.cause')
      .data(stack(causes));

    cause.enter().append('path')
      .attr('class', 'cause');

    cause
      .attr('d', function(d) { return area(d.values); })
      .style('fill', function(d) { return color(d.name); });

    cause.exit().remove();

    // Add the legend.
    // See http://bl.ocks.org/mbostock/3888852
    // Use sorted causes for legend.
    var legend = g.selectAll('.legend')
      .data(causes.map(function(d){ return d.name; }).reverse());

    var legendEnter = legend.enter().append('g')
      .attr('class', 'legend');

    legend.attr('transform', function(d, i) {
      return 'translate(' + (width + 3) + ',' + i * 20 + ')'; 
    });

    // TODO remove hard code size.
    legendEnter.append('rect')
      .attr('width', 18)
      .attr('height', 18);
    legend.select('rect')
      .style('fill', color);

    legendEnter.append('text')
      .attr('x', 20)
      .attr('y', 10)
      .attr('dy', '.35em');
    legend.select('text')
      .text(getShortName);

    legend.exit().remove();

    // Add the X axis (years).
    xAxisG.call(xAxis);
  }

  // Replace missing data with 0 and parse strings into numbers.
  function clean(value){
    return value === '~' ? 0 : parseFloat(value);
  }

  return {
    init: init,
    update: update
  };
});

tree.js

// A tree visualization of Cause of Death hierarchy from the Centers for Disease Control.
// This module implements the tree visualization.
//
//
// Details of the hierarchy can be found here:
// https://github.com/curran/hierarchy/tree/gh-pages/cdc/mortality
//
// Draws from:
//
// Radial Tree
// http://bl.ocks.org/mbostock/4063550
//
// Linear Tree
// http://bl.ocks.org/mbostock/4063570
//
// Margin Convention
// http://bl.ocks.org/mbostock/3019563
//
// Collapsible Tree Layout
// http://mbostock.github.io/d3/talk/20111018/tree.html
//
// Curran Kelleher 2/18/2014
define(['getShortName'], function (getShortName) {

  // A function called when the user navigates in the tree.
  var onNavigate = function(){},

      // Keeps track of the root of the visible tree.
      root;

  // This function should be called once to set up the visualization.
  function init(svg, outerWidth, outerHeight, margin, hierarchy){

    var nodeRadius = 4,
        labelOffset = 7,
        width = outerWidth - margin.left - margin.right,
        height = outerHeight - margin.top - margin.bottom,

        tree = d3.layout.tree()
          .size([height, width])
          .separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; }),

        diagonal = d3.svg.diagonal()
          .projection(function(d) { return [d.y, d.x]; }),
        
        g = svg.append('g')
          .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')

        // Use a separate group for links so they always appear behind nodes.
        linkG = g.append('g');

    // Initialize the visualization to show the top of the tree.
    navigate(hierarchy);

    function navigate(newRoot) {
      root = newRoot;
      onNavigate(root, childNames(root));
      update();
    }

    function update(){

      // Toggles nodes to show direct children only.
      showSubtree(root);

      var nodes = layoutNodes(root);
      var links = tree.links(nodes);

      var link = linkG.selectAll('.link')
          .data(links);
      link.enter().append('path')
          .attr('class', 'link');
      link.attr('d', diagonal);
      link.exit().remove();

      var node = g.selectAll('.node')
          .data(nodes);
      
      var nodeEnter = node.enter().append('g')
          .attr('class', 'node');

      node.attr('transform', function(d) { return 'translate(' + d.y + ',' + d.x + ')'; });

      nodeEnter.append('circle')
        .attr('r', nodeRadius)
        .on('click', handleClick);

      node.select('circle')
        .attr('class', function (d) {
          return hasChildren(d) ? 'with-children' : 'without-children';
        })
        .call(setCursor);

      nodeEnter.append('text')
        .attr('dy', '.35em')
        .attr('dx', labelOffset + 'px')
        .attr('text-anchor', 'start')
        .on('click', handleClick);
      node.select('text')
        .text(function(d) { return getShortName(d.name); })
        .call(setCursor);

      node.exit().remove();
    }

    // Handles the special case of a single child,
    // which is not handled properly by D3's tree layout.
    function layoutNodes(root){
      var nodes = tree.nodes(root);
      if(nodes.length === 2) {
        nodes.forEach(function(node){
          node.x = height / 2;
        });
      }
      return nodes;
    }

    // Handles clicking on a node in the tree (node or text).
    function handleClick(d){
      // If the user clicks on the root node, navigate up the tree.
      if(d.isRoot && d.parent) {
        navigate(d.parent);
      } else if(d._children) {
        // Otherwise navigate down the tree.
        navigate(d);
      }
    }
    
    // Returns whether or not a node has more than one child,
    // regardless of whether it is collapsed or not.
    function hasChildren(d){
      var children = d.children || d._children;
      return (children && children.length > 1)
    }

    // Sets the cursor of the given selection (circle or text)
    // to be a pointer if the node has more than one child,
    // as an affordance that it can be clicked.
    function setCursor(selection){
      selection.style('cursor', function (d) {
        return hasChildren(d) ? 'pointer' : 'auto';
      });
    }
  }

  // Expands and collapses nodes such that:
  // `root` is expanded, and
  // the children of `root` are collapsed.
  function showSubtree(root){
    expand(root);

    // The `isRoot` flag is used in the click event 
    // handler to determine which navigation direction
    // is intended - clicking on the root moves up the tree.
    root.isRoot = true;

    if(root.children){
      root.children.forEach(function(node) {
        collapse(node);
        node.isRoot = false;
      });
    }
  }

  function expand(d){
    if (d._children) {
      d.children = d._children;
      d._children = null;
    }
  }

  function collapse(d) {
    if (d.children) {
      d._children = d.children;
      d.children = null;
    }
  }

  function childNames(d){
    var children = d.children || d._children;
    return _.pluck(children, 'name');
  }

  return {
    init: init,
    onNavigate: function(callback) {
      onNavigate = callback;

      // Call the callback once initially.
      if(root){
        onNavigate(root, childNames(root));
      }
    }
  };
});