Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

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

Closed
JamoCA opened this issue Mar 14, 2018 · 22 comments
Closed

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

JamoCA opened this issue Mar 14, 2018 · 22 comments

Comments

@JamoCA
Copy link

JamoCA commented Mar 14, 2018

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?

@Mottie
Copy link
Owner

Mottie commented Mar 14, 2018

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.

@Sneezeplease
Copy link

Hello,

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

@Mottie
Copy link
Owner

Mottie commented Mar 21, 2018

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?

@JamoCA
Copy link
Author

JamoCA commented Mar 23, 2018

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.)

@Mottie
Copy link
Owner

Mottie commented Apr 26, 2018

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

@JamoCA
Copy link
Author

JamoCA commented Jul 10, 2018

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.

@Mottie
Copy link
Owner

Mottie commented Jul 10, 2018

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

@JamoCA
Copy link
Author

JamoCA commented Oct 23, 2018

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?

@Mottie
Copy link
Owner

Mottie commented Oct 23, 2018

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!

@JamoCA
Copy link
Author

JamoCA commented Apr 2, 2019

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. :(

@Mottie
Copy link
Owner

Mottie commented Apr 2, 2019

Sorry :(

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

@JamoCA
Copy link
Author

JamoCA commented Nov 18, 2019

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

@Mottie
Copy link
Owner

Mottie commented Nov 19, 2019

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);

@JamoCA
Copy link
Author

JamoCA commented Nov 19, 2019

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?

@Mottie
Copy link
Owner

Mottie commented Nov 20, 2019

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

@VaelVictus
Copy link

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.

@Mottie
Copy link
Owner

Mottie commented Nov 27, 2019

Hi @VaelVictus!

Thanks!

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

@Mottie Mottie closed this as completed in 755aa8a Dec 1, 2019
@Mottie
Copy link
Owner

Mottie commented Dec 1, 2019

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

@VaelVictus
Copy link

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

@szabizs
Copy link

szabizs commented Sep 11, 2020

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

@Mottie
Copy link
Owner

Mottie commented Sep 11, 2020

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!

@szabizs
Copy link

szabizs commented Sep 14, 2020

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 👍

# for free to join this conversation on GitHub. Already have an account? # to comment
Projects
None yet
Development

No branches or pull requests

5 participants