UNPKG

14.9 kBJavaScriptView Raw
1import {Feature} from '../../feature';
2import {createText, elm} from '../../dom';
3import {isArray, isEmpty, EMPTY_FN} from '../../types';
4import {numSortAsc} from '../../sort';
5import {FORMATTED_NUMBER} from '../../const';
6import formatNumber from 'format-number';
7import {defaultsFn, defaultsArr} from '../../settings';
8import {bound} from '../../event';
9
10const EVENTS = [
11 'after-filtering',
12 'after-page-change',
13 'after-page-length-change'
14];
15
16const SUM = 'sum';
17const MEAN = 'mean';
18const MIN = 'min';
19const MAX = 'max';
20const MEDIAN = 'median';
21const Q1 = 'q1';
22const Q3 = 'q3';
23
24/**
25 * Column calculations extension
26 */
27export default class ColOps extends Feature {
28
29 /**
30 * Creates an instance of ColOps
31 *
32 * @param {TableFilter} tf TableFilter instance
33 * @param {Object} opts Configuration object
34 */
35 constructor(tf, opts) {
36 super(tf, ColOps);
37
38 /**
39 * Callback fired before columns operations start
40 * @type {Function}
41 */
42 this.onBeforeOperation = defaultsFn(opts.on_before_operation, EMPTY_FN);
43
44 /**
45 * Callback fired after columns operations are completed
46 * @type {Function}
47 */
48 this.onAfterOperation = defaultsFn(opts.on_after_operation, EMPTY_FN);
49
50 /**
51 * Configuration options
52 * @type {Object}
53 */
54 this.opts = opts;
55
56 /**
57 * List of DOM element IDs containing column's calculation result
58 * @type {Array}
59 */
60 this.labelIds = defaultsArr(opts.id, []);
61
62 /**
63 * List of columns' indexes for calculations
64 * @type {Array}
65 */
66 this.colIndexes = defaultsArr(opts.col, []);
67
68 /**
69 * List of operations - possible values: 'sum', 'mean', 'min', 'max',
70 * 'median', 'q1', 'q3'
71 * @type {Array}
72 */
73 this.operations = defaultsArr(opts.operation, []);
74
75 /**
76 * List of write methods used to write the result - possible values:
77 * 'innerHTML', 'setValue', 'createTextNode'
78 * @type {Array}
79 */
80 this.outputTypes = defaultsArr(opts.write_method, []);
81
82 /**
83 * List of format objects used for formatting the result -
84 * refer to https://github.com/componitable/format-number to check
85 * configuration options
86 * @type {Array}
87 */
88 this.formatResults = defaultsArr(opts.format_result, []);
89
90 /**
91 * List of row indexes displaying the results
92 * @type {Array}
93 */
94 this.totRowIndexes = defaultsArr(opts.tot_row_index, []);
95
96 /**
97 * List of row indexes excluded from calculations
98 * @type {Array}
99 */
100 this.excludeRows = defaultsArr(opts.exclude_row, []);
101
102 /**
103 * List of decimal precision for calculation results
104 * @type {Array}
105 */
106 this.decimalPrecisions = defaultsArr(opts.decimal_precision, 2);
107
108 this.enable();
109 }
110
111 /**
112 * Initializes ColOps instance
113 */
114 init() {
115 if (this.initialized) {
116 return;
117 }
118 // subscribe to events
119 this.emitter.on(EVENTS, bound(this.calcAll, this));
120
121 this.calcAll();
122
123 /** @inherited */
124 this.initialized = true;
125 }
126
127 /**
128 * Calculates columns' values
129 * Configuration options are stored in 'opts' property
130 * - 'id' contains ids of elements showing result (array)
131 * - 'col' contains the columns' indexes (array)
132 * - 'operation' contains operation type (array, values: 'sum', 'mean',
133 * 'min', 'max', 'median', 'q1', 'q3')
134 * - 'write_method' array defines which method to use for displaying the
135 * result (innerHTML, setValue, createTextNode) - default: 'innerHTML'
136 * - 'tot_row_index' defines in which row results are displayed
137 * (integers array)
138 *
139 * - changes made by Nuovella:
140 * (1) optimized the routine (now it will only process each column once),
141 * (2) added calculations for the median, lower and upper quartile.
142 */
143 calcAll() {
144 let tf = this.tf;
145 if (!tf.isInitialized()) {
146 return;
147 }
148
149 this.onBeforeOperation(tf, this);
150 this.emitter.emit('before-column-operation', tf, this);
151
152 let { colIndexes, operations: colOperations, outputTypes,
153 totRowIndexes, excludeRows, formatResults,
154 decimalPrecisions } = this;
155
156 //nuovella: determine unique list of columns to operate on
157 let uIndexes = [];
158 colIndexes.forEach((val) => {
159 if (uIndexes.indexOf(val) === -1) {
160 uIndexes.push(val);
161 }
162 });
163
164 let nbCols = uIndexes.length,
165 rows = tf.dom().rows,
166 colValues = [];
167
168 for (let u = 0; u < nbCols; u++) {
169 //this retrieves col values
170 //use uIndexes because we only want to pass through this loop
171 //once for each column get the values in this unique column
172 colValues.push(
173 tf.getVisibleColumnData(uIndexes[u], false, excludeRows)
174 );
175
176 let curValues = colValues[u];
177
178 //next: calculate all operations for this column
179 let result = 0,
180 operations = [],
181 precisions = [],
182 labels = [],
183 writeType,
184 formatResult = [],
185 idx = 0;
186
187 for (let k = 0; k < colIndexes.length; k++) {
188 if (colIndexes[k] !== uIndexes[u]) {
189 continue;
190 }
191 operations[idx] = (colOperations[k] || 'sum').toLowerCase();
192 precisions[idx] = decimalPrecisions[k];
193 labels[idx] = this.labelIds[k];
194 writeType = isArray(outputTypes) ? outputTypes[k] : null;
195 formatResult[idx] =
196 this.configureFormat(uIndexes[u], formatResults[k]);
197 idx++;
198 }
199
200 for (let i = 0; i < idx; i++) {
201 // emit values before column calculation
202 this.emitter.emit(
203 'before-column-calc',
204 tf,
205 this,
206 uIndexes[u],
207 curValues,
208 operations[i],
209 precisions[i]
210 );
211
212 result = Number(this.calc(curValues, operations[i], null));
213
214 // emit column calculation result
215 this.emitter.emit(
216 'column-calc',
217 tf,
218 this,
219 uIndexes[u],
220 result,
221 operations[i],
222 precisions[i]
223 );
224
225 // write result in expected DOM element
226 this.writeResult(
227 result,
228 labels[i],
229 writeType,
230 precisions[i],
231 formatResult[i]
232 );
233
234 }//for i
235
236 // row(s) with result are always visible
237 let totRow = totRowIndexes && totRowIndexes[u] ?
238 rows[totRowIndexes[u]] : null;
239 if (totRow) {
240 totRow.style.display = '';
241 }
242 }//for u
243
244 this.onAfterOperation(tf, this);
245 this.emitter.emit('after-column-operation', tf, this);
246 }
247
248 /**
249 * Make desired calculation on specified column.
250 * @param {Number} colIndex Column index
251 * @param {String} [operation=SUM] Operation type
252 * @param {Number} precision Decimal precision
253 * @returns {Number}
254 */
255 columnCalc(colIndex, operation = SUM, precision) {
256 let excludeRows = this.excludeRows || [];
257 let colValues = tf.getVisibleColumnData(colIndex, false, excludeRows);
258
259 return Number(this.calc(colValues, operation, precision));
260 }
261
262 /**
263 * Make calculation on passed values.
264 * @param {Array} values List of values
265 * @param {String} [operation=SUM] Optional operation type
266 * @param {Number} precision Optional result precision
267 * @returns {Number}
268 * @private
269 */
270 calc(colValues, operation = SUM, precision) {
271 let result = 0;
272
273 if (operation === Q1 || operation === Q3 || operation === MEDIAN) {
274 colValues = this.sortColumnValues(colValues, numSortAsc);
275 }
276
277 switch (operation) {
278 case MEAN:
279 result = this.calcMean(colValues);
280 break;
281 case SUM:
282 result = this.calcSum(colValues);
283 break;
284 case MIN:
285 result = this.calcMin(colValues);
286 break;
287 case MAX:
288 result = this.calcMax(colValues);
289 break;
290 case MEDIAN:
291 result = this.calcMedian(colValues);
292 break;
293 case Q1:
294 result = this.calcQ1(colValues);
295 break;
296 case Q3:
297 result = this.calcQ3(colValues);
298 break;
299 }
300
301 return isEmpty(precision) ? result : result.toFixed(precision);
302 }
303
304 /**
305 * Calculate the sum of passed values.
306 * @param {Array} [values=[]] List of values
307 * @returns {Number}
308 */
309 calcSum(values = []) {
310 if (isEmpty(values)) {
311 return 0;
312 }
313 let result = values.reduce((x, y) => Number(x) + Number(y));
314 return result;
315 }
316
317 /**
318 * Calculate the mean of passed values.
319 * @param {Array} [values=[]] List of values
320 * @returns {Number}
321 */
322 calcMean(values = []) {
323 let result = this.calcSum(values) / values.length;
324 return Number(result);
325 }
326
327 /**
328 * Calculate the max value of passed values.
329 * @param {Array} [values=[]] List of values
330 * @returns {Number}
331 */
332 calcMax(values = []) {
333 return Math.max.apply(null, values);
334 }
335
336 /**
337 * Calculate the min value of passed values.
338 * @param {Array} [values=[]] List of values
339 * @returns {Number}
340 */
341 calcMin(values = []) {
342 return Math.min.apply(null, values);
343 }
344
345 /**
346 * Calculate the median of passed values.
347 * @param {Array} [values=[]] List of values
348 * @returns {Number}
349 */
350 calcMedian(values = []) {
351 let nbValues = values.length;
352 let aux = 0;
353 if (nbValues % 2 === 1) {
354 aux = Math.floor(nbValues / 2);
355 return Number(values[aux]);
356 }
357 return (Number(values[nbValues / 2]) +
358 Number(values[((nbValues / 2) - 1)])) / 2;
359 }
360
361 /**
362 * Calculate the lower quartile of passed values.
363 * @param {Array} [values=[]] List of values
364 * @returns {Number}
365 */
366 calcQ1(values = []) {
367 let nbValues = values.length;
368 let posa = 0.0;
369 posa = Math.floor(nbValues / 4);
370 if (4 * posa === nbValues) {
371 return (Number(values[posa - 1]) +
372 Number(values[posa])) / 2;
373 }
374 return Number(values[posa]);
375 }
376
377 /**
378 * Calculate the upper quartile of passed values.
379 * @param {Array} [values=[]] List of values
380 * @returns {Number}
381 */
382 calcQ3(values = []) {
383 let nbValues = values.length;
384 let posa = 0.0;
385 let posb = 0.0;
386 posa = Math.floor(nbValues / 4);
387 if (4 * posa === nbValues) {
388 posb = 3 * posa;
389 return (Number(values[posb]) +
390 Number(values[posb - 1])) / 2;
391 }
392 return Number(values[nbValues - posa - 1]);
393 }
394
395 /**
396 * Sort passed values with supplied sorter function.
397 * @param {Array} [values=[]] List of values to be sorted
398 * @param {Function} sorter Sorter function
399 * @returns {Array}
400 */
401 sortColumnValues(values = [], sorter) {
402 return values.sort(sorter);
403 }
404
405 /**
406 * Write calculation result in passed DOM element with supplied write method
407 * and decimal precision.
408 * @param {Number} [result=0] Calculation result
409 * @param {DOMElement} label DOM element
410 * @param {String} [writeType='innerhtml'] Write method
411 * @param {Number} [precision=2] Applied decimal precision
412 * @private
413 */
414 writeResult(result = 0, label, writeType = 'innerhtml',
415 precision = 2, format = {}) {
416 let labelElm = elm(label);
417
418 if (!labelElm) {
419 return;
420 }
421
422 result = result.toFixed(precision);
423 if (isNaN(result) || !isFinite(result)) {
424 result = '';
425 } else {
426 result = formatNumber(format)(result);
427 }
428
429 switch (writeType.toLowerCase()) {
430 case 'innerhtml':
431 labelElm.innerHTML = result;
432 break;
433 case 'setvalue':
434 labelElm.value = result;
435 break;
436 case 'createtextnode':
437 let oldNode = labelElm.firstChild;
438 let txtNode = createText(result);
439 labelElm.replaceChild(txtNode, oldNode);
440 break;
441 }
442 }
443
444 /**
445 * Configure the format options used to format the operation result based
446 * on column type.
447 * @param {Number} colIndex Column index
448 * @param {Object} [format={}] Format object
449 * @returns {Object}
450 * @private
451 */
452 configureFormat(colIndex, format = {}) {
453 let tf = this.tf;
454 if (tf.hasType(colIndex, [FORMATTED_NUMBER])) {
455 let colType = tf.colTypes[colIndex];
456 if (colType.decimal && !format.decimal) {
457 format.decimal = colType.decimal;
458 }
459 if (colType.thousands && !format.integerSeparator) {
460 format.integerSeparator = colType.thousands;
461 }
462 } else {
463 format.decimal = format.decimal || '';
464 format.integerSeparator = format.integerSeparator || '';
465 }
466 return format;
467 }
468
469 /** Remove extension */
470 destroy() {
471 if (!this.initialized) {
472 return;
473 }
474 // unsubscribe to events
475 this.emitter.off(EVENTS, bound(this.calcAll, this));
476
477 this.initialized = false;
478 }
479
480}