Big data visualization
11.1 Big geodata
11.1.3 Mixed-mode rendering techniques
The drawback with using canvas is that you can’t easily provide the level of interac- tivity you may want for your data visualization. Typically, you draw your interactive elements with SVG and your large datasets with canvas. If we assume that the coun- tries we’re drawing aren’t going to provide any interactivity, but the triangles will, then we can render the triangles as SVG and render the countries as canvas using the code in the following listing. This requires that we initialize two versions of
d3.geo.path—one for drawing SVG and one for drawing canvas—and then we use both in our zoomed function.
Draws each triangle to canvas
Figure 11.4 Drawing our map with canvas produces higher performance, but slightly less crisp graphics. On the left, it may seem like the triangles are as smoothly rendered as the earlier SVG triangles, but if you zoom in as we’ve done on the right, you can start to see clearly the slightly pixelated canvas rendering.
function createMap(countries) {
var projection = d3.geo.mercator().scale(100).translate([250,250]); var svgPath = d3.geo.path().projection(projection);
var canvasPath = d3.geo.path().projection(projection); var mapZoom = d3.behavior.zoom()
.translate(projection.translate()) .scale(projection.scale()) .on("zoom", zoomed); d3.select("svg").call(mapZoom); var g = d3.select("svg"); g.selectAll("path.sample") .data(sampleData) .enter() .append("path") .attr("class", "sample")
.on("mouseover", function() {d3.select(this).style("fill", "pink")}); zoomed();
function zoomed() {
projection.translate(mapZoom.translate()).scale(mapZoom.scale()); var context = d3.select("canvas").node().getContext("2d"); context.clearRect(0,0,500,500);
canvasPath.context(context); context.strokeStyle = "black"; context.fillStyle = "gray"; context.lineWidth = "1px";
for (var x in countries.features) { context.beginPath(); canvasPath(countries.features[x]); context.stroke(); context.fill(); } d3.selectAll("path.sample").attr("d", svgPath); }; };
This allows us to maintain interactivity, such as the mouseover function on our trian- gles to change any triangle’s color to pink when moused over. This approach maxi- mizes performance by rendering any graphics that have no interactivity using HTML5
canvas instead of SVG. As shown in figure 11.5, the appearance produced using this method is virtually identical to that using canvas only or SVG only.
But what if you have massive numbers of elements and you really do want interac- tivity on all them, but you also want to give the user the ability to pan and drag? In that case, you have to embrace an extension of this mixed-mode rendering. You render in canvas whenever users are interacting in such a way that they can’t interact with other elements. In other words, we need to render the triangles in canvas when the map is
Listing 11.7 Rendering SVG and canvas simultaneously
We need to instantiate a different d3.geo.path for canvas and for SVG. Updates the map when it’s first created Draws canvas features with canvasPath Draws SVG features with svgPath
being zoomed and panned, but render them in SVG when the map isn’t in motion and the user is mousing over certain elements.
How do you determine when a zoom event starts and when it finishes? In the past you had to set a timer, check to see if the user was still zooming, and then redraw the elements. But, fortunately, D3 introduced a pair of new events to the zoom control:
"zoomstart" and "zoomend". These fire, as you may guess, when the zoom event begins and ends, respectively. The following listing shows how you’d initialize a zoom behavior with different functions for these different events.
var projection = d3.geo.mercator().scale(100).translate([250,250]); var svgPath = d3.geo.path().projection(projection);
var canvasPath = d3.geo.path().projection(projection); mapZoom = d3.behavior.zoom() .translate(projection.translate()) .scale(projection.scale()) .on("zoom", zoomed) .on("zoomstart", zoomInitialized) .on("zoomend", zoomFinished); d3.select("svg").call(mapZoom); var g = d3.select("svg").append("g") g.selectAll("path.sample").data(sampleData) .enter() .append("path")
Listing 11.8 Mixed rendering based on zoom interaction
Figure 11.5 Background countries are drawn with canvas, while foreground triangles are drawn with SVG to use interactivity. SVG graphics are individual elements in the DOM and therefore amenable to having click, mouseover, and other event listeners attached to them.
Assigns separate functions for each zoom state
.attr("class", "sample") .on("mouseover", function() {
d3.select(this).style("fill", "pink"); });
zoomFinished();
This allows us to restore our canvas drawing code for triangles to the zoomed function and to move the SVG rendering code out of the zoomed function and into a new zoom- Finished function. We also need to hide the SVG triangles when zooming or panning starts by creating a zoomInitialized function that itself also fires the zoomed function (to draw the triangles we just hid, but in canvas). Finally, our zoomFinished function also contains the canvas drawing code necessary to only draw the countries. The dif- ferent drawing strategies based on zoom events are shown in table 11.1.
As you can see in the following listing, this code is inefficient, but I wanted to be explicit about this functionality, because it’s a bit convoluted.
function zoomed() {
projection.translate(mapZoom.translate()).scale(mapZoom.scale()); var context = d3.select("canvas").node().getContext("2d"); context.clearRect(0,0,500,500);
canvasPath.context(context); context.strokeStyle = "black"; context.fillStyle = "gray"; context.lineWidth = "1px";
for (var x in countries.features) { context.beginPath(); canvasPath(countries.features[x]); context.stroke() context.fill(); } context.strokeStyle = "black"; context.fillStyle = "rgba(255,0,0,.2)"; context.lineWidth = 1;
for (var x in sampleData) { context.beginPath();
Table 11.1 Rendering action based on zoom event
zoom event Countries rendered as Triangles rendered as
zoomed Canvas Canvas
zoomInitialized Canvas Hide SVG
zoomFinished Canvas SVG
Listing 11.9 Zoom functions for mixed rendering
We have to call zoomFinished (listing 11.9) to draw the canvas countries with SVG triangles.
Draws all elements as canvas during zooming
canvasPath(sampleData[x]); context.stroke() context.fill(); } }; function zoomInitialized() { d3.selectAll("path.sample") .style("display", "none"); zoomed(); }; function zoomFinished() {
var context = d3.select("canvas").node().getContext("2d"); context.clearRect(0,0,500,500);
canvasPath.context(context) context.strokeStyle = "black"; context.fillStyle = "gray"; context.lineWidth = "1px";
for (var x in countries.features) { context.beginPath(); canvasPath(countries.features[x]); context.stroke() context.fill(); } d3.selectAll("path.sample") .style("display", "block") .attr("d", svgPath); };
As a result of this new code, we have a map that uses canvas rendering when users zoom and pan, but SVG rendering when the map is fixed in place and users have the ability to click, mouse over, or otherwise interact with the graphical elements. It’s the best of both worlds. The only drawback of this approach is that we have to invest more time making sure our <canvas> element and our <svg> element line up per- fectly, and that our opacity, fill colors, and so on are close enough matches that it’s not jarring to the user to see the different modes. I haven’t done this in the previous code, so that you can see that the two modes are in operation at the same time, and that’s reflected in the difference between the two graphical outputs in figure 11.6.
The kind of pixel-perfect alignment necessary to make the transition from one mode to another, as well as the fastidious color matching also required, isn’t some- thing I have the space to explain in this book, but you’ll need to do both to make the best interactive information visualization. If you look closely at figure 11.6, you’ll notice that the canvas element (on the right) is a pixel or so shifted up and to the left, and that’s without testing it in other browsers that may have different default settings for <canvas> or <svg> or both.
Finally, using canvas and SVG drawing simultaneously may present a difficulty. Say we want to draw a canvas layer over an SVG layer because we want the canvas layer to
Hides SVG elements when zooming starts
Calls zoomed to draw with canvas the SVG triangles we just hid
Only draws countries with canvas at the end of the zoom
Shows SVG elements when zoom ends
Sets the new position of SVG elements
appear above some of our SVG elements visually but below other SVG elements, and we want interactivity on all them. In that case we’d need to sandwich our canvas layer between our SVG layers and set the pointer-events style of our canvas layer, as shown in figure 11.7.
Figure 11.6 The same randomly generated triangles rendered in SVG while the map isn’t being zoomed or panned (left) and in canvas while the map is being zoomed or panned (right). Notice that only the SVG triangles have different fill values based on user interaction, because that isn’t factored into the canvas drawing code for the triangles on the right.
<canvas style="pointer-events: none;"> <svg>
<svg>
<canvas>
Figure 11.7 Placing interactive SVG elements below a <canvas> element requires that you set its
pointer-events style to "none", even if it has a transparent background, in order to register click
If you add further alternating layers of interactivity but with graphical placement above and below, then you can end up making a <canvas> and <svg> layer cake in your DOM that’s hard to manage and also hard to mentally conceptualize.