tablesorter/js/widgets/widget-math.js
2015-07-22 23:29:51 -05:00

426 lines
14 KiB
JavaScript

/*! Widget: math - updated 5/17/2015 (v2.22.0) *//*
* Requires tablesorter v2.16+ and jQuery 1.7+
* by Rob Garrison
*/
/*jshint browser:true, jquery:true, unused:false */
/*global jQuery: false */
;(function($) {
'use strict';
var ts = $.tablesorter,
math = {
events : ( 'tablesorter-initialized update updateAll updateRows addRows updateCell ' +
'filterReset filterEnd ' ).split(' ').join('.tsmath '),
// get all of the row numerical values in an arry
getRow : function(table, wo, $el, dataAttrib) {
var $t, txt,
c = table.config,
arry = [],
$row = $el.closest('tr'),
$cells = $row.children().not('[' + dataAttrib + '=ignore]');
if (!$row.hasClass(wo.filter_filteredRow || 'filtered')) {
if (wo.math_ignore.length) {
$cells = $cells.not('[data-column=' + wo.math_ignore.join('],[data-column=') + ']');
}
arry = $cells.not($el).map(function(){
$t = $(this);
txt = $t.attr(c.textAttribute);
if (typeof txt === 'undefined') {
txt = this.textContent || $t.text();
}
txt = ts.formatFloat(txt.replace(/[^\w,. \-()]/g, ''), table) || 0;
return isNaN(txt) ? 0 : txt;
}).get();
}
return arry;
},
// get all of the column numerical values in an arry
getColumn : function(table, wo, $el, type, dataAttrib) {
var i, txt, $t, len, mathAbove,
arry = [],
c = table.config,
filtered = wo.filter_filteredRow || 'filtered',
cIndex = parseInt( $el.attr('data-column'), 10 ),
$rows = c.$table.children('tbody').children(),
$row = $el.closest('tr');
// make sure tfoot rows are AFTER the tbody rows
// $rows.add( c.$table.children('tfoot').children() );
if (type === 'above') {
len = $rows.index($row);
i = len;
while (i >= 0) {
$t = $rows.eq(i).children().filter('[data-column=' + cIndex + ']');
mathAbove = $t.filter('[' + dataAttrib + '^=above]').length;
// ignore filtered rows & rows with data-math="ignore" (and starting row)
if ( ( !$rows.eq(i).hasClass(filtered) && $rows.eq(i).not('[' + dataAttrib + '=ignore]').length && i !== len ) || mathAbove && i !== len ) {
// stop calculating 'above', when encountering another 'above'
if (mathAbove) {
i = 0;
} else if ($t.length) {
txt = $t.attr(c.textAttribute);
if (typeof txt === 'undefined') {
txt = $t[0].textContent || $t.text();
}
txt = ts.formatFloat(txt.replace(/[^\w,. \-()]/g, ''), table) || 0;
arry.push(isNaN(txt) ? 0 : txt);
}
}
i--;
}
} else {
$rows.each(function(){
$t = $(this).children().filter('[data-column=' + cIndex + ']');
if (!$(this).hasClass(filtered) && $t.not('[' + dataAttrib + '^=above],[' + dataAttrib + '^=col]').length && !$t.is($el)) {
txt = $t.attr(c.textAttribute);
if (typeof txt === 'undefined') {
txt = ($t[0] ? $t[0].textContent : '') || $t.text();
}
// isNaN('') => false
txt = ts.formatFloat(txt.replace(/[^\w,. \-()]/g, ''), table) || 0;
arry.push(isNaN(txt) ? 0 : txt);
}
});
}
return arry;
},
// get all of the column numerical values in an arry
getAll : function(table, wo, dataAttrib) {
var txt, $t, col,
arry = [],
c = table.config,
filtered = wo.filter_filteredRow || 'filtered',
$rows = c.$table.children('tbody').children();
$rows.each(function(){
if (!$(this).hasClass(filtered)) {
$(this).children().each(function(){
$t = $(this);
col = parseInt( $t.attr('data-column'), 10);
if (!$t.filter('[' + dataAttrib + ']').length && $.inArray(col, wo.math_ignore) < 0) {
txt = $t.attr(c.textAttribute);
if (typeof txt === 'undefined') {
txt = ($t[0] ? $t[0].textContent : '') || $t.text();
}
txt = ts.formatFloat(txt.replace(/[^\w,. \-()]/g, ''), table) || 0;
arry.push(isNaN(txt) ? 0 : txt);
}
});
}
});
return arry;
},
recalculate : function(table, c, wo, init) {
if (c && (!wo.math_isUpdating || init)) {
// add data-column attributes to all table cells
if (init) {
ts.computeColumnIndex( c.$table.children('tbody').children() );
}
// data-attribute name (defaults to data-math)
var dataAttrib = 'data-' + (wo.math_data || 'math'),
// all non-info tbody cells
$mathCells = c.$tbodies.find('[' + dataAttrib + ']');
math.mathType( table, wo, $mathCells, wo.math_priority, dataAttrib );
// only info tbody cells
$mathCells = c.$table.find('.' + c.cssInfoBlock + ', tfoot').find('[' + dataAttrib + ']');
math.mathType( table, wo, $mathCells, wo.math_priority, dataAttrib );
// find the 'all' total
math.mathType( table, wo, c.$table.find('[' + dataAttrib + '^=all]'), [ 'all' ], dataAttrib );
wo.math_isUpdating = true;
c.$table.trigger('update');
}
},
mathType : function(table, wo, $cells, priority, dataAttrib) {
if ($cells.length) {
var formula, t, $t, arry, getAll,
eq = ts.equations;
if (priority[0] === 'all') {
// no need to get all cells more than once
getAll = math.getAll(table, wo, dataAttrib);
}
$.each( priority, function(i, type) {
$cells.filter('[' + dataAttrib + '^=' + type + ']').each(function(){
$t = $(this);
formula = ($t.attr(dataAttrib) || '').replace(type + '-', '');
arry = (type === 'row') ? math.getRow(table, wo, $t, dataAttrib) :
(type === 'all') ? getAll : math.getColumn(table, wo, $t, type, dataAttrib);
if (eq[formula]) {
t = eq[formula](arry);
if (table.config.debug && console && console.log) {
console.log($t.attr(dataAttrib), arry, '=', t);
}
math.output( $t, wo, t, arry );
}
});
});
}
},
output : function($cell, wo, value, arry) {
// get mask from cell data-attribute: data-math-mask="#,##0.00"
var result = ts.formatMask( $cell.attr('data-' + wo.math_data + '-mask') || wo.math_mask, value, wo.math_wrapPrefix, wo.math_wrapSuffix );
if ($.isFunction(wo.math_complete)) {
result = wo.math_complete($cell, wo, result, value, arry);
}
if (result !== false) {
$cell.html(result);
}
}
};
// Modified from https://code.google.com/p/javascript-number-formatter/
/**
* @preserve IntegraXor Web SCADA - JavaScript Number Formatter
* http:// www.integraxor.com/
* author: KPL, KHL
* (c)2011 ecava
* Dual licensed under the MIT or GPL Version 2 licenses.
*/
ts.formatMask = function(m, v, tmpPrefix, tmpSuffix) {
if ( !m || isNaN(+v) ) {
return v; // return as it is.
}
var isNegative, result, decimal, group, posLeadZero, posTrailZero, posSeparator, part, szSep,
integer, str, offset, i, l, len, start, tmp, end, inv,
prefix = '',
suffix = '';
// find prefix/suffix
len = m.length;
start = m.search( /[0-9\-\+#]/ );
tmp = start > 0 ? m.substring(0, start) : '';
prefix = tmp;
if ( start > 0 && tmpPrefix ) {
if ( /\{content\}/.test(tmpPrefix || '') ) {
prefix = (tmpPrefix || '').replace(/\{content\}/g, tmp || '');
} else {
prefix = (tmpPrefix || '') + tmp;
}
}
// reverse string: not an ideal method if there are surrogate pairs
inv = m.split('').reverse().join('');
end = inv.search( /[0-9\-\+#]/ );
i = len - end;
i += (m.substring( i, i + 1 ) === '.') ? 1 : 0;
tmp = end > 0 ? m.substring( i, len) : '';
suffix = tmp;
if (tmp !== '' && tmpSuffix) {
if ( /\{content\}/.test(tmpSuffix || '') ) {
suffix = (tmpSuffix || '').replace(/\{content\}/g, tmp || '');
} else {
suffix = tmp + (tmpSuffix || '');
}
}
m = m.substring(start, i);
// convert any string to number according to formation sign.
v = m.charAt(0) == '-' ? -v : +v;
isNegative = v < 0 ? v = -v : 0; // process only abs(), and turn on flag.
// search for separator for grp & decimal, anything not digit, not +/- sign, not #.
result = m.match( /[^\d\-\+#]/g );
decimal = ( result && result[result.length - 1] ) || '.'; // treat the right most symbol as decimal
group = ( result && result[1] && result[0] ) || ','; // treat the left most symbol as group separator
// split the decimal for the format string if any.
m = m.split( decimal );
// Fix the decimal first, toFixed will auto fill trailing zero.
v = v.toFixed( m[1] && m[1].length );
v = +(v) + ''; // convert number to string to trim off *all* trailing decimal zero(es)
// fill back any trailing zero according to format
posTrailZero = m[1] && m[1].lastIndexOf('0'); // look for last zero in format
part = v.split('.');
// integer will get !part[1]
if ( !part[1] || ( part[1] && part[1].length <= posTrailZero ) ) {
v = (+v).toFixed( posTrailZero + 1 );
}
szSep = m[0].split( group ); // look for separator
m[0] = szSep.join(''); // join back without separator for counting the pos of any leading 0.
posLeadZero = m[0] && m[0].indexOf('0');
if ( posLeadZero > -1 ) {
while ( part[0].length < ( m[0].length - posLeadZero ) ) {
part[0] = '0' + part[0];
}
} else if ( +part[0] === 0 ) {
part[0] = '';
}
v = v.split('.');
v[0] = part[0];
// process the first group separator from decimal (.) only, the rest ignore.
// get the length of the last slice of split result.
posSeparator = ( szSep[1] && szSep[ szSep.length - 1 ].length );
if ( posSeparator ) {
integer = v[0];
str = '';
offset = integer.length % posSeparator;
l = integer.length;
for ( i = 0; i < l; i++ ) {
str += integer.charAt(i); // ie6 only support charAt for sz.
// -posSeparator so that won't trail separator on full length
/*jshint -W018 */
if ( !( ( i - offset + 1 ) % posSeparator ) && i < l - posSeparator ) {
str += group;
}
}
v[0] = str;
}
v[1] = ( m[1] && v[1] ) ? decimal + v[1] : '';
// put back any negation, combine integer and fraction, and add back prefix & suffix
return prefix + ( ( isNegative ? '-' : '' ) + v[0] + v[1] ) + suffix;
};
ts.equations = {
count : function(arry) {
return arry.length;
},
sum : function(arry) {
var total = 0;
$.each( arry, function(i) {
total += arry[i];
});
return total;
},
mean : function(arry) {
var total = ts.equations.sum( arry );
return total / arry.length;
},
median : function(arry) {
// https://gist.github.com/caseyjustus/1166258
arry.sort( function(a, b){ return a - b; } );
var half = Math.floor( arry.length / 2 );
return (arry.length % 2) ? arry[half] : ( arry[half - 1] + arry[half] ) / 2.0;
},
mode : function(arry) {
// http://stackoverflow.com/a/3451640/145346
if ( arry.length === 0 ) { return 'none'; }
var i, el,
modeMap = {},
maxCount = 1,
modes = [ arry[0] ];
for (i = 0; i < arry.length; i++) {
el = arry[i];
modeMap[el] = modeMap[el] ? modeMap[el] + 1 : 1;
if ( modeMap[el] > maxCount ) {
modes = [ el ];
maxCount = modeMap[el];
} else if (modeMap[el] === maxCount) {
modes.push(el);
maxCount = modeMap[el];
}
}
// returns arry of modes if there is a tie
return modes.sort( function(a, b){ return a - b; } );
},
max : function(arry) {
return Math.max.apply( Math, arry );
},
min : function(arry) {
return Math.min.apply( Math, arry );
},
range: function(arry) {
var v = arry.sort(function(a, b){ return a - b; });
return v[ arry.length - 1 ] - v[0];
},
// common variance equation
// (not accessible via data-attribute setting)
variance: function(arry, population) {
var avg = ts.equations.mean( arry ),
v = 0,
i = arry.length;
while (i--) {
v += Math.pow( ( arry[i] - avg ), 2 );
}
v /= ( arry.length - (population ? 0 : 1) );
return v;
},
// variance (population)
varp : function(arry) {
return ts.equations.variance(arry, true);
},
// variance (sample)
vars : function(arry) {
return ts.equations.variance(arry);
},
// standard deviation (sample)
stdevs : function(arry) {
var vars = ts.equations.variance(arry);
return Math.sqrt( vars );
},
// standard deviation (population)
stdevp : function(arry) {
var varp = ts.equations.variance(arry, true);
return Math.sqrt( varp );
}
};
// add new widget called repeatHeaders
// ************************************
ts.addWidget({
id: 'math',
priority: 100,
options: {
math_data : 'math',
// column index to ignore
math_ignore : [],
// mask info: https://code.google.com/p/javascript-number-formatter/
math_mask : '#,##0.00',
// complete executed after each fucntion
math_complete : null, // function($cell, wo, result, value, arry){ return result; },
// order of calculation; 'all' is last
math_priority : [ 'row', 'above', 'col' ],
// template for or just prepend the mask prefix & suffix with this HTML
// e.g. '<span class="red">{content}</span>'
math_prefix : '',
math_suffix : '',
math_event : 'recalculate'
},
init : function(table, thisWidget, c, wo) {
c.$table
.off( (math.events + ' updateComplete.tsmath ' + wo.math_event).replace(/\s+/g, ' ') )
.on(math.events + ' ' + wo.math_event, function(e) {
var init = e.type === 'tablesorter-initialized';
if ( !wo.math_isUpdating || init ) {
if ( !/filter/.test(e.type) ) {
// redo data-column indexes on update
ts.computeColumnIndex( c.$table.children('tbody').children() );
}
math.recalculate( table, c, wo, init );
}
})
.on('updateComplete.tsmath', function(){
setTimeout(function(){
wo.math_isUpdating = false;
}, 20);
});
wo.math_isUpdating = false;
},
// this remove function is called when using the refreshWidgets method or when destroying the tablesorter plugin
// this function only applies to tablesorter v2.4+
remove: function(table, c, wo, refreshing) {
if (refreshing) { return; }
$(table)
.off( (math.events + ' updateComplete.tsmath ' + wo.math_event).replace(/\s+/g, ' ') )
.find('[data-' + wo.math_data + ']').empty();
}
});
})(jQuery);