Filtering using the rendered text, however sorting using the original value.
Filtering using the rendered text, however sorting using the original value.

I have a table with a column displaying the date of an event. The server returns the date in the format GMT/UTC such as "2012-01-26T19:43". Therefore, I have to use I use fnRender() to display this date in a friendlier format, and also in the timezone of the user.
date.format('MMM dd, yyyy @ hh:mm') -> "Jan 26, 2012 @ 19:43"
The problem is once I use fnRender(), the sorting uses the new rendered value, which is incorrect. For instance, the month of April is sorted before January because A < J. When I set bUseRendered=false, the sorting is correct, however the filtering is incorrect. For instance, if I search for "Jan", the table returns nothing because it searches for "2012-01-26T19:43".
I wonder if a new flag could be added, such as bFilterUsingDisplayText=true so the search/filtering is always done according to what is rendered on the table. This way, if a user types something in the search box, he/she will see a filtering of the the content of the table rather than a filtering of the data sent by the server. In other words, I would like to have a mechanism to separate the sorting from the filtering.
If this is not possible, could you please tell me which line I need to add or modify to change the filtering behavior. Perhaps replacing the string 'filter' to 'display' when calling _fnGetRowData() or somewhere else could do the trick.
Thank you for your great work. I am very impressed by the ease of use of this DataTable. Keep up the good work.
date.format('MMM dd, yyyy @ hh:mm') -> "Jan 26, 2012 @ 19:43"
The problem is once I use fnRender(), the sorting uses the new rendered value, which is incorrect. For instance, the month of April is sorted before January because A < J. When I set bUseRendered=false, the sorting is correct, however the filtering is incorrect. For instance, if I search for "Jan", the table returns nothing because it searches for "2012-01-26T19:43".
I wonder if a new flag could be added, such as bFilterUsingDisplayText=true so the search/filtering is always done according to what is rendered on the table. This way, if a user types something in the search box, he/she will see a filtering of the the content of the table rather than a filtering of the data sent by the server. In other words, I would like to have a mechanism to separate the sorting from the filtering.
If this is not possible, could you please tell me which line I need to add or modify to change the filtering behavior. Perhaps replacing the string 'filter' to 'display' when calling _fnGetRowData() or somewhere else could do the trick.
Thank you for your great work. I am very impressed by the ease of use of this DataTable. Keep up the good work.
This discussion has been closed.
Replies
Great question :-). I'm actually planning on writing a blog post about this exact topic in the next day or two, however, it is actually reasonably simple to use orthogonal data for sorting, filtering and display with DataTables 1.9 (currently in beta, but the final release should be next week: http://datatables.net/download ).
So the way that this can be done is that in 1.9 mDataProp can be used as a function, and that function will tell you what type of data the table is requesting (i.e. sorting data or display data etc).
This is how you might use different data for sorting/type detection (what sorting will be applied) and for filtering/display. I've used column index 0 below, but it can be adjusted as needed, and a date rendering function called renderDate():
[code]
$(document).ready(function() {
var oTable = $('#example').dataTable( {
"aoColumnDefs": [
{
"aTargets": [ 0 ],
"mDataProp": function ( source, type, val ) {
if (type === 'set') {
source[0] = val;
// Store the computed display for speed
source.date_rendered = renderDate( val );
return;
}
else if (type === 'display' || type === 'filter') {
return source.date_rendered;
}
// 'sort' and 'type' both just use the raw data
return source[0];
}
]
} );
} );
[/code]
Now whenever DataTables wants some data it will call your mDataProp function with the request type set and you return what data you want for it to act on for that type.
mDataProp is fully documented here: http://datatables.net/docs/DataTables/1.9.beta.3/DataTable.defaults.columns.html#mDataProp .
Regards,
Allan
I am looking for a solution where I can modify the file jquery.datatables.js so it would work on all tables. I more than a dozen of pages using your tables, and some tables have a lot of columns, including dates, amounts $, percentage, and so on.
So what I would suggest in this case is simply use a function to wrap the mDataProp function that I have above. For example:
[code]
function render( column, renderType ) {
return function ( source, type, val ) {
if (type === 'set') {
source[column] = val;
// Store the computed display for speed
if ( renderType === "date" ) {
source[column+'_rendered'] = renderDate( val );
}
else if ( renderType === "percentage" ) {
source[column+'_rendered'] = renderPercent( val );
}
else {
source[column+'_rendered'] = val;
}
return;
}
else if (type === 'display' || type === 'filter') {
return source[column+'_rendered'];
}
// 'sort' and 'type' both just use the raw data
return source[column];
};
};
$(document).ready(function() {
var oTable = $('#example').dataTable( {
"aoColumns": [
{ "mDataProp": render( 0, 'date' ) },
{ "mDataProp": render( 1, 'percentage' ) }
null // regular column
]
} );
} );
[/code]
This way you can easily reuse your rendering code and still take advantage of the flexibility of mDataProp.
Regards,
Allan
This is the code I used.
[code]
function render( column, renderType ) {
return function ( source, type, val ) {
if (type === 'set') {
source[column] = val;
// Store the computed display for speed
if ( renderType === "date" ) {
source[column+'_rendered'] = FormatDateTimeAgo( val );
}
else if ( renderType === "percentage" ) {
source[column+'_rendered'] = FormatPercentage( val );
}
else {
source[column+'_rendered'] = val;
}
return;
}
else if (type === 'display' || type === 'filter') {
return source[column+'_rendered'];
}
// 'sort' and 'type' both just use the raw data
return source[column];
};
};
$(document).ready(function()
{
var oTable = $('#test').dataTable(
{
"aoColumns":
[
null, // regular column
{ "mDataProp": render( 1, 'percentage' ) },
{ "mDataProp": render( 2, 'date' ) }
],
"aaData":
[
[ "Dan", .4, "2012-01-17T13:32:00" ],
[ "Jim", .25, "2012-01-16T20:32:43" ],
[ "Smith", .5, "2012-01-27T11:22:33" ],
[ "Bart", .99, "2012-01-22T02:03:00" ]
]
});
}); // ready()
[/code]
Ideally, it would be best to not have to change anything for the columns. Almost each column has a function fnRender(), many of them containing hyperlinks, which makes it very difficult to move into mDataProp. Also, I noticed that when I call fnRender(), the original data is destroyed in the original oData. For instance, I have a customer in [0] and his identifier is at [6]. Therefore I format the cell this way.
[code]
"fnRender": function(oObj)
{
return '' + oObj.aData[0] + '';
}
[/code]
The problem is if I have another fnRender() for another column, the content of oObj.aData[0] has been replaced with the hyperlink. Is there a way to render the data for the display cell, however preserve the original data for all calls to fnRender()?
> Is there a way to render the data for the display cell, however preserve the original data for all calls to fnRender()?
Yes - you can set the bUseRendered parameter for that column to false. That will stop the data being overwritten in the data source. However! As you have seen this effects both filtering and filtering...
This problem that you are seeing with fnRender is exactly the problem that mDataProp is trying to solve by providing the ability to separate the data used for sorting, filtering, display and type detection from each other, and potentially from the original source data as well.
So while I realise it might be a little bit of a brain bender to get going with mDataProp, and a nuisance to convert from fnRender to mDataProp, I think it provides a number of real advantages and allows you to do exactly what you are looking to do.
Regards,
Allan
The only exception I can think of is numbers with thousands separator: You would want to enter the filter without the separator, or to have the separator ignored.
Thus, a bUseRendered which affects both sorting and filtering is not very useful IMO. This should be solved in a generic way based on data types.
Tobias
There are a number of other exceptions as well - telephone numbers, combining information from multiple columns into one where you would want to sort on the rendered data or transforming from one formatted data type to another (a translation perhaps). So bUseRendered certainly has a place, however it is not a complete solution as you point out yourself and as Dan also notes.
What is really needed is a method to allow complete separation between sorting, filtering and display and this is what mDataProp offers. bUseRendered is now outmoded and I think will be deprecated and ultimately removed from DataTables in favour of the more complete and flexible mDataProp method.
It would be possible (and I did consider this) having different rendering and 'use' flags for display, filtering, sorting and type detection, but I think that would ultimately be confusing and add potentially a large overhead to the initialisation code for - hence I went with the mDataProp as a function method.
Allan
My suggestion would be not storing additional data, however use different data based on the column flags (bUseRendered or bFilterUsingDisplayText) just before sorting and/or before filtering. For instance, if filtering is done using the display data, then the function building the array of strings could fetch the display data rather than the original data. This would not impact the table initialization at all, but only at the moment of filtering by adding a single "if" per cell in the table.
Using mDataProp as a function has no impact on memory over the alternative of using flags, since the data is going to be stored somewhere (be it by DataTables core you your mDataProp function - actually it doesn't need to store the data, it could just recalculate the data on each request - a tradeoff between computation and memory), and I think in terms of processing will have a good deal less overhead than the complex logic that would be needed for the number of flags needed to bring that ability up to that capability of mDataProp as it currently stands.
Allan
{ "mDataProp": DataPropDate }
where
[code]function DataPropDate(source, type, val)
{
if (type === 'display' || type == 'filter')
return FormatDate(val);
return val;
}[/code]
This way, when I format a date, I do not have to create a new function each time. The problem is the parameter val is not defined when calling mDataProp.
One solution is fnRowDataCallback() which could solve many issues at once.
http://datatables.net/forums/discussion/8356/suggestion-fnrowdatacallback-to-fetch-the-row-data
The code that you posted before looks ideal (where you have { "mDataProp": render( 1, 'percentage' ) }).
Allan
Allan
Example:
[code]function DataPropDate(source, iColumn, type, val)
{
if (type === 'set')
{
source[iColumn] = val;
return;
}
if (type === 'display' || type == 'filter')
return FormatDate(source[iColumn]);
return source[iColumn];
}[/code]
The thing is that the data might not come from aaData - nor does the code know which for the properties or array indexes in the source data that the data comes from. So I'm afraid I don't think this is possible.
> If this is not possible, could val contains the index of the column, so this parameter is available inside the function
I most certainly see the value in providing that information. At the moment, that information is not available to mDataProp (its like asking an array element what its index is, without being able to look it up!). I will look at adding the ability to pass that information through to the function in the next release as it could be exceptionally useful.
Allan
I agree the mDataProp is the best solution in the long run. Calling mDataProp with the type 'sort' is far more efficient than parsing the dates each time a date comparison must be made. If sorting becomes slow, the mDataProp may called with the type 'sort' and then cache the values, then sort them using plain string comparison.
On a side note, I have written a genealogy software containing over 300,000 lines of C++ code, and for sorting purpose, I use a callback to get a string suitable for sorting. This string is often unreadable, however it makes sorting very efficient, because the processing is done only one per table cell, rather than having a callback to compare each table cell which may require significant processing just to fetch the value, and later more processing to compare with another cell. An example is sorting an array of the children age of a parent. First, fetching the array of children is costly, then computing the age of each child to the selected parent requires again more CPU because dates may contain approximate values. Then comparing this array is another story. My sort routine is O(n log n), so if I have to fetch the values each time I need to compare dates for sorting, it would be very slow. On the other hand, using a function to return a string for comparing the age of the children makes the process very snappy, even for large databases of over 100,000 individuals.
Allan
For those having a similar problem and interested in a solution, I pasted below the full source code of my solution.
In short, I define aoColumns to the following:
[code]
"aoColumns":
[
{ "sClass": "NoNumber" },
{ "mDataProp": function(rgData, eType) { return FormatTypePercent(eType, 1 - (rgData[2] / rgData[1])); } },
{ "mDataProp": DataPropSales },
{ "mDataProp": DataPropCommissionPercent },
{ "mDataProp": DataPropCommission },
{ "mDataProp": DataPropNet },
],[/code]
The first column has a special class to not align the text to the right, as it is the case for the numbers. Since my tables contains mostly numbers, the CSS aligns all the cells to the right, and if a column display text, such as the Affiliate Name, then I use the class NoNumber.
To put some variety of implementation, the second column is an inline implementation of mDataProp displaying the profit %.
The third column calls the function DataPropSales():
[code]function DataPropSales(rgData, eType, val)
{
return FormatTypeAmount(eType, rgData[1]);
}
function FormatTypeAmount(eType, n)
{
if (eType === 'display')
return '$' + n;
if (eType == 'filter')
return n + ' $' + n;
return n;
}[/code]
Here is the full code. Notice, the code won't compile, as it uses functions from my main .js file, however you should get the idea how to use mDataProp.
[code]
Affiliate Name
Profits %
Sales
Commission %
Commission
Net
Total
function DataPropSales(rgData, eType, val)
{
return FormatTypeAmount(eType, rgData[1]);
}
function DataPropCommissionPercent(rgData, eType, val)
{
return FormatTypePercent(eType, rgData[2] / rgData[1]);
}
function DataPropProfitsPercent(rgData, eType, val)
{
return FormatTypePercent(eType, 1 - (rgData[2] / rgData[1]));
}
function DataPropCommission(rgData, eType, val)
{
return FormatTypeAmount(eType, rgData[2]);
}
function DataPropNet(rgData, eType, val)
{
return FormatTypeAmount(eType, rgData[1] - rgData[2]);
}
$(document).ready(function()
{
g_oTable = $('#test').dataTable(
{
"bJQueryUI": true,
"sScrollY": "150px",
"bPaginate": false,
"bScrollCollapse": true,
"fnFooterCallback": function(oRow, aaData, iStart, iEnd, aiDisplay)
{
var nTotalSales = 0;
var nTotalCommissions = 0;
for (var iRow = aiDisplay.length - 1; iRow >= 0; iRow--)
{
var rgData = aaData[aiDisplay[iRow]];
nTotalSales += rgData[1];
nTotalCommissions += rgData[2];
}
// Modify the footer row to display the sums and percentage
var nPercentCommission = nTotalCommissions / nTotalSales;
var rgCells = oRow.getElementsByTagName('th');
rgCells[1].innerHTML = FormatPercentage(1 - nPercentCommission);
rgCells[2].innerHTML = FormatAmount(nTotalSales);
rgCells[3].innerHTML = FormatPercentage(nPercentCommission);
rgCells[4].innerHTML = FormatAmount(nTotalCommissions);
rgCells[5].innerHTML = FormatAmount(nTotalSales - nTotalCommissions);
},
"aoColumns":
[
{ "sClass": "NoNumber" },
{ "mDataProp": function(rgData, eType) { return FormatTypePercent(eType, 1 - (rgData[2] / rgData[1])); } },
{ "mDataProp": DataPropSales },
{ "mDataProp": DataPropCommissionPercent },
{ "mDataProp": DataPropCommission },
{ "mDataProp": DataPropNet },
],
"aaData":
[
// Affiliate Name, Sales, Commission
[ "Dan", 1000000, 400000 ],
[ "Jim", 20000, 5000 ],
[ "Smith", 80000, 40000 ],
[ "Bart", 100000, 99000 ],
[ "Dan", 1000000, 400000 ],
[ "Jim", 20000, 5000 ],
[ "Smith", 80000, 40000 ],
[ "Mark", 500, 600 ]
]
});
}); // ready()
[/code]
Allan