Book Image

Expert Data Visualization

By : Jos Dirksen
Book Image

Expert Data Visualization

By: Jos Dirksen

Overview of this book

Do you want to make sense of your data? Do you want to create interactive charts, data trees, info-graphics, geospatial charts, and maps efficiently? This book is your ideal choice to master interactive data visualization with D3.js V4. The book includes a number of extensive examples that to help you hone your skills with data visualization. Throughout nine chapters these examples will help you acquire a clear practical understanding of the various techniques, tools and functionality provided by D3.js. You will first setup your D3.JS development environment and learn the basic patterns needed to visualize your data. After that you will learn techniques to optimize different processes such as working with selections; animating data transitions; creating graps and charts, integrating external resources (static as well as streaming); visualizing information on maps; working with colors and scales; utilizing the different D3.js APIs; and much more. The book will also guide you through creating custom graphs and visualizations, and show you how to go from the raw data to beautiful visualizations. The extensive examples will include working with complex and realtime data streams, such as seismic data, geospatial data, scientific data, and more. Towards the end of the book, you will learn to add more functionality on top of D3.js by using it with other external libraries and integrating it with Ecmascript 6 and Typescript
Table of Contents (10 chapters)

How does D3 work?

At this point, you should have a working environment, so let's start by looking at some code and see if we can get D3 up and running. As we've mentioned at the beginning of this chapter, D3 is most often used to create and manipulate SVG elements using a data-driven approach. SVG elements can represent shapes, lines, and also allow for grouping. If you need a reference to check what attributes are available for a specific SVG element, the Mozilla Developer Network also has an excellent page on that: https://developer.mozilla.org/en-US/docs/Web/SVG.

In this section, we'll perform the following steps:

  1. Create and add an empty SVG group (g) element, to which we'll add our data elements.
  2. Use a JavaScript array that contains some sample data to add rectangles to the SVG element created in the previous step.
  3. Show how changes in the data can be used to update the drawn rectangles.
  4. Explain how to handle added and removed data elements using D3.

At the end of these steps, you should have a decent idea of how D3 binds data to elements, and how you can update the bound data.

Creating a group element

The first thing we need to do is create a g element to which we can add our own elements. Since we're visualizing data using SVG, we need to create this element inside the root SVG element we defined in our HTML skeleton in the previous section. We do this in the following manner:

function show() { 

var margin = { top: 20, bottom: 20, right: 40, left: 40 },
width = 400 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;

var chart = d3.select(".chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
}

In this code fragment, we see the first usage of the D3 API. We use d3.select to search for the first element with the class chart. This will find the SVG element we defined in our HTML template (<svg class="chart"></svg>), and this will allow us to modify that element. D3 uses a W3C Selectors API string to select elements (more information here: https://www.w3.org/TR/selectors-api/). Summarizing this means that you can use the same kind of selector strings that are also used in CSS to select specific elements:

  • .className: selects the elements that have a class with the name className.
  • .elemName: selects the elements of type elemName
  • #id: selects the element that has an attribute id with a value id.
  • .className1 .className2: selects all elements with the class name .className2 which are descendants from the element with class name .className2
A lot more options are available: a good overview can be found here: https://www.w3.org/TR/CSS21/selector.html

Now that we have the SVG element, we use the attr function to set its width and height, leaving a bit of margin at all sides. Finally, we add the g element using the append function and position that element by taking into account the margins we defined by setting the transform attribute. D3 has a fluent API which means we can just chain commands and functions together (as you can see in the previous code fragment). This also means that the result of the final operation is assigned to the chart variable. So in this case, the chart variable is the g element we appended to the svg element.

A g element isn't rendered when you add it to a SVG element. The g element is just a container in which you can add other elements. The most useful part of the g element is that all of the transformations applied to this element are also applied to the children. So if you move the g element, the children will move as well. Additionally, all the attributes defined on this element are inherited by its children.

This might seem like a lot of work to just get an empty group to add elements to, but it is good practice to use a setup like this. Using margins allows us to more easily add axes or legends later on, without having to reposition everything and having a clear and well defined height and weight allows us to use other D3 features (such as scales) to correctly position elements, as we'll see later in this chapter.

At this point, it's also a good point to explain the transform attribute we use to position the g element inside the svg element. The transform attribute allows a couple of operations we can use to change the position and rotation of any SVG elements (such as g, text, rect). You'll see it used throughout this book, since it is the standard way to position SVG elements. The following table shows what can be done with the transform attribute:

Operation Description
translate(x [y]) With the translate attribute, we can move the specified element along its X or Y axis. For example, with translate(40 60), we move the specified element 40 pixels to the right and 60 down. If you just want to move an element along the X axis, you can omit the second parameter.
scale(x [y]) The scale operator, as the name implies, allows you to scale an element along the x and y axes. To double the width of an element, you can use scale(1 2), to half the size you use scale(0.5 0.5). Once again, the first parameter is mandatory, and the second one is optional.
rotate(a [x] [y]) The rotate operation allows rotation of the element around a given point (x and y) for a degrees. If the x and y parameters aren't provided, the element is rotated around its center. You can specify a positive a to rotate clockwise (for example, rotate(120)) and a negative value to rotate counter-clockwise (rotate(-10)).
skewX(a) / skewY(a) The skewX and skewY functions allow you to skew (to slant) an element alongside an axis by the specified a degrees: skewX(20) or skewY(-30).
matrix(a b c d e f) The final option you can use is the matrix function. With the matrix operator you can specify an arbitrary matrix operation to be applied to the element. All the previous operations could be written using the matrix operator, but this isn't really that convenient. For instance, we could rewrite translate(40 60) like matrix(1 0 0 1 40 60)

If you entered this code in your editor and looked at it in your browser you wouldn't really see anything yet. The reason is that we didn't specify a background color (using the fill attribute) for the svg or g element, so the default background color is used. We can, however, check what has happened. We mentioned that besides a good editor to create code, we'll also do a lot of debugging inside the browser, and Chrome has some of the best support. If you open the previous code in your browser, you can already see what is happening when you inspect the elements:

As you can see in this screenshot, the correct attributes have been set on the svg element, a g element is added, and the g element is transformed to position it correctly. If we want to style the svg element, we can use standard CSS for this. For instance, the following code (if added to the css file for this example) will set the background-color attribute of the svg element to black.

svg { 
background-color: black;
}

It is good to understand that CSS styles and element attributes have different priorities. Styles set using the style property have the highest priority, next the styles applied through the CSS classes, and the element properties set directly on the element have the lowest priority.

When we now open the example in the browser, you'll see the svg element as a black rectangle:

At this point, we've got an svg element with a specific size, and one g element to which we'll add other elements in the rest of this example.

Adding rectangles to the group element

In this step, we'll look at the core functionality of D3 which shows how to bind data to elements. We'll create an example that shows a number of rectangles based on some random data. We'll update the data every couple of seconds, and see how we can use D3 to respond to these changes. If you want to look at this example in action, open the example D01-01.html from the chapter 01 folder in your browser. The result looks something like this:

The size and number of rectangles in the screen is randomly determined and the colors indicate whether a rectangle is added or an existing one is updated. If the rectangle is blue, an existing rectangle was selected and updated; if a rectangle is green, it was added to the rectangles already available. It works something like this:

  1. The first time the rectangles are shown, no rectangles are on screen, so all the rectangles are newly added and colored green. So, for this example, assume we add three rectangles, which, since no rectangles are present, they rendered green.
  2. After a couple of seconds, the data is updated. Now assume five rectangles need to be rendered. For this, we'll update the three rectangles which are already there with the new data. These are rendered blue since we're updating them. And we add two new rectangles, which are rendered green, just like in the first step.
  3. After another couple of seconds, the data is updated again. This time we need to render four rectangles. This means updating the first four rectangles, which will turn them blue, and we'll remove the last one, since that one isn't needed anymore.

To accomplish this, we'll first show you the complete code and then step through the different parts:

function show() { 
'use strict';

var margin = { top: 20, bottom: 20, right: 40, left: 40 },
width = 800 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;

var chart = d3.select(".chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + ","
+ margin.top + ")");

function update() {

var rectangleWidth = 100,
data = [],
numberOfRectangles = Math.ceil(Math.random() * 7);

for (var i = 0 ; i < numberOfRectangles ; i++) {
data.push((Math.random() * rectangleWidth / 2)
+ rectangleWidth / 2);
}

// Assign the data to the rectangles (should there be any)
var rectangles = chart.selectAll("rect").data(data);

// Set a style on the existing rectangles so we can see them
rectangles.attr("class", "update")
.attr("width", function(d) {return d})
.attr("height", function(d) {return d});

rectangles.enter()
.append("rect")
.attr("class", "enter")
.attr("x", function(d, i) { return i * (rectangleWidth + 5) })
.attr("y", 50)
.attr("width", function(d) {return d})
.attr("height", function(d) {return d});

// Handle rectangles which are left over
rectangles.exit().remove();

// we could also change the ones to be remove
// rectangles
// .exit()
// .attr("class", "remove");
}

// set initial value
update();
// and update every 3 seconds
d3.interval(function() { update(); }, 3000);
}

In the beginning of this function, you once again see the code we use to create and set up our SVG and main g elements. Let's ignore that and move on to the update() function. When this function is called it will take a couple of steps:

Creating dummy data

The first thing it does is that it creates some dummy data. This is the data that determines how many rectangles to render, and how large the rectangles will be:

var rectangleWidth = 100, 
data = [],
numberOfRectangles = Math.ceil(Math.random() * 7);

for (var i = 0 ; i < numberOfRectangles ; i++) {
data.push((Math.random() * rectangleWidth / 2)
+ rectangleWidth / 2);
}

This is just plain JavaScript, and this will result in the data array being filled with one to seven numeric values ranging from 50 to 100. It could look something like this:

[52.653238934888726, 88.52709144102309, 81.70794256804369, 58.10611357491862]

Binding the data and updating existing rectangles

The next step is assigning this data to a D3 selection. We do this by using the selectAll function on the chart variable we defined earlier (remember this is the main g element, we added initially):

var rectangles = chart.selectAll("rect").data(data);

This call will select all the rectangles which are already appended as children to the chart variable. The first time this is called, rectangles will have no children, but on subsequent calls this will select any rectangles that have been added in the previous call to the update() function. To differentiate between newly added rectangles and rectangles which we'll reuse, we add a specific CSS class. Besides just adding the CSS class, we also need to make sure they have the correct width and height properties set, since the bound data has changed.

In the case of rectangles which we reuse, we do that like this:

rectangles.attr("class", "update") 
.attr("width", function(d) {return d})
.attr("height", function(d) {return d});

To set the CSS we use the attr function to set the class property, which points to a style defined in our CSS file. The width and height properties are set in the same manner, but their value is based on the value of the passed data. You can do this by setting the value of that attribute to a function(d) {...}. The d which is passed in to this function is the value of the corresponding element from the bound data array. So the first rectangle which is found is bound to data[0], the second to data[1], and so on. In this case, we set both the width and the height of the rectangle to the same value.

The CSS for this class is very simple, and just makes sure that the newly added rectangles are filled with a nice blue color:

.update { 
fill: steelblue;
}

Adding new rectangles if needed

At this point, we've only updated the style and dimensions of the rectangles which are updated. We repeat pretty much the same process for the rectangles that need to be created. This happens when our data array is larger than the number of rectangles we can find:

rectangles.enter() 
.append("rect")
.attr("class", "enter")
.attr("x", function(d, i) { return i * (rectangleWidth + 5) })
.attr("y", 50)
.attr("width", function(d) {return d})
.attr("height", function(d) {return d});

Not that different from the update call, but this time we first call the enter() function and then create the SVG element we want to add like this: .append("rect"). After the append call, we configure the rectangle and set its class, width, and height properties, just like we did in the previous section (this time the CSS will render the newly added rectangle in green). If you look at the code, you can see that we also set the position of this element by setting the x and y attributes of the added rectangle. This is needed since this is the first time this rectangle is added, and we need to determine where to position it. We fix the y position to 50, but need to make the x position dependent on the position of the element from the data array to which it is bound. We once again bind the attribute to a function. This time we specify a function with two arguments: function(d, i) {...}. The first one is the element from the data array, and the second argument (i), is the position in the data array. So the first element has i = 0, the second i = 1, and so on. Now, when we add a new rectangle we calculate its x position by just multiplying its array position with the maximum rectangleWidth and add a couple of pixels margin. This way none of our rectangles will overlap.

If you look at the code for adding new elements, and updating existing ones, you might notice some duplicate code. In both instances, we use .attr to set the width and the height properties. If we'd wanted to, we could remove this duplication by using the .merge function. The code to set the new width and height for the new elements and the updated ones would then look like this:

rectangles.attr("class", "update"); 

rectangles.enter()
.append("rect")
.attr("class", "enter")
.attr("x", function(d, i) { return i * (rectangleWidth + 5) })
.attr("y", 50)
.merge(rectangles)
.attr("width", function(d) {return d})
.attr("height", function(d) {return d});

This means that after merging the new and updated elements together, on that combined set, we use the .attr function to set the width and the height property. Personally, I'd like to keep these steps separate, since it is more clear what happens in each of the steps.

Removing elements which aren't needed anymore

The final step we need to take is to remove rectangles that aren't needed anymore. If in the first call to update we add five rectangles, and in the next call only three are needed, we're stuck with two leftover ones. D3 also has an elegant mechanism to deal with that:

rectangles.exit().remove();

The call to exit() will select the elements for which no data is available. We can then do anything we want with those rectangles. In this case, we just remove them by calling remove(), but we could also change their opacity to make them look transparent, or animate them to slowly disappear.

For instance, if we replace the previous line of code with this:

rectangles.exit().attr("class", "remove");

Then set the CSS for the remove class to this:

.remove { 
fill: red;
opacity: 0.2;
}

In that case, we'd see the following:

In the preceding screenshot, we've reused two existing rectangles, and instead of removing the five we don't need, we change their style to the remove class, which renders them semi-transparent red.