diff --git a/docs/example-widget-math.html b/docs/example-widget-math.html new file mode 100644 index 000000000..167201777 --- /dev/null +++ b/docs/example-widget-math.html @@ -0,0 +1,473 @@ + + + + + jQuery plugin: Tablesorter 2.0 - Math Widget (beta) + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

+
+ +
+ +

Notes

+
+
    +
  • This widget will only work in tablesorter version 2.16+ and jQuery version 1.7+.
  • +
  • It adds basic math capabilities. A full list of default formulas is listed in the "Attribute Settings" section.
  • +
  • Add your own custom formulas which manipulating an array of values gathered from the table by row, column or a column block (above).
  • +
  • This is by no means a comprehensive widget that performs like a spreadsheet, but you can customize the data gathering "type" and available "formula", as desired.
  • +
  • The widget will update the calculations based on filtered rows, and will update if any data within the table changes (using update events).
  • +
  • This widget is not optimized for very large tables, for two reasons: +
      +
    • On initialization, it cycles through every table row, calculates the column index, and adds a data-column attribute.
    • +
    • 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.
    • +
    +
  • +
  • When setting tablesorter's debug option to true, this widget will output each {type}-{formula} value found, the array of numbers used and the result.
  • +
+
+ +

Options

+
+

Math widget default options (added inside of tablesorter widgetOptions)

+
+ TIP! Click on the link in the option column to reveal full details (or toggle|show|hide all) or double click to update the browser location. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionDefaultDescription
math_data'math' + Set this option to point to the named data-attribute. For example, when set to 'math', the widget looks for settings within the data-math attribute. +
[ ] + Set this option the column index of columns of data to ignore. +
+
+ To ignore the first and second columns in a table, set this option using zero-based column indexs as follows: +
// column index(es) to ignore
+math_ignore : [0,1]
+
+
'#,##0.00' + Set this option with an output formatting mask to use * +
+

Features

+
    +
  • Short, fast, flexible yet standalone. Only 75 lines including MIT license info, blank lines & comments.
  • +
  • Accept standard number formatting like #,##0.00 or with negation -000.####.
  • +
  • Accept any country format like # ##0,00, #,###.##, #'###.## or any type of non-numbering symbol.
  • +
  • Accept any numbers of digit grouping. #,##,#0.000 or #,###0.## are all valid.
  • +
  • Accept any redundant/fool-proof formatting. ##,###,##.# or 0#,#00#.###0# are all OK.
  • +
  • Auto number rounding.
  • +
  • Simple interface, just supply mask & value like this: $.tablesorter.formatMask( "0.0000", 3.141592 )
  • +
+

Limitation

+
    +
  • No prefix or suffix is allowed except leading negation symbol. So $#,##0.00 or #,###.##USD will not yield expected outcome. Use '$'+ $.tablesorter.formatMask('#,##0.00', 123.45) or $.tablesorter.formatMask('#,##0.00', 456.789) + 'USD'
  • +
  • No scientific/engineering formatting.
  • +
  • Not for date or phone formation.
  • +
  • No color control.
  • +
+

Note

+
    +
  • When there's only one symbol is supplied, system will always treat the single symbol as Decimal. For instance, $.tablesorter.formatMask( '#,###', 1234567.890) will output 1234567,890.
  • +
  • To force a single symbol as Separator, add a trailing dot to the end like this: $.tablesorter.formatMask( '#,###.', 1234567.890) which will then output 1,234,567.
  • +
  • Original plugin demo
  • +
+ * The number formatter code was copied from javascript-number-formatter (MIT). +
+
null + This function is called after each calculation is made to allow re-formatting, adding prefixes, suffixes, etc to the result. +
+
+ Use this option as follows: +
// complete executed after each function
+math_complete : function($cell, wo, result, value, arry){
+    return '$ ' + result + $cell.attr('data-suffix');
+}
+
    +
  • $cell - the target cell (jQuery object)
  • +
  • wo - tablesorter's widget options (from table.config.widgetOptions).
  • +
  • result - the formatted result of the calculation.
  • +
  • value - an unformatted result of the calculation.
  • +
  • arry - the array of values gathered by the widget.
  • +
+ In this function, if a anything is returned, it will be automatically added to the $cell as html. Or, return false and no change is made to the cell contents; use this method if you manipulate the $cell contents and don't want the widget to do it.
+
+ If you need to format the data output after manipulating the value, you can use wo.math_mask, or a different mask, by using the $.tablesorter.formatMask( mask, value ); function. For example: +
math_complete : function($cell, wo, result, value, arry){
+    var percent = Math.round( value * 1e4 ) / 100; // percent with two decimal places
+    return $.tablesorter.formatMask( wo.math_mask, percent ) + ' %';
+}
+ More details can be found in the math_mask description. +
+
[ 'row', 'above', 'col' ]' + This is the order of calculations. +
+
    +
  • By default, the widget cycles through the calculated cells as follows: +
      +
    • Search all non-informational tbodies for data-math table cells (data-attribute set by math_data option).
    • +
    • Cycle through these cells by priority: row, above, col (set by this option).
    • +
    • Search all informational tbodies, and tfoot for data-math table cells.
    • +
    • Cycle through these cells by priority: row, above, col (set by this option).
    • +
    • Search the entire table for data-math cells of the "all" type.
    • +
    +
  • +
  • So, all row calculations are made first, followed by "above" calculations then "col" (column) calculations.
  • +
  • The "all" type calculations are always performed last, and therefore the type is not included in this list.
  • +
  • Change this order if the order of calculations needs to be made column first, followed by rows.
  • +
  • For more details about the differences between "col" and "above" types, see the next section.
  • +
+
+
+
+ +

Attribute Settings

+
+ The math widget data-attibute setting requires two parts: type & formula +
<td data-math="{type}-{formula}"></td>
+ When set, the data is gathered based on the math type ("row", "column", "above" or "all") and passed to the formula as an array. +

{type} (data gathering)

+
    +
  • row - gather the table cell values from the same row as the data-math attribute.
  • +
  • above - gather the table cell values from the same column as the data-math attribute, but stop when the first table cell is reached, or when another cell with a data-attribute with an "above" type is reached; see the first table demo below to see why this is useful.
  • +
  • col - gather the table cell values from the same column as the data-math attribute.
  • +
  • all - gather all table cell values with a data-math attribute that start with "all".
  • +
+

{formula} (defaults)

+
    +
  • count - returns the count (length) of the data set.
  • +
  • sum - returns the sum of all values in the data set.
  • +
  • max - returns the maximum value in the data set.
  • +
  • min - returns the minimum values in the data set.
  • +
  • mean - returns the mean (average) of all values in the data set; it uses the sum formula in part of the calculation.
  • +
  • median - returns the median (middle value) of the data set.
  • +
  • mode - returns an array of the mode(s) (most frequent value or values) in the data set; an array is always returned, even if only one mode exists (see the second demo below).
  • +
  • range - returns the range (highest minus lowest value) of the data set.
  • +
  • varp - returns the variance of the data set (population).
  • +
  • vars - returns the variance of the data set (sample).
  • +
  • stdevp - returns the standard deviation of the data set (population).
  • +
  • stdevs - returns the standard deviation of the data set (sample).
  • +
  • custom (not a default) +
      +
    • Custom formulas can have any name
    • +
    • Return your result after making whatever calculation from the array of data passed to the formula
    • +
    • For example: +
      // adding a custom equation... named "product"
      +// access from data-math="row-product" (or "above-product", or "col-product")
      +$.tablesorter.equations['product'] = function(arry) {
      +    // multiple all array values together
      +    var product = 1;
      +    $.each(arry, function(i,v){
      +        // oops, we shouldn't have any zero values in the array
      +        if (v !== 0) {
      +            product *= v;
      +        }
      +    });
      +    return product;
      +};
      +
    • +
    +
  • +
+

Ignoring cells

+
    +
  • Entire row: if the <tr> math data-attribute contains the keyword "ignore" then that entire row of cells will be skipped when building the array of data to be used for calculations. +
    <tr data-math="ignore"><td>1</td><td>2</td><td>3</td></tr>
    +
  • +
  • Cell: if the table cell math data-attribute contains the keyword "ignore" then that cell will be skipped when building the array of data to be used for calculations. +
    <td data-math="ignore">1</td>
    +
  • +
  • Column: set the widget math_ignore option with an array of zero-based column indexes of columns to ignore or skip when building the array of data for calculations. +
    math_ignore : [0,1]
    +
  • +
+
+
+ +

+ +

Demo

+ +

Row & Column Sums

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RegionSalesmanFastCarRapidZooSuperGlueGrand Total
Column Totalscol-sumcol-sumcol-sumcol-sum
Grand Totalall-sum
MiddleJoseph$ 423$ 182$ 255row-sum
MiddleLawrence$ 5,908$ 4,642$ 4,593row-sum
MiddleMaria$ 6,502$ 3,969$ 5,408row-sum
MiddleMatt$ 4,170$ 6,093$ 5,039row-sum
Middle Totalabove-sumabove-sumabove-sumabove-sum
NorthJoseph$ 3,643$ 5,846$ 6,574row-sum
NorthLawrence$ 4,456$ 6,658$ 7,685row-sum
NorthMaria$ 6,235$ 4,616.99$ 3,612.33row-sum
NorthMatt$ 3,868$ 3,926$ 3,254row-sum
North Totalabove-sumabove-sumabove-sumabove-sum
WestJoseph$ 5,507$ 5,186$ 4,882row-sum
WestLawrence$ 4,082$ 5,272$ 6,124row-sum
WestMaria$ 5,520$ 5,461$ 4,872row-sum
WestMatt$ 6,737$ 4,598$ 4,233row-sum
West Totalabove-sumabove-sumabove-sumabove-sum
+ + +

Math Formulas

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FormulaABCDEFResult (expected result)
Default Formulas
Count (row-count)101010102020
Sum (row-sum)102010103020
Max (row-max)20603015305
Min (row-min)20603015305
Mean (row-mean)102030103020
Median (row-median)1053443
Mode (row-mode)122232
Mode (row-mode)122134
Range (row-range)1-22460
Variance [population] (row-varp)274554
Standard Deviation [population] (row-stdevp)274554
Variance [sample] (row-vars)274554
Standard Deviation [sample] (row-stdevs)274554
Custom Formulas
Custom ( (A+B+C)*D - (E/F)*100 )5232012
Product ( A*B*C*D*E*F )1234510
+ +

Javascript

+ +
+

+	
+ +

HTML

+
+

+	
+ +
+ + + + diff --git a/docs/index.html b/docs/index.html index 8156b6d4b..e0b1596da 100644 --- a/docs/index.html +++ b/docs/index.html @@ -477,9 +477,12 @@

  • basic (v2.8; v2.15).
  • Grouping + filter + child rows (v2.15.12)
  • -
  • Header titles widget (v2.15.6; 2.15.7)
  • +
  • + Beta Math widget (v2.16). +

    +
  • Pager plugin (v2.15).
  • diff --git a/js/widgets/widget-math.js b/js/widgets/widget-math.js new file mode 100644 index 000000000..050ec6868 --- /dev/null +++ b/js/widgets/widget-math.js @@ -0,0 +1,359 @@ +/*! tablesorter math widget - beta testing +* 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 = { + + // get all of the row numerical values in an arry + getRow : function(table, wo, $el, dataAttrib) { + var txt, + arry = [], + $row = $el.closest('tr'), + $cells = $row.children(); + if (!$row.hasClass(wo.filter_filteredRow || 'filtered')) { + if (wo.math_ignore.length) { + $cells = $cells.not('[' + dataAttrib + '=ignore]').not('[data-column=' + wo.math_ignore.join('],[data-column=') + ']'); + } + arry = $cells.not($el).map(function(){ + txt = this.textContent || $(this).text(); + txt = ts.formatFloat(txt.replace(/[^\w,. \-()]/g, ""), table); + 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[0].textContent || $t.text(); + txt = ts.formatFloat(txt.replace(/[^\w,. \-()]/g, ""), table); + 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[0] ? $t[0].textContent : '') || $t.text(); + txt = ts.formatFloat(txt.replace(/[^\w,. \-()]/g, ""), table); + 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[0] ? $t[0].textContent : '') || $t.text(); + txt = ts.formatFloat(txt.replace(/[^\w,. \-()]/g, ""), table); + arry.push(isNaN(txt) ? 0 : txt); + } + }); + } + }); + return arry; + }, + + recalculate : function(table, c, wo, init){ + if (c && !wo.math_isUpdating) { + + // 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) { + var result = ts.formatMask( wo.math_mask, value ); + 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){ + var isNegative, result, decimal, group, pos_lead_zero, pos_trail_zero, pos_separator, part, szSep, + integer, str, offset, i, l; + if ( !m || isNaN(+v) ) { + return v; // return as it is. + } + // 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 + pos_trail_zero = 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 <= pos_trail_zero ) { + v = (+v).toFixed( pos_trail_zero + 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. + + pos_lead_zero = m[0] && m[0].indexOf('0'); + if ( pos_lead_zero > -1 ) { + while ( part[0].length < ( m[0].length - pos_lead_zero ) ) { + 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. + pos_separator = ( szSep[1] && szSep[ szSep.length - 1 ].length ); + if (pos_separator) { + integer = v[0]; + str = ''; + offset = integer.length % pos_separator; + l = integer.length; + for ( i = 0; i < l; i++ ) { + str += integer.charAt(i); // ie6 only support charAt for sz. + // -pos_separator so that won't trail separator on full length + if ( !(( i - offset + 1 ) % pos_separator) && i < l - pos_separator ) { + str += group; + } + } + v[0] = str; + } + + v[1] = ( m[1] && v[1] ) ? decimal + v[1] : ""; + return ( isNegative ? '-' : '' ) + v[0] + v[1]; // put back any negation and combine integer and fraction. + }; + + 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' ] + }, + init : function(table, thisWidget, c, wo){ + c.$table + .bind('tablesorter-initialized update updateRows addRows updateCell filterReset filterEnd '.split(' ').join('.tsmath '), function(e){ + if (!wo.math_isUpdating) { + math.recalculate(table, c, wo, e.type === 'tablesorter-initialized'); + } + }) + .bind('updateComplete.tsmath', function(){ + setTimeout(function(){ + wo.math_isUpdating = false; + }, 500); + }); + 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){ + $(table) + .unbind('tablesorter-initialized update updateRows addRows updateCell filterReset filterEnd '.split(' ').join('.tsmath ')) + .find('[data-' + wo.math_data + ']').empty(); + } + }); + +})(jQuery); \ No newline at end of file