Infinite timeline scrolling chart with HTML/CSS/JS

Awhile ago, my fellow evangelist Christophe Coenraets created a cool desktop application (and later a mobile version) as a concept of how an application used by sales people could look in the 21st century. Part of that app was a nice chart that allowed you to quickly see all the projects while swiping back and forth on the timeline.

Now, that little piece was developed using the Flex framework. In a moment of complete boredom I decided to recreate the chart part using HTML, CSS, and JavaScript. And to make things more interesting, I wanted the same code to run on desktop and mobile devices (tablets and smartphones).

Here is a screenshot of Christophe’s original app:

And here is a screenshot of my HTML version:

Features

This chart could be used to visualize sales leads or accounts. Imagine a data structure like this:


[
  {date, probability, revenue, account, project-name},
  {date, probability, revenue, account, project-name},
  ...
]

How would you represent such a structure in a meaningful way? This chart uses three pieces of information to represent the data:

  • The date of the project. Obviously this is used to position each circle on the horizontal axis.
  • The probability of the project. This is used to position the circles on the vertical axis.
  • The revenue of the project. The bigger the revenue, the bigger the circle diameter.

You can scroll the chart on the horizontal axis, the timeline axis (the time is represented as months). By default you see a little bit more than 3 months. If you want to change this, just grab one of the vertical lines and drag to left (if you want to have more months) or to the right (if you want to have fewer months).

If you touch (or move your mouse over) one of the circles you will see a tooltip that displays additional info.

That’s it. Although it may look simple, I think that it does a good job at representing the data. Add to this the interactivity layer which I think that feels quite natural, especially on a device like iPad, and you have a winner.

How it works

If you want to build a chart using web standard technologies then most likely you will be using one or a combination of these: the canvas element, SVG, and other HTML elements (could be divs or images).

If you want to have some sort of interactivity, like maybe being able to “click” on something drawn on the chart then SVG is the best choice (you can draw shapes while being able to listen for events for each shape) and canvas is the worst option (you work with pixels so there is no built-in way of telling on what shape a user clicked).

However if you want to run the code on desktop and mobile browsers, then SVG is not the best option anymore. On some mobile platforms SVG is not supported at all and on most mobile platforms it is not hardware accelerated (I will come back to the importance of being hardware accelerated).

So SVG was gone which left me with Canvas and HTML elements. And actually this is how I built the chart: a canvas element for drawing the chart axes and labels and on top of it a number of divs for rendering the chart data (circles) and supporting the interactive part of the chart. Here is a diagram of how the part work together:

Here are some explanations about these layers:

  1. The first element is a canvas. This is used for drawing the axes and labels plus the vertical and horizontal lines
  2. On top of the canvas element there is a div (absolute positioning) that acts like a mask (overflow is set to hidden). This div covers just the plotting area of the canvas (so the labels drawn on the canvas are outside of the div). The handlers for dragging the timeline are registered on this element.
  3. Next, there are number of divs added to the previous div. These divs are kept in synch with the vertical lines drawn on the canvas (position and height). These divs can be dragged in order to change the timeline “density” (to display more or fewer months).
  4. Finally, the data are drawn using a bunch of div elements (with border-radius to make them look like a circle) and all these elements are added to the mask div.

The algorithm that draws the data, scrolls the chart, and changes the timeline “density” is pretty simple. Basically, when the chart is drawn for the first time I save the first Date (the one at the chart origin) that is part of the current view (I will call this the origin date) and the number of pixels for one day. From there on, every time the chart is scrolled all I have to do is to adjust the origin date based on the amount of scrolling (number of pixels), scrolling direction (adds/subtracts days), and the number of pixels for a day.

When the chart “density” is changed, I recalculate the number of pixels taken by a day and the new density (the number of months to be displayed in the same time).

Extending the chart

If you want to use this chart, chances are that you would want a different way to represent the data. The good news is that I built the chart with extensibility in mind (though I have to say that this part could be improved).

There are a number of methods you will need to touch. The most important one is plotData(). This method is responsible of drawing the data (the circles/divs). This is its signature:


/**
 * Draws the data.
 * @param div to be used as the parent
 * for drawing the data
 * @param currentDateInterval current date
 * interval {sDate: Date, eDate: Date, xDates: Array}
 * sDate and eDate represent the first and last date
 * part of the current chart
 * @param pxPerDay pixels for one day
 * @param dataProvider {array} the data provider
 */

function plotData (chartDiv,
         currentDateInterval,
         pxPerDay,
         dataProvider) {

...

}

Those four arguments are all you need to draw the data. If you want to overwrite this method, you can pass to the constructor your custom plotData() function as the third argument.

plotData() is the main area where you want to first take a look if you want to customize the chart. Next in line are the method responsible for displaying the tooltip (obviously your data will be different so the tooltip content will differ too) and the algorithm for drawing the vertical axis (the example uses percentages for the Y axis; again your need may be different).

Lessons learned

The first lesson I learned (not that I didn’t know :D) is that even the most performant mobile devices are miles away of desktops. Then, mobile browsers are not equal to desktop browsers (some versions ). There are differences in capabilities. For example, canvas is not hardware accelerated on all Android phones while the desktop WebKit based browsers have support for hardware acceleration for canvas.

Because of all this you have to test your code as quick as possible on mobile devices. This way you identify the performance issues sooner than later.

Debugging your code on mobile devices can be frustrating. Most of the development time for this example was spent on desktop. The last feature I added was the ability to change the timeline density. On the desktop it worked perfectly. On my iPad and Nexus phone it didn’t. The console provided by Safari mobile console and weinre were useless. I found the problem in less than a minute thanks to my Nexus phone and Chrome for Android – I used the USB debugging feature. Needless to say that once I got the code running on my Nexus, it was running on my iPad too.

I think that having access just to JavaScript Console on a mobile device is not enough. If you do serious web development for mobile then you should get yourself a device that gives you JavaScript debugging and the rest of the web inspector tools. As far as I know, right now only Chrome for Android and PlayBook offer this. Get yourself one of these and you will be happier :).

Another lesson that I learned the hard way is working correctly with touch events. If your application relies on the touchstart, touchmove, touchend events then you have to fire an event.preventDefault() in the touchstart and touchmove event handlers. Failing to do this you’ll be waiting the whole day for the touchend event to be triggered – it will never happen. Another benefit is that you don’t get the element selected when you move it around (I mean the element is not highlighted or whatever you want to call this default browser behavior).

As I was testing the scrolling performance on iPad and Nexus I was initially disappointed by the performance. I mean I was only able to scroll couple of pixels. Once I added the preventDefault() calls everything worked fine.

When you have code that runs on desktop and devices you have to be careful with the size of the UI elements. Make them too small and it will be hard to be used. This is exactly what happened to me. The width of the vertical lines for adjusting the timeline density where set to 4 pixels. On a desktop it was more than enough to be able to click and drag. On my phone it was almost impossible. Making them 22 pixels wide solved the problem.

The last thing I did was to preserve resources when it made sense to reuse them. As you know it is pretty expensive to add and remove HTML elements (in terms of time, which translates in frames per second, which translates in UI responsiveness). In this application, I cache the divs used for plotting the data or vertical lines when I don’t need them. This means that even if there are 1,000 circles the maximum number of divs is equal to the maximum number of circles at one point. As the chart is scrolled to the left or to the right, the circles that are getting out of the view go back into the cache and the ones that are coming into the view are rendered reusing divs from the cache (if the cache is empty a new div is created).

Getting the code

All the code is hosted on GitHub. You can take the code, fork it, and do whatever you want from here. If all you want is to take a quick drive test, visit this link.

Conclusions

Building interactive experiences that run well across devices using web standard technologies is possible today. Or to be more precise, if you want to write once and run everywhere without dropping CSS/HTML capabilities then you will have to target modern browsers.

Most of the times you will find yourself using a mix of techniques when building code that runs on desktop and mobile devices, techniques that represent the common denominator between the mobile and desktop browser capabilities.

If you take my code and build something new with it, I’d be happy to see the result :)

Leave a Reply

Your email address will not be published. Required fields are marked *