D3.js Earthquake Visualizations

earthquake visualizations

I the last few months I’ve become very interested in data visualizations and I spent a lot of time working with d3.js. D3 is an amazing javascript library used to manipulate data and has tools to create almost every kind of chart or visualization imaginable. The ease of adding interactivity make it more powerful.

I went through all of the examples in the book, Interactive Data Visualization for the Webwritten by Scott Murphy. The book goes over a variety of bar and pie charts as well as different approaches for adding and manipulating your data. Check out this post I did that sums up the examples and some of the updates I had to make for d3.js version 4.

Learn By Doing

Getting excited for new datasets available goes hand and hand when learning all about data visualization, in my case anyways. I wanted to expand on some of the examples and tutorials I went through and find my own datasets to explore.

Living in California means I’ve dealt with Earthquakes for much of my life, even though we can go years between actually feeling them. I knew the USGS site had a lot of information about Earthquakes as they happen, especially the Did You Feel It site. When checking out their site I found that they had some great API’s available to work with their datasets. They make them available in GeoJSON which gives you the coordinates so I could map them out on a map.

One of the examples that I worked on in the book used GeoJSON to create a map of a State, Country, or the entire Globe depending on the dataset you use. D3 then draws out a path element for the borders. There are varying levels of detailed files that you can get and either draw out each country individually or as one object. For this project I opted to do the entire globe as one object since it would draw faster and I was more interested in the Earthquakes themselves.

g.insert('path', '.graticule')
	 .datum(topojson.feature(world, world.objects.land))
	 .attr('class', 'land')
	 .attr('d', path)
	 .style('fill', 'steelblue')

Earthquakes v1.0

Before I could get to the earthquake data I needed a way to represent them on the map. In this example I don’t really need each individual country drawn out so I use .datum(topojson.feature(world, world.objects.land)). This way the land is drawn out as one object and speeds up that part of the process. Next I needed to plot of the earthquakes from the USGS API.

Fortunetly this part is easy. Within the callback of the d3.json for the world map I used another d3.json call to the USGS API for all of the earthquakes in the last 24 hours. They have several variations including

  • Past Hour
  • Past Day
  • Past 7 Days
  • Past 30 Days

And each of these feeds have magnitude options

  • Significant Earthquakes
  • M 4.5+ Earthquakes
  • M 2.5+ Earthquakes
  • M 1.0+ Earthquakes
  • All Earthquakes

I had already decided on the all earthquakes over the last day feed. Their feed is a standard GeoJSON and you can see the details here.

Once you have the data you just need to append circles using the geometry data.

.attr('cx', function(d) {
    return projection([d.geometry.coordinates[0], d.geometry.coordinates[1]])[0];
})
.attr('cy', function(d) {
    return projection([d.geometry.coordinates[0], d.geometry.coordinates[1]])[1];
})

I’ve skipped some of the basic things but you can see my full code in my pen at the bottom.

See the Pen Earthquakes in the last day by Anthony Skelton (@ajskelton) on CodePen.

Adding Animation

I had all of the quakes displaying, but I wanted some way to have the circles show up in the order that they happened. If there were twenty quakes but one an hour in an area, that would be very different all twenty in just one hour.

Now I needed to first set the radius of all of the quakes to zero and have them expand to the correct size over time. But the problem was I needed to find a way figure out how long to set between each quakes reveal. Each quake has the a time object in their properties. I decided to start with a new date object that was 24 hours ago.

// Setup 24 hours ago object
var dateObj = new Date();
dateObj.setDate(dateObj.getDate() - 1);

Next, I loop through my array of quakes and find the difference between the time object of the current quake and my starting point of 24 hours ago.

// Function that calculates the difference between the earthquake and 24 hours ago and
// creates a delay property.
var setQuakeDelay = function() {
	for(var i = 0, max = quake.length; i < max; i++){
		var timeDiff = quake[i].properties.time - dateObj;
		var timeDiffObj = new Date(timeDiff);
		quake[i].delay = Date.parse(timeDiffObj) / 5000; // Speed up the animation, otherwise it would take 24 hours ><
	}
}
setQuakeDelay();

Now each quake has a new property of delay that is a number of seconds from the starting point that the earthquake happened. I divided the number by 5000 to speed up the process.

The next step is animating all of the quake circles with their new delay.

// Changes the radius of the earthquakes to their magnitue using a transition
// and the delay created from the setQuakeDelay function
var quakeCircles = svg.selectAll('.quake-circle')
		      .data(quake)
		      .transition()
		      .delay(function(d) {
		          return d.delay;
		      })
		      .duration(1000)
		      .attr('r', function(d) {
		          if(d.properties.mag < 0) {
			      return 0.1;
		          } else {
			      return d.properties.mag				 
		          }
		       });

Perfect! Now the circles are all animating from 0 to their magnitude. I wanted to draw the viewers attention to each quake as it appears but some of the circles are very small. I decided to add another circle that pulses out and dissolves. I append these similar to the original circles. I later realized I needed the actual quakes to be added second so they are on top of the pulse. This matters for the text title tooltip that I add on to each quake.

I animate the pulse circles similarly to the quake circles, except that the size of the circle is multipled by a factor of 8. I also remove the pulse circle afterwards to clean up the DOM.

// Changes the radius of the pulse circle to eight times the magnitude
// and fades out as it expands over two seconds
var pulseCircles = svg.selectAll('.pulse-circle')
			 .data(quake)
			 .transition()
			 .delay(function(d) {
				 return d.delay;
			 })
			 .duration(2000)
			 .attr('r', function(d) {
			   if(d.properties.mag < 0) {
					 return 0.1 * 8;
				 } else {
				 	 return d.properties.mag * 8;
				 }
			 })
			 .style('opacity', 0)
		   .remove()

Adding an Timeline Axis

It’s great that the quakes are happening at roughly the right time increments over the last 24 hours, but I wanted to have a clock or something so you could see the pace of the quakes as they are happening.

After I append the world map I add an x-axis along the top of the map. First I set my variable x to d3.scaleTime() using the 24 hour ago object as the domain. This way my ticks represent the hours starting at the correct time.

// Create the x scale based on the domain of the 24 hour ago object and now
	var x = d3.scaleTime()
		  .domain([dateObj, new Date()])
		  .range([0, width - margin.right - margin.left]);

	// Append the xAxis on top
	var xAxis = svg.append('g')
		       .attr('id', 'xAxis')
		       .attr('transform', 'translate(20, 20)')
		       .call(d3.axisTop(x));

I create a red dot similar to my earthquakes as a progress marker along the timeline, but I need to set the length of time it should take to traverse the axis. For this I grab the longest delay property of an earthquake by accessing the last quake in my array.

var longestDelay = quake[quake.length - 1].delay;

This is the longest delay an animation should take, so that’s a great place to start. I could have tried to make this more accurate but I opted to just add 1000 ms and wrapped up the project. I would like to next build in the option to repeat the animation, and also zoom in on certain areas, but for now I have version 2 complete.

Click Run Pen to watch all of the earthquakes in the last 24 hours. It’s not responsive, so watch on a larger screen for optimal results.

Earthquakes v2.0

See the Pen Earthquakes in the last 24 hours by Anthony Skelton (@ajskelton) on CodePen.

I changed up the colors some from v1.0 as a suggestion from my wife. With the darker gray colors for the oceans and land mass it lets the red and white earthquake circles really pop.

D3.js version 4

d3.js version 4

Interactive Data Visualization for the Webwritten by Scott Murphy is a great introduction to using D3.js and begin to visualize your data. However, it was written for version 3, and version 4 came out earlier this year. I started working through the examples in CodePen and added d3 with their quick and easy framework dropdown. This adds the newest version which I hadn’t realized was so different than the one used in the book. I decided to stick with it and work out the examples using the new version. This post will help you translate the examples into d3.js version 4.

Bar Charts using d3.js version 4

See the Pen d3.js | Hover, Highlight, and Tooltips for Bar Chart by Anthony Skelton (@ajskelton) on CodePen.

In this bar chart example you first setup the xScale with an scale ordinal. Here’s the version 3 code, followed by the version 4.

/* VERSION 3 */
var xScale = d3.scale.ordinal()
                .domain(d3.range(dataset.length))
                .rangeRoundBands([0, w], 0.05);

/* VERSION 4 */
var xScale = d3.scaleBand()
	       .domain(d3.range(dataset.length))
	       .rangeRound([0, w])
               .paddingInner(0.05);

One of the most consistant changes is dropping the first scale method and combining it with the call to the specific method. For example d3.scale.linear is now d3.scaleLinear. In the above example the scale is still an ordinal scale in version 4, it’s now a specific ordinal scale for bands. You also have d3.scalePoint and the standard d3.scaleOrdinal at your disposal.

Similarly we don’t need to specify the .rangeRoundBands, because we’re already talking about bands. This changes to just .rangeRound. However you can no longer define a padding in the rangeRound, so there is another method for doing just that, .paddingInner.

Another change for our bar chart is determining the width of each individual bar. Our scaleBand scale has a method called band.bandwidth() that returns with width of each band so that they evenly take up the allotted space. In version 3 you used rangeBands() which took a log and a high value, often [ 0 , width ], and also a second parameter that added spacing between the bands.

/* VERSION 3 */
.attr("width", xScale.rangeBand())

/* VERSION 4 */
.attr("width", xScale.bandwidth())

The new version makes it much easier because it knows on a scaleBand you’re going to want to know how wide to make each band so they evenly distribute on the range that we declare with band.rangeRound() (or just band.range() ).

Time for Pie Charts

See the Pen d3.js | Simple Ring Chart by Anthony Skelton (@ajskelton) on CodePen.

With version 4 you can use svg or canvas to draw your shapes. Because of this only a couple of changes are needed to follow the Pie Chart tutorial.

Here’s how the two versions compare:

/* VERSION 3 */
var pie = d3.layout.pie();

var w = 300;
var h = 300;

var outerRadius = w / 2;
var innerRadius = 0;
var arc = d3.svg.arc()
                .innerRadius(innerRadius)
                .outerRadius(outerRadius);

var arcs = svg.selectAll("g.arc")
        .data(pie(dataset))
        .enter()
        .append("g")
        .attr("class", "arc")
        .attr("transform", "translate(" + outerRadius + ", " + outerRadius + ")");

/* VERSION 4 */
var pie = d3.pie();

var w = 300,
    h = 300;

var outerRadius = w / 2;
var innerRadius = w / 3;

var arc = d3.arc()
            .innerRadius(innerRadius)
            .outerRadius(outerRadius);

var arcs = svg.selectAll('g.arc')
              .data(pie(dataset))
	      .enter()
              .append('g')
              .attr('class', 'arc')
              .attr('transform', 'translate(' + outerRadius + ', ' + outerRadius + ')');

You’ll see the only difference is the initialization change from d3.layout.pie() to d3.pie() and the call for the arc generator from d3.svg.arc() to d3.arc().

This example also uses a set of colors that D3.js comes with that we can use to speed up development and not set specific colors. Here’s the comparison between versions:

/* VERSION 3 */
var color = d3.scale.category10();

/* VERSION 4 */
var color = d3.scaleOrdinal(d3.schemeCategory10);

Again a small difference but you’re code wont work if you don’t use the new syntax. Also in version 4 you get three versions of a 20 color scheme category you can access with

  • d3.schemeCategory20
  • d3.schemeCategory20b
  • d3.schemeCategory20c

Also in version 4 we get a new module called d3-scale-chromatic. This offers a more powerful color scheme. You will have to make sure to include the module when you need to use it. Read more and check out all of the schemes on the modules github.

Use the force

See the Pen d3.js | Force Simulation by Anthony Skelton (@ajskelton) on CodePen.

Using data visulizations in d3.js version 4 is a little different and splits everything out into batches of methods for each part of the visualization. You create a new force similar to other shapes and layouts by using the updated d3.forceSimulation. This is your simulation and there a several methods for

/* VERSION 3 */
var simulation = d3.layout.force()
                   .nodes(dataset.nodes)
                   .links(dataset.edges)
                   .size([w, h])
                   .linkDistance([50])
                   .charge([-100])
                   .start();

/* VERSION 4 */
var simulation = d3.forceSimulation(dataset.nodes)
                   .force('charge', d3.forceManyBody())
                   .force('link', d3.forceLink(dataset.links))
                   .force('center', d3.forceCenter(width / 2, height / 2));


simulation.nodes(dataset.nodes)
          .on('tick', ticked);

simulation.force('link')
          .links(dataset.links);

We start the same by dropping the extra layout method and just use d3.forceSimulation(), but the next step is accessing the .force methods of the simulation. Each of these return the force of the specified name to create a new simulation to layout.

Mapping with GeoJSON

See the Pen d3.js | GeoJSON by Anthony Skelton (@ajskelton) on CodePen.

The examples for GeoJSON in d3.js version 4 are very similar to version 3, with just a couple changes that we’ve seen in all of the past examples.

/* VERSION 3 */
var projection = d3.geo.albersUsa()
                       .translate([w/2, h/2]);

var path = d3.geo.path()
                 .projection(projection);

/* VERSION 4 /*
var projection = d3.geoAlbersUsa()
                   .translate([width/2, height/2]);

var path = d3.geoPath()
    .projection(projection);

Wrapping up

With these updates you should be able to make it through all of the examples in Interactive Data Visualization for the Web and get you started on your journey with d3.js version 4 data visualizations.