Several times now I have tried to make a simple web application that tracks some personal data. Each time I halt on different place, as is usual, but this time it was when I tried to display the data in a form that allows deriving insights – a chart.
Plenty of libraries allowing for drawing charts on a web site have been published on the Internet, but when I tried to quickly use one or two, I ended up stuck because of lack of easy to understand documentation. So I decided in a whim I’m going to go with the hardest approach you could choose – writing another one from scratch using the most user unfriendly technology available for the task: an HTML5 Canvas. After all that’s the engineer’s way, right? Reinvent the square wheel again and again… Sigh.
Learning the Canvas Basics
As my first goal was to create a simple line chart, I had to start with very simple things: drawing a coordinate system and naming the axes. So I went on with the first line.
Drawing a line on a Canvas
Going through the MDN Canvas API documentation, I found out that I need to get the 2D context of the canvas and execute these functions:
const context = canvas.getContext('2d'); context.beginPath(); context.moveTo(10, 10); context.lineTo(10,400); context.stroke();
And I had a vertical line! Marvelous, I felt like I’m going to be finished with this chart within an hour. Let me explain what’s happening here.
- We get the canvas and its 2D context.
- We start a path using the
beginPathmethod. This is used for drawing a set of lines each starting where the other one ends. However, we will only draw one line.
- We move to the point where the line starts with the
- We instruct the canvas to draw a line to the point where it should end.
strokeflushes our commands and draws the line on the canvas.
Dealing with the Coordinate System of the Canvas
My hopes of advancing rapidly, however, proved to be an illusion. Once I did the same for the horizontal line which would be the X axis, I found this:
I forgot how computer graphics work… Of course the coordinates are in the Fourth quadrant. This basically means that if I just use the numbers I get from my data, the chart would be completely upside down.
Because at that point (and even now) I am a complete novice in computer graphics, I kept on trying to brute-force my way through the development of a line chart. Instead of reading a bit about coordinate systems and how to deal with them, I decided I’ll just introduce compensation constants to the coordinates which I get from my data.
This quickly devolved into a mess which is not worthy of presenting even on the Internet, but was the reason to waste an entire day of my weekend into it.
Playing Around with Transforms
Once I was fed up with coordinate silliness, I decided to have a look around on the internet. While I didn’t find a panacea to my problems, I found a keyword – “transform”. Something immediately clicked in my monkey brain and I knew, this is the solution to the upside-down chart.
Fortunately, these transforms are done with simple methods on the context. Simple, if you know what you want. And read beforehand. Instead, I decided to YOLO it with the hardest to understand of the available methods and some StackOverflow answers.
setTransform on a canvas
I found out the canvas context has a function called
setTransform that takes a bunch of arguments. Like 6 of them. This isn’t really great, because 6 is a big number and I’m usually struggling even with 3. But if I look at the documentation often enough that isn’t really a problem. So I decided to use it.
I read there that the 6 parameters are the upper two rows of a 3×3 matrix, which describes how exactly would the viewport of the canvas change. Each of the parameters are responsible for a certain operation and using all of them somehow allows you to do whatever you want.
Since I wanted the picture to flip horizontally, I needed to use this matrix:
It basically tells the canvas to scale the vetical coordinates with -1 and to move the viewport up by 400 pixels.
Once I applied this matrix I had the axes drawn in the right places! Perfect.
At this point I felt like labeling the axes. This meant I had to write down their names, preferably next to each axis and centered either vertically or horizontally, depending on the axis itself.
fillText(text, x, y) function of the context, I wrote the name of the x-axis. And this was the result:
Let’s just ignore the fact that the Y-axis label is not well placed. I think the bigger problem here is that it isn’t that easy to read the labels when they’re upside down. It doesn’t really look like English, does it?
This is when I felt like I had to read a bit more in the documentation to find out what exactly has happened and how to fix it. There I found out about the transform stack and how to put stuff on it and then take it out. The natural progression was, of course, to use it whether it makes things easier or not. I mean abuse it. That was the word, yes.
I found the methods
restore() of the 2d context. They manipulate the transform stack, namely
save() puts the current transform on the stack, and
restore() takes the last transform on the stack and reapplies it. This way you can reset the canvas to its original viewport and then return back to the previous point you were drawing easily.
It gets even better, when you find out you can store multiple transforms and then layer them back on using
restore() multiple times. So I used all of this at once. And here’s how it went.
For each feature of the chart – axis, label, tick on an axis and whatever else was there I had to:
- Save the current transform using
- Clear the transform using
- Set the new transform using
translate(), rotate(), scale()or
- Draw something;
- Restore the previous transform using
Further it required from me to remember what the previous state of the transform was, because I needed to decide at stage 5, whether I really want to restore it or not.
While this proved to look neat in the documentation, when I applied it, it ended up being completely custom and unfit for reuse. Further it relied on constant debugging to check what’s happening, since the state of the transform stack was very important. And the result wasn’t really stunning:
Look at the mess this chart is. It has the X-axis labels span from way-too-far left to way-too-far right, the Y-axis labels are shifted up for no reason and the labels of the Y-axes is so far away, no one even knows what this text does there.
A chart shows a lot of data by design, but if you have to keep track not only of it but also of some meta data that is only used because of choice of technology, then the hell is imminent and it inflicts at least three times more mental pain.
When encountering such a thing, usually a developer chooses one of two paths: change the technology in hopes the meta data there is much less, or keep working with the same technology, but introduce a library that deals with the bloat.
And I looked into libraries that introduce a DOM on top of a Canvas element. But because I’m so stubborn, I decided I’ll keep on going without one. If I weren’t that stubborn, I could have used
paper.js, that offer a much simpler, high-level API to Canvas. But let’s keep doing stuff the most painful way.
HTML5 Canvas isn’t easy
As a conclusion of this part I can say that using HTML5 Canvas isn’t easy. It takes time to learn the API and then it takes much more time to apply it in a way that paints what you want. It doesn’t support simple things like clicking on an element that you have drawn out of the box and I still haven’t found a way to test it.
This said, it does provide enough, to create insanely complex things like games and animation. So I think it’s worth playing around with it, learning how to use it. Therefore I’ll continue this project and create at least an adequate line chart, that I can use for other projects. And I’ll do this by starting with rewriting what I have done up to now to get my first data set charted.
But I think I need a strategy first.
Until next time!