I am new to d3.js. I figured out there are two ways to get the objects drawn - SVG and Canvas. My use case is around <100 nodes and edges. I have already tried few examples using canvas and it looks great.
I see there is a SO post around the difference between SVG and Canvas.
Both seem ok for my use, however, I am inclined towards canvas (as I have already few example working). please correct me if I am missing anything in d3.js context?
D3 charts are most often rendered using SVG, a retained mode graphics model, which is easy to use, but performance is limited. SVG charts can typically handle around 1,000 datapoints. Since D3 v4 you've also had the option to render charts using canvas, which is an immediate mode graphics model.
SVG gives better performance with smaller number of objects or larger surface. Canvas gives better performance with smaller surface or larger number of objects. SVG is vector based and composed of shapes. Canvas is raster based and composed of pixel.
js, an introduction. canvas is an HTML element which can be used to draw graphics. It is an alternative to svg. Most basic shape.
It provides options to draw different shapes such as Lines, Rectangles, Circles, Ellipses, etc. Hence, designing visualizations with SVG gives you more power and flexibility.
The differences listed in the linked question/answers speak to the general differences between svg and canvas (vector/raster, etc). However, with d3 these differences have additional implications, especially considering that a core part of d3 is data binding.
Perhaps the most central feature of d3 is data binding. Mike Bostock states he needed to create d3 once he joined data to elements:
The defining moment was when I got the data-join working for the first time. It was magic. I wasn’t even sure I understood how it worked, but it was a blast to use. I realized there could be a practical tool for visualization that didn’t needlessly restrict the types of visualizations you could make. link
With SVG, data binding is easy - we can assign a datum to an individual svg element and then use that datum to set its attributes/update it/etc. This is built upon the statefulness of svg - we can re-select a circle and modify it or access its properties.
With Canvas, canvas is stateless, so we can't bind data to shapes within the canvas as the canvas only comprises of pixels. As such we can't select and update elements within the canvas because the canvas doesn't have any elements to select.
Based on the above, we can see that the enter/update/exit cycle (or basic append statements) are needed for svg in idiomatic D3: we need to enter elements to see them and we style them often based on their datum. With canvas, we don't need to enter anything, same with exiting/updating. There are no elements to append in order to see, so we can draw visualizations without the enter/update/exit or the append/insert approaches used in d3 svg visualizations, if we want.
Canvas without data binding
I'll use the example bl.ock in your last question, here. Because we don't need to append elements at all (or append data to them), we use a forEach loop to draw each feature (which is counter to idiomatic D3 with SVG). Since there are no elements to update, we have to redraw each feature each tick - redrawing the entire frame (notice the clearing of the canvas each tick). Regarding the drag, d3.drag and d3.force has some functionality anticipating use with canvas, and can allow us to modify the data array directly through drag events - bypassing any need for node elements in the DOM to directly interact with the mouse (d3.force is also modifying the data array directly - but it does this in the svg example as well).
Without data binding we draw elements based on the data directly:
data.forEach(function(d) {
// drawing instructions:
context.beginPath()....
})
If the data changes, we will probably redraw the data.
Canvas with Data Binding
That said, you can implement data binding with canvas, but it requires a different approach using dummy elements. We go through the regular update/exit/enter cycle, but as we are using dummy elements, nothing is rendered. We re-render the canvas whenever we want (it may be continuously if we are using transitions), and draw things based on the dummy elements.
To make a dummy parent container we can use:
// container for dummy elements:
var faux = d3.select(document.createElement("custom"));
Then we can make selections as needed, using enter/exit/update/append/remove/transition/etc:
// treat as any other DOM elements:
var bars = faux.selectAll(".bar").data(data).enter()....
But as elements in these selections aren't rendered, we need to specify how and when to draw them. Without data binding and Canvas we drew elements based on the data directly, with data binding and Canvas we draw based on the selection/element in the faux DOM:
bars.each(function() {
var selection = d3.select(this);
context.beginPath();
context.fillRect(selection.attr("x"), selection.attr("y")...
...
})
Here we can redraw the elements whenever we exit/enter/update etc which may have some advantages. This also allows D3 transitions by redrawing continuously while transitioning properties on the faux elements.
The below example has a complete enter/exit/update cycle with transitions, demonstrating canvas with data binding:
var canvas = d3.select("body")
.append("canvas")
.attr("width", 600)
.attr("height", 200);
var context = canvas.node().getContext("2d");
var data = [1,2,3,4,5];
// container for dummy elements:
var faux = d3.select(document.createElement("custom"));
// normal update exit selection with dummy elements:
function update() {
// modify data:
manipulateData();
var selection = faux.selectAll("circle")
.data(data, function(d) { return d;});
var exiting = selection.exit().size();
var exit = selection.exit()
.transition()
.attr("r",0)
.attr("cy", 70)
.attr("fill","white")
.duration(1200)
.remove();
var enter = selection.enter()
.append("circle")
.attr("cx", function(d,i) {
return (i + exiting) * 20 + 20;
})
.attr("cy", 50)
.attr("r", 0)
.attr("fill",function(d) { return ["orange","steelblue","crimson","violet","yellow"][d%5]; });
enter.transition()
.attr("r", 8)
.attr("cx", function(d,i) {
return i * 20 + 20;
})
.duration(1200);
selection
.transition()
.attr("cx", function(d,i) {
return i * 20 + 20;
})
.duration(1200);
}
// update every 1.3 seconds
setInterval(update,1300);
// rendering function, called repeatedly:
function render() {
context.clearRect(0, 0, 600, 200);
faux.selectAll("circle").each(function() {
var sel = d3.select(this);
context.beginPath();
context.arc(sel.attr("cx"),sel.attr("cy"),sel.attr("r"),0,2*Math.PI);
context.fillStyle = sel.attr("fill");
context.fill();
context.stroke();
})
window.requestAnimationFrame(render)
}
window.requestAnimationFrame(render)
// to manipulate data:
var index = 6; // to keep track of elements.
function manipulateData() {
data.forEach(function(d,i) {
var r = Math.random();
if (r < 0.5 && data.length > 1) {
data.splice(i,1);
}
else {
data.push(index++);
}
})
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Block version.
Summary
With canvas, data binding requires a set of dummy elements, but, once bound you can easily use transitions and the update/enter/exit cycle. But, rendering is detached from update/enter/exit and transitions - it is up to you to decide how and when to redraw the visualization. This drawing takes place outside of the update/enter/exit and transition methods.
With svg, the enter/update/exit cycle and transitions update elements in the visualization, linking rendering and data in one step.
In canvas with data binding on faux elements, the visualization represents the faux nodes. In svg the visualization is the nodes.
Data binding is a fundamental difference, idiomatic D3 requires it in SVG but gives us the choice of whether we want to use it when working with Canvas. However there other differences between Canvas and SVG in relation to D3 mentioned below:
Perhaps the most substantial concern with using Canvas is that it is stateless, just a collection of pixels rather than elements. This makes mouse events difficult when interacting with specific rendered shapes. While the mouse can interact with the Canvas, standard events are triggered for interactions with specific pixels.
So while with SVG we can assign a click listener (for example) to each node in a force layout, with Canvas, we set one click listener fro the entire canvas and then based on position have to determine what node should be considered "clicked".
The D3-force canvas example mentioned above uses a force layout's .find
method and uses that to find the node closest to a mouse click and then sets the drag subject to that node.
There are a few ways we could determine what rendered shape is being interacted with:
Each shape in the visible canvas is drawn on the invisible canvas, but on the invisible canvas it has a unique color. Taking the xy of a mouse event on the visible canvas we can use that to get the pixel color at the same xy on the invisible canvas. Since colors are numbers in HTML, we can convert that color to an datum's index.
Inverting scales (scaled xy position to unscaled input values) for heatmap/gridded data (example)
Using an unrendered Voronoi diagram's .find
method to find nearest node to event (for points, circles)
.find
method to find nearest node to event (for points, circles, mostly in the context of force layouts)The first may be the most common, and certainly the most flexible, but the others may be preferable depending on context.
I'll very quickly touch on performance. In the question's linked post "What's the difference between SVG and Canvas" it may not be bold enough in the answers there, but in general canvas and svg differ in rendering time when handling thousands of nodes, especially if rendering thousands of nodes that are being animated.
Canvas becomes increasingly more performant as more nodes are rendered and as the nodes do more things (transition, move, etc).
Here's a quick comparison of Canvas (with data binding on faux nodes) and SVG and 19 200 simultaneous transitions:
The Canvas should be the smoother of the two.
Lastly I'll touch on D3's modules. Most of these don't interact with the DOM at all and can be used easily for either SVG or Canvas. For example d3-quadtree or d3-time-format aren't SVG or Canvas specific as they don't deal with the DOM or rendering at all. Modules such as d3-hierarchy don't actually render anything either, but provide the information needed to render in either Canvas or SVG.
Most modules and methods that provide SVG path data can also be used to generate canvas path method calls, and consequently can be used for either SVG and Canvas relatively easily.
I'll mention a couple modules specifically here:
D3-selection
Obviously this module requires selections, selections require elements. So to use this with Canvas for things like the enter/update/exit cycle or selection .append/remove/lower/raise we want to use faux elements with Canvas.
With Canvas, event listeners assigned with selection.on()
can work with or without data binding, the challenges of mouse interactions are noted above.
D3-transition
This module transitions the properties of elements, so it would generally be used with Canvas only if we were using data binding with faux elements.
D3-axis
This module is strictly SVG unless willing to do a fair amount of work to shoehorn it into a Canvas use. This module is extremely useful when working with SVG, especially when transitioning the axis.
D3-path
This takes Canvas path commands and converts them to SVG path data. Useful for taking adopting canvas code to SVG situations. Mostly used internally with D3 to produce SVG path data.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With