1 | import {Feature} from '../../feature';
|
2 | import {createText, elm} from '../../dom';
|
3 | import {isArray, isEmpty, EMPTY_FN} from '../../types';
|
4 | import {numSortAsc} from '../../sort';
|
5 | import {FORMATTED_NUMBER} from '../../const';
|
6 | import formatNumber from 'format-number';
|
7 | import {defaultsFn, defaultsArr} from '../../settings';
|
8 | import {bound} from '../../event';
|
9 |
|
10 | const EVENTS = [
|
11 | 'after-filtering',
|
12 | 'after-page-change',
|
13 | 'after-page-length-change'
|
14 | ];
|
15 |
|
16 | const SUM = 'sum';
|
17 | const MEAN = 'mean';
|
18 | const MIN = 'min';
|
19 | const MAX = 'max';
|
20 | const MEDIAN = 'median';
|
21 | const Q1 = 'q1';
|
22 | const Q3 = 'q3';
|
23 |
|
24 | /**
|
25 | * Column calculations extension
|
26 | */
|
27 | export 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 | }
|