Learning Responsive Data Visualization
上QQ阅读APP看书,第一时间看更新

Creating Scales and Axis

D3 is often mistaken for a charting library, which is not really correct. D3 provides loads of built-in functions to easily create interactive charts; however, it is not a charting library. One of these typical built-in charting helpers are the closely related Scales and axis generators. You will learn about them in this section.

At the end of this section, we want to be able to draw a simple axis as the one in the following figure. Therefore, we need to scale our dataset to a specific pixel range (using Scales) and create all the SVG elements for the axis (using axis generators):

Axis in D3

Mapping data values to a Pixel Range with scales

The concept of scales is very important when we deal with graphics and visualization. It is a common task to scale values from a certain domain to a certain range of pixels. We need them to create tick values on axes, and we need them to scale our data points to the area of the chart.

The following figure explains the problem a little bit better. Imagine we want to map a certain domain of values in our dataset (let's say only the positive values) to a certain pixel range in the chart (let's say from 20px to 460px). Scales give us exactly this functionality in D3:

Scale in D3

I want to emphasize again on the naming convention in D3. We are speaking in terms of domains about a certain extent of the dataset and in terms of ranges about a certain pixel range. Whenever we want to apply linear transformations (as in the previous figure), we speak about linear scales.

Linear Scales for linear Mappings

Let's take a look at the simplest mappings-linear transformations, and create one according to the previous figure:

var scale = d3.scale.linear()
  .domain([0, 52])
  .range([20, 460]);

In the preceding code, we observe that we can use the d3.scale.linear() function to create an identity scale (maps domain [0, 1] to a range [0, 1]), and then specify a domain and a range for this scale.

Let's see how this scale object works; actually, we can just call it as a function, and it will now scale any value from the dataset to the pixel range. Let's try this and map the whole dataset with this scale:

console.log([0, 3, 9, 24, 45, 52].map(scale));

If we check the output in the Developer Tools console, we get an array similar to the following:

[20, 45.38461538461539, 96.15384615384615, 223.0769230769231, 400.7692307692308, 460]

We can see in the output of the preceding function that every value from the input array is transformed with the scale function to the new pixel range.

Ordinal scale for Mapping non-numeric data

Often, we have to deal with non-numerical—the so-called ordinal data; if we look into a bar chart, we usually have values that belong to a certain label rather than a specific x value. In this case, it's perfect to use ordinal scales in order to map these non-numeric values to the pixel range.

First, let's create a simple one:

var data = ['Bread', 'Eggs', 'Milk', 'Beer'];

var scale = d3.scale.ordinal()
  .domain(data)
  .range([0, 1, 2, 3]);

In the preceding example, we first create a dataset, which we also define as the domain of the ordinal scale. In the second step, we define a range on which the data values should be projected. If we map the whole dataset, the result is equal to our defined range:

console.log(data.map(scale));
// [0, 1, 2, 3]

If we want to define a continuous pixel range for the discrete points, we can use the .rangePoints() method instead. Let's look at an example:

var scale = d3.scale.ordinal()
  .domain(data)
  .rangePoints([0, 10]);

console.log(data.map(scale));
// [0, 3.3333333333333335, 6.666666666666667, 10]

In addition, we can also define a range that considers the structure of a bar chart—namely the .rangeBands() method. This function takes into account the fact that we need to shift the bars half the width of one bar to the left to match the tick values.

var scale = d3.scale.ordinal()
  .domain(data)
  .rangeBands([0, 10]);

console.log(data.map(scale));
// [0, 2.5, 5, 7.5]
Time scales for Mapping time series data

Finally, there also exists a scale, when working with time series data, which maps JavaScript Date objects to pixel ranges. Let's see an example; first, we define an array of Date objects:

var data = [
  new Date(2015, 8, 1),
  new Date(2015, 8, 2),
  new Date(2015, 8, 3),
  new Date(2015, 8, 4)
];

Now, we can define our scale; we will use the d3.extent(data) function to return the [min, max] value of the data:

var scale = d3.time.scale()
  .domain(d3.extent(data))
  .range([20, 460]);

If we map our dataset to the new range, we get values inside the [20, 460] range:

console.log(data.map(scale));
// [20, 166.66666666666666, 313.3333333333333, 460]

Creating Axis

When creating axis in D3, we want to use the feature provided by scales in order to map the dataset to a range of pixels. In the following figure, we can see an example from the Developer Tools and how the final result of the axes looks:

D3 axis selected in the Developer Tools

First, we need to define a scale for the axis that handles the mapping:

var scale = d3.scale.linear()
  .domain([0, 10])
  .range([20, 380]);

Now, we can create an axis generator that works very similarly to the previously mentioned generators:

var axis = d3.svg.axis()
  .scale(scale);

Finally, we can draw the axis by simply passing the generator function to the .call() method on any (empty) selection in which we want to create the axis:

d3.select('svg')
  .call(axis);

That was it, no magic, no struggle.

Note

D3 has loads of built-in functionality when dealing with axes. It's absolutely worth taking a look at the corresponding section in the Wiki page available at https://github.com/mbostock/d3/wiki/SVG-Axes.