Mottie / tablesorter

Github fork of Christian Bach's tablesorter plugin + awesomeness ~

Home Page:https://mottie.github.io/tablesorter/docs/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

data-math="ignore" is not ignored (Math Widget)

JamoCA opened this issue · comments

I'm generating a table with numeric values, but some records don't have any data. In this case, I'm not outputting any value and the TD element is empty. I noticed that the math widget was converting these empty values to "0" when averaging, so I added data-math="ignore" to these empty TD elements. The cells are still being converted to "0" and included in the average.

For example, the mean of non-ignored fields should be "100". The value "60" is returned, so this must mean that it's evaluating ignored cells as "0" instead of ignoring them. (By comparison, if I create 5 cells in Excel and only 3 are populated w/100, the average is "100".)

<script>
$(function(){
	$('#mathTable').tablesorter({
		widgets: ['math'],
		widgetOptions : {
			math_debug: true
		}
	});
});
</script>

<table id="mathTable" class="tablesorter-blue">
<thead>
	<tr><th>ID</th><th>Value</th></tr>
</thead>
<tfoot>
<tr>
	<th data-math="col-count"></th>
	<th data-math="col-mean"></th>
</tr>
</tfoot>
<tbody>
	<tr><td>1</td><td>100</td></tr>
	<tr><td>2</td><td>100</td></tr>
	<tr><td>3</td><td>100</td></tr>
	<tr><td>4</td><td data-math="ignore"></td></tr>
	<tr><td>5</td><td data-math="ignore"></td></tr>
</tbody>
</table>

The console outputs:

col-count  (5) [0, 1, 2, 3, 4] = 5
widget-math.min.js:2 col-mean  (5) [100, 100, 100, 0, 0] = 60

Am I missing something?

Hi @JamoCA!

The data-math="ignore" is meant to be added to the tr (demo):

<tr data-math="ignore"><td>4</td><td></td></tr>

Is there a case where we'd need data only from specific cells in a row? If so, I'll need to modify the widget.

Hello,

I would very much like to see this added to the widget. I am running into a similar problem.

Hi @Sneezeplease!

Do you mean you want the ability to add data-math="ignore" to a cell as well as a row? Or something else?

Yes... ignore a table "TD" cell . Please, please please.

That's what I initially thought it was doing. I didn't realize, even after reviewing the documentation/sample, that it was a row-only attribute.

The reason for it is because I'm averaging some data for a client, but some records don't have any response data. In those cases, I leave the table cell is empty (ie, null) but tablesorter auto-translates the missing value to "0" and then the averages are incorrect & unusable. (By contrast, MS Excel automatically ignores empty cells when performing math functions.)

Sorry, I missed adding this enhancement in today's release. I'll make sure to include it in the next one.

I just noticed that a new release became available 2 hours ago. I didn't see any change in behavior. In my sample (above), I'm expecting that the result of 3 populated cells w/100 be displayed as "100" instead of "60" as the two empty (non-zero) cells are ignored, . The v2.30.7 release is still including the ignored cells. (I tested count, mean or sum functions.)

Was the enhancement included in the v2.30.7 release? If not, do you have a separate script I could try? Thanks.

Sorry, I keep neglecting old issues... I'll really try to include this in the next update.

I checked v2.31.0 (8/27/2018) and data-math="ignore" is still including the non-existent values in the calculations. Was the enhancement added to v2.31?

Sorry, no this enhancement wasn't included. I've been busy and neglecting work on this repo 😿. It is on my to-do list for the next update though!

I saw that there's been a 2.31.1 update to the math widget on 11/20/2018. It doesn't look like this enhancement has been added yet. :(

Sorry :(

New baby, new job... I'm a bit overwhelmed.

A project that I used this widget on just reported calculation errors due to empty non-numeric null-ish values being averaged as "0" and throwing off the final values.

I've attempted to add support for cell-based "ignore", but not sure if it's correct or performant:
https://gist.github.com/JamoCA/72aa5d40af8fa756c55e408674b324c1
compare w/current math widget:
https://github.com/Mottie/tablesorter/blob/master/js/widgets/widget-math.js

Sorry for the long long delay on this... I modified your changes a bit. It appears to work, please test it out more thoroughly for me. I'll update the repo if it works well.

widget-math.js
/*! Widget: math - updated 11/18/2019 (v2.31.2) */ /*
* 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 = {
      error: {
        0: "Infinity result: Divide by zero",
        1: "Need more than one element to make this calculation",
        undef: "No elements found"
      },

      // value returned when calculation is not possible, e.g. no values, dividing by zero, etc.
      invalid: function(c, name, errorIndex) {
        // name = function returning invalid results
        // errorIndex = math.error index with an explanation of the error
        console.warn(name, math.error[errorIndex]);
        return (c && c.widgetOptions.math_none) || ""; // text for cell
      },

      events: "tablesorter-initialized update updateAll updateRows addRows updateCell filterReset "
        .split(" ")
        .join(".tsmath "),

      processText: function(c, $cell) {
        var tmp,
          wo = c.widgetOptions,
          txt = ts.getElementText(c, $cell, math.getCellIndex($cell)),
          prefix = c.widgetOptions.math_prefix;
        if (wo.math_textAttr) {
          txt = $cell.attr(wo.math_textAttr) || txt;
        }
        if (/</.test(prefix)) {
          // prefix contains HTML; remove it & any text before using formatFloat
          tmp = $("<div>" + prefix + "</div>")
            .text()
            .replace(/\{content\}/g, "")
            .trim();
          txt = txt.replace(tmp, "");
        }
        txt = ts.formatFloat(txt.replace(/[^\w,. \-()]/g, ""), c.table) || 0;
        // isNaN('') => false
        return isNaN(txt) ? 0 : txt;
      },

      // get all of the row numerical values in an arry
      getRow: function(c, $el, hasFilter) {
        var $cells,
          wo = c.widgetOptions,
          arry = [],
          $row = $el.closest("tr"),
          isFiltered = $row.hasClass(wo.filter_filteredRow || "filtered");
        if (hasFilter) {
          $row = $row.filter(hasFilter);
        }
        if (hasFilter || !isFiltered) {
          $cells = $row.children().not("[" + wo.math_dataAttrib + "=ignore]");
          if (wo.math_ignore.length) {
            $cells = $cells.filter(function() {
              // using $.inArray is not optimal (needed for IE8)
              return (
                $.inArray(math.getCellIndex($(this)), wo.math_ignore) === -1
              );
            });
          }
          arry = $cells
            .not($el)
            .map(function() {
              return math.processText(c, $(this));
            })
            .get();
        }
        return arry;
      },

      // get all of the column numerical values in an arry
      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,
          mathIgnore = "[" + mathAttr + "=ignore]",
          filtered = wo.filter_filteredRow || "filtered",
          cIndex = math.getCellIndex($el),
          // 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() {
              return math.getCellIndex($(this)) === cIndex;
            });
            // ignore filtered rows & rows with data-math="ignore" (and starting row)
            if (
              ((hasFilter || !$tr.hasClass(filtered)) &&
                $tr.not(mathIgnore).length &&
                index !== len) ||
              (mathAbove && index !== len)
            ) {
              // stop calculating 'above', when encountering another 'above'
              if (mathAbove) {
                index = 0;
              } else if ($t.length && $t.not(mathIgnore).length) {
                arry[arry.length] = math.processText(c, $t);
              }
            }
            index--;
          }
        } else if (type === "below") {
          len = $rows.length;
          // 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() {
              return math.getCellIndex($(this)) === cIndex;
            });
            if (
              (hasFilter || !$tr.hasClass(filtered)) &&
              $tr.not(mathIgnore).length &&
              $t.length &&
              $t.not(mathIgnore)
            ) {
              arry[arry.length] = math.processText(c, $t);
            }
          }
        } else {
          $mathRows = $rows.not(mathIgnore);
          len = $mathRows.length;
          for (index = 0; index < len; index++) {
            $tr = $mathRows.eq(index);
            if (hasFilter) {
              $tr = $tr.filter(hasFilter);
            }
            $t = $tr.children().filter(function() {
              return math.getCellIndex($(this)) === cIndex;
            });
            if (
              (hasFilter || !$tr.hasClass(filtered)) &&
              $t.not(mathAttrs.join(",")).length &&
              !$t.is($el) &&
              $t.not(mathIgnore).length
            ) {
              arry[arry.length] = math.processText(c, $t);
            }
          }
        }
        return arry;
      },

      // get all of the column numerical values in an arry
      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",
          $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 (hasFilter || !$row.hasClass(filtered)) {
            $cells = $row.children().not(mathIgnore);
            cellLen = $cells.length;
            for (cellIndex = 0; cellIndex < cellLen; cellIndex++) {
              $t = $cells.eq(cellIndex);
              col = math.getCellIndex($t);
              if (
                !$t.filter("[" + mathAttr + "]").length &&
                $.inArray(col, wo.math_ignore) < 0
              ) {
                arry[arry.length] = math.processText(c, $t);
              }
            }
          }
        }
        return arry;
      },

      setColumnIndexes: function(c) {
        var $table = c.$table,
          last = 1,
          // only target rows with a colspan or rows included in a rowspan
          $rows = $table.children("tbody").children().filter(function() {
            var cells,
              indx,
              $this = $(this),
              include = $this.children("[colspan]").length > 0;
            if (last > 1) {
              last--;
              include = true;
            } else if (last < 1) {
              last = 1;
            }
            if ($this.children("[rowspan]").length > 0) {
              cells = this.cells;
              // find max rowspan (in case more than one cell has a rowspan)
              for (indx = 0; indx < cells.length; indx++) {
                last = Math.max(cells[indx].rowSpan, last);
              }
            }
            return include;
          });
        // pass `c` (table.config) to computeColumnIndex so it won't add a data-column
        // to every tbody cell, just the ones where the .cellIndex property doesn't match
        // the calculated cell index - hopefully fixes the lag issue in #1048
        ts.computeColumnIndex($rows, c);
      },

      getCellIndex: function($cell) {
        var indx = $cell.attr("data-column");
        return typeof indx === "undefined"
          ? $cell[0].cellIndex
          : parseInt(indx, 10);
      },

      recalculate: function(c, wo, init) {
        if (c && (!wo.math_isUpdating || init)) {
          var time,
            mathAttr,
            $mathCells,
            indx,
            len,
            changed = false,
            filters = {};
          if (c.debug || wo.math_debug) {
            time = new Date();
          }

          // add data-column attributes to all table cells
          if (init) {
            math.setColumnIndexes(c);
          }

          // data-attribute name (defaults to data-math)
          wo.math_dataAttrib = "data-" + (wo.math_data || "math");

          // all non-info tbody cells
          mathAttr = wo.math_dataAttrib;
          $mathCells = c.$tbodies.children("tr").children("[" + mathAttr + "]");
          changed = math.mathType(c, $mathCells, wo.math_priority) || changed;
          // only info tbody cells
          $mathCells = c.$table
            .children("." + c.cssInfoBlock + ", tfoot")
            .children("tr")
            .children("[" + mathAttr + "]");
          math.mathType(c, $mathCells, wo.math_priority);

          // find the 'all' total
          $mathCells = c.$table
            .children()
            .children("tr")
            .children("[" + mathAttr + "^=all]");
          len = $mathCells.length;
          // get math filter, if any
          // hasFilter = $row.attr( mathAttr + '-filter' ) || wo.math_rowFilter;
          for (indx = 0; indx < len; indx++) {
            var $cell = $mathCells.eq(indx),
              filter = $cell.attr(mathAttr + "-filter") || wo.math_rowFilter;
            filters[filter] = filters[filter]
              ? filters[filter].add($cell)
              : $cell;
          }
          $.each(filters, function(hasFilter, $cells) {
            changed = math.mathType(c, $cells, ["all"], hasFilter) || changed;
          });

          // trigger an update only if cells inside the tbody changed
          if (changed) {
            wo.math_isUpdating = true;
            if (c.debug || wo.math_debug) {
              console[console.group ? "group" : "log"](
                "Math widget updating the cache after recalculation"
              );
            }

            // update internal cache, but ignore "remove-me" rows and do not resort
            ts.updateCache(c, function() {
              math.updateComplete(c);
              if (!init && typeof wo.math_completed === "function") {
                wo.math_completed(c);
              }
              if (c.debug || wo.math_debug) {
                console.log(
                  "Math widget update completed" + ts.benchmark(time)
                );
              }
            });
          } else {
            if (!init && typeof wo.math_completed === "function") {
              wo.math_completed(c);
            }
            if (c.debug || wo.math_debug) {
              console.log(
                "Math widget found no changes in data" + ts.benchmark(time)
              );
            }
          }
        }
      },

      updateComplete: function(c) {
        var wo = c.widgetOptions;
        if (
          wo.math_isUpdating &&
          (c.debug || wo.math_debug) &&
          console.groupEnd
        ) {
          console.groupEnd();
        }
        wo.math_isUpdating = false;
      },

      mathType: function(c, $cells, priority, hasFilter) {
        if ($cells.length) {
          var getAll,
            changed = false,
            wo = c.widgetOptions,
            mathAttr = wo.math_dataAttrib,
            equations = ts.equations;
          if (priority[0] === "all") {
            // mathType is called multiple times if more than one "hasFilter" is used
            getAll = math.getAll(c, hasFilter);
          }
          if (c.debug || wo.math_debug) {
            console[console.group ? "group" : "log"](
              "Tablesorter Math widget recalculation"
            );
          }
          // $.each is okay here... only 4 priorities
          $.each(priority, function(i, type) {
            var index,
              arry,
              formula,
              result,
              $el,
              $targetCells = $cells.filter("[" + mathAttr + "^=" + type + "]"),
              len = $targetCells.length;
            if (len) {
              if (c.debug || wo.math_debug) {
                console[console.group ? "group" : "log"](type);
              }
              for (index = 0; index < len; index++) {
                $el = $targetCells.eq(index);
                // Row is filtered, no need to do further checking
                if (
                  $el.parent().hasClass(wo.filter_filteredRow || "filtered")
                ) {
                  continue;
                }
                hasFilter = $el.attr(mathAttr + "-filter") || wo.math_rowFilter;
                formula = ($el.attr(mathAttr) || "").replace(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 || wo.math_debug) {
                      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"
                    );
                  }
                  changed = math.output($el, c, result, arry) || changed;
                }
              }
              if ((c.debug || wo.math_debug) && console.groupEnd) {
                console.groupEnd();
              }
            }
          });
          if ((c.debug || wo.math_debug) && console.groupEnd) {
            console.groupEnd();
          }
          return changed;
        }
        return false;
      },

      output: function($cell, c, value, arry) {
        // get mask from cell data-attribute: data-math-mask="#,##0.00"
        var $el,
          wo = c.widgetOptions,
          changed = false,
          prev = $cell.html(),
          mask = $cell.attr("data-" + wo.math_data + "-mask") || wo.math_mask,
          target = $cell.attr("data-" + wo.math_data + "-target") || "",
          result = ts.formatMask(mask, value, wo.math_prefix, wo.math_suffix);
        if (target) {
          $el = $cell.find(target);
          if ($el.length) {
            $cell = $el;
          }
        }
        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;
      }
    };

  // 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(mask, val, tmpPrefix, tmpSuffix) {
    if (!mask || isNaN(+val)) {
      return val; // return as it is.
    }

    var isNegative,
      result,
      decimal,
      group,
      posLeadZero,
      posTrailZero,
      posSeparator,
      part,
      szSep,
      integer,
      str,
      offset,
      index,
      end,
      inv,
      suffix = "",
      // find prefix/suffix
      len = mask.length,
      start = mask.search(/[0-9\-\+#]/),
      tmp = start > 0 ? mask.substring(0, start) : "",
      prefix = tmp;
    if (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 = mask.split("").reverse().join("");
    end = inv.search(/[0-9\-\+#]/);
    index = len - end;
    index += mask.substring(index, index + 1) === "." ? 1 : 0;
    tmp = end > 0 ? mask.substring(index, len) : "";
    suffix = tmp;
    if (tmpSuffix) {
      if (/\{content\}/.test(tmpSuffix || "")) {
        suffix = (tmpSuffix || "").replace(/\{content\}/g, tmp || "");
      } else {
        suffix = tmp + (tmpSuffix || "");
      }
    }
    mask = mask.substring(start, index);

    // convert any string to number according to formation sign.
    val = mask.charAt(0) === "-" ? -val : +val;
    isNegative = val < 0 ? (val = -val) : 0; // process only abs(), and turn on flag.

    // search for separator for grp & decimal, anything not digit, not +/- sign, not #.
    result = mask.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.
    mask = mask.split(decimal);
    // Fix the decimal first, toFixed will auto fill trailing zero.
    val = val.toFixed(mask[1] && mask[1].length);
    val = +val + ""; // convert number to string to trim off *all* trailing decimal zero(es)

    // fill back any trailing zero according to format
    posTrailZero = mask[1] && mask[1].lastIndexOf("0"); // look for last zero in format
    part = val.split(".");
    // integer will get !part[1]
    if (!part[1] || (part[1] && part[1].length <= posTrailZero)) {
      val = (+val).toFixed(posTrailZero + 1);
    }
    szSep = mask[0].split(group); // look for separator
    mask[0] = szSep.join(""); // join back without separator for counting the pos of any leading 0.

    posLeadZero = mask[0] && mask[0].indexOf("0");
    if (posLeadZero > -1) {
      while (part[0].length < mask[0].length - posLeadZero) {
        part[0] = "0" + part[0];
      }
    } else if (+part[0] === 0) {
      part[0] = "";
    }

    val = val.split(".");
    val[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 = val[0];
      str = "";
      offset = integer.length % posSeparator;
      len = integer.length;
      for (index = 0; index < len; index++) {
        str += integer.charAt(index); // ie6 only support charAt for sz.
        // -posSeparator so that won't trail separator on full length
        /*jshint -W018 */
        if (
          !((index - offset + 1) % posSeparator) &&
          index < len - posSeparator
        ) {
          str += group;
        }
      }
      val[0] = str;
    }

    val[1] = mask[1] && val[1] ? decimal + val[1] : "";
    // put back any negation, combine integer and fraction, and add back prefix & suffix
    return prefix + ((isNegative ? "-" : "") + val[0] + val[1]) + suffix;
  };

  ts.equations = {
    count: function(arry) {
      return arry.length;
    },
    sum: function(arry) {
      var index,
        len = arry.length,
        total = 0;
      for (index = 0; index < len; index++) {
        total += arry[index];
      }
      return total;
    },
    mean: function(arry) {
      var total = ts.equations.sum(arry);
      return total / arry.length;
    },
    median: function(arry, c) {
      var half,
        len = arry.length;
      if (len > 1) {
        // https://gist.github.com/caseyjustus/1166258
        arry.sort(function(a, b) {
          return a - b;
        });
        half = Math.floor(len / 2);
        return len % 2 ? arry[half] : (arry[half - 1] + arry[half]) / 2;
      }
      return math.invalid(c, "median", 1);
    },
    mode: function(arry) {
      // http://stackoverflow.com/a/3451640/145346
      var index,
        el,
        m,
        modeMap = {},
        maxCount = 1,
        modes = [arry[0]];
      for (index = 0; index < arry.length; index++) {
        el = arry[index];
        modeMap[el] = modeMap[el] ? modeMap[el] + 1 : 1;
        m = modeMap[el];
        if (m > maxCount) {
          modes = [el];
          maxCount = m;
        } else if (m === maxCount) {
          modes[modes.length] = el;
          maxCount = m;
        }
      }
      // 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, c) {
      var divisor,
        avg = ts.equations.mean(arry),
        v = 0,
        i = arry.length;
      while (i--) {
        v += Math.pow(arry[i] - avg, 2);
      }
      divisor = arry.length - (population ? 0 : 1);
      if (divisor === 0) {
        return math.invalid(c, "variance", 0);
      }
      v /= divisor;
      return v;
    },
    // variance (population)
    varp: function(arry, c) {
      return ts.equations.variance(arry, true, c);
    },
    // variance (sample)
    vars: function(arry, c) {
      return ts.equations.variance(arry, false, c);
    },
    // standard deviation (sample)
    stdevs: function(arry, c) {
      var vars = ts.equations.variance(arry, false, c);
      return Math.sqrt(vars);
    },
    // standard deviation (population)
    stdevp: function(arry, c) {
      var varp = ts.equations.variance(arry, true, c);
      return Math.sqrt(varp);
    }
  };

  // add new widget called repeatHeaders
  // ************************************
  ts.addWidget({
    id: "math",
    priority: 100,
    options: {
      math_data: "math",
      math_debug: false,
      // 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; },
      // math_completed called after all math calculations have completed
      math_completed: function(/* config */) {},
      // order of calculation; 'all' is last
      math_priority: ["row", "above", "below", "col"],
      // template for or just prepend the mask prefix & suffix with this HTML
      // e.g. '<span class="red">{content}</span>'
      math_prefix: "",
      math_suffix: "",
      // cell attribute containing the math value to use
      math_textAttr: "",
      // no matching math elements found (text added to cell)
      math_none: "N/A",
      math_event: "recalculate",
      // use this filter to target specific rows (e.g. ':visible', or ':not(.empty-row)')
      math_rowFilter: ""
    },
    init: function(table, thisWidget, c, wo) {
      // filterEnd fires after 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) {
          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
            if (!/filter/.test(e.type) && !init) {
              // redo data-column indexes on update
              math.setColumnIndexes(c);
            }
            math.recalculate(c, wo, init);
          }
        })
        .on(update, function() {
          setTimeout(function() {
            math.updateComplete(c);
          }, 40);
        });
      wo.math_isUpdating = false;
      // math widget initialized after table - see #946
      if (table.hasInitialized) {
        math.recalculate(c, wo, true);
      }
    },
    // 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;
      }
      c.$table
        .off(
          (math.events + " updateComplete.tsmath " + wo.math_event).replace(
            /\s+/g,
            " "
          )
        )
        .children()
        .children("tr")
        .children("[data-" + wo.math_data + "]")
        .empty();
    }
  });
})(jQuery);

Thanks! This "mathIgnore" update provides me with the correct column-based values when generically tested against count, mean & sum.

I noticed that quotes changed from 'single' to "double". I'm curious... is this a coding practice change or just the effect of using a different IDE/editor?

When I post code here in a GitHub comment, prettier-github bot automatically reformats the code to use double quotes.

Hey Mottie, grats on the new baby and job. (though not so new anymore...)

We just ran into this bug, same as OP: some columns have no data, and median and average are affected by the null = 0 in this case. When do you think you'll ship this fix out? I tried to bring your commented file into our system but couldn't get it working. I'm sure it was just something in our system, though.

Hi @VaelVictus!

Thanks!

I'll try to get a new release out this weekend some time. Happy Thanksgiving!

Sorry for the super-long delay! Version 2.31.2 is now available.

Just tested it; works perfectly, thank you very much!

This was what I've been looking for, didn't find it in the docs, only here.

Hi @szabizs!

It's in the docs here - https://mottie.github.io/tablesorter/docs/example-widget-math.html#attribute_settings - under "Ignoring cells". Please let me know if that is what you're referring to, or if there is something missing in that section so I can improve it. Thanks!

Hi @szabizs!

It's in the docs here - https://mottie.github.io/tablesorter/docs/example-widget-math.html#attribute_settings - under "Ignoring cells". Please let me know if that is what you're referring to, or if there is something missing in that section so I can improve it. Thanks!

@Mottie sorry for the confusion, I've searched for the work skip rather than ignore, that's why I wasn't able to find it 👍