Creating an SVG based Charting library from scratch
Building what we needed, reaching over 450 points on HackerNews
We come across plenty of data scenarios for our ERPNext users. It all began when we needed them to be able to track sales. It would be nice to fit in a tiny sales history graph on the user company master of our product, so we started looking for options. At the time, we were already using c3.js for our report pages. However, it didn’t blend very well with our classic design:
As it were, there weren’t many simplistic JS libraries for a plain graph. Style patterns are not impossible to override in an existing project, but this cannot make up for the inherent choices in structure and behaviour. So instead of configuring a large library to fit our needs, we took the much straightforward way of designing our own. After all, our wants were simple; a way to translate value pairs into relative shapes or positions.
I found the inspiration to come up with something close to home, right where our projects live. GitHub has it own suite of consistent graphs suited to whatever works best for its use cases. Despite the assortment, something that immediately strikes you about all of them is how bare the data points look:
Less lines and subtle cues, markers seemingly secondary and only there to assist; all reducing the clutter. Github knows its audience is at the page for the trends, and they (the users) don’t expect too many guides. It knows that on the web, rather than trying to find the exact value by visually aligning points with a verbose axis, they would prefer to hover to know the exact data when needed with tooltips.
All that resonated just too well with us, and was our first cue to begin.
We decided to focus on the essentials needed to get the ball rolling. I began throwing together some SVG rectangles and lines to come up with a bar graph.
At this point, the only interesting thing it did was finding the Y intervals from the value range and figuring out a good bar width. The API needed to get this one going was simple:
In the meantime for our use case, we realised that line graphs were much better at showing trends, so I added it as an alternative mode. Now, aside from the data points, there was a single entity, the path, for the entire graph.
Updating values on the chart or any interaction was an opportunity to animate. Animations, besides being cool, are one of those things that inherently need you to write modular code. So in case they weren’t already, all the individual pieces needed to be taken apart:
“Oh you want to move that thing around? Too bad you won’t be able to do it with all those strings attached; you’d have to drag it out as a thing unto itself first.”
which is great for UI development.
Data points were easier to do it, but I knew that I’d later on have to gracefully animate axis lines as well (while making them appear and disappear depending on whether there were more or less) if there were changes in the range.
We had use cases of pies as well, but I could simply just go with something much simpler and space-efficient.
The only thing that still needed d3.js was our annual heatmap, and cal-heatmap, the library we used for it, was an all in one package. As this one was quite static, and I decided to keep discrete and continuous options.
That led us to obviate three of our dependencies, reducing the codebase by over 330kb. And now we had some decent, responsive stuff and started using them in ERPNext.
The Path to Zero-dependency
Another round of focussed purging got those removed, and the code now ran free from any external dependencies. I could fire it up in a browser and everything would just work.
Well, except the animations.
CSS3 is convenient for translations, but actual shape morphing (like for the paths) is trickier for a conventional approach, and there aren’t many good alternatives. I discovered SMIL as the sole contender with decent support, that could morph paths with equal points, with finer control over easing with beziers. An absolute bare-bones approach would be to figure out intermediate attribute values for the best route from one to the next, mapped with respect to the easing function and rendered with time; an interesting future project.
Getting it all out the door
While they were used in ERPNext with real data, I had the chance to break many of the initial assumptions with edge cases (too many points, negative values and such) and program for more generic inputs. Once they seemed stable enough, I went on to set them up as a separate repository and began packaging.
I realized that designing the landing page would actually take quite some time, and decided to actually do it before completing the project. The page would demonstate the use of the library, so if possible, I wanted real use cases to drive what the library could do, rather than the page to be driven by what the library had. I went ahead and placed all the buttons I’d like to see, whether or not their function was possible at the time. Thinking from the point of view of an onlooker made me radically rethink the features.
With those in place, I went ahead and programmed in the functions that were missing. Like physical laws, every new step revealed a bit more of the underlying structure and gave a chance for cleaning up. For instance, for adding and removing values, updating values had to be upgraded to take in a different number of points than the current set. I also tried to let the user have more control over the rendering, but in a way that keeps the design consistent.
Finally all seemed well, and we went ahead and released them out into the wild. We have been fortunate enough to receive an amazing response from the open source community over a short span of time, and got to be a part of many interesting discussions. We’ve also had some awesome contributions (including a pie chart!) and ideas for more features. There’s still plenty of room for improvement, and I’ll continue working to make them better, following which we’ll look forward to release the next upgrade to features. Be sure to keep a lookout for what Frappé comes up with next!