Until now, we have used D3 to create visualizations by manipulating SVG elements and divs. In some cases, it can be more convenient to render the visualizations using the canvas elements, for performance reasons or if we need to transform and render raster images. In this section, we will learn how to create figures with the HTML5 canvas element and how to use D3 to render figures with the canvas element.
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 filled with a different color, shown as follows:
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 exist 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.
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:
We will create a small network chart using the force layout of D3 and canvas instead of SVG. To make the graph look more interesting, we will randomly generate the data. We will generate 250
nodes that are 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 and randomly assign 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 a 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 doesn'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 by beginning a new path for each link, by 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 we still have to redraw them if the SVG elements are changed).