Creating a layout algorithm

Every chart makes assumptions about the kind and structure of the data that they can display. A scatter plot needs pairs of quantitative values, a bar chart requires categories with a quantitative dimension, and a tree map needs nested objects. To use a chart, the user will need to group, split, or nest the original dataset to fit the chart requirements. Functions that perform these transformations are called layout algorithms. D3 already provides a good set of layouts, from the simple pie layout to the more complex force layout. In this section, we will lean how to implement a layout algorithm, and we will use it to create a simple visualization using the barcode dataset.

The radial layout

The array with dates used in the barcode example can be visualized in several ways. The barcode chart represents every data item as a small bar in a time interval. Another useful way to display a series of events is to group them in intervals. The most common among these kind of visualizations is a bar chart, with one bar for each time interval and the height of each bar representing the number of events that occurred in the corresponding time interval.

A radial chart is a circular arrangement of arc segments, each one representing a category. In this chart, each arc has the same angle, and the area of each arc is proportional to the number of items in the category. We will create a radial layout that groups and counts the events in hourly segments and compute the start and end angles for each arc.

The purpose of a layout algorithm is to allow the user to easily transform its dataset to the format required by a chart. The layout usually allows a certain amount of customization. We will implement the layout function as a closure with accessors to configure the layout behavior as follows:

var radialLayout = function() {

    // Layout algorithm.
    function layout(data) {
        var grouped = [];
        // Transform and returns the grouped data ...
        return grouped;
    }
    return layout;
};

The usage of a layout is similar to the usage of the barcode chart. First, we invoke RadialLayout to get the layout function and then call the layout with the dataset as the argument in order to obtain the output dataset. We will generate an array of random dates using the addData function from the previous section:

// Generate a dataset with random dates.
var data = addData([], 300, 20 * 60);

// Get the layout function.
var layout = radialLayout();

// Compute the ouput data.
var output = layout(data);

We need the layout to group and count the data items for each hour and to compute the start and end angles for each arc. To make the counting process easier, we will use a map to temporarily store the output items. D3 includes d3.map, a dictionary-like structure that provides key-value storage:

function layout(data) {
    // Create a map to store the data for each hour.
    var hours = d3.range(0, 24),
        gmap = d3.map(),
        groups = [];

    // Append a data item for each hour.
    hours.forEach(function(h) {
        gmap.set(h, {hour: h, startAngle: 0, endAngle: 0, count: 0});
    });

    // ...

    // Copy the values of the map and sort the output data array.
    groups = gmap.values();
    groups.sort(function(a, b) { return a.hour > b.hour ? 1 : -1; });
    return groups;
}

As the layout must return an array, we will need to transfer the map values to the grouped array and sort it to return it as the output. The output items don't have any useful information yet:

[
    {hour:  0, startAngle: 0, endAngle: 0, count: 0},
    ...
    {hour: 23, startAngle: 0, endAngle: 0, count: 0}
]

The next thing to do is to count the items that belong to each hour. To do this, we iterate through the input data and compute the hour of the date attribute:

    // Count the items belonging to each hour
    data.forEach(function(d) {
        // Get the hour from the date attribute of each data item.
        var hour = d.date.getHours();

        // Get the output data item corresponding to the item hour.
        var val = gmap.get(hour);

        // We increment the count and set the value in the map.
        val.count += 1;
        gmap.set(hour, val);
    });

At this point, the output contains the count attribute with the correct value. As we did in the barcode chart, we will add a configurable accessor function to retrieve the date attribute:

var radialLayout = function() {
    // Default date accessor
    var value = function(d) { return d.date; }

    function layout(data) {
        // Content of the layout function ...
    }

    // Date Accessor Function
    layout.value = function(accessorFunction) {
        if (!arguments.length) { return value; }
        value = accessorFunction;
        return layout;
};

In the layout function, we replace the references to d.date with invocations to the date accessor method, value(d). The user now can configure the date accessor function with the same syntax as that in the barcode chart:

// Create and configure an instance of the layout function.
var layout = radialLayout()
    .value(function(d) { return d.date; });

Computing the angles

With the count attribute ready, we can proceed to compute the start and end angles for each output item. The angle for each arc will be the the same, so we can compute itemAngle and then iterate through the hours array as follows:

    // Compute equal angles for each hour item.
    var itemAngle = 2 * Math.PI / 24;

    // Adds a data item for each hour.
    hours.forEach(function(h) {
        gmap.set(h, {
            hour: h,
            startAngle: h * itemAngle,
            endAngle: (h + 1) * itemAngle,
            count: 0
        });
    });

The output dataset now has the start and end angles set. Note that each data item has a value that is 1/24th of the circumference:

[
    {hour:  0, startAngle: 0,      endAngle: 0.2618, count:  7},
    {hour:  1, startAngle: 0.2618, endAngle: 0.5236, count: 14},
    ...
    {hour: 23, startAngle: 6.0214, endAngle: 6.2832, count: 17}
]

Here, we used the entire circumference, but a user might want to use a semicircle or want to start in a different angle. We will add the startAngle and endAngle attributes and the angleExtent accessor method in order to allow the user to set the angle extent of the chart:

var radialLayout = function() {

    // Default values for the angle extent.
    var startAngle = 0,
        endAngle = 2 * Math.PI;

    // Layout function ...

    // Angle Extent
    layout.angleExtent = function(value) {
        if (!arguments.length) { return value; }
        startAngle = value[0];
        endAngle = value[1];
        return layout;
    };
};

We need to change the itemAngle variable in order to use the new angle range. Also, we add the layout start angle to the start and end angles for each output item:

    // Angle for each hour item.
    var itemAngle = (endAngle - startAngle) / 24;

    // Append a data item for each hour.
    hours.forEach(function(h) {
        gmap.set(h, {
            hour: h,
            startAngle: startAngle + h * itemAngle,
            endAngle: startAngle + (h + 1) * itemAngle,
            count: 0
        });
    });

We can configure the start and end angles of the layout to use a fraction of the circumference:

// Create and configure the layout function.
var layout = radialLayout()
    .angleExtent([Math.PI / 3, 2 * Math.PI / 3]);

In this section, we implemented a simple layout algorithm that counts and groups an array of events in hours and computes the start and end angles to display the returned value as a radial chart. As we did in the barcode chart example, we implemented the layout as a closure with getter and setter methods.

Using the layout

In this section, we will use the radial layout to create a radial chart. To keep the code simple, we will create the visualization without creating a chart function. We begin by creating a container for the radial chart:

<div class="chart-example" id="radial-chart"></div>

We define the visualization variables and append the svg element to the container. We append a group and translate it to the center of the svg element:

// Visualization Variables
var width = 400,
    height = 200,
    innerRadius = 30,
    outerRadius = 100;

// Append a svg element to the div and set its size.
var svg = d3.select('#radial-chart').append('svg')
    .attr('width', width)
    .attr('height', height);

// Create the group and translate it to the center.
var g = svg.append('g')
    .attr('transform', 'translate(' + [width / 2, height / 2] + ')'),

We represent each hour as an arc. To compute the arcs, we need to create a radius scale:

// Compute the radius scale.
var rScale = d3.scale.sqrt()
    .domain([0, d3.max(output, function(d) { return d.count; })])
    .range([2, outerRadius - innerRadius]);

As we have the angles and the radius, we can configure the d3.svg.arc generator to create the arc paths for us. The arc generator will use the startAngle and endAngle attributes to create the arc path:

// Create an arc generator.
var arc = d3.svg.arc()
    .innerRadius(innerRadius)
    .outerRadius(function(d) { 
        return innerRadius +    rScale(d.count);
    });

The arc function receives objects with startAngle, endAngle, and count attributes and returns the path string that represents the arc. Finally, we select the path objects in the container group, bind the data, and append the paths:

// Append the paths to the group.
g.selectAll('path')
    .data(output)
    .enter()
    .append('path')
        .attr('d', function(d) { return arc(d); })
        .attr('fill', 'grey')
        .attr('stroke', 'white')
        .attr('stroke-width', 1);

The radial chart represents the number of items in each hour as radial arcs. Refer to the following screenshot:

Using the layout

We have shown you how to use the radial layout to create a simple visualization. As we mentioned previously, the layout can be used to create other charts as well. For instance, if we ignore the start and end angles, we can use the radial layout to create a bar chart or even use the output data to create a table with the data aggregated by hour.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset