Animating HTML "Ranking" Tables with jQuery

This post details a jQuery plugin which animates updates to html tables representing rankings. The code can be downloaded here: rankingTableUpdate.js. If you're just interested in how to use the plugin your web page skip ahead to the usage section.

The example below shows the plugin at work, looking at the final few race results of the 2010 F1 season:

Standings after Korea
1 Fernando Alonso 231
2 Mark Webber 220
3 Lewis Hamilton 210
4 Sebastian Vettel 206
5 Jenson Button 189

I starting looking into this because I love to follow sport, so spend a reasonable about of time on the web looking at league tables, race results, championship standing and all sorts of performance related tables. One thing that I often want to do is to see how a team or person is progressing; how many places did QPR go up the league on their last win, who's now beating my PB on the Park run, how many places in the F1 championship did Sebastian Vettel up to win the title? Normally the only way to check these things is to look at the table before and after the event and try and pick out what has changed, but by animating this change we can provide a visual cue so it's possible to quickly assertain what happened between the states.

Ranking TablesBefore I go into any details about the plugin, it's useful to think a bit more about these kinds of tables..

All these tables have the same basic structure; they are all represented as html tables, each row represents a single entity (be it an individual, team, company etc.) and each column represents a single property for all the entities like the number of points they have, their names or their nationality. The crucial difference between these kind of tables and just lists of items is that an entities position in the table gives their ranking, with the first row being the highest performer and the last row beign the worst performer.

The columns of the table broadly speaking can be split into 2 categories - "updating" columns and "constant" columns, namely those which can change for a single row from one state of the table to the next and those which can't. For example, if you're representing the Premier League standing as a table, constant columns might be "team name" or the "stadium", whereas updating columns might be "points" or "goals scored". There are typically two special columns, one constant column which acts as an identifier for the entity (typically the entities name) and one updating column which holds the ranking of the entity, which doesn't really tell you any more information, since you can figure this out from which row it's in.

Between one state of the table and the next, there are five possible things which an entity can do:

  • Move Up
  • Move Down
  • Drop Off the Table
  • Appear for the First Time
  • Stay in the Same Place

It makes sense to think of the top of the table always being position one, the second as position two etc., rather than thinking of the bottom of the table as the last position and work up. This is significant since the table may vary in size between transitions, like in the following results table which shows the best mile times over 2009 and 2010 in the ScottLogic running club:

Pos Name Best Mile
1 Andrew K 5.13
2 Balazs S 5.41
3 Mark R 5.58
4 Colin E 6.26
5 Gergely O 6.39
6 Johnny C 6.59
7 Mike P 7.00
8 Chris P 7.55

The jQuery PluginWhenever you're developing a jQuery Plugin or any kind of Javascript library it's difficult to pitch how extendable/customisable to make it; obviously it's nice to allow the user to adapt how the animation works but allowing too many options can make to hard to use, or make it so that the user has to pretty much write it themselves in order to use it! I've tried to strike a balance, in that I've made it easy to change a few properties: the colouring it uses, how long the animation takes and far the rows move to the side to the side, but fixed the basic structure of the animation is fixed. This way you don't need to know how to use the underlying animation library in order to use it. It took quite a lot of playing around to figure out what looks good and makes sense in terms of the animation, eventually I settled on the using three phases which operate as follows:

  • Phase 1: if the table needs to get taller expand it to the required height. Move rows to the left/right, fade out all values in updating columns except if it's the position column and the row doesn't move. Fade in the background colour to indicate the change. When complete, switch the (now hidden) updating values for their new equivilents from the new table.
  • Phase 2: move the rows up/down.
  • Phase 3: if the table needs to be made smaller, shrink it to the correct height. Move visible rows left/right so that they are back inline with the table. Fade back in the values in the updating columns and fade the background colour of the rows to their colour in the new table. When finished clean up and make the switch the old table for the new one (hopefully the user shouldn't notice this, as it shouldn't make no visible change).

I guess if it's not customisable enough for you then rewrite it yourself! If you're not up for that, contact me and if I've got the time and sufficient motivation I'll take a look.

How to use itFirst a disclaimer! Please bare in mind that the code supplied should be considered as experimental - it's not a finished product, so don't expect it to work in all circumstances on all browsers!

The plugin assumes you're using jQuery version 1.4.3 and also makes use of a lesser known Javascript animation library by Bernie Sumption available here: animator.js, so remember to import both these before the rankingTableUpdate.js file.

The plugin adds a single function to the jQuery.fn namespace called "rankingTableUpdate". This function operates on the first element of the jQuery object on which it is run, which must be an HTML table, and takes 2 parameters; the new table to animate the old one to (which can be an HTML element or a jQuery object in which the first element is an HTML table) and an optional "options" object which describes how the animation should proceed. A typical invocation, using the standard style setting (as seen in the above two examples) would be something like:

$(oldTableElement).rankingTableUpdate(newTableElement);

Once the animation is finished the DOM is left in the same state as if you had called the jQuery code: $(oldTableElement).replaceWith(newTableElement); so it's safe to apply it again and again later on, to update to subsequent table states without worrying about changes that have been made before. It also executes the "onComplete" callback function specified in the configuration object passed to it.

The rankingTableUpdate function returns the jQuery object on which it's called to allow chaining, but be aware that the object returned is the original one unchanged; i.e. it contains the old and not the new table element.

To use the plugin, the table that's going to be updated needs to define a header in which the first row (which can be hidden or not) has the same number of cells as the table body and each cell should have one of the following class names:

  • anim:position if it's the column which has the row numbers in it, there should only be at most one cell with this class name. (the inclusion of such a column is optional).
  • anim:id if the column is to be used to uniquely identify the row, there should be exactly one of these cells. It's possible to configure the animation so that it uses a given function to extract a row ids from their cells in this column; so if you need to work with id's split over multiple columns, you can just alter function appropriately, see the configuration section.
  • anim:constant if the column will not change value and should remain on the screen when the animation is taking place.
  • anim:update if the column should fade away and come back with the new value.

If you don't supply any of these class names on the cells then the column is assumed to be an updating one.

For example, the following table fine:

<table>
    <thead>
        <tr>
            <th class="anim:position">Pos</th>
            <th class="anim:id">Name</th>
            <th class="anim:update">Value</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>1</td>
            <td>A</td>
            <td>Some number</td>
        </tr>
        <tr>
            <td>2</td>
            <td>B</td>
            <td>Some number</td>
        </tr>
    </tbody>
</table>

Configuring the animation using the options parameterIf you don't supply an options parameter, it gets set to the following:

var defaultOptions = {
    onComplete: function(){
        /*do nothing*/
    }, //callBack to execute when the animation completes
    duration: [1000, 0, 500, 0, 500], //ms to do each phase and the delay between them
    extractIdFromCell: function(td){
        return $.trim($(td).html());
    }, //function to use to extract the id value from a cell in the id column
    animationSettings: {
        up: {
            left: -25, // Move left
            backgroundColor: '#004400' // Dullish green
        },
        down: {
            left: 25, // Move right
            backgroundColor: '#550000' // Dullish red
        },
        fresh: {
            left: 0, //Stay put in first stage.
            backgroundColor: '#FFFF33' // Yellow
        },
        drop: {
            left: 0, //Stay put in first stage.
            backgroundColor: '#550055' // Purple
        }
    }
};

Hopefully these properties are pretty much self-documenting, to override any default value you just need to supply the new value for the required property, if you don't supply anything the default is used. To make it a bit easier you can supply a single value for the duration which makes all phases take a third of the specified time and sets the wait between the phases to 0 milliseconds, e.g. duration : 3000 becomes duration: [1000, 0, 1000, 0, 1000].

The only slight oddity about the duration setting is that it's ignored in the case that there are no updating columns, other than a position column, and the rows don't need to move (basically when the animation won't do anything!). In this case, the tables will be switched as normal and the onComplete callback run after a short wait (100ms). This short wait is there so it's easier to work with; you can always assume that the animation won't finish instantly and hog the thread.

The extractIdFromCell function in the options object is one thing that usually doesn't need to be set, however sometimes you need to work with tables that don't have one natural "id" column but are uniquely identifiable using multiple columns. The easiest way to make the plugin work in this case is to define any one of the constant columns as the id column, put a hidden span element inside it which uniquely identifies the row, then set the extractIdFromCell function to extract the value from this span. For example, the cell of you're selected id columns could look like this:

<td>
    <span style="display:none">unique-value<span>
    constant value
</td>

and you could include the following function in the the options parameter:

extractIdFromCell: function(td){
    return $(td).find('span')[0].html();
}

Alternatively, since the function is passed the table element of the id column you could could leave the table as is and make set the extractIdFromCell function to build the id by concatenating the values of all the cells making up the id. For example, if the first two columns make up the id and you've set the first column (index 0) to be the id one, you could do:

extractIdFromCell: function(td){
    return $(td).html() + $(td.parentNode.cells[1]).html();
}

As an example, the invocation of the plugin's function in the following financial-style table looks like this:

$(oldTableElement).rankingTableUpdate(newTableElement, {
    duration: [1000,200,700,200,1000],
    onComplete: function(){
        updating = false;
        //change update status if required...
    },
    animationSettings: {
        up: {
            left: 0,
            backgroundColor: '#CCFFCC'
        },
        down: {
            left: 0,
            backgroundColor: '#FFCCCC' //the same red as 'down'
        },
        fresh: {
            left: 0,
            backgroundColor: '#CCFFCC' //the same green as 'up'
        },
        drop: {
            left: 0,
            backgroundColor: '#FFCCCC'
        }
    }
});

Line 4 in the above code, sets an updating flag to false to show that the animation is finished. This is fairly typical as it's nearly always the case that you want to prevent the update being called whilst it's currently animating.

The following finance-inspired example shows a portfolio of fake shares, which (when you click on the red link!) updates every 10 seconds with new "live" prices and occasional changes to the number of positions as they are closed and opened. The example uses the plugin when updating and also when sorting the table:

Although the plugin is not specifically designed to animate sorting, it can be accomplished by setting all columns to be constant, cloning the table, sorting a clone then replacing the table with the sorted clone. This obviously isn't the most efficient way to do it but works ok for small examples! Note: this only works correctly if you've not used the id or name html attributes.

StylingThere are currently quite a few restrictions on the css styling you should use for your tables if you want the animation to look good. A (perhaps non-complete!) list is:

  1. Don't use top/bottom table borders or any cell borders. Generally it looks better with no left/right borders at all if you're going to move the rows to the left and right. If you need borders between the cells, currently you need to hack it by doing something like putting "cells between the cells" with the background colour set to the border and have a width the size of the border. (Although I don't endorse such things!)
  2. Don't use images as a background for the table.
  3. If you're going to use borders on the rows, use border-bottom and make sure that the header row has one as well. For example, if you look at the formula 1 table, the header row is hidden by setting it's height to 0, but it still has a border so that there is a line at the top of the table.
  4. The cell, row and table classes should remain the same between table states. If you need to change the style of a cell, simply wraps it's content's in a span and add the new style/classname to that instead.

    e.g. A typical use-case is that you want a value to be red/green depending on whether it went up or down, this can be accomplished by supplying the new cell as:

    <td><span style="color: green;">255</span></td>

    rather than:

    <td style="color:green">255</td>
  5. As with any updating table, make sure that the width of the columns doesn't change.
  6. All rows in the tbody must have the same height and have the same number of cells.
  7. Ensure that the contents of each table cell won't change style when wrapped inside a div
  8. Make sure the table header has the same height in both tables.
  9. Don't use named colours outside the standard set of 16 and don't use the Chrome style "rbga" colours, you can use any hex or "rgb" colours though.
  10. Don't use table footers; the way it works just won't allow for it (at the moment at least!), instead you could use the "standard hack" approach for doing this - add a div after the table that looks like a table footer, as used in all the examples on this page.
  11. If you want to rows to move left and right (as is standard), then you need to have enough space on the right hand side of the containing div to hold space for both the left and right movement (with the default settings this is 50px). See "behind the magic!" section for details on why this is (currently!) the case.

Browser Compatibility IssuesThe examples all work as expected in: Chrome, Safari, IE8 and Firefox.

In Opera they work fine except that the bottom border of the last row is chopped off. In IE6 they work as normal, except that the values do no fade in and out, they just get switched over.

As yet, I've not tested any other browsers, so if it's pot luck whether the plugin will work or not!

Behind the MagicIf you're like me when it comes to films, and don't want to spoil the illusion of "the plugin does some magic", you may not want to read this section! As with most things written in Javascript, what you want to accomplish is typically relatively straight forward (or you'd try and avoid using it!) but it's a case of the devil in the details - actually getting it to do exactly as you want is rather difficult! Due to this I'll just give an overview of how it works and if you're really interested in the gritty details, then I refer you to the source code.

The basic idea behind the animation is as follows: Each cell's content is wrapped in the div and it's the attributes of this wrapping div that are altered to give the impression of animation. The wrapping div is given relative positioning and moved around by changing it's left and top style attributes, it's backgroundColor is also altered to fit the animation. The fade in and out of the updating values is done by wrapping the content of the wrapping div once again in new div which has it's opacity (or filter:alpha value if IE) affected by the animation.

e.g. if the original cell in an updating column looks is defined as:

<td>value</td>

Then during the animation it could become something like:

<td>
    <div style="position: relative; top: -15px; left: 12px; background-color: #003300">
        <div style="opacity: 0.5">value</div>
    </div>
</td>

To animate the changing height of the table, the table itself is wrapped in a div which has it's height attribute animated and it's overflow-y property set to hidden. If you've never tried this before you might be surprised (as I was!) that setting overflow-y to anything other than default on a DOM element prevents elements inside it extending beyond the left and right hand edge of the containing element, even with relative positioning! For more information see this post on the overflow property

This means that you if any row is supposed to move to the left then, even if you remove the scroll-bar on the x-axis, you won't be able to see it. To fix this problem the hack is to:

  1. Set this new table-wrapping div's position to "relative", and move it to the left the maximum number of pixels that a row will extend the left hand side of the table.
  2. Move the table the same number of pixels to the right so it stays in the same place on the screen.

The downside of this is that you need to make make sure that you've got enough space to the right of the div containing the table initially to fit the left and right movement of the rows. There are plenty of possible fixes, but since it's not a massive issue I thought I'd leave it as is.

The resizing of the table is pretty straight forward when it needs to shrink, but when it needs to get bigger it's a little more complicationed. Since the new table and the old one should have all rows the same height, when the table needs to increase in size, it's because there are more fresh rows than dropped ones. When this is the case, to make it look good, the fresh rows need to come up from the bottom of the table, after its increased in height. To move them up, the fresh rows need to be added to the table, then moved, just the other rows, however to make this look right, you need to put in some enough "dummy" rows before adding the fresh ones, so when the table gets bigger, you are just seeing the new dummy rows at the bottom and not the new fresh rows. For example, if the animation needs to add a new row and isn't dropping any, then the bottom of the tbody definition will go from:

    ...
    <tr><td>This is the last row in the original table</td></tr>
</tbody>

to something like:

    ...
    <tr><td>This is the last row in the original table</td></tr>
    <tr><td style="color: tableBGColor; background-color: tableBGColor;">Dummy Row</td></tr>
    <tr><td>The fresh row to add</td></tr>
</tbody>

Then the animation expands the height of the table wrapping div so that it shows the dummy row, but not the fresh one. Note that some text is added to the dummy row which is the same colour as the background so that the row is not artificially small (which it could be with no content at all).

Apart from that, the rest of the code deals with figuring out what has happened to the rows (did they go up, down, etc.) and dealing with an array of annoying details, like figuring out what colour to set things or what distance to move things. I should probably point out that one great (and almost remarkable) thing about this code, is that it doesn't need to mess with the z-index. The rows all move over/under each other like you'd (or at least I did!) expect. I was prepared for some horrible fixes around this, but, as chance would have it, the top down way in which the DOM elements are processed and added to the page causes the effect I wanted.

If you like it, please feel free to use it and let me know what you think.

MORE BY MARK

blog comments powered by Disqus