8 min read

This article by Pablo Navarro Castillo, author of Mastering D3.js, includes that most of the time, we use D3 to create SVG-based charts and visualizations. If the number of elements to render is huge, or if we need to render raster images, it can be more convenient to render our visualizations using the HTML5 canvas element. In this article, we will learn how to use the force layout of D3 and the canvas element to create an animated network visualization with random data.

(For more resources related to this topic, see here.)

Creating figures with canvas

The HTML canvas element allows you to create raster graphics using JavaScript. It was first introduced in HTML5. It enjoys more widespread support than SVG, and can be used as a fallback option. Before diving deeper into integrating canvas and D3, we will construct a small example with canvas.

The canvas element should have the width and height attributes. This alone will create an invisible figure of the specified size:

<!— Canvas Element -->
<canvas id="canvas-demo" width="650px" height="60px"></canvas>

If the browser supports the canvas element, it will ignore any element inside the canvas tags. On the other hand, if the browser doesn’t support the canvas, it will ignore the canvas tags, but it will interpret the content of the element. This behavior provides a quick way to handle the lack of canvas support:

<!— Canvas Element -->
<canvas id="canvas-demo" width="650px" height="60px">
<!-- Fallback image -->
<img src="img/fallback-img.png" width="650" height="60"></img>
</canvas>

If the browser doesn’t support canvas, the fallback image will be displayed. Note that unlike the <img> element, the canvas closing tag (</canvas>) is mandatory. To create figures with canvas, we don’t need special libraries; we can create the shapes using the canvas API:

<script>
    // Graphic Variables
    var barw = 65,
        barh = 60;

    // Append a canvas element, set its size and get the node.
    var canvas = document.getElementById('canvas-demo');

    // Get the rendering context.
    var context = canvas.getContext('2d');

    // Array with colors, to have one rectangle of each color.
    var color = ['#5c3566', '#6c475b', '#7c584f', '#8c6a44', '#9c7c39',
'#ad8d2d', '#bd9f22', '#cdb117', '#ddc20b', '#edd400'];

    // Set the fill color and render ten rectangles.
    for (var k = 0; k < 10; k += 1) {
        // Set the fill color.
        context.fillStyle = color[k];
        // Create a rectangle in incremental positions.
        context.fillRect(k * barw, 0, barw, barh);
    }
</script>

We use the DOM API to access the canvas element with the canvas-demo ID and to get the rendering context. Then we set the color using the fillStyle method, and use the fillRect canvas method to create a small rectangle. Note that we need to change fillStyle every time or all the following shapes will have the same color. The script will render a series of rectangles, each one filled with a different color, shown as follows:

A graphic created with canvas

Canvas uses the same coordinate system as SVG, with the origin in the top-left corner, the horizontal axis augmenting to the right, and the vertical axis augmenting to the bottom. Instead of using the DOM API to get the canvas node, we could have used D3 to create the node, set its attributes, and created scales for the color and position of the shapes. Note that the shapes drawn with canvas don’t exists in the DOM tree; so, we can’t use the usual D3 pattern of creating a selection, binding the data items, and appending the elements if we are using canvas.

Creating shapes

Canvas has fewer primitives than SVG. In fact, almost all the shapes must be drawn with paths, and more steps are needed to create a path. To create a shape, we need to open the path, move the cursor to the desired location, create the shape, and close the path. Then, we can draw the path by filling the shape or rendering the outline. For instance, to draw a red semicircle centered in (325, 30) and with a radius of 20, write the following code:

    // Create a red semicircle.
    context.beginPath();
    context.fillStyle = '#ff0000';
    context.moveTo(325, 30);
    context.arc(325, 30, 20, Math.PI / 2, 3 * Math.PI / 2);
    context.fill();

The moveTo method is a bit redundant here, because the arc method moves the cursor implicitly. The arguments of the arc method are the x and y coordinates of the arc center, the radius, and the starting and ending angle of the arc. There is also an optional Boolean argument to indicate whether the arc should be drawn counterclockwise. A basic shape created with the canvas API is shown in the following screenshot:

Integrating canvas and D3

We will create a small network chart using the force layout of D3 and canvas instead of SVG. To make the graph looks more interesting, we will randomly generate the data. We will generate 250 nodes sparsely connected. The nodes and links will be stored as the attributes of the data object:

    // Number of Nodes
    var nNodes = 250,
        createLink = false;

    // Dataset Structure
    var data = {nodes: [],links: []};

We will append nodes and links to our dataset. We will create nodes with a radius attribute randomly assigning it a value of either 2 or 4 as follows:

    // Iterate in the nodes
    for (var k = 0; k < nNodes; k += 1) {
        // Create a node with a random radius.
        data.nodes.push({radius: (Math.random() > 0.3) ? 2 : 4});

        // Create random links between the nodes.
    }

We will create a link with probability of 0.1 only if the difference between the source and target indexes are less than 8. The idea behind this way to create links is to have only a few connections between the nodes:

        // Create random links between the nodes.
        for (var j = k + 1; j < nNodes; j += 1) {

            // Create a link with probability 0.1
            createLink = (Math.random() < 0.1) && (Math.abs(k - j) < 8);

            if (createLink) {
                // Append a link with variable distance between the nodes
                data.links.push({
                    source: k,
                    target: j,
                    dist: 2 * Math.abs(k - j) + 10
                });
            }
        }

We will use the radius attribute to set the size of the nodes. The links will contain the distance between the nodes and the indexes of the source and target nodes. We will create variables to set the width and height of the figure:

    // Figure width and height
    var width = 650,
        height = 300;

We can now create and configure the force layout. As we did in the previous section, we will set the charge strength to be proportional to the area of each node. This time, we will also set the distance between the links, using the linkDistance method of the layout:

// Create and configure the force layout
var force = d3.layout.force()
    .size([width, height])
    .nodes(data.nodes)
    .links(data.links)
    .charge(function(d) { return -1.2 * d.radius * d.radius; })
    .linkDistance(function(d) { return d.dist; })
    .start();

We can create a canvas element now. Note that we should use the node method to get the canvas element, because the append and attr methods will both return a selection, which don’t have the canvas API methods:

// Create a canvas element and set its size.
var canvas = d3.select('div#canvas-force').append('canvas')
    .attr('width', width + 'px')
    .attr('height', height + 'px')
    .node();

We get the rendering context. Each canvas element has its own rendering context. We will use the ‘2d’ context, to draw two-dimensional figures. At the time of writing this, there are some browsers that support the webgl context; more details are available at https://developer.mozilla.org/en-US/docs/Web/WebGL/Getting_started_with_WebGL. Refer to the following ‘2d’ context:

// Get the canvas context.
var context = canvas.getContext('2d');

We register an event listener for the force layout’s tick event. As canvas doesn’t remember previously created shapes, we need to clear the figure and redraw all the elements on each tick:

force.on('tick', function() {
    // Clear the complete figure.
    context.clearRect(0, 0, width, height);

    // Draw the links ...
    // Draw the nodes ...
});

The clearRect method cleans the figure under the specified rectangle. In this case, we clean the entire canvas. We can draw the links using the lineTo method. We iterate through the links, beginning a new path for each link, moving the cursor to the position of the source node, and by creating a line towards the target node. We draw the line with the stroke method:

        // Draw the links
        data.links.forEach(function(d) {
            // Draw a line from source to target.
            context.beginPath();
            context.moveTo(d.source.x, d.source.y);
            context.lineTo(d.target.x, d.target.y);
            context.stroke();
        });

We iterate through the nodes and draw each one. We use the arc method to represent each node with a black circle:

        // Draw the nodes
        data.nodes.forEach(function(d, i) {
            // Draws a complete arc for each node.
            context.beginPath();
            context.arc(d.x, d.y, d.radius, 0, 2 * Math.PI, true);
            context.fill();
        });

We obtain a constellation of disconnected network graphs, slowly gravitating towards the center of the figure. Using the force layout and canvas to create a network chart is shown in the following screenshot:

We can think that to erase all the shapes and redraw each shape again and again could have a negative impact on the performance. In fact, sometimes it’s faster to draw the figures using canvas, because this way the browser doesn’t have to manage the DOM tree of the SVG elements (but it still have to redraw them if the SVG elements are changed).

Summary

In this article, we will learn how to use the force layout of D3 and the canvas element to create an animated network visualization with random data.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here