官术网_书友最值得收藏!

Getting started with D3.js

In this chapter, we will create our first bar chart using D3.js and SVG; however, to get there, we will have to go through some basics. These basic concepts are very important because they will give you the freedom to create visualizations for almost everything that you have in your mind. The more of them you know and understand, the more crazy things you can do with D3.

D3.js is a powerful JavaScript library for data-driven DOM manipulations and includes predefined abstractions, transformations, and helpers to work with SVG elements. This makes it an excellent library to write interactive data visualization components using web standards and the complete capabilities of modern web browsers.

Don't worry if you think that this definition is full of fancy buzzwords because in this section, we will walk through all of them. You will understand exactly what they mean and why this makes D3 and SVG a great combination to create visualizations.

Selecting and manipulating DOM elements

D3 lets us select, append, insert, and remove elements in the DOM tree. These functions will look very familiar if you've already worked with other JavaScript frameworks, such as jQuery.

Let's look at some examples to see how this works in D3. We define a simple shopping list in HTML that contains a few items that we need to buy:

<ul>
<li>Ham</li>
<li>Cheese</li>
<li>Butter</li>
<li>Bread</li>
</ul>

To select elements in the DOM tree, D3 provides the d3.select(selector) and d3.selectAll(selector) methods, which return a Selection object. The Selection of the first function contains an array with only a single element, whereas the second one contains an array with multiple elements; both the functions take a CSS3 selector string as an argument (for example, #id, .class, tagname, :pseudo, and more.).

Note

More information about Selections can be found on the D3 Github page at https://github.com/mbostock/d3/wiki/Selections. More information about CSS3 Selectors can be found on MDN at https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_started/Selectors.

Under the hood, D3 uses the native document.querySelector(selector) and document.querySelectorAll(selector)functions:

Note

More information about these functions can be found on MDN at https://developer.mozilla.org/en/docs/Web/API/Document/querySelector and https://developer.mozilla.org/en/docs/Web/API/Document/querySelectorAll.

var $ul = d3.select('ul');
var $li = d3.selectAll('li');

In the preceding code, we create the $ul Selection of the ul element and the $li Selection, which contains all the li elements.

Note

Throughout this book, we will use this convention to prefix selections with the $ sign; this will make it easier for you to recognize selections.

Once we have a Selection, we can add more elements to the DOM. One possibility is to append a new DOM node at the end of the Selection using the selection.append (tagName) method; we need to specify the type of element that we want to append by its tag name (for example, p, p, span, li, ul, and more):

var $el1 = $ul.append('li');
$el1.text('Milk');

In the preceding code, we can see that the selection.append (tagName) method returns a Selection that contains the element that was just created. We then use the selection.text (content) method to set the inner text of the node to 'Milk'. We can also insert an element before a certain position using the selection.insert(tagName, selector) method:

var $el2 = $ul.insert('li', ':last-child');
$el2.text('Beer');

In the preceding code, we inserted a li element with the Eggs text before the last child of the list Selection called $ul. If we want to remove an element from the DOM, we can call the selection.remove() method:

$ul.select(':first-child').remove();

We remove the first child of the list by calling the selection.remove() function. However, we observe that we can call the function directly on the returned object of the selection.select() method rather than creating a new variable that contains this element. This technique is called function chaining and is often used when working with D3. We can also chain the selection.insert() and selection.append()functions from the previous examples and apply the selection.text() method directly on the returned selection.

We may just want to strike through the last element instead of removing it. The selection.attr(name, value) function allows us to modify any attribute of the selected elements:

$ul.select(':first-child')
  .attr('class', 'strike-through');

In the preceding code, we used the selection.attr(name, value) method to access the class attribute and set it to strike-through. If we now define a class style in the header, we see that the first element has a line through the text

<style>
  .strike-through {
    text-decoration: line-through;
  }
</style>

Optionally, we can also use the selection.classed(className, hasClass) method to add or remove classes to the selections:

$ul.select(':first-child')
  .classed('strike-through', true);

In the preceding code, we observe that we now only mention a single class that comes handy when you want to toggle a class and keep the others untouched.

Note

Under the hood, D3 uses the native element.setAttribute(name, value) and element.setAttributeNS(namespace, name, value) functions to access HTML and SVG attributes. This makes D3 very powerful as it directly uses the underlying web standards rather than implementing an abstraction on top.

Manipulating SVG elements

Until now, we have only used D3 to manipulate HTML nodes; if you compare this functionality to jQuery, you learned nothing new or fancy. The good news is that we can also apply all the D3 functions to SVG elements as these elements are also just embedded in the DOM tree.

Let's create a small SVG scene that will contain our elements; we can also add some styling to the svg element for better visibility:

<style>
  svg {
    border: 1px dashed black;
    background-color: #efefef;
  }
</style>
<svg width="800" height="400"></svg>

Now, we can again use the selection.append() and selection.attr() functions to add elements to the scene and define their attributes. Similar to the previous example, we will also chain the functions together rather than creating a variable for the new element:

// Select the SVG element
var $svg = d3.select('svg');

// Append a new circle element
$svg.append('circle')
  .attr({
    cx: 75,
    cy: 75,
    r: 50,
    fill: 'red'
  });

In the preceding example, we can select and manipulate SVG elements exactly as we did before with HTML elements. We also observe that the selection.attr() function accepts an object that contains multiple name: value pairs. This becomes very handy when working with SVG elements as we often have to define multiple attributes at once. The following figure shows the output of this example in the browser:

Simple SVG scene with one circle

Data-driven transformations

In the previous section we saw that the out-of-the box handling of SVG elements makes D3 much more suitable for creating visualizations than for example jQuery. But this is not the only difference between both the frameworks. One of the most important, and also most unique, features of D3 is data joining. Data joins are a core concept of D3, and for most of the people this is the most difficult thing to understand while learning D3. However, it is just an extension of data binding that also takes the current selection into account. You will quickly find out that it is a very logical rather than mysterious feature.

Data Binding and Dynamic Properties

D3 stands for data-driven documents; but what is a data-driven approach? Data-driven means that the visualization is implicitly defined through the data itself rather than explicitly looping through the data.

Let's take an example using the explicit loop to draw multiple circles on the scene. We are use the same SVG scene as the previous example:

// Explicit for loop
for (var i=0;i<5;i++){
  $svg.append('circle')
    .attr({
      cx: 125 * (i + 1),
      cy: 75,
      r: 50,
      fill: 'red'
    });
}

In the preceding example, we use an explicit loop to iterate over five numbers to draw five items. The following figure shows the output of the preceding example in the web browser:

Simple SVG scene with multiple elements

Obviously, if we are dealing with datasets, which could be an example extracted from the logs of an application, we don't want to use the for() loop but iterate over the data array directly using the forEach() loop. Let's create a pseudo data array containing the integers in a [0, 5) range (0 is included, but 5 is not included anymore) with the d3.range(stop) function:

// Create a pseudo dataset
var data = d3.range(5);

// Explicit forEach loop
data.forEach(function(d, i){
  $svg.append('circle')
    .attr({
      cx: 125 * (d + 1),
      cy: 75,
      r: 50,
      fill: 'red'
    });
});

The preceding code already looks a little bit cleaner than the primitive for() loop. We observe that the callback function has two attributes—d and i. In general, d is a reference to the current element of the array, and i is the index of the current element in the array. Great! Let's add one final step to transform the loop to a data-driven implementation:

// Create a pseudo dataset
var data = d3.range(5);

// Data-driven approach
$svg.selectAll('circle')
.data(data)
  .enter()
  .append('circle')
  .attr({
      cy: 75,
      r: 50,
      fill: 'red'
    })
.attr('cx', function(d, i) { return 125 * (d + 1) });

First, I want to draw your attention to the .data(data) method; this function is used to bind the data array to the Selection. We will discuss this function and the preceding .enter() function in more detail in the following section. The second line that I would like to point out is .attr('cx', function(d, i){ … }); this is the so-called dynamic property—a property that will be dynamically computed by the bound data array where the d and i arguments are used exactly like in the previous forEach callback.

Let's summarize the data-driven approach. We bind a data array to a selection of DOM elements and use dynamic properties to define the attributes of the elements by the bound data. Thus, we don't need to loop through the dataset anymore. However, the previous example is a little bit more complicated than just this, which is due to data joins.

Data Join – next-level data binding

As we saw in the previous example, the selection.data(data) function binds a dataset to a selection—at least this is what we covered so far. To be more precise, the function does a little bit more than this; it creates a join between a selection and dataset. In the following figure we can see how the data join looks:

Data join

We can think of the data join by simply comparing the dataset (the red circle in the previous figure) with the Selection (the blue circle in the previous figure) of DOM elements. After comparing the elements of the dataset with the elements in the Selection, we obtain three possible sets of elements:

  • The enter set: These elements exist in the dataset but not in the Selection, so we want to create them
  • The update set: These elements exist in the dataset and in the Selection, so we want to update them
  • The exit set: These elements exist in the Selection but not in the dataset, so we want to remove them

To better understand how this works, we will illustrate it with some code. We will again use the same HTML setup as we did in the previous examples. First, we create a dataset and join it with a Selection:

// Create a dataset
var data = d3.range(3);

// Create a data join
var $$circles = $svg.selectAll('circle')
  .data(data);

In the preceding example, we first create a dataset in the [0, 3) range. Then, we create a Selection of all the circle elements and join them with the dataset. In this case, there are no circle elements in the scene, so the Selection is empty. This means that the enter set will contain all the three dataset elements; the update and exit sets will be empty.

Note

Throughout this book, we will use the convention to the data joins prefix with two $ signs; this will make it easier for you to spot a Join and differentiate it from a Selection.

We can now use the join.enter() function to return the elements of the enter set (the data array) and add them to the DOM:

// Get the enter set
$$circles.enter()
  .append('circle')
  .attr({cy: 75, r: 50})
  .attr('cx', function(d, i) { return 125 * (d + 1) });

Calling the join.enter() function removes the elements from the enter set and adds them to the update set. Imagine that the enter set is an array, that stores all values from the dataset that are not in the current selection. After calling .enter(), one would usually like to add the elements to the DOM. This is why D3 automatically assumes this and moves all the values from the enter set to the update set. Now, after calling .enter(), the enter set gets empty and the update set contains all the values from the enter set. We can retrieve the update set using the data join as a Selection:

// Modify the update set
$$circles.attr('fill', 'blue');

The preceding code will color all the circles in blue because all the elements from the dataset exist now in the Selection of HTML elements.

Finally, we will remove one element from the dataset and join the data again with the Selection; we can then access the exit set by calling the join.exit() function and therefore remove the element from the Selection:

// Remove an element from the dataset
data.splice(0, 1);

// Create a data join
$$circles = $svg.selectAll('circle')
  .data(data);

// Get the exit set
$$circles.exit()
  .remove();

In the preceding code, we see that we can simply join the dataset again with the Selection to obtain the updated sets. This is very powerful because we can use one function to define how the elements should be drawn on the screen. To update the elements, we will again call the function with the updated dataset; all we need to do is define what happens with the new, existing, and removed elements.

To summarize the data join section, I will list the three functions that we used to access the three sets of the data join:

  • The enter set: selection.data(data).enter()
  • The update set: selection.data(data)
  • The exit set: selection.data(data).exit()

The following figure visualizes the first two steps. We start with an empty selection at the left of the figure; there are no circles yet in the scene. Then, we join the dataset to this empty selection; we obtain an enter set with three elements. For each element in the enter set, we create a circle in the scene and configure its position using the dynamic property for the cx attribute.

If we want to update the circles now, we can simply use the data join as a selection. This will return an update set that contains all the three circles.

Data join: enter and update sets

In the following figure, we remove one element form the dataset, see what happens in the update, and exit set. After the previous operation, our selection contained three circle elements. If we remove the last element from the dataset, the update set will contain only two elements. The last element of the Selection will now be in the exit set, and it can be removed from the selection in the following step:

Data join: update and exit set

Using the update pattern

It is a little bit difficult to understand which set to use in order to update specific elements. Therefore, we can use the so-called Update Pattern that defines the order in which the elements can be updated.

Let's create a function to illustrate this pattern:

function draw(data) {

  // Create a data join
  var $$circles = $svg.selectAll('circle')
    .data(data);

  // Only previously existing elements
  $$circles.attr('fill', 'blue');

  // Only new elements
  $$circles.enter()
    .append('circle');

  // All elements
  $$circles.attr('fill', 'red');

  // Only removed elements
  $$circles.exit()
    .remove();
}

If we execute the preceding function multiple times with different data arguments, we can properly define what happens to the following elements:

  • Only old (previously existing) elements
  • Only new elements
  • All elements
  • Only removed elements
Note

There are three great examples about the update pattern on Mike Bostock's Bl.ocks page. I recommend taking a look at all three and trying to understand what exactly is going on in the examples:

Defining an Identifier for the elements

If we look at the API documentation of the selection.data(data, keyFn) method, we can see that this function takes a second argument called keyFn. With this keyFn argument, one can define an accessor function that identifies the element of the array in a data join.

Let's look at an example where we use the draw(data) function that we implemented in the previous section:

// Create 4 elements
draw([0, 1, 2, 3]);
// Update 4 elements
draw([1, 2, 3, 4]);

If we don't define a keyFn argument, D3 uses the index of the data array as an identifier for the element. In the preceding example, the function first creates four circles; in the second step, it updates these four circles. However, if we look closely, we see that a part of the data is the same but only shifted to the left. Thus, we would rather want a behavior in which 0 gets removed from the Selection, [1, 2, 3] stays the same, and 4 gets added to the Selection. We can achieve this using the array value as an identifier rather than the array index. Let's define a custom keyFn that returns the array element as identifier.

var $$circles = $svg.selectAll('circle')
  .data(data, function(d, i){ return d; });

In the preceding example, we define an identifier function in the same way as the callback functions that we used in dynamic properties.

主站蜘蛛池模板: 亳州市| 呼图壁县| 保亭| 布尔津县| 岑巩县| 琼海市| 永新县| 两当县| 朝阳区| 克拉玛依市| 慈溪市| 朝阳区| 牡丹江市| 射洪县| 山西省| 常山县| 湘乡市| 洛南县| 泰来县| 金溪县| 合川市| 峨边| 太仓市| 台南市| 江孜县| 阿拉善盟| 肇东市| 汤原县| 集贤县| 岫岩| 吴桥县| 崇仁县| 巫溪县| 四会市| 保康县| 章丘市| 阳原县| 洛南县| 宁蒗| 若尔盖县| 长沙县|