mirror of
https://github.com/koalyptus/TableFilter.git
synced 2024-06-28 10:20:17 +02:00
480 lines
14 KiB
JavaScript
480 lines
14 KiB
JavaScript
import {Feature} from '../../feature';
|
|
import {createText, elm} from '../../dom';
|
|
import {isArray, isEmpty, EMPTY_FN} from '../../types';
|
|
import {numSortAsc} from '../../sort';
|
|
import {FORMATTED_NUMBER} from '../../const';
|
|
import formatNumber from 'format-number';
|
|
import {defaultsFn, defaultsArr} from '../../settings';
|
|
|
|
const EVENTS = [
|
|
'after-filtering',
|
|
'after-page-change',
|
|
'after-page-length-change'
|
|
];
|
|
|
|
const SUM = 'sum';
|
|
const MEAN = 'mean';
|
|
const MIN = 'min';
|
|
const MAX = 'max';
|
|
const MEDIAN = 'median';
|
|
const Q1 = 'q1';
|
|
const Q3 = 'q3';
|
|
|
|
/**
|
|
* Column calculations extension
|
|
*/
|
|
export default class ColOps extends Feature {
|
|
|
|
/**
|
|
* Creates an instance of ColOps
|
|
*
|
|
* @param {TableFilter} tf TableFilter instance
|
|
* @param {Object} opts Configuration object
|
|
*/
|
|
constructor(tf, opts) {
|
|
super(tf, opts.name);
|
|
|
|
/**
|
|
* Callback fired before columns operations start
|
|
* @type {Function}
|
|
*/
|
|
this.onBeforeOperation = defaultsFn(opts.on_before_operation, EMPTY_FN);
|
|
|
|
/**
|
|
* Callback fired after columns operations are completed
|
|
* @type {Function}
|
|
*/
|
|
this.onAfterOperation = defaultsFn(opts.on_after_operation, EMPTY_FN);
|
|
|
|
/**
|
|
* Configuration options
|
|
* @type {Object}
|
|
*/
|
|
this.opts = opts;
|
|
|
|
/**
|
|
* List of DOM element IDs containing column's calculation result
|
|
* @type {Array}
|
|
*/
|
|
this.labelIds = defaultsArr(opts.id, []);
|
|
|
|
/**
|
|
* List of columns' indexes for calculations
|
|
* @type {Array}
|
|
*/
|
|
this.colIndexes = defaultsArr(opts.col, []);
|
|
|
|
/**
|
|
* List of operations - possible values: 'sum', 'mean', 'min', 'max',
|
|
* 'median', 'q1', 'q3'
|
|
* @type {Array}
|
|
*/
|
|
this.operations = defaultsArr(opts.operation, []);
|
|
|
|
/**
|
|
* List of write methods used to write the result - possible values:
|
|
* 'innerHTML', 'setValue', 'createTextNode'
|
|
* @type {Array}
|
|
*/
|
|
this.outputTypes = defaultsArr(opts.write_method, []);
|
|
|
|
/**
|
|
* List of format objects used for formatting the result -
|
|
* refer to https://github.com/componitable/format-number to check
|
|
* configuration options
|
|
* @type {Array}
|
|
*/
|
|
this.formatResults = defaultsArr(opts.format_result, []);
|
|
|
|
/**
|
|
* List of row indexes displaying the results
|
|
* @type {Array}
|
|
*/
|
|
this.totRowIndexes = defaultsArr(opts.tot_row_index, []);
|
|
|
|
/**
|
|
* List of row indexes excluded from calculations
|
|
* @type {Array}
|
|
*/
|
|
this.excludeRows = defaultsArr(opts.exclude_row, []);
|
|
|
|
/**
|
|
* List of decimal precision for calculation results
|
|
* @type {Array}
|
|
*/
|
|
this.decimalPrecisions = defaultsArr(opts.decimal_precision, 2);
|
|
|
|
this.enable();
|
|
}
|
|
|
|
/**
|
|
* Initializes ColOps instance
|
|
*/
|
|
init() {
|
|
if (this.initialized) {
|
|
return;
|
|
}
|
|
// subscribe to events
|
|
this.emitter.on(EVENTS, () => this.calcAll());
|
|
|
|
this.calcAll();
|
|
|
|
/** @inherited */
|
|
this.initialized = true;
|
|
}
|
|
|
|
/**
|
|
* Calculates columns' values
|
|
* Configuration options are stored in 'opts' property
|
|
* - 'id' contains ids of elements showing result (array)
|
|
* - 'col' contains the columns' indexes (array)
|
|
* - 'operation' contains operation type (array, values: 'sum', 'mean',
|
|
* 'min', 'max', 'median', 'q1', 'q3')
|
|
* - 'write_method' array defines which method to use for displaying the
|
|
* result (innerHTML, setValue, createTextNode) - default: 'innerHTML'
|
|
* - 'tot_row_index' defines in which row results are displayed
|
|
* (integers array)
|
|
*
|
|
* - changes made by Nuovella:
|
|
* (1) optimized the routine (now it will only process each column once),
|
|
* (2) added calculations for the median, lower and upper quartile.
|
|
*/
|
|
calcAll() {
|
|
let tf = this.tf;
|
|
if (!tf.isInitialized()) {
|
|
return;
|
|
}
|
|
|
|
this.onBeforeOperation(tf, this);
|
|
this.emitter.emit('before-column-operation', tf, this);
|
|
|
|
let { colIndexes, operations: colOperations, outputTypes,
|
|
totRowIndexes, excludeRows, formatResults,
|
|
decimalPrecisions } = this;
|
|
|
|
//nuovella: determine unique list of columns to operate on
|
|
let uIndexes = [];
|
|
colIndexes.forEach((val) => {
|
|
if (uIndexes.indexOf(val) === -1) {
|
|
uIndexes.push(val);
|
|
}
|
|
});
|
|
|
|
let nbCols = uIndexes.length,
|
|
rows = tf.dom().rows,
|
|
colValues = [];
|
|
|
|
for (let u = 0; u < nbCols; u++) {
|
|
//this retrieves col values
|
|
//use uIndexes because we only want to pass through this loop
|
|
//once for each column get the values in this unique column
|
|
colValues.push(
|
|
tf.getVisibleColumnData(uIndexes[u], false, excludeRows)
|
|
);
|
|
|
|
let curValues = colValues[u];
|
|
|
|
//next: calculate all operations for this column
|
|
let result = 0,
|
|
operations = [],
|
|
precisions = [],
|
|
labels = [],
|
|
writeType,
|
|
formatResult = [],
|
|
idx = 0;
|
|
|
|
for (let k = 0; k < colIndexes.length; k++) {
|
|
if (colIndexes[k] !== uIndexes[u]) {
|
|
continue;
|
|
}
|
|
operations[idx] = (colOperations[k] || 'sum').toLowerCase();
|
|
precisions[idx] = decimalPrecisions[k];
|
|
labels[idx] = this.labelIds[k];
|
|
writeType = isArray(outputTypes) ? outputTypes[k] : null;
|
|
formatResult[idx] =
|
|
this.configureFormat(uIndexes[u], formatResults[k]);
|
|
idx++;
|
|
}
|
|
|
|
for (let i = 0; i < idx; i++) {
|
|
// emit values before column calculation
|
|
this.emitter.emit(
|
|
'before-column-calc',
|
|
tf,
|
|
this,
|
|
uIndexes[u],
|
|
curValues,
|
|
operations[i],
|
|
precisions[i]
|
|
);
|
|
|
|
result = Number(this.calc(curValues, operations[i], null));
|
|
|
|
// emit column calculation result
|
|
this.emitter.emit(
|
|
'column-calc',
|
|
tf,
|
|
this,
|
|
uIndexes[u],
|
|
result,
|
|
operations[i],
|
|
precisions[i]
|
|
);
|
|
|
|
// write result in expected DOM element
|
|
this.writeResult(
|
|
result,
|
|
labels[i],
|
|
writeType,
|
|
precisions[i],
|
|
formatResult[i]
|
|
);
|
|
|
|
}//for i
|
|
|
|
// row(s) with result are always visible
|
|
let totRow = totRowIndexes && totRowIndexes[u] ?
|
|
rows[totRowIndexes[u]] : null;
|
|
if (totRow) {
|
|
totRow.style.display = '';
|
|
}
|
|
}//for u
|
|
|
|
this.onAfterOperation(tf, this);
|
|
this.emitter.emit('after-column-operation', tf, this);
|
|
}
|
|
|
|
/**
|
|
* Make desired calculation on specified column.
|
|
* @param {Number} colIndex Column index
|
|
* @param {String} [operation=SUM] Operation type
|
|
* @param {Number} precision Decimal precision
|
|
* @returns {Number}
|
|
*/
|
|
columnCalc(colIndex, operation = SUM, precision) {
|
|
let excludeRows = this.excludeRows || [];
|
|
let colValues = tf.getVisibleColumnData(colIndex, false, excludeRows);
|
|
|
|
return Number(this.calc(colValues, operation, precision));
|
|
}
|
|
|
|
/**
|
|
* Make calculation on passed values.
|
|
* @param {Array} values List of values
|
|
* @param {String} [operation=SUM] Optional operation type
|
|
* @param {Number} precision Optional result precision
|
|
* @returns {Number}
|
|
* @private
|
|
*/
|
|
calc(colValues, operation = SUM, precision) {
|
|
let result = 0;
|
|
|
|
if (operation === Q1 || operation === Q3 || operation === MEDIAN) {
|
|
colValues = this.sortColumnValues(colValues, numSortAsc);
|
|
}
|
|
|
|
switch (operation) {
|
|
case MEAN:
|
|
result = this.calcMean(colValues);
|
|
break;
|
|
case SUM:
|
|
result = this.calcSum(colValues);
|
|
break;
|
|
case MIN:
|
|
result = this.calcMin(colValues);
|
|
break;
|
|
case MAX:
|
|
result = this.calcMax(colValues);
|
|
break;
|
|
case MEDIAN:
|
|
result = this.calcMedian(colValues);
|
|
break;
|
|
case Q1:
|
|
result = this.calcQ1(colValues);
|
|
break;
|
|
case Q3:
|
|
result = this.calcQ3(colValues);
|
|
break;
|
|
}
|
|
|
|
return isEmpty(precision) ? result : result.toFixed(precision);
|
|
}
|
|
|
|
/**
|
|
* Calculate the sum of passed values.
|
|
* @param {Array} [values=[]] List of values
|
|
* @returns {Number}
|
|
*/
|
|
calcSum(values = []) {
|
|
if (isEmpty(values)) {
|
|
return 0;
|
|
}
|
|
let result = values.reduce((x, y) => Number(x) + Number(y));
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Calculate the mean of passed values.
|
|
* @param {Array} [values=[]] List of values
|
|
* @returns {Number}
|
|
*/
|
|
calcMean(values = []) {
|
|
let result = this.calcSum(values) / values.length;
|
|
return Number(result);
|
|
}
|
|
|
|
/**
|
|
* Calculate the max value of passed values.
|
|
* @param {Array} [values=[]] List of values
|
|
* @returns {Number}
|
|
*/
|
|
calcMax(values = []) {
|
|
return Math.max.apply(null, values);
|
|
}
|
|
|
|
/**
|
|
* Calculate the min value of passed values.
|
|
* @param {Array} [values=[]] List of values
|
|
* @returns {Number}
|
|
*/
|
|
calcMin(values = []) {
|
|
return Math.min.apply(null, values);
|
|
}
|
|
|
|
/**
|
|
* Calculate the median of passed values.
|
|
* @param {Array} [values=[]] List of values
|
|
* @returns {Number}
|
|
*/
|
|
calcMedian(values = []) {
|
|
let nbValues = values.length;
|
|
let aux = 0;
|
|
if (nbValues % 2 === 1) {
|
|
aux = Math.floor(nbValues / 2);
|
|
return Number(values[aux]);
|
|
}
|
|
return (Number(values[nbValues / 2]) +
|
|
Number(values[((nbValues / 2) - 1)])) / 2;
|
|
}
|
|
|
|
/**
|
|
* Calculate the lower quartile of passed values.
|
|
* @param {Array} [values=[]] List of values
|
|
* @returns {Number}
|
|
*/
|
|
calcQ1(values = []) {
|
|
let nbValues = values.length;
|
|
let posa = 0.0;
|
|
posa = Math.floor(nbValues / 4);
|
|
if (4 * posa === nbValues) {
|
|
return (Number(values[posa - 1]) +
|
|
Number(values[posa])) / 2;
|
|
}
|
|
return Number(values[posa]);
|
|
}
|
|
|
|
/**
|
|
* Calculate the upper quartile of passed values.
|
|
* @param {Array} [values=[]] List of values
|
|
* @returns {Number}
|
|
*/
|
|
calcQ3(values = []) {
|
|
let nbValues = values.length;
|
|
let posa = 0.0;
|
|
let posb = 0.0;
|
|
posa = Math.floor(nbValues / 4);
|
|
if (4 * posa === nbValues) {
|
|
posb = 3 * posa;
|
|
return (Number(values[posb]) +
|
|
Number(values[posb - 1])) / 2;
|
|
}
|
|
return Number(values[nbValues - posa - 1]);
|
|
}
|
|
|
|
/**
|
|
* Sort passed values with supplied sorter function.
|
|
* @param {Array} [values=[]] List of values to be sorted
|
|
* @param {Function} sorter Sorter function
|
|
* @returns {Array}
|
|
*/
|
|
sortColumnValues(values = [], sorter) {
|
|
return values.sort(sorter);
|
|
}
|
|
|
|
/**
|
|
* Write calculation result in passed DOM element with supplied write method
|
|
* and decimal precision.
|
|
* @param {Number} [result=0] Calculation result
|
|
* @param {DOMElement} label DOM element
|
|
* @param {String} [writeType='innerhtml'] Write method
|
|
* @param {Number} [precision=2] Applied decimal precision
|
|
* @private
|
|
*/
|
|
writeResult(result = 0, label, writeType = 'innerhtml',
|
|
precision = 2, format = {}) {
|
|
let labelElm = elm(label);
|
|
|
|
if (!labelElm) {
|
|
return;
|
|
}
|
|
|
|
result = result.toFixed(precision);
|
|
if (isNaN(result) || !isFinite(result)) {
|
|
result = '';
|
|
} else {
|
|
result = formatNumber(format)(result);
|
|
}
|
|
|
|
switch (writeType.toLowerCase()) {
|
|
case 'innerhtml':
|
|
labelElm.innerHTML = result;
|
|
break;
|
|
case 'setvalue':
|
|
labelElm.value = result;
|
|
break;
|
|
case 'createtextnode':
|
|
let oldNode = labelElm.firstChild;
|
|
let txtNode = createText(result);
|
|
labelElm.replaceChild(txtNode, oldNode);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configure the format options used to format the operation result based
|
|
* on column type.
|
|
* @param {Number} colIndex Column index
|
|
* @param {Object} [format={}] Format object
|
|
* @returns {Object}
|
|
* @private
|
|
*/
|
|
configureFormat(colIndex, format = {}) {
|
|
let tf = this.tf;
|
|
if (tf.hasType(colIndex, [FORMATTED_NUMBER])) {
|
|
let colType = tf.colTypes[colIndex];
|
|
if (colType.decimal && !format.decimal) {
|
|
format.decimal = colType.decimal;
|
|
}
|
|
if (colType.thousands && !format.integerSeparator) {
|
|
format.integerSeparator = colType.thousands;
|
|
}
|
|
} else {
|
|
format.decimal = format.decimal || '';
|
|
format.integerSeparator = format.integerSeparator || '';
|
|
}
|
|
return format;
|
|
}
|
|
|
|
/** Remove extension */
|
|
destroy() {
|
|
if (!this.initialized) {
|
|
return;
|
|
}
|
|
// unsubscribe to events
|
|
this.emitter.off(EVENTS, () => this.calcAll());
|
|
|
|
this.initialized = false;
|
|
}
|
|
|
|
}
|