Figure 1. Demonstration Screen Shot.Table sorted on DateReceived in descending order
Introduction
This jQuery plugin sorts HTML by the values in a selected column. It can be attached to the header row of a table so that when the user clicks on a column header, the table is sorted on that column. If the user clicks the column header again, the sort is reversed. The developer can also implement sorts based on other events and restore the table to its original order. The Plugin recognizes numeric columns and date columns and sorts accordingly.
Background
I had a Hall of Fame table in a browser based word game that I wrote. It needed a sort capability so I created this plugin.
Limitations
- It doesn't work properly with tables that use "colspan" or "rowspan".
- It only recognizes dates with numeric components and a single delimiter. The default delimiter is
'/'
but that can be changed using an optional setting. It recognizes US (mm/dd/yy][yy]), British dates (dd/mm/yy[yy]) and European dates ([yy]yy/mm/dd). It doesn't care if dates are not strictly valid - that is the job of the application that populates the table in the first place. It examines every date in a column to determine the date format. If there are only a few dates and they are ambiguous, then it may get the format wrong. if your table is small, you may want to use the optional setting to specify the date format. - If it finds a cell value in a column that doesn't look like a number or a date, it treats the complete column as a column of string values.
- It slows down when tables contain thousands of rows.
Advantages
- It is easy to add the ability to sort an HTML table to a web page.
- It recognizes dates and numbers and sorts them correctly. There is an option to explicitly specify the date format.
- The sort retains the original table order within any sort. This means it is stable.
- Tables can be returned to their original sort order.
- It doesn't impact the HTML structure of the table.
- It uses jQuery and the JavaScript Array sort method, so it will work with most browsers.
- It is almost instantaneous on tables under a thousand rows in modern browsers.
- String sorts are case sensitive, but there is an option to override that.
- No server activity is required.
Demonstration Web Pages
There are two demonstration web pages included in the download.
DemoTable.html
This page contains a table with data that looks realistic. See figure 1. The table has some simple styling to make it OK nice. It has four buttons underneath it to illustrate basic functionality of the Plugin. The first lets you restore the table to its original order. The next three let you choose the date format for the Date Received
column so you can verify the automatic date format selection algorithm. There is some code to total the Price
and Miles
columns, but that is just to populate the footer row.
BigDemoTable.html
This page contains a table with 3000 rows of data. See figure 2. It is intended for testing all the options and for performance testing. The table also has some simple styling to make it look OK. Sorting on the RANDOM
before sorting on another column effectively puts every other column in random order. There is some code to generate the EURO NUM
column for testing a possible European number format.
Figure 2. Demonstration Screen Shot. Large table sorted randomly.
Adding a reference to your web page
Assuming you place the tablesort.js
JavaScript file in a subfolder named js
, you would add the following line to your web page to reference the TableSort jQuery Plugin.
<script src="js/tablesort.js" type="text/javascript"></script>
Using the code
The easiest way to use the Plugin is to attach a click event to every header cell in your HTML table(s) to invoke the plugin. If we use a selector like $('#DemoTable tr:first').children()
, we don't have to worry about whether the header row uses <td>
or <th>
tags. The code sample below assumes the HTML table already exists when the page has finished loading. If you are loading the table using an AJAX call, or building the table dynamically, then you would attach the column header events after the table is created.
<script type="text/javascript">
<!--
$(document).ready(function () {
$('#DemoTable tr:first').children().click(function (e) {
$('#DemoTable').sortByColumn($(this).text());
});
});
If you want to return the table to its original state, just invoke the Plugin without specifying a column header.
$('#DemoTable').sortByColumn();
Options
Instead of passing in the column header text, you can pass in an object literal that specifies an extended set of options. You only need to supply the ones you want. The options are:
columnText
dateFormat
dateDelimiter
decimalDelimiter
caseSensitive
ascendingClass
descendingClass
The columnText
option must match the text in the first cell of a header column.
$('#DemoTable').sortByColumn({ columnText: $(this).text()});
The dateFormat
option can be any value but only 'US'
, 'UK'
and 'EU'
are recognized. The default is ''
and the Plugin will determine which date format to use based on the contents of the column.
$('#DemoTable').sortByColumn({ columnText: $(this).text(), dateFormat: 'UK'});
The dateDelimiter
option can be set to a single character used to delimit date components. The default is '/'
.
$('#DemoTable').sortByColumn({ columnText: $(this).text(), dateDelimiter: '-'});
The decimalDelimiter
option can be set to a single character used to delimit the fractional part of a decimal number. The default is '.'
.
$('#DemoTable').sortByColumn({ columnText: $(this).text(), decimalDelimiter: ','});
The caseSensitive
option can be set to true
or false
. The default is true
.
$('#DemoTable').sortByColumn({ columnText: $(this).text(), caseSensitive: false});
The ascendingClass
option is the name of the class that will be added to the header cell of the current sort column if it is in ascending order. The descendingClass
option is the name of the class that will be added to the header cell of the current sort column if it is in descending order. In both cases, the default is ''
.
$('#DemoTable').sortByColumn({ columnText: $(this).text(), ascendingClass: 'redbordertop', descendingClass: 'redborderbottom'});
How it works
The Plugin extracts the data in the table into an array of objects. It invokes the JavaScript Array.Sort
method to sort the array. Then it inserts the data from the sorted array of objects back into the table.
This is in contrast to the last implementation I worked with, which exchanged DOM elements and used a hard-coded sort algorithm. It started with a bubble-sort but I switched it to a comb sort to improve performance.
The Plugin identifies the column clicked by matching the jQuery text()
of the column with the string value passed to the Plugin. It strips whitespace before making the comparison. It is recommended that you use the jQuery text
method to specify the sort column.
It uses the jQuery data
method with a key of _dir_
to store the current sorted state of a column. The values can be a
for ascending, d
for descending, or undefined
. If your jQuery selector has returned column header cells, then you could determine the sort status of each column by checking $(this).data('_dir_')
for 'a'
, 'd'
or ''
.
It uses the following object to store the data from each row.
var RowData = function () {
this.Seq = 0;
this.Key = '';
this.KeyType = 0;
this.Cells = new Array();
}
The sort is done using Key
, rather than the corresponding element in the Cells
array. This enables us to sort on the actual numeric value (numbers) or the upshifted string (case insensitive string sort).
var rownum = 0;
this.find('tr').not(':first').each(function () {
if ($(this).data('_seq_')) {
return false;
}
$(this).data('_seq_', rownum);
rownum++;
});
The data in the table is transferred to an array of RowData
objects. The Seq
property tracks the original sequence of the table. It does this by saving the original sequence of each row using the jQuery data
method with a key of _seq_
. The sort comparison method resolves identical keys by looking at the Seq
property. This ensures stability with respect to the original sort order
. The sort uses the Key
and KeyType
in its comparison method. Here is the code that does the actual sort. If the columnText
is not supplied, the sort returns the table to its original order.
if (settings.columnText) {
tableData.sort(function (a, b) {
switch (a.KeyType) {
case 0:
if (a.Key > b.Key) {
return sortAsc ? 1 : -1;
}
if (a.Key < b.Key) {
return sortAsc ? -1 : 1;
}
break;
case 1:
if (parseFloat(a.Key) > parseFloat(b.Key)) {
return sortAsc ? 1 : -1;
}
if (parseFloat(a.Key) < parseFloat(b.Key)) {
return sortAsc ? -1 : 1;
}
break;
default:
var res = DateCompare(a.KeyType, a.Key, b.Key);
if (res == 1) {
return sortAsc ? 1 : -1;
}
if (res == -1) {
return sortAsc ? -1 : 1;
}
}
if (a.Seq > b.Seq) {
return 1;
}
return -1;
});
}
else {
tableData.sort(function (a, b) {
if (a.Seq > b.Seq) {
return 1;
}
return -1;
});
}
Note that the Key
needs to match the KeyType
. While a numeric field may contain "$12,123.00", the key
is reduced to its actual numeric value of "12123" using a regular expression. It does this before the sort, instead of in the comparison routine, to reduce the regular expression performance hit.
Once the sort is done, the data is copied from the array of RowData
objects back to the HTML table. It skips the first row and any footer row.
rowX = -1;
this.find('tr').not(':first').each(function () {
if (!$(this).parent().is('tfoot')) {
rowX++;
var rowData = tableData[rowX];
$(this).data('_seq_', rowData.Seq);
colX = -1;
$(this).children().each(function () {
colX++;
$(this).text(rowData.Cells[colX]);
});
}
});
Determining Date Formats
The basic idea is that years can have any value, days can only be in the range 1-31 and months can only be in the range 1-12. The Plugin scans any column containing three integers separated by the dateDelimiter
. It computes how many valid days and months it finds and determines the date format from the results. In a sufficiently large sample, you will get one position where no field exceeds 12 and another where no field exceeds 31. That will tell you which fields represents the day, month and year. It may not always work, since dates in one format are also valid in another format. But the odds are good! If you know the date format, you can set it explicitly using the dateFormat
option.
Dealing with numbers
The way numbers are written varies from country to country. In English language countries, the period is used as the decimal delimiter and the comma as the digit group separator. In European countries, the comma may be used as the decimal delimiter and a space or period as the digit group separator. To ensure correct sorting, you can set the decimalDelimiter
as an option. The digit group separator is stripped out by a regular expression that only allows digits, a minus sign and the current decimal delimiter. (var numExp = new RegExp('[^0-9' + settings.decimalDelimiter + '-]+', 'g');
).
This is the option you would use to set the decimal delimiter to a comma.
$('#DemoTable').sortByColumn({ columnText: $(this).text(), decimalDelimiter: ','});
Figure 3. "Date Received" converted to 'EU', Miles sorted using a comma decimal delimiter.
Why no glyphs?
Some table sorting systems use glyphs in the column header to indicate the status of the sort. This may be problematic if there is no space for the glyph or adding one changes the width of the column. This Plugin leaves the method for indicating sort direction up to the developer by letting them create CSS classes that can be applied to header cells to indicate if they are currently sorted in ascending or descending order. In DemoTable.html
, the developer chose to use a top red border to indicate ascending and a bottom red border to indicate descending. That seems to work quite well. Here are the classes and the code that passes them to the Plugin.
.ascending {
border-top: solid 3px red;
}
…
.descending {
border-bottom: solid 3px red;
}
…
$('#DemoTable th').click(function (e) {
$('#DemoTable').sortByColumn({ columnText: $(this).text(),
ascendingClass: 'ascending',
descendingClass: 'descending'});
});
Performance
Performance testing was done on a Dell XPS computer running Windows 10 (64-bit) with 16GB RAM and an Intel i7-3770 processor @ 3.40 GHz. It used BigDemoTable.html
which has 3000 rows and 8 columns.
Browser | Version | Fastest Time (secs) | Slowest Time(secs) |
Google Chrome | 50.0.2661.75 m | 0.97 | 1.32 |
Mozilla Firefox | 45.0.2 | 0.75 | 0.85 |
Microsoft Edge | 25.10586.0.0 | 3.66 | 4.10 |
Microsoft IE | 11.212.10586.0 | 3.89 | 4.19 |
Firefox edges out Chrome while the Microsoft siblings are significantly slower. Note that 3000 rows is the equivalent of 50 to 100 page downs, depending on monitor resolution. Most applications would not deliver anything like that volume of data to a browser.
Creating a JQuery Plugin
It seems intimidating but it isn't that difficult. The basics are covered at this jQuery site and in this Code Project article.
Download Contents
The dowload contains the two demonstration pages and a sub folder called js
that contains the TableSort Plugin (tablesort.js
) and a minified version (tablesort.min.js
).
Points of Interest
- It is relatively easy to create a jQuery Plugin that adds functionality without much coding effort.
- Dealing with various international formats and dates makes a generalized sorting Plugin more complicated to code.
- The internal Array.Sort() method is fast. Profiling shows most of the CPU time is spent extracting data from an html table and putting back the sorted data.
History
First version for Code-Project