block by bessiec 8e898da6849a87c19e24a2c4bceea598

D3 Collapsing Bubbles of the Origin of Migrants into the US

Full Screen

index.html

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>D3 Migration Bubble Chart</title>
  <meta name="description" content="Example Bubble chart implementation in JS. Based on NYT visualization">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
  <style>
		a, a:visited, a:active {
		  color: #444;
		}

		.container {
		  max-width: 1200px;
		  margin: auto;
		}

		.button {
		  min-width: 130px;
		  padding: 4px 5px;
		  cursor: pointer;
		  text-align: center;
		  font-size: 13px;
		  border: 1px solid #e0e0e0;
		  text-decoration: none;
		}

		.button.active {
		  background: #000;
		  color: #fff;
		}


		#vis {
		  width: 1300px;
		  height: 800px;
		  clear: both;
		  margin-bottom: 10px;
		}

		#toolbar {
		  margin-top: 10px;
		}

		.year {
		  font-size: 21px;
		  fill: #aaa;
		  cursor: default;
		}

		.tooltip {
			position: absolute;
			top: 100px;
			left: 100px;
		  -moz-border-radius:5px;
			border-radius: 5px;
		  border: 2px solid #000;
		  background: #fff;
			opacity: .9;
		  color: black;
			padding: 10px;
			width: 300px;
			font-size: 12px;
			z-index: 10;
		}

		.tooltip .title {
			font-size: 13px;
		}

		.tooltip .name {
		  font-weight:bold;
		}

		.footer {
		  text-align: center;
		}
	</style>
</head>


<body>
  <div class="container">
    <h1>Origin of Migrants into the United States</h1>
    <div id="toolbar">
      <a href="#" id="all" class="button active">All Available Years</a>
      <a href="#" id="year" class="button">By Year</a>
    </div>
    <div id="vis"></div>

    <div class="footer">
      <p>Code base from Jim Vallandingham's demonstration of animated bubble charts in JavaScript and D3.js</p>
      <p><a href="//vallandingham.me/bubble_charts_in_js.html">Blog Post</a> | <a href="https://github.com/vlandham/bubble_chart">Code</a></p>
      <p>Dataset from <a href="//www.global-migration.info/">THE GLOBAL FLOW OF PEOPLE</a></p>
    </div>
  </div>

 <script src="d3.js"></script>
  <script>


		/* bubbleChart creation function. Returns a function that will
		 * instantiate a new bubble chart given a DOM element to display
		 * it in and a dataset to visualize.
		 *
		 * Organization and style inspired by:
		 * https://bost.ocks.org/mike/chart/
		 *
		 */
		function bubbleChart() {
		  // Constants for sizing
		  var width = 1200;
		  var height = 800;

		  // tooltip for mouseover functionality
		  var tooltip = floatingTooltip('gates_tooltip', 240);

		  // Locations to move bubbles towards, depending
		  // on which view mode is selected.
		  var center = { x: width / 2, y: height / 2 };

		  var yearCenters = {
		    1990: { x: width / 3, y: height / 2 },
		    1995: { x: width / 2.2, y: height / 2 },
		    2000: { x: .62 * width, y: height / 2 },
		    2005: { x: .7 * width , y: height / 2 }
		  };

		  // X locations of the year titles.
		  var yearsTitleX = {
		    1990: 200,
		    1995: width * .38 ,
		    2000: width * 5/8,
		    2005: width - 200
		  };

		  // Used when setting up force and
		  // moving around nodes
		  var damper = 0.102;

		  // These will be set in create_nodes and create_vis
		  var svg = null;
		  var bubbles = null;
		  var nodes = [];

		  // Charge function that is called for each node.
		  // Charge is proportional to the diameter of the
		  // circle (which is stored in the radius attribute
		  // of the circle's associated data.
		  // This is done to allow for accurate collision
		  // detection with nodes of different sizes.
		  // Charge is negative because we want nodes to repel.
		  // Dividing by 8 scales down the charge to be
		  // appropriate for the visualization dimensions.
		  function charge(d) {
		    return -Math.pow(d.radius, 2.0) / 8;
		  }

		  // Here we create a force layout and
		  // configure it to use the charge function
		  // from above. This also sets some contants
		  // to specify how the force layout should behave.
		  // More configuration is done below.
		  var force = d3.layout.force()
		    .size([width, height])
		    .charge(charge)
		    .gravity(-0.01)
		    .friction(.9);

		  // Nice looking colors - no reason to buck the trend
		  var fillColor = d3.scale.ordinal()
		    .domain(['low', 'medium', 'high'])
		    .range(['#2A70AE', '#5486BB', '#99BBDD']);

		  // Sizes bubbles based on their area instead of raw radius
		  var radiusScale = d3.scale.pow()
		    .exponent(0.5)
		    .range([2, 85]);

		  /*
		   * This data manipulation function takes the raw data from
		   * the CSV file and converts it into an array of node objects.
		   * Each node will store data and visualization values to visualize
		   * a bubble.
		   *
		   * rawData is expected to be an array of data objects, read in from
		   * one of d3's loading functions like d3.csv.
		   *
		   * This function returns the new node array, with a node in that
		   * array for each element in the rawData input.
		   */
		  function createNodes(rawData) {
		    // Use map() to convert raw data into node data.
		    // Checkout //learnjsdata.com/ for more on
		    // working with data.
		    var myNodes = rawData.map(function (d) {
		      return {
		        id: d.id,
		        continent: d.continent,
		        radius: radiusScale(+d.migration_number),
		        country_orig: d.country_orig,
		        migration_number: +d.migration_number,
		        year: d.year,
		        x: Math.random() * 900,
		        y: Math.random() * 800
		      };
		    });

		    // sort them to prevent occlusion of smaller nodes.
		    myNodes.sort(function (a, b) { return b.value - a.value; });

		    return myNodes;
		  }

		  /*
		   * Main entry point to the bubble chart. This function is returned
		   * by the parent closure. It prepares the rawData for visualization
		   * and adds an svg element to the provided selector and starts the
		   * visualization creation process.
		   *
		   * selector is expected to be a DOM element or CSS selector that
		   * points to the parent element of the bubble chart. Inside this
		   * element, the code will add the SVG continer for the visualization.
		   *
		   * rawData is expected to be an array of data objects as provided by
		   * a d3 loading function like d3.csv.
		   */
		  var chart = function chart(selector, rawData) {
		    // Use the max total_amount in the data as the max in the scale's domain
		    // note we have to ensure the total_amount is a number by converting it
		    // with `+`.
		    var maxAmount = d3.max(rawData, function (d) { return +d.migration_number; });
		    radiusScale.domain([0, maxAmount]);

		    nodes = createNodes(rawData);
		    // Set the force's nodes to our newly created nodes array.
		    force.nodes(nodes);

		    // Create a SVG element inside the provided selector
		    // with desired size.
		    svg = d3.select(selector)
		      .append('svg')
		      .attr('width', width)
		      .attr('height', height);

		    // Bind nodes data to what will become DOM elements to represent them.
		    bubbles = svg.selectAll('.bubble')
		      .data(nodes, function (d) { return d.id; });

		    // Create new circle elements each with class `bubble`.
		    // There will be one circle.bubble for each object in the nodes array.
		    // Initially, their radius (r attribute) will be 0.
		    bubbles.enter().append('circle')
		      .classed('bubble', true)
		      .attr('r', 0)
		      .attr('fill', function (d) { return fillColor(d.migration_number); })
		      .attr('stroke', function (d) { return d3.rgb(fillColor(d.migration_number)).darker(); })
		      .attr('stroke-width', 2)
		      .on('mouseover', showDetail)
		      .on('mouseout', hideDetail);

		    // Fancy transition to make bubbles appear, ending with the
		    // correct radius
		    bubbles.transition()
		      .duration(2000)
		      .attr('r', function (d) { return d.radius; });

		    // Set initial layout to single group.
		    groupBubbles();
		  };

		  /*
		   * Sets visualization in "single group mode".
		   * The year labels are hidden and the force layout
		   * tick function is set to move all nodes to the
		   * center of the visualization.
		   */
		  function groupBubbles() {
		    hideYears();

		    force.on('tick', function (e) {
		      bubbles.each(moveToCenter(e.alpha))
		        .attr('cx', function (d) { return d.x; })
		        .attr('cy', function (d) { return d.y; });
		    });

		    force.start();
		  }

		  /*
		   * Helper function for "single group mode".
		   * Returns a function that takes the data for a
		   * single node and adjusts the position values
		   * of that node to move it toward the center of
		   * the visualization.
		   *
		   * Positioning is adjusted by the force layout's
		   * alpha parameter which gets smaller and smaller as
		   * the force layout runs. This makes the impact of
		   * this moving get reduced as each node gets closer to
		   * its destination, and so allows other forces like the
		   * node's charge force to also impact final location.
		   */
		  function moveToCenter(alpha) {
		    return function (d) {
		      d.x = d.x + (center.x - d.x) * damper * alpha;
		      d.y = d.y + (center.y - d.y) * damper * alpha;
		    };
		  }

		  /*
		   * Sets visualization in "split by year mode".
		   * The year labels are shown and the force layout
		   * tick function is set to move nodes to the
		   * yearCenter of their data's year.
		   */
		  function splitBubbles() {
		    showYears();

		    force.on('tick', function (e) {
		      bubbles.each(moveToYears(e.alpha))
		        .attr('cx', function (d) { return d.x; })
		        .attr('cy', function (d) { return d.y; });
		    });

		    force.start();
		  }

		  /*
		   * Helper function for "split by year mode".
		   * Returns a function that takes the data for a
		   * single node and adjusts the position values
		   * of that node to move it the year center for that
		   * node.
		   *
		   * Positioning is adjusted by the force layout's
		   * alpha parameter which gets smaller and smaller as
		   * the force layout runs. This makes the impact of
		   * this moving get reduced as each node gets closer to
		   * its destination, and so allows other forces like the
		   * node's charge force to also impact final location.
		   */
		  function moveToYears(alpha) {
		    return function (d) {
		      var target = yearCenters[d.year];
		      d.x = d.x + (target.x - d.x) * damper * alpha * 1.1;
		      d.y = d.y + (target.y - d.y) * damper * alpha * 1.1;
		    };
		  }

		  /*
		   * Hides Year title displays.
		   */
		  function hideYears() {
		    svg.selectAll('.year').remove();
		  }

		  /*
		   * Shows Year title displays.
		   */
		  function showYears() {
		    // Another way to do this would be to create
		    // the year texts once and then just hide them.
		    var yearsData = d3.keys(yearsTitleX);
		    var years = svg.selectAll('.year')
		      .data(yearsData);

		    years.enter().append('text')
		      .attr('class', 'year')
		      .attr('x', function (d) { return yearsTitleX[d]; })
		      .attr('y', 40)
		      .attr('text-anchor', 'middle')
		      .text(function (d) { return d; });
		  }


		  /*
		   * Function called on mouseover to display the
		   * details of a bubble in the tooltip.
		   */
		  function showDetail(d) {
		    // change outline to indicate hover state.
		    d3.select(this).attr('stroke', 'black');

		    var content = '<span class="name">Country Origin: </span><span class="value">' +
		                  d.country_orig +
		                  '</span><br/>' +
		                  '<span class="name">Number of Migrants: </span><span class="value">' +
		                  addCommas(d.migration_number) +
		                  '</span><br/>' +
		                  '<span class="name">Year: </span><span class="value">' +
		                  d.year +
		                  '</span>';
		    tooltip.showTooltip(content, d3.event);
		  }

		  /*
		   * Hides tooltip
		   */
		  function hideDetail(d) {
		    // reset outline
		    d3.select(this)
		      .attr('stroke', d3.rgb(fillColor(d.group)).darker());

		    tooltip.hideTooltip();
		  }

		  /*
		   * Externally accessible function (this is attached to the
		   * returned chart function). Allows the visualization to toggle
		   * between "single group" and "split by year" modes.
		   *
		   * displayName is expected to be a string and either 'year' or 'all'.
		   */
		  chart.toggleDisplay = function (displayName) {
		    if (displayName === 'year') {
		      splitBubbles();
		    } else {
		      groupBubbles();
		    }
		  };


		  // return the chart function from closure.
		  return chart;
		}

		/*
		 * Below is the initialization code as well as some helper functions
		 * to create a new bubble chart instance, load the data, and display it.
		 */

		var myBubbleChart = bubbleChart();

		/*
		 * Function called once data is loaded from CSV.
		 * Calls bubble chart function to display inside #vis div.
		 */
		function display(error, data) {
		  if (error) {
		    console.log(error);
		  }

		  myBubbleChart('#vis', data);
		}

		/*
		 * Sets up the layout buttons to allow for toggling between view modes.
		 */
		function setupButtons() {
		  d3.select('#toolbar')
		    .selectAll('.button')
		    .on('click', function () {
		      // Remove active class from all buttons
		      d3.selectAll('.button').classed('active', false);
		      // Find the button just clicked
		      var button = d3.select(this);

		      // Set it as the active button
		      button.classed('active', true);

		      // Get the id of the button
		      var buttonId = button.attr('id');

		      // Toggle the bubble chart based on
		      // the currently clicked button.
		      myBubbleChart.toggleDisplay(buttonId);
		    });
		}

		/*
		 * Helper function to convert a number into a string
		 * and add commas to it to improve presentation.
		 */
		function addCommas(nStr) {
		  nStr += '';
		  var x = nStr.split('.');
		  var x1 = x[0];
		  var x2 = x.length > 1 ? '.' + x[1] : '';
		  var rgx = /(\d+)(\d{3})/;
		  while (rgx.test(x1)) {
		    x1 = x1.replace(rgx, '$1' + ',' + '$2');
		  }

		  return x1 + x2;
		}

		// Load the data.
		d3.csv('migration_bubble.csv', display);

		// setup the buttons.
		setupButtons();

		/*
		 * Creates tooltip with provided id that
		 * floats on top of visualization.
		 * Most styling is expected to come from CSS
		 * so check out bubble_chart.css for more details.
		 */
		function floatingTooltip(tooltipId, width) {
		  // Local variable to hold tooltip div for
		  // manipulation in other functions.
		  var tt = d3.select('body')
		    .append('div')
		    .attr('class', 'tooltip')
		    .attr('id', tooltipId)
		    .style('pointer-events', 'none');

		  // Set a width if it is provided.
		  if (width) {
		    tt.style('width', width);
		  }

		  // Initially it is hidden.
		  hideTooltip();

		  /*
		   * Display tooltip with provided content.
		   *
		   * content is expected to be HTML string.
		   *
		   * event is d3.event for positioning.
		   */
		  function showTooltip(content, event) {
		    tt.style('opacity', 1.0)
		      .html(content);

		    updatePosition(event);
		  }

		  /*
		   * Hide the tooltip div.
		   */
		  function hideTooltip() {
		    tt.style('opacity', 0.0);
		  }

		  /*
		   * Figure out where to place the tooltip
		   * based on d3 mouse event.
		   */
		  function updatePosition(event) {
		    var xOffset = 20;
		    var yOffset = 10;

		    var ttw = tt.style('width');
		    var tth = tt.style('height');

		    var wscrY = window.scrollY;
		    var wscrX = window.scrollX;

		    var curX = (document.all) ? event.clientX + wscrX : event.pageX;
		    var curY = (document.all) ? event.clientY + wscrY : event.pageY;
		    var ttleft = ((curX - wscrX + xOffset * 2 + ttw) > window.innerWidth) ?
		                 curX - ttw - xOffset * 2 : curX + xOffset;

		    if (ttleft < wscrX + xOffset) {
		      ttleft = wscrX + xOffset;
		    }

		    var tttop = ((curY - wscrY + yOffset * 2 + tth) > window.innerHeight) ?
		                curY - tth - yOffset * 2 : curY + yOffset;

		    if (tttop < wscrY + yOffset) {
		      tttop = curY + yOffset;
		    }

		    tt.style({ top: tttop + 'px', left: ttleft + 'px' });
		  }

		  return {
		    showTooltip: showTooltip,
		    hideTooltip: hideTooltip,
		    updatePosition: updatePosition
		  };
		}



  </script>



</body>
</html>