Visualizations without SVG

In this section, we will create a visualization without using SVG. We will create a bubble chart to show the usage of browser versions in the browser market. A circle will represent each browser version; the area of the circle will be proportional to the global browser usage. Each browser will be assigned a different color. We will use the force layout to group the bubbles on the screen. To follow the examples, open the chapter03/01-bubble-chart.html file.

Loading and sorting the data

To make the processing easier, the browser market data was arranged in a JSON file. The main object will have a name that describes the dataset and an array that will contain the data for each browser version. Refer to the following code:

{
  "name": "Browser Market",
  "values": [
    {
      "name": "Internet Explorer",
      "version": 8,
      "platform": "desktop",
      "usage": 8.31,
      "current": "false"
    },
    // more items ...
  ]
}

We will use the d3.json method to load the JSON data. The d3.json method creates an asynchronous request to the specified URL and invokes the callback argument when the file is loaded or when the request fails. The callback function receives an error (if any) and the parsed data. There are similar methods to load text, CSV, TSV, XML, and HTML files. Refer to the following code:

<script>
    // Load the data asynchronously.
    d3.json('/chapter03/browsers.json', function(error, data) {

    // Handle errors getting or parsing the JSON data.
    if (error) {
        console.error('Error accessing or parsing the JSON file.'),
        return error;
    }

    // visualization code ...

});
</script>

Note that the callback function will be invoked only when the data is loaded. This means that the code after the d3.json invocation will be executed while the request is being made, and the data won't be available at this point. The visualization code should go either inside the callback or somewhere else and should use events to notify the charting code that the data is ready. For now, we will add the rendering code inside the callback.

We will create the circles with the div elements. To avoid the smaller elements being hidden by the bigger elements, we will sort the items and create the elements in decreasing usage order, as follows:

    // Access the data items.
    var items = data.values;

    // Sort the items by decreasing usage.
    items.sort(function(a, b) { return b.usage-a.usage; });

The Array.prototype.sort instance method sorts the array in place using a comparator function. The comparator should receive two array items: a and b. If a must go before b in the sorted array, the comparator function must return a negative number; if b should go first, the comparator function must return a positive value.

The force layout method

The force layout is a method to distribute elements in a given area, which avoids overlap between the elements and keeps them in the drawing area. The position of the elements is computed based on simple constraints, such as adding a repulsive force between the elements and an attractive force that pulls the elements towards the center of the figure. The force layout is specially useful to create bubble and network charts.

Although the layout doesn't enforce any visual representation, it's commonly used to create network charts, displaying the nodes as circles and the links as lines between the nodes. We will use the force layout to compute the position of the circles, without lines between them, as follows:

    // Size of the visualization container.
    var width = 700,
        height = 200;

    // Configure the force layout.
    var force = d3.layout.force()
        .nodes(items)
        .links([])
        .size([width, height]);

As we don't intend to represent the relationships between the browser versions, we will set the links attribute to an empty array. To start the force computation, we invoke the start method as follows:

    // Start the force simulation.
    force.start();

The force layout will append additional properties to our data items. Of these new attributes, we will use only the x and y attributes, which contain the computed position for each node. Note that the original data items shouldn't have names that conflict with these new attributes:

{
    "name": "Android Browser",
    "version": 3,
    "platform": "mobile",
    "usage": 0.01,
    "current": "false",
    "index": 0,
    "weight": 0,
    "x": 522.7463498711586,
    "y": 65.54744869936258,
    "px": 522.7463498711586,
    "py": 65.54744869936258
}

Having computed the position of the circles, we can proceed to draw them. As we promised not to use SVG, we will need to use other elements to represent our circles. One option is to use the div elements. There are several ways to specify the position of divs, but we will use absolute positioning.

A block element styled with absolute positioning can be positioned by setting its top and left offset properties (the bottom and right offsets can be specified as well). The offsets will be relative to their closest positioned ancestors in the DOM tree. If none of its ancestors are positioned, the offset will be relative to the viewport (or the body element, depending on the browser). We will use a positioned container div and then set the position of the divs to absolute. The container element will be the div with the #chart ID. We will select this to modify its style to use the relative position and set its width and height to appropriate values, as follows:

<!-- Container div -->
<div id="chart"></div>

We will also set padding to 0 so that we don't have to account for it in the computation of the inner element positions. Note that in order to specify the style attributes that represent length, we need to specify the units, except when the length is zero, as follows:

    // Select the container div and configure its attributes
    var containerDiv = d3.select('#chart')
        .style('position', 'relative')
        .style('width', width + 'px')
        .style('height', height + 'px')
        .style('padding', 0)
        .style('background-color', '#eeeeec'),

We can now create the inner divs. As usual, we will select the elements to be created, bind the data array to the selection, and append the new elements on enter. We will also set the style attributes to use absolute positioning, and set their offsets and their width and height to 10px as follows:

    // Create a selection for the bubble divs, bind the data
    // array and set its attributes.
    var bubbleDivs = containerDiv.selectAll('div.bubble')
        .data(items)
        .enter()
        .append('div')
        .attr('class', 'bubble')
        .style('position', 'absolute')
        .style('width', '10px')
        .style('height', '10px')
        .style('background-color', '#222'),

The force layout will compute the position of the nodes in a series of steps or ticks. We can register a listener function to be invoked on each tick event and update the position of the nodes in the listener as follows:

    // Register a listener function for the force tick event, and
    // update the position of each div on tick.
    force.on('tick', function() {
        bubbleDiv
           .style('top', function(d) { return (d.y - 5) + 'px'; })
           .style('left', function(d) { return (d.x - 5)+ 'px'; });
    });

The divs will move nicely to their positions. Note that we subtract half of the div width and height when setting the offset. The divs will be centered in the position computed by the force layout as shown in the following screenshot:

The force layout method

The nodes are nicely positioned, but they all have the same size and color

Setting the color and size

Now that we have our nodes positioned, we can set the color and size of the div elements. To create a color scale for the nodes, we need to get a list with unique browser names. In our dataset, the items are browser versions; therefore, most of the browser names are repeated. We will use the d3.set function to create a set and use it to discard duplicated names, as follows:

// Compute unique browser names.
var browserList = items.map(function(d) { return d.name; }),
    browserNames = d3.set(browserList).values();

With the browser list ready, we can create a categorical color scale. Categorical scales are used to represent values that are different in kind; in our case, each browser will have a corresponding color:

    // Create a categorical color scale with 10 levels.
    var cScale = d3.scale.category10()
        .domain(browserNames);

The default range of d3.scale.category10 is a set of 10 colors with similar lightness but different hue, specifically designed to represent categorical data. We could use a different set of colors, but the default range is a good starting point. If we had more than 10 browsers, we would need to use a color scale with more colors. We will also set the border-radius style attribute to half the height (and width) of the div in order to give the divs a circular appearance. Note that the border-radius attribute is not supported in all the browsers but has better support than SVG. In browsers that don't support this attribute, the divs will be shown as squares:

    // Create a selection for the bubble divs, bind the data
    // array and set its attributes.
    var bubbleDivs = containerDiv.selectAll('div.bubble')
        .data(items)
        .enter()
        .append('div')
        // set other attributes ...
        .style('border-radius', '5px')
        .style('background-color', function(d) { 
            return cScale(d.name);
        });

We can now compute the size of the circles. To provide an accurate visual representation, the area of the circles should be proportional to the quantitative dimensions that they represent, in our case, the market share of each version. As the area of a circle is proportional to the square of the radius, the radius of the circles must be proportional to the square root of the market share. We set the minimum and maximum radius values and use this extent to create the scale:

    // Minimum and maximum radius
    var radiusExtent = [10, 50];

    // create the layout ...

    // Create the radius scale
    var rScale = d3.scale.sqrt()
        .domain(d3.extent(items, function(d) { return d.usage; }))
        .range(radiusExtent);

We will use the radius to compute the width, height, position, and border radius of each circle. To avoid calling the scale function several times (and to have cleaner code), we will add the radius as a new attribute of our data items:

    // Add the radius to each item, to compute it only once.
    items.forEach(function(d) {
        d.r = rScale(d.usage);
    });

We can modify the width, height, and border radius of the divs to use the new attribute, as follows:

    // Create the bubble divs.
    var bubbleDivs = containerDiv.selectAll('div.bubble')
        .data(items)
        .enter()
        .append('div')
        // set other attributes ...
        .style('border-radius', function(d) { return d.r + 'px'; })
        .style('width', function(d) { return (2 * d.r) + 'px'; })
        .style('height', function(d) { return (2 * d.r) + 'px'; });

We need to update the position of the div elements to account for the new radius:

    // Update the div position on each tick.
    force.on('tick', function() {
        bubbleDiv
            .style('top', function(d) { return (d.y - d.r) + 'px'; })
            .style('left', function(d) { return (d.x - d.r)+ 'px'; });
    });

The first draft of the visualization is shown in the following screenshot:

Setting the color and size

At this point, we have the first draft of our visualization, but there are still some things that need to be improved. The space around each div is the same, regardless of the size of each circle. We expect to have more space around bigger circles and less space around smaller ones. To achieve this, we will modify the charge property of the force layout, which controls the strength of repulsion between the nodes.

The charge method allows us to set the charge strength of each node. The default value is -30; we will use a function to set greater charges for bigger circles. In physical systems, the charge is proportional to the volume of the body; so, we will set the charge to be proportional to the area of each circle as follows:

// Configure the force layout.
var force = d3.layout.force()
    .nodes(items)
    .links([])
    .size([width, height])
    .charge(function(d) { return -0.12 * d.r * d.r; })
    .start();

We don't know in advance which proportionality constant will give a good layout; we need to tweak this value until we are satisfied with the visual result. Bubbles created with chart with the divs and force layout is shown in the following screenshot:

Setting the color and size

Now that we have a good first version of our visualization, we will adapt the code to use a reusable chart pattern. As you will surely remember, a reusable chart is implemented as a closure with the setter and getter methods as follows:

function bubbleChart() {

    // Chart attributes ...

    function chart(selection) {
        selection.each(function(data) {

            // Select the container div and configure its attributes.
            var containerDiv = d3.select(this);

            // create the circles ...
        });
    }

    // Accessor methods ...

    return chart;
};

The code in the chart function is basically the same code that we have written until now. We also added accessor methods for the color scale, width, height, and radius extent, as well as accessor functions for the value, name, and charge function to allow users to adapt to the repulsion force when using the chart with other datasets. We can create and invoke the charting function in the callback of d3.json as follows:

d3.json('../data/browsers.json', function(error, data) {

    // Handle errors getting or parsing the JSON data.
    if (error) { return error; }

    // Create the chart instance.
    var chart = bubbleChart()
        .width(500);

    // Bind the chart div to the data array.
    d3.select('#chart')
        .data([data.values])
        .call(chart);
});

The visualization is incomplete without a legend, so we will create a legend now. This time, we will create the legend as a reusable chart from the beginning.

Creating a legend

The legend should display which color represents which browser. It can also have additional information such as the aggregated market share of each browser. We must use the same color code as that used in the visualization:

function legendChart() {

// Chart Properties ...

        // Charting function.
    function chart(selection) {
        selection.each(function(data) {

        });
    }

    // Accessor methods ...

    return chart;
};

We will implement the legend as a div element that contains paragraphs; each paragraph will have the browser name and a small square painted with the corresponding color. In this case, the data will be a list of browser names. We will add a configurable color scale to make sure that the legend uses the same colors that are used in the bubble chart:

function legendChart() {

    // Color Scale
    var cScale = d3.scale.category20();

    // Charting function.
    function chart(selection) {
        // chart content ...
    }

    // Color Scale Accessor
    chart.colorScale = function(value) {
        if (!arguments.length) { return cScale; }
        cScale = value;
        return chart;
    };

    return chart;
};

We can create a div for the legend and put it alongside the chart div as follows:

d3.json('/chapter03/browsers.json', function(error, data) {
    // Create an instance of the legend chart.
    var legend = legendChart()
        .colorScale(chart.colorScale());

    // Select the container and invoke the legend.
    var legendDiv = d3.select('#legend')
        .data([chart.colorScale().domain()])
        .call(legend);
});

We used the domain of the chart's color scale as the dataset for the legend and set the color scale of the legend with the color scale of the chart. This will ensure that you have the same items and colors in the legend that are in the chart. We also added a width attribute and its corresponding accessor. In the legend chart function, we can create the title and the legend items using the data:

        // Select the container element and set its attributes.
        var containerDiv = d3.select(this)
            .style('width', width + 'px'),

        // Add the label 'Legend' on enter
        containerDiv.selectAll('p.legent-title')
            .data([data])
            .enter().append('p')
            .attr('class', 'legend-title')
            .text('Legend'),

        // Add a div for each data item
        var itemDiv = containerDiv.selectAll('div.item')
            .data(data)
            .enter().append('div')
            .attr('class', 'item'),

We have labels that show up in the legend, but we need to add a marker with the corresponding color. To keep things simple, we will add two points and set them with the same background and text color:

        itemP.append('span').text('..')
            .style('color', cScale)
            .style('background', cScale);

To finish the legend, we will compute the market share of each browser. We will create a map to store each browser name and its aggregated usage, as follows:

    // Create a map to aggregate the browser usage
    var browsers = d3.map();

    // Adds up the usage for each browser.
    data.values.forEach(function(d) {
        var item = browsers.get(d.name);
        if (item) {
            browsers.set(d.name, {
                name: d.name, 
                usage: d.usage + item.usage
            });
        } else {
            browsers.set(d.name, {
                name: d.name, 
                usage: d.usage
            });
        }
    });

The final version of the bubble chart is shown in the following screenshot:

Creating a legend

In this example, we created a simple visualization using the div elements and displayed them as circles with the help of the border-radius attribute. Using divs with rounded corners is not the only alternative to create this visualization without using SVG; we could have used raster images of circles instead of div elements, using absolute positioning and changing the image width and height.

One great example of a sophisticated visualization made without SVG is the Electoral Map by the New York Times graphic department (http://elections.nytimes.com/2012/ratings/electoral-map). In this visualization, the user can create their own scenarios for the presidential elections of 2012 in the United States.

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

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