Using canvas
So far we've been rendering our visualizations with SVG. SVG is great because it follows a familiar structure, offers infinitely scalable vector graphics, and works everywhere. There are some advanced SVG features you can't use everywhere, but the core is solid.
However, SVG has a big flaw: it's slow.
Anything more than a few hundred SVG nodes and your browser starts to struggle. Especially if those thousands of elements move around.
A web animation panel moderator at ForwardJS once asked me, "But why would you want thousands of SVG elements?".
It was my first time participating in a panel, stage lights shining upon me, a mildly disinterested audience staring into their phones ... I bombed: "Errr ... because you can?".
What I should have said was: "Because there have been thousands of UFO sightings, there are thousands of counties in the US, millions of taxi rides, hundreds of millions of people having this or that datapoint. And you want to show change over time."
That's the real answer.
Sometimes, when you're visualizing data, you have a lot of data. The data changes over time. Animation is the best way to show change over time.
Once upon a time, I worked on a D3 video course for Packt and used UFO sightings as an example. At peak UFO sighting, right before smartphones become a thing, the animation takes up to 2 seconds to redraw a single frame.
Terrible.
So if SVG is slow and you need to animate thousands of elements, what are you to do? HTML5 Canvas.
Why Canvas
Unlike SVG, HTML5 Canvas lets you draw rasterized images. This means you're no longer operating at the level of shapes because you're working with pixels on the screen.
With SVG and other vector formats, you tell the browser what you want to render. With Canvas and other raster formats, you tell the browser how you want to render. The browser doesn't know what you're doing; it gets a field of pixel colors and renders them as an image.
That's much faster for computers to handle. In some cases browsers can even use hardware acceleration β the GPU βΒ to render HTML5 Canvas elements. With a bit of care, you can do almost anything you want, even on a mobile phone.
Phones these days have amazing GPUs and kind of terrible CPUs in comparison. The CPU burns more battery, works slower, warms up your phone more, etc.
If SVG wasn't so easy to use, I'd almost suggest going straight to Canvas for any sort of complex animation. Mobile traffic is, what, 60% to 70% of web traffic these days?
Isn't that too hard?
You might think all this pixel stuff sounds complicated. We've stayed in shape and component land so far. We didn't care about any pixels or rendering details. Draw a rectangle and a wild rectangle appears.
How do you know which pixel should do what when you render with Canvas?
HTML5 Canvas does offer some shape primitives. It has circles and rectangles and things like that, but they suffer from the same problem that SVG does. The browser has to use your CPU to calculate those, and at around 10,000 elements, things break down.
10,000 elements is still a hell of a lot more than the 3,000 or so that SVG gives you.
If your app allows it, you can use sprites: Tiny images copy-pasted on the Canvas as bytestreams. I have yet to find an upper bound for those. My JavaScript became the bottleneck π
But I'm getting ahead of myself. We'll talk about sprites later.
The trouble with HTML5 Canvas
The tricky thing with HTML5 Canvas is that the API is low level and that canvas is flat. As far as your JavaScript and React code are concerned, it's a flat image. It could be anything.
The lack of structure makes it difficult to detect clicks on elements, interactions between shapes, when something covers something else, how the user interacts with your stuff and so on Anything that requires understanding what's rendered.
You have to move most of that logic into your data store and manually keep track.
As you can imagine, this becomes cumbersome. And you still can't detect user interaction because all you get is "User clicked on coordinate (x, y). Have fun."
At the same time, the low level API makes abstractions difficult. You can't create components for "this is a map" or "histogram goes here". You're always down to circles and rectangles and basic shapes.
Your code soon looks like the D3.js spaghetti we tried to avoid in the first place.