1
0
Fork 0
mirror of https://github.com/koalyptus/TableFilter.git synced 2024-05-18 06:16:39 +02:00
TableFilter/src/extensions/colOps/colOps.js

483 lines
14 KiB
JavaScript
Raw Normal View History

import {Feature} from '../../feature';
2016-05-25 09:31:53 +02:00
import {createText, elm} from '../../dom';
2017-01-14 14:09:28 +01:00
import {isArray, isFn, isUndef, isEmpty, EMPTY_FN} from '../../types';
2017-01-11 04:13:57 +01:00
import {numSortAsc} from '../../sort';
2017-03-18 00:20:51 +01:00
import {FORMATTED_NUMBER} from '../../const';
import formatNumber from 'format-number';
2014-11-15 15:34:32 +01:00
2016-11-02 13:01:31 +01:00
const EVENTS = [
'after-filtering',
'after-page-change',
'after-page-length-change'
];
2017-01-14 14:09:28 +01:00
const SUM = 'sum';
const MEAN = 'mean';
const MIN = 'min';
const MAX = 'max';
const MEDIAN = 'median';
const Q1 = 'q1';
const Q3 = 'q3';
2016-09-01 10:27:02 +02:00
/**
* Column calculations extension
*/
export default class ColOps extends Feature {
2014-11-15 15:34:32 +01:00
/**
2016-09-01 10:27:02 +02:00
* Creates an instance of ColOps
*
* @param {TableFilter} tf TableFilter instance
* @param {Object} opts Configuration object
2014-11-15 15:34:32 +01:00
*/
constructor(tf, opts) {
super(tf, opts.name);
2014-11-15 15:34:32 +01:00
2016-09-01 10:27:02 +02:00
/**
* Callback fired before columns operations start
* @type {Function}
*/
2016-05-15 04:56:12 +02:00
this.onBeforeOperation = isFn(opts.on_before_operation) ?
2016-12-22 12:01:59 +01:00
opts.on_before_operation : EMPTY_FN;
2016-09-01 10:27:02 +02:00
/**
* Callback fired after columns operations are completed
* @type {Function}
*/
2016-05-15 04:56:12 +02:00
this.onAfterOperation = isFn(opts.on_after_operation) ?
2016-12-22 12:01:59 +01:00
opts.on_after_operation : EMPTY_FN;
2015-02-14 09:59:12 +01:00
2016-09-01 10:27:02 +02:00
/**
* Configuration options
* @type {Object}
*/
this.opts = opts;
2016-09-01 10:27:02 +02:00
2017-01-14 14:09:28 +01:00
/**
* List of DOM element IDs containing column's calculation result
* @type {Array}
*/
this.labelIds = opts.id || [];
/**
* List of columns' indexes for calculations
* @type {Array}
*/
this.colIndexes = opts.col || [];
/**
* List of operations - possible values: 'sum', 'mean', 'min', 'max',
* 'median', 'q1', 'q3'
* @type {Array}
*/
this.operations = opts.operation || [];
/**
* List of write methods used to write the result - possible values:
* 'innerHTML', 'setValue', 'createTextNode'
* @type {Array}
*/
this.outputTypes = opts.write_method || [];
2017-03-18 00:20:51 +01:00
/**
* 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 = opts.format_result || [];
2017-01-14 14:09:28 +01:00
/**
* List of row indexes displaying the results
* @type {Array}
*/
this.totRowIndexes = opts.tot_row_index || [];
/**
* List of row indexes excluded from calculations
* @type {Array}
*/
this.excludeRows = opts.exclude_row || [];
/**
* List of decimal precision for calculation results
* @type {Array}
*/
this.decimalPrecisions = isUndef(opts.decimal_precision) ?
2 : opts.decimal_precision;
2017-01-14 14:09:28 +01:00
this.enable();
2014-11-15 15:34:32 +01:00
}
2016-09-02 05:32:59 +02:00
/**
* Initializes ColOps instance
*/
init() {
if (this.initialized) {
return;
}
// subscribe to events
2017-01-14 14:09:28 +01:00
this.emitter.on(EVENTS, () => this.calcAll());
2017-01-14 14:09:28 +01:00
this.calcAll();
2016-09-01 10:27:02 +02:00
2017-01-14 14:09:28 +01:00
/** @inherited */
this.initialized = true;
}
2014-11-15 15:34:32 +01:00
/**
* Calculates columns' values
* Configuration options are stored in 'opts' property
2014-11-15 15:34:32 +01:00
* - '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.
*/
2017-01-14 14:09:28 +01:00
calcAll() {
2016-09-01 10:27:02 +02:00
let tf = this.tf;
if (!tf.isInitialized()) {
2014-11-15 15:34:32 +01:00
return;
}
2016-12-22 12:01:59 +01:00
this.onBeforeOperation(tf, this);
2016-09-02 05:32:59 +02:00
this.emitter.emit('before-column-operation', tf, this);
2014-11-15 15:34:32 +01:00
2017-03-18 00:20:51 +01:00
let { colIndexes, operations: colOperations, outputTypes,
totRowIndexes, excludeRows, formatResults } = this;
let decimalPrecisions = isUndef(this.decimalPrecisions) ?
2 : this.decimalPrecisions;
2014-11-15 15:34:32 +01:00
//nuovella: determine unique list of columns to operate on
let uIndexes = [];
2017-01-14 14:09:28 +01:00
colIndexes.forEach((val) => {
if (uIndexes.indexOf(val) === -1) {
uIndexes.push(val);
2014-11-15 15:34:32 +01:00
}
2017-01-14 14:09:28 +01:00
});
let nbCols = uIndexes.length,
rows = tf.dom().rows,
2017-01-14 14:09:28 +01:00
colValues = [];
for (let u = 0; u < nbCols; u++) {
2017-01-14 14:09:28 +01:00
//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
2017-01-15 07:26:35 +01:00
colValues.push(
2017-03-30 14:14:27 +02:00
tf.getVisibleColumnData(uIndexes[u], false, excludeRows)
2017-01-14 14:09:28 +01:00
);
let curValues = colValues[u];
//next: calculate all operations for this column
let result = 0,
operations = [],
precisions = [],
labels = [],
writeType,
2017-03-18 00:20:51 +01:00
formatResult = [],
idx = 0;
2017-01-14 14:09:28 +01:00
for (let k = 0; k < colIndexes.length; k++) {
if (colIndexes[k] !== uIndexes[u]) {
continue;
}
operations[idx] = (colOperations[k] || 'sum').toLowerCase();
2017-01-14 14:09:28 +01:00
precisions[idx] = decimalPrecisions[k];
labels[idx] = this.labelIds[k];
writeType = isArray(outputTypes) ? outputTypes[k] : null;
2017-03-18 00:20:51 +01:00
formatResult[idx] =
this.configureFormat(uIndexes[u], formatResults[k]);
idx++;
2014-11-15 15:34:32 +01:00
}
for (let i = 0; i < idx; i++) {
// emit values before column calculation
2017-01-15 04:57:52 +01:00
this.emitter.emit(
'before-column-calc',
tf,
this,
uIndexes[u],
curValues,
operations[i],
precisions[i]
);
result = Number(this.calc(curValues, operations[i], null));
2017-01-14 14:09:28 +01:00
// emit column calculation result
2017-01-14 14:09:28 +01:00
this.emitter.emit(
'column-calc',
2017-01-15 04:57:52 +01:00
tf,
2017-01-14 14:09:28 +01:00
this,
uIndexes[u],
result,
operations[i],
precisions[i]
2016-11-02 13:01:31 +01:00
);
2014-11-15 15:34:32 +01:00
// write result in expected DOM element
2017-01-14 14:09:28 +01:00
this.writeResult(
result,
labels[i],
2017-01-10 04:07:16 +01:00
writeType,
2017-03-18 00:20:51 +01:00
precisions[i],
formatResult[i]
2017-01-14 14:09:28 +01:00
);
2017-01-11 04:13:57 +01:00
2017-01-14 14:09:28 +01:00
}//for i
2014-11-15 15:34:32 +01:00
2017-01-14 14:09:28 +01:00
// row(s) with result are always visible
let totRow = totRowIndexes && totRowIndexes[u] ?
rows[totRowIndexes[u]] : null;
if (totRow) {
totRow.style.display = '';
}
}//for u
2014-11-15 15:34:32 +01:00
2016-12-22 12:01:59 +01:00
this.onAfterOperation(tf, this);
2016-09-02 05:32:59 +02:00
this.emitter.emit('after-column-operation', tf, this);
2014-11-15 15:34:32 +01:00
}
2017-01-14 14:09:28 +01:00
/**
* Make desired calculation on specified column.
* @param {Number} colIndex Column index
2017-01-16 03:12:21 +01:00
* @param {String} [operation=SUM] Operation type
* @param {Number} precision Decimal precision
2017-01-14 14:09:28 +01:00
* @returns {Number}
*/
columnCalc(colIndex, operation = SUM, precision) {
let excludeRows = this.excludeRows || [];
2017-03-30 14:14:27 +02:00
let colValues = tf.getVisibleColumnData(colIndex, false, excludeRows);
2017-03-31 05:56:51 +02:00
return Number(this.calc(colValues, operation, precision));
2017-01-14 14:09:28 +01:00
}
/**
* 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) {
2017-01-13 07:05:39 +01:00
colValues = this.sortColumnValues(colValues, numSortAsc);
}
switch (operation) {
2017-01-14 14:09:28 +01:00
case MEAN:
2017-01-15 04:57:52 +01:00
result = this.calcMean(colValues);
2017-01-13 07:05:39 +01:00
break;
2017-01-14 14:09:28 +01:00
case SUM:
2017-01-15 04:57:52 +01:00
result = this.calcSum(colValues);
2017-01-13 07:05:39 +01:00
break;
2017-01-14 14:09:28 +01:00
case MIN:
2017-01-13 07:05:39 +01:00
result = this.calcMin(colValues);
break;
2017-01-14 14:09:28 +01:00
case MAX:
2017-01-13 07:05:39 +01:00
result = this.calcMax(colValues);
break;
2017-01-14 14:09:28 +01:00
case MEDIAN:
2017-01-13 07:05:39 +01:00
result = this.calcMedian(colValues);
break;
2017-01-14 14:09:28 +01:00
case Q1:
2017-01-13 07:05:39 +01:00
result = this.calcQ1(colValues);
break;
2017-01-14 14:09:28 +01:00
case Q3:
2017-01-13 07:05:39 +01:00
result = this.calcQ3(colValues);
break;
}
2017-01-14 14:09:28 +01:00
return isEmpty(precision) ? result : result.toFixed(precision);
2017-01-13 07:05:39 +01:00
}
2017-01-14 14:09:28 +01:00
/**
* Calculate the sum of passed values.
* @param {Array} [values=[]] List of values
* @returns {Number}
*/
2017-01-11 07:13:38 +01:00
calcSum(values = []) {
2017-01-15 04:57:52 +01:00
if (isEmpty(values)) {
return 0;
}
2017-01-18 11:29:00 +01:00
let result = values.reduce((x, y) => Number(x) + Number(y));
return result;
2017-01-11 07:13:38 +01:00
}
2017-01-15 04:57:52 +01:00
/**
* 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);
}
2017-01-14 14:09:28 +01:00
/**
* Calculate the max value of passed values.
* @param {Array} [values=[]] List of values
* @returns {Number}
*/
2017-01-11 04:13:57 +01:00
calcMax(values = []) {
return Math.max.apply(null, values);
}
2017-01-14 14:09:28 +01:00
/**
* Calculate the min value of passed values.
* @param {Array} [values=[]] List of values
* @returns {Number}
*/
2017-01-11 04:13:57 +01:00
calcMin(values = []) {
return Math.min.apply(null, values);
}
2017-01-14 14:09:28 +01:00
/**
* Calculate the median of passed values.
* @param {Array} [values=[]] List of values
* @returns {Number}
*/
2017-01-12 04:07:21 +01:00
calcMedian(values = []) {
let nbValues = values.length;
2017-01-11 07:13:38 +01:00
let aux = 0;
if (nbValues % 2 === 1) {
aux = Math.floor(nbValues / 2);
2017-01-14 14:09:28 +01:00
return Number(values[aux]);
2017-01-11 07:13:38 +01:00
}
2017-01-18 11:29:00 +01:00
return (Number(values[nbValues / 2]) +
Number(values[((nbValues / 2) - 1)])) / 2;
2017-01-11 07:13:38 +01:00
}
2017-01-14 14:09:28 +01:00
/**
* Calculate the lower quartile of passed values.
* @param {Array} [values=[]] List of values
* @returns {Number}
*/
2017-01-12 04:07:21 +01:00
calcQ1(values = []) {
let nbValues = values.length;
2017-01-11 07:13:38 +01:00
let posa = 0.0;
posa = Math.floor(nbValues / 4);
if (4 * posa === nbValues) {
2017-01-18 11:29:00 +01:00
return (Number(values[posa - 1]) +
Number(values[posa])) / 2;
2017-01-11 07:13:38 +01:00
}
2017-01-14 14:09:28 +01:00
return Number(values[posa]);
2017-01-11 07:13:38 +01:00
}
2017-01-14 14:09:28 +01:00
/**
* Calculate the upper quartile of passed values.
* @param {Array} [values=[]] List of values
* @returns {Number}
*/
2017-01-12 04:07:21 +01:00
calcQ3(values = []) {
let nbValues = values.length;
2017-01-11 07:13:38 +01:00
let posa = 0.0;
let posb = 0.0;
posa = Math.floor(nbValues / 4);
if (4 * posa === nbValues) {
posb = 3 * posa;
2017-01-18 11:29:00 +01:00
return (Number(values[posb]) +
Number(values[posb - 1])) / 2;
2017-01-11 07:13:38 +01:00
}
2017-01-14 14:09:28 +01:00
return Number(values[nbValues - posa - 1]);
2017-01-11 07:13:38 +01:00
}
2017-01-14 14:09:28 +01:00
/**
* Sort passed values with supplied sorter function.
* @param {Array} [values=[]] List of values to be sorted
* @param {Function} sorter Sorter function
* @returns {Array}
*/
2017-01-11 04:13:57 +01:00
sortColumnValues(values = [], sorter) {
return values.sort(sorter);
}
2017-01-14 14:09:28 +01:00
/**
* 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
*/
2017-03-18 00:20:51 +01:00
writeResult(result = 0, label, writeType = 'innerhtml',
precision = 2, format = {}) {
2017-01-10 04:07:16 +01:00
let labelElm = elm(label);
2017-01-10 04:07:16 +01:00
if (!labelElm) {
return;
}
result = result.toFixed(precision);
if (isNaN(result) || !isFinite(result)) {
result = '';
2017-03-18 00:20:51 +01:00
} else {
result = formatNumber(format)(result);
2017-01-10 04:07:16 +01:00
}
switch (writeType.toLowerCase()) {
case 'innerhtml':
labelElm.innerHTML = result;
break;
case 'setvalue':
labelElm.value = result;
break;
case 'createtextnode':
2017-01-14 14:09:28 +01:00
let oldNode = labelElm.firstChild;
let txtNode = createText(result);
labelElm.replaceChild(txtNode, oldNode);
2017-01-10 04:07:16 +01:00
break;
}
}
2017-03-18 00:20:51 +01:00
/**
* 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;
}
2017-01-14 14:09:28 +01:00
/** Remove extension */
destroy() {
if (!this.initialized) {
return;
}
// unsubscribe to events
2017-01-14 14:09:28 +01:00
this.emitter.off(EVENTS, () => this.calcAll());
2016-11-02 13:01:31 +01:00
this.initialized = false;
}
}