d3.js - a tiny introduction with Moon Phase Visualizer

Fig A Fig a Fig b Month 1

About

This tutorial aims to give a brief introduction to d3.js with which Moon phase visualizer demo is built.

Introduction

This d3 tutorial gives a brief introduction to the following. The above are just a fraction of the actual d3 library functionalities. The two topics are introduced here because the Moon Phase Visualizer demo is built with a basic usage of above functionalities of d3.js. This tutorial is intentionally short and aimed at just giving a good brief introduction for d3.js beginners.

What is d3.js?

d3 Selectors

When you select html element(s) with the above functions, you can do all the fancy things like change its attributes, style etc. For example, in the Moon Phase Visualizer demo, the rotation of moon is just selecting the circle element (moon) and just update the x and y position over the period of the time.
			
	// select the moon using 'select'. 'select' returns only one element
	var moon = d3.select("#moon"); 

	// calculate the co-ordinates
	// x = somevalue; y = somevalue;

	// now update the position using our selection
	moon.attr("cx", x).attr("cy", y);

	// ------------------------------------------------------------------

	// With 'selectAll' we can select all the elements for the selection
	// Here there are 2 timer elements in the page; returns both
	var timer = d3.selectAll(".phase.timer"); 

	// calculate the time
	// time = somevalue;

	// now update 'all' the elements from the 'selectAll'.
	timer.text( time ); 

But the real power of d3.js is not just selecting random elements, but help associating some data with the elements (before or after selecting the elements) and manipulate the elements with respect to the data associated with the elements. d3.js does this with Data Joins.

d3 Data Joins

Data Joins helps us to associate some data (which is given as an array) with html elements. If the data array changes, corresponding html elements can be easily associated or disassociated with the data.

Data Joins involve the following

Star Show with Data Joins

The left main screen (Fig A) in the above demo contains number of stars as background. Each star is represented by an svg circle element. Each star has a distinct position in the form of an 'x' and 'y' co-ordinate. The positions are generated randomly. For every few seconds, some stars disappear which will become red before disappearing, and new stars will appear which will be shown as blue. This was done using d3 Data Joins and lets see how its done.

  1. Our data is represented by a set of (x,y) co-ordinates which will be attributes of an object. Now, we have to display this data as something visual, which is a star with that particular co-ordinates as its position. It is important to remember that we still dont have any circle elements in the page representing the stars.
    
    // our data array containing star co-ordinates; initially it will be empty
    var starData = [];
    
    // lets fill the starData with sets of 10 random co-ordinates; Each co-ordinate represent a star position.
    // [ {x: 1, y: 1}, {x: 34, y: 56}, {x: 32, y: 8}, {x: 22, y: 48} ....... ]  // total of 10 sets 
    
    // the holder that will contain all the stars
    var canvas = d3.select("#starCanvas");
    
    
    1. Now, let us try to associate the co-ordinates data with circle element
      
      canvas.selectAll("circle") // select the circle elements (0 elements currently)
      		.data( starData ); // bind the data to the circles (10 co-ordinates)
      
      
      If we had initially 5 circle elements, the previous statement would have bound the first 5 co-ordinates to the 5 circle elements. If we see the above statement, we have 0 circle elements. But we have 10 co-ordinates. There are no circles to associate the co-ordinate data. In other words, we have 10 extra co-ordinate data for which we need circle elements to associate with.
    2. enter() & append(). Calling enter() returns the set of data for which there are no elements associated yet. In this case, all the 10 co-ordinates. Calling append("circle") creates new 'circle' elements and associates the data with it. The method append() appends elements of whatever tag is given as its argument. Expanding on the previous statement, we have
      
      canvas.selectAll("circle") // select the circle elements (0 elements currently)
      		.data( starData ); // bind the data to the circles (10 co-ordinates)
      		.enter() // gets the new data without elements associated
      		.append("circle") // add the new elements for the new data
      
      
    3. At this point, we have new circle elements in the html page for each set of co-ordinates. Our aim is to make the co-ordinates as the position of the circle element. SVG circle elements takes the following attributes: cx - x position, cy - y position, r - radius of the circle. Now we have a unique situation. We have associated the co-ordinate data to the circle element. How to tell d3.js to take that data and use it on the html elements as attributes? Let us expand further.
      
      canvas.selectAll("circle") // select the circle elements (0 elements currently)
      		.data( starData ); // bind the data to the circles (10 co-ordinates)
      		.enter() // gets the new data without elements associated
      		.append("circle") // add the new elements for the new data
      		.attr("r", 1); // static radius value is used for all elements
      		.attr("cx", function (d) { return d.x; } ) // dynamically takes x position from bound data for this element
      		.attr("cy", function (d) { return d.y; } ) // dynamically takes y position from bound data for this element
      
      
      d3.js has attr() methods, that takes a value or a function as second argument. If a value is used, it is used directly. This is done for the r attribute. All the stars have a default radius of one. But for the (x,y) positions we need values from our co-ordinates which we associated with the circle. d3.js allows us to use a function as second argument, with our bound data as the argument. So we return our (x, y) value from our co-ordinate object for (cx, cy) attributes respectively.
  2. Let us remove some stars. For this, we don't have to do ugly stuff like look for circle elements in html page and remove them. By now, we can see that d3.js maintains a close relationship between our data and the corresponding html elements. First we have to remove data from our data array. Then calling exit() will return the html elements for which there is no data in our data array. At this point, we have html elements that have no data from our data array. They are not removed yet. We can do something with those extra html elements if we need to before removing them. Only by calling remove(), the extra html elements are actually removed.
    						
    // let us remove 3 stars from the data Array
    starData = starData.slice(0, starData.length - 3);
    
    canvas.selectAll("circle") // select the circle elements (10 elements currently)
    		.data( starData ); // bind the new data to the circles (7 co-ordinates)
    		.exit() // gets the extra circle elements not bound to new data, in this case last 3
    		// let us change their color to red for 2 seconds before removing them
    		.transition().duration(2000).style("stroke", "red").style("fill", "red")
    		// only now the extra html elements are removed.
    		.remove(); 
    
    
  3. If we decide to add new stars, all we have to do is to update our data array and then let d3.js automatically add html elements from our data using our enter() and append() which we saw previously.
    					
    // let us add 3 stars to our data array
    starData.push( {x: 23, y: 44} );
    starData.push( {x: 33, y: 54} );
    starData.push( {x: 43, y: 24} );
    
    canvas.selectAll("circle") // select the circle elements (7 elements currently)
    		.data( starData ); // bind the new data to the circles (10 co-ordinates)
    		.enter() // gets the extra data without bound html elements (last 3 co-ordinates)
    		.append("circle") // add the new elements for the new data
    		.attr("r", 1); // static radius value is used for all elements
    		.attr("cx", function (d) { return d.x; } ) // dynamically takes x position from bound data for this element
    		.attr("cy", function (d) { return d.y; } ) // dynamically takes y position from bound data for this element
    		.style("stroke", "blue").style("fill", "blue") // initial star color
    		// let us change their color back to white from blue after 2 seconds
    		.transition().duration(2000).style("stroke", "white").style("fill", "white");
    
    
  4. How to update the existing elements to something we need? For example, if we want to change all the existing star color to 'silver'. In this case there are no additions or deletions. If we do a data join, we will get the existing html elements bound to the current data. Then we can manipulate the existing elements to our wish.
    
    canvas.selectAll("circle") // select the circle elements (10 elements currently)
    		.data( starData ); // bind the current data to the circles (10 co-ordinates)
    		// now we have got our existing stars; do whatever we want to do to them
    		.style("stroke", "silver").style("fill", "silver");
    
    
    Every time Data Join is done, the data is updated with the corresponding html element. For example, if we have changed some of the stars' co-ordinates, previous data join would have updated them. To reflect that change, we need to update their position. Expanding on the previous statement,
    
    // we have updated some of the stars' co-ordinates; So update to their new positions
    canvas.selectAll("circle") // select the circle elements 
    		.data( starData ); // bind the current data to the circles (changed co-ordinates will be updated now)
    		// update the circle elements to new position
    		.attr("cx", function (d) { return d.x; } ) // all stars gets their new x co-ordinate
    		.attr("cy", function (d) { return d.y; } ) // all stars gets their new y co-ordinate
    
    
  5. It is important to note that we have not used the optional 'key' function in our data binding, since we are using the default index based mapping. That is, first data in the data array should correspond to the first html element and so on. d3.js allows us to define a custom key function to define the relation between our data and the html element.

Resources

d3.js wiki page gives a lot of interesting resources to check out. Particularly, Thinking with Joins by Michael Bostock (the creator of d3.js) is really great to learn about d3.js data joins.

Feedback

Feedbacks and suggestions are greatly appreciated. If you are familiar with Github you can use Github issues for any changes or suggestions in this article. Also, you can ping your feedbacks to initdot@gmail.com.
This demo is currently best viewed in chrome. Tested in Chrome and Firefox.
This demo is not to scale. They are not shown with complete scientific accuracy for demonstration purposes.