Math: lots of tweaks. See #1083

- Initial calculation performed once - changed binding to either "filterEnd"
  or "pagerComplete", not both.
- Added change flag so a cache update is only performed when cell content
  inside of a sortable tbody was modified.
- Fix data-math-filter on "all" cell calculation with fixes to getRow &
  getColumn functions so that filters get priority over the "filtered" row
  check.
This commit is contained in:
Rob Garrison 2015-12-12 20:41:27 -06:00
parent 3ada0de10a
commit 4e1c96d437
2 changed files with 150 additions and 80 deletions

View File

@ -18,7 +18,7 @@
<!-- Tablesorter: required -->
<link rel="stylesheet" href="../css/theme.blue.css">
<script src="../js/jquery.tablesorter.js"></script>
<script src="../js/jquery.tablesorter.widgets.js"></script>
<script src="../js/widgets/widget-filter.js"></script>
<script src="../js/widgets/widget-math.js"></script>
@ -182,7 +182,15 @@
<li>In <span class="version">v2.24.7</span>
<ul>
<li><code>math_rowFilter</code> can now be overridden by the row's <code>data-math-filter</code> attribute.</li>
<li>On initialization, only tbody cells with a <code>rowspan</code> or <code>colspan</code> are now processed and only get a "data-column" set if the internal <code>cellIndex</code> doesn't match the calculated cell index. This should improve performance and reduce lag after initialization and updating the table (see <a href="https://github.com/Mottie/tablesorter/issues/1048">issue #1048</a>.</li>
<li>
Lots of optimizations!
<ul>
<li>On initialization, only tbody cells with a <code>rowspan</code> or <code>colspan</code> are now processed and only get a "data-column" set if the internal <code>cellIndex</code> doesn't match the calculated cell index.</li>
<li>Initial calculation is no longer performed three times in a row</li>
<li>The cache is now only updated if cell content inside of a sortable tbody was modified.</li>
<li>These changes should improve performance and reduce lag after initialization and updating the table (see <a href="https://github.com/Mottie/tablesorter/issues/1048">issue #1048</a>.</li>
</ul>
</li>
</ul>
</li>
<li>In <span class="version">v2.24.6</span>, added <code>math_rowFilter</code> option.</li>
@ -192,6 +200,12 @@
<li>Added <code>math_none</code> option which allows customizing the text added to a cell when there are no matching math elements (.</li>
</ul>
</li>
</ul>
<div class="accordion start-closed">
<h3 id="old-notes"><a href="#">Older Notes</a></h3>
<div>
<ul>
<li>In <span class="version updated">v2.22.0</span>,
<ul>
<li>Fixed an issue with a sum column encountering a cell without a defined "data-math" attribute returning an empty string instead of zero. See <a href="https://github.com/Mottie/tablesorter/issues/873">issue #873</a>.</li>
@ -209,8 +223,13 @@
<ul>
<li>Two new options: <code>math_prefix</code> and <code>math_suffix</code>, which will be added before or after the prefix or suffix, respectively.</li>
<li>Added "Mask Examples" section with examples, and how to use the <code>$.tablesorter.formatMask</code> function.</li>
</ul><br>
</ul>
</li>
</ul>
</div>
</div>
<ul>
<li>This widget will <strong>only work</strong> in tablesorter version 2.16+ and jQuery version 1.7+.</li>
<li>It adds basic math capabilities. A full list of default formulas is listed in the "Attribute Settings" section.</li>
<li>Add your own custom formulas which manipulating an array of values gathered from the table by row, column or a column block (above).</li>
@ -218,7 +237,7 @@
<li>The widget will update the calculations based on filtered rows, and will update if any data within the table changes (using update events).</li>
<li>This widget is not optimized for very large tables, for two reasons:
<ul>
<li>On initialization, it cycles through every table row, calculates the column index<del>, and adds a <code>data-column</code> attribute</del> (Fixed in <span class="version updated">v2.24.7</span>).</li>
<li>On initialization, it cycles through every table row, calculates the column index <del>, and adds a <code>data-column</code> attribute</del> (Fixed in <span class="version updated">v2.24.7</span>).</li>
<li>It uses the update method whenever it recalculates values to make the results sortable. This occurs when any of the update methods are used and after the table is filtered.</li>
</ul>
</li>
@ -442,6 +461,7 @@ math_suffix : '{content}<span class="red">!</span>'
math_rowFilter : ':visible:not(.filtered)'</pre>
In <span class="version">v2.24.7</span>, this setting my be overridden in specific rows by adding a <code>data-math-filter</code> attribute (The "math" portion is set by the <code>math_data</code> option)
<pre class="prettyprint lang-html">&lt;tr data-math-filter=":visible"&gt;...&lt;/tr&gt;</pre>
<span class="label label-info">Note</span> The <code>data-math-filter</code> attribute is ignored if set to an empty string; instead use <code>data-math-filter="*"</code> to target all rows as done in the first example on this page.
</div>
</td>
</tr>
@ -580,9 +600,17 @@ BAD =&gt; No minus (-) here! $#,###.00 or [-] here either &lt;= BAD</textarea>
<th data-math="col-sum">col-sum</th>
<th data-math="col-sum">col-sum</th>
</tr>
<tr>
<th colspan="5">Visible Total</th>
<th data-math="all-sum"></th>
</tr>
<tr>
<th colspan="5">Grand Total</th>
<th data-math="all-sum" data-math-mask="##0.0000">all-sum</th>
<!--
The "data-math-filter" attribute can not be set to an empty string AND override the widgetOptions.math_rowFilter,
so set to to an asterisk ("*") to target all rows.
-->
<th data-math="all-sum" data-math-filter="*" data-math-mask="##0.0000"></th>
</tr>
</tfoot>

View File

@ -25,8 +25,8 @@
return c && c.widgetOptions.math_none || 'none'; // text for cell
},
events : ( 'tablesorter-initialized update updateAll updateRows addRows updateCell ' +
'filterReset filterEnd ' ).split(' ').join('.tsmath '),
events : ( 'tablesorter-initialized update updateAll updateRows addRows updateCell filterReset ' )
.split(' ').join('.tsmath '),
processText : function( c, $cell ) {
var txt = ts.getElementText( c, $cell, math.getCellIndex( $cell ) );
@ -36,17 +36,16 @@
},
// get all of the row numerical values in an arry
getRow : function( c, $el ) {
getRow : function( c, $el, hasFilter ) {
var $cells,
wo = c.widgetOptions,
arry = [],
$row = $el.closest( 'tr' ),
isFiltered = $row.hasClass( wo.filter_filteredRow || 'filtered' ),
hasFilter = $row.attr( wo.math_dataAttrib + '-filter' ) || wo.math_rowFilter;
isFiltered = $row.hasClass( wo.filter_filteredRow || 'filtered' );
if ( hasFilter ) {
$row = $row.filter( hasFilter );
}
if ( !isFiltered || hasFilter ) {
if ( hasFilter || !isFiltered ) {
$cells = $row.children().not( '[' + wo.math_dataAttrib + '=ignore]' );
if ( wo.math_ignore.length ) {
$cells = $cells.filter( function( indx ) {
@ -62,33 +61,38 @@
},
// get all of the column numerical values in an arry
getColumn : function( c, $el, type ) {
getColumn : function( c, $el, type, hasFilter ) {
var index, $t, $tr, len, $mathRows, mathAbove,
wo = c.widgetOptions,
arry = [],
$row = $el.closest( 'tr' ),
mathAttr = wo.math_dataAttrib,
hasFilter = $row.attr( mathAttr + '-filter' ) || wo.math_rowFilter,
mathIgnore = '[' + mathAttr + '=ignore]',
filtered = wo.filter_filteredRow || 'filtered',
cIndex = math.getCellIndex( $el ),
$rows = c.$table.children( 'tbody' ).children();
// make sure tfoot rows are AFTER the tbody rows
// $rows.add( c.$table.children( 'tfoot' ).children() );
// get all rows to keep row indexing
$rows = c.$table.children( 'tbody' ).children(),
mathAttrs = [
'[' + mathAttr + '^=above]',
'[' + mathAttr + '^=below]',
'[' + mathAttr + '^=col]',
'[' + mathAttr + '^=all]'
];
if ( type === 'above' ) {
len = $rows.index( $row );
index = len;
while ( index >= 0 ) {
$tr = $rows.eq( index );
mathAbove = $tr.children().filter( mathAttrs[0] ).length;
if ( hasFilter ) {
$tr = $tr.filter( hasFilter );
}
$t = $tr.children().filter( function( indx ) {
return math.getCellIndex( $( this ) ) === cIndex;
});
mathAbove = $t.filter( '[' + mathAttr + '^=above]' ).length;
// ignore filtered rows & rows with data-math="ignore" (and starting row)
if ( ( ( !$tr.hasClass( filtered ) || hasFilter ) &&
$tr.not( '[' + mathAttr + '=ignore]' ).length &&
if ( ( ( hasFilter || !$tr.hasClass( filtered ) ) &&
$tr.not( mathIgnore ).length &&
index !== len ) ||
mathAbove && index !== len ) {
// stop calculating 'above', when encountering another 'above'
@ -105,24 +109,23 @@
// index + 1 to ignore starting node
for ( index = $rows.index( $row ) + 1; index < len; index++ ) {
$tr = $rows.eq( index );
if ( $tr.children().filter( mathAttrs[1] ).length ) {
break;
}
if ( hasFilter ) {
$tr = $tr.filter( hasFilter );
}
$t = $tr.children().filter( function( indx ) {
return math.getCellIndex( $( this ) ) === cIndex;
});
if ( $t.filter( '[' + mathAttr + '^=below]' ).length ) {
break;
}
if ( ( !$tr.hasClass( filtered ) || hasFilter ) &&
$tr.not( '[' + mathAttr + '=ignore]' ).length &&
if ( ( hasFilter || !$tr.hasClass( filtered ) ) &&
$tr.not( mathIgnore ).length &&
$t.length ) {
arry.push( math.processText( c, $t ) );
}
}
} else {
$mathRows = $rows.not( '[' + mathAttr + '=ignore]' );
$mathRows = $rows.not( mathIgnore );
len = $mathRows.length;
for ( index = 0; index < len; index++ ) {
$tr = $mathRows.eq( index );
@ -132,8 +135,8 @@
$t = $tr.children().filter( function( indx ) {
return math.getCellIndex( $( this ) ) === cIndex;
});
if ( ( !$tr.hasClass( filtered ) || hasFilter ) &&
$t.not( '[' + mathAttr + '^=above],[' + mathAttr + '^=below],[' + mathAttr + '^=col]' ).length &&
if ( ( hasFilter || !$tr.hasClass( filtered ) ) &&
$t.not( mathAttrs.join( ',' ) ).length &&
!$t.is( $el ) ) {
arry.push( math.processText( c, $t ) );
}
@ -143,22 +146,22 @@
},
// get all of the column numerical values in an arry
getAll : function( c ) {
getAll : function( c, hasFilter ) {
var $t, col, $row, rowIndex, rowLen, $cells, cellIndex, cellLen,
arry = [],
wo = c.widgetOptions,
mathAttr = wo.math_dataAttrib,
mathIgnore = '[' + mathAttr + '=ignore]',
filtered = wo.filter_filteredRow || 'filtered',
hasFilter = wo.filter_rowFilter,
$rows = c.$table.children( 'tbody' ).children().not( '[' + mathAttr + '=ignore]' );
$rows = c.$table.children( 'tbody' ).children().not( mathIgnore );
rowLen = $rows.length;
for ( rowIndex = 0; rowIndex < rowLen; rowIndex++ ) {
$row = $rows.eq( rowIndex );
if ( hasFilter ) {
$row = $row.filter( hasFilter );
}
if ( !$row.hasClass( filtered ) || hasFilter ) {
$cells = $row.children().not( '[' + mathAttr + '=ignore]' );
if ( hasFilter || !$row.hasClass( filtered ) ) {
$cells = $row.children().not( mathIgnore );
cellLen = $cells.length;
// $row.children().each(function(){
for ( cellIndex = 0; cellIndex < cellLen; cellIndex++ ) {
@ -215,7 +218,9 @@
recalculate : function(c, wo, init) {
if ( c && ( !wo.math_isUpdating || init ) ) {
var undef, time, mathAttr, $mathCells;
var undef, time, mathAttr, $mathCells, indx, len,
changed = false,
filters = {};
if ( c.debug ) {
time = new Date();
}
@ -231,7 +236,7 @@
// all non-info tbody cells
mathAttr = wo.math_dataAttrib;
$mathCells = c.$tbodies.children( 'tr' ).children( '[' + mathAttr + ']' );
math.mathType( c, $mathCells, wo.math_priority );
changed = changed || math.mathType( c, $mathCells, wo.math_priority );
// only info tbody cells
$mathCells = c.$table
@ -242,8 +247,20 @@
// find the 'all' total
$mathCells = c.$table.children().children( 'tr' ).children( '[' + mathAttr + '^=all]' );
math.mathType( c, $mathCells, [ 'all' ] );
len = $mathCells.length;
// get math filter, if any
// hasFilter = $row.attr( mathAttr + '-filter' ) || wo.math_rowFilter;
$mathCells.each( function( indx, cell ) {
var $cell = $( cell ),
filter = $mathCells.eq( indx ).attr( mathAttr + '-filter' ) || wo.math_rowFilter;
filters[ filter ] = filters[ filter ] ? filters[ filter ].add( $cell ) : $cell;
});
$.each( filters, function( hasFilter, $cells ) {
changed = changed || math.mathType( c, $cells, [ 'all' ], hasFilter );
});
// trigger an update only if cells inside the tbody changed
if ( changed ) {
wo.math_isUpdating = true;
if ( c.debug ) {
console[ console.group ? 'group' : 'log' ]( 'Math widget triggering an update after recalculation' );
@ -258,6 +275,7 @@
console.log( 'Math widget update completed' + ts.benchmark( time ) );
}
}
}
},
updateComplete : function( c ) {
@ -266,22 +284,24 @@
wo.math_isUpdating = false;
},
mathType : function( c, $cells, priority ) {
mathType : function( c, $cells, priority, hasFilter ) {
if ( $cells.length ) {
var formula, result, $el, arry, getAll, $targetCells, index, len,
var getAll,
changed = false,
wo = c.widgetOptions,
mathAttr = wo.math_dataAttrib,
equations = ts.equations;
if ( priority[0] === 'all' ) {
// no need to get all cells more than once
getAll = math.getAll( c );
// mathType is called multiple times if more than one "hasFilter" is used
getAll = math.getAll( c, hasFilter );
}
if (c.debug) {
console[ console.group ? 'group' : 'log' ]( 'Tablesorter Math widget recalculation' );
}
// $.each is okay here... only 4 priorities
$.each( priority, function( i, type ) {
$targetCells = $cells.filter( '[' + mathAttr + '^=' + type + ']' );
var index, arry, formula, result, $el,
$targetCells = $cells.filter( '[' + mathAttr + '^=' + type + ']' ),
len = $targetCells.length;
if ( len ) {
if (c.debug) {
@ -293,39 +313,58 @@
if ( $el.parent().hasClass( wo.filter_filteredRow || 'filtered' ) ) {
continue;
}
hasFilter = hasFilter || $el.attr( mathAttr + '-filter' ) || wo.math_rowFilter;
formula = ( $el.attr( mathAttr ) || '' ).replace( type + '-', '' );
arry = ( type === 'row' ) ? math.getRow( c, $el ) :
( type === 'all' ) ? getAll : math.getColumn( c, $el, type );
arry = ( type === 'row' ) ? math.getRow( c, $el, hasFilter ) :
( type === 'all' ) ? getAll : math.getColumn( c, $el, type, hasFilter );
if ( equations[ formula ] ) {
if ( arry.length ) {
result = equations[ formula ]( arry, c );
if ( c.debug ) {
console.log( $el.attr( mathAttr ), arry, '=', result );
console.log( $el.attr( mathAttr ), hasFilter ? '("' + hasFilter + '")' : '', arry, '=', result );
}
} else {
// mean will return a divide by zero error, everything else shows an undefined error
result = math.invalid( c, formula, formula === 'mean' ? 0 : 'undef' );
}
math.output( $el, wo, result, arry );
changed = math.output( $el, c, result, arry ) || changed;
}
}
if ( c.debug && console.groupEnd ) { console.groupEnd(); }
}
});
if ( c.debug && console.groupEnd ) { console.groupEnd(); }
return changed;
}
return false;
},
output : function( $cell, wo, value, arry ) {
output : function( $cell, c, value, arry ) {
// get mask from cell data-attribute: data-math-mask="#,##0.00"
var mask = $cell.attr( 'data-' + wo.math_data + '-mask' ) || wo.math_mask,
var $el,
wo = c.widgetOptions,
changed = false,
prev = $cell.html(),
mask = $cell.attr( 'data-' + wo.math_data + '-mask' ) || wo.math_mask,
result = ts.formatMask( mask, value, wo.math_wrapPrefix, wo.math_wrapSuffix );
if ( typeof wo.math_complete === 'function' ) {
result = wo.math_complete( $cell, wo, result, value, arry );
}
if ( result !== false ) {
changed = prev !== result;
$cell.html( result );
}
// check if in a regular tbody, otherwise don't pass a changed flag
// to prevent unnecessary updating of the table cache
if ( changed ) {
$el = $cell.closest( 'tbody' );
// content was changed in a tfoot, info-only tbody or the resulting tbody is in a nested table
// then don't signal a change
if ( !$el.length || $el.hasClass( c.cssInfoBlock ) || $el.parent()[0] !== c.table ) {
return false;
}
}
return changed;
}
};
@ -560,10 +599,13 @@
},
init : function( table, thisWidget, c, wo ) {
// filterEnd fires after updateComplete
var update = ts.hasWidget( table, 'filter' ) ? 'filterEnd' : 'updateComplete';
var update = ( ts.hasWidget( table, 'filter' ) ? 'filterEnd' : 'updateComplete' ) + '.tsmath';
// filterEnd is when the pager hides rows... so bind to pagerComplete
math.events += ( ts.hasWidget( table, 'pager' ) ? 'pagerComplete' : 'filterEnd' ) + '.tsmath ';
c.$table
.off( ( math.events + ' updateComplete.tsmath ' + wo.math_event ).replace( /\s+/g, ' ' ) )
.on( math.events + ' ' + wo.math_event, function( e ) {
.off( ( math.events + 'updateComplete.tsmath ' + wo.math_event ).replace( /\s+/g, ' ' ) )
.on( math.events + wo.math_event, function( e ) {
if ( !this.hasInitialized ) { return; }
var init = e.type === 'tablesorter-initialized';
if ( !wo.math_isUpdating || init ) {
// don't setColumnIndexes on init here, or it gets done twice
@ -574,7 +616,7 @@
math.recalculate( c, wo, init );
}
})
.on( update + '.tsmath', function() {
.on( update, function() {
setTimeout( function(){
math.updateComplete( c );
}, 40 );