mirror of
https://github.com/apache/superset.git
synced 2026-04-20 00:24:38 +00:00
chore(plugin-chart-pivot-table): migrate react-pivottable into superset codebase (#17769)
* chore(plugin-chart-pivot-table): migrate react-pivottable into superset codebase * Fix lint errors * Use named export * Clean up the code
This commit is contained in:
committed by
GitHub
parent
c5af7a48df
commit
9c9edbe8bc
853
superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/utilities.js
vendored
Normal file
853
superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/utilities.js
vendored
Normal file
@@ -0,0 +1,853 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { t } from '@superset-ui/core';
|
||||
|
||||
const addSeparators = function (nStr, thousandsSep, decimalSep) {
|
||||
const x = String(nStr).split('.');
|
||||
let x1 = x[0];
|
||||
const x2 = x.length > 1 ? decimalSep + x[1] : '';
|
||||
const rgx = /(\d+)(\d{3})/;
|
||||
while (rgx.test(x1)) {
|
||||
x1 = x1.replace(rgx, `$1${thousandsSep}$2`);
|
||||
}
|
||||
return x1 + x2;
|
||||
};
|
||||
|
||||
const numberFormat = function (optsIn) {
|
||||
const defaults = {
|
||||
digitsAfterDecimal: 2,
|
||||
scaler: 1,
|
||||
thousandsSep: ',',
|
||||
decimalSep: '.',
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
};
|
||||
const opts = { ...defaults, ...optsIn };
|
||||
return function (x) {
|
||||
if (Number.isNaN(x) || !Number.isFinite(x)) {
|
||||
return '';
|
||||
}
|
||||
const result = addSeparators(
|
||||
(opts.scaler * x).toFixed(opts.digitsAfterDecimal),
|
||||
opts.thousandsSep,
|
||||
opts.decimalSep,
|
||||
);
|
||||
return `${opts.prefix}${result}${opts.suffix}`;
|
||||
};
|
||||
};
|
||||
|
||||
const rx = /(\d+)|(\D+)/g;
|
||||
const rd = /\d/;
|
||||
const rz = /^0/;
|
||||
const naturalSort = (as, bs) => {
|
||||
// nulls first
|
||||
if (bs !== null && as === null) {
|
||||
return -1;
|
||||
}
|
||||
if (as !== null && bs === null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// then raw NaNs
|
||||
if (typeof as === 'number' && Number.isNaN(as)) {
|
||||
return -1;
|
||||
}
|
||||
if (typeof bs === 'number' && Number.isNaN(bs)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// numbers and numbery strings group together
|
||||
const nas = Number(as);
|
||||
const nbs = Number(bs);
|
||||
if (nas < nbs) {
|
||||
return -1;
|
||||
}
|
||||
if (nas > nbs) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// within that, true numbers before numbery strings
|
||||
if (typeof as === 'number' && typeof bs !== 'number') {
|
||||
return -1;
|
||||
}
|
||||
if (typeof bs === 'number' && typeof as !== 'number') {
|
||||
return 1;
|
||||
}
|
||||
if (typeof as === 'number' && typeof bs === 'number') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 'Infinity' is a textual number, so less than 'A'
|
||||
if (Number.isNaN(nbs) && !Number.isNaN(nas)) {
|
||||
return -1;
|
||||
}
|
||||
if (Number.isNaN(nas) && !Number.isNaN(nbs)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// finally, "smart" string sorting per http://stackoverflow.com/a/4373421/112871
|
||||
let a = String(as);
|
||||
let b = String(bs);
|
||||
if (a === b) {
|
||||
return 0;
|
||||
}
|
||||
if (!rd.test(a) || !rd.test(b)) {
|
||||
return a > b ? 1 : -1;
|
||||
}
|
||||
|
||||
// special treatment for strings containing digits
|
||||
a = a.match(rx);
|
||||
b = b.match(rx);
|
||||
while (a.length && b.length) {
|
||||
const a1 = a.shift();
|
||||
const b1 = b.shift();
|
||||
if (a1 !== b1) {
|
||||
if (rd.test(a1) && rd.test(b1)) {
|
||||
return a1.replace(rz, '.0') - b1.replace(rz, '.0');
|
||||
}
|
||||
return a1 > b1 ? 1 : -1;
|
||||
}
|
||||
}
|
||||
return a.length - b.length;
|
||||
};
|
||||
|
||||
const sortAs = function (order) {
|
||||
const mapping = {};
|
||||
|
||||
// sort lowercased keys similarly
|
||||
const lMapping = {};
|
||||
order.forEach((element, i) => {
|
||||
mapping[element] = i;
|
||||
if (typeof element === 'string') {
|
||||
lMapping[element.toLowerCase()] = i;
|
||||
}
|
||||
});
|
||||
return function (a, b) {
|
||||
if (a in mapping && b in mapping) {
|
||||
return mapping[a] - mapping[b];
|
||||
}
|
||||
if (a in mapping) {
|
||||
return -1;
|
||||
}
|
||||
if (b in mapping) {
|
||||
return 1;
|
||||
}
|
||||
if (a in lMapping && b in lMapping) {
|
||||
return lMapping[a] - lMapping[b];
|
||||
}
|
||||
if (a in lMapping) {
|
||||
return -1;
|
||||
}
|
||||
if (b in lMapping) {
|
||||
return 1;
|
||||
}
|
||||
return naturalSort(a, b);
|
||||
};
|
||||
};
|
||||
|
||||
const getSort = function (sorters, attr) {
|
||||
if (sorters) {
|
||||
if (typeof sorters === 'function') {
|
||||
const sort = sorters(attr);
|
||||
if (typeof sort === 'function') {
|
||||
return sort;
|
||||
}
|
||||
} else if (attr in sorters) {
|
||||
return sorters[attr];
|
||||
}
|
||||
}
|
||||
return naturalSort;
|
||||
};
|
||||
|
||||
// aggregator templates default to US number formatting but this is overrideable
|
||||
const usFmt = numberFormat();
|
||||
const usFmtInt = numberFormat({ digitsAfterDecimal: 0 });
|
||||
const usFmtPct = numberFormat({
|
||||
digitsAfterDecimal: 1,
|
||||
scaler: 100,
|
||||
suffix: '%',
|
||||
});
|
||||
|
||||
const baseAggregatorTemplates = {
|
||||
count(formatter = usFmtInt) {
|
||||
return () =>
|
||||
function () {
|
||||
return {
|
||||
count: 0,
|
||||
push() {
|
||||
this.count += 1;
|
||||
},
|
||||
value() {
|
||||
return this.count;
|
||||
},
|
||||
format: formatter,
|
||||
};
|
||||
};
|
||||
},
|
||||
|
||||
uniques(fn, formatter = usFmtInt) {
|
||||
return function ([attr]) {
|
||||
return function () {
|
||||
return {
|
||||
uniq: [],
|
||||
push(record) {
|
||||
if (!Array.from(this.uniq).includes(record[attr])) {
|
||||
this.uniq.push(record[attr]);
|
||||
}
|
||||
},
|
||||
value() {
|
||||
return fn(this.uniq);
|
||||
},
|
||||
format: formatter,
|
||||
numInputs: typeof attr !== 'undefined' ? 0 : 1,
|
||||
};
|
||||
};
|
||||
};
|
||||
},
|
||||
|
||||
sum(formatter = usFmt) {
|
||||
return function ([attr]) {
|
||||
return function () {
|
||||
return {
|
||||
sum: 0,
|
||||
push(record) {
|
||||
if (!Number.isNaN(parseFloat(record[attr]))) {
|
||||
this.sum += parseFloat(record[attr]);
|
||||
}
|
||||
},
|
||||
value() {
|
||||
return this.sum;
|
||||
},
|
||||
format: formatter,
|
||||
numInputs: typeof attr !== 'undefined' ? 0 : 1,
|
||||
};
|
||||
};
|
||||
};
|
||||
},
|
||||
|
||||
extremes(mode, formatter = usFmt) {
|
||||
return function ([attr]) {
|
||||
return function (data) {
|
||||
return {
|
||||
val: null,
|
||||
sorter: getSort(
|
||||
typeof data !== 'undefined' ? data.sorters : null,
|
||||
attr,
|
||||
),
|
||||
push(record) {
|
||||
let x = record[attr];
|
||||
if (['min', 'max'].includes(mode)) {
|
||||
x = parseFloat(x);
|
||||
if (!Number.isNaN(x)) {
|
||||
this.val = Math[mode](x, this.val !== null ? this.val : x);
|
||||
}
|
||||
}
|
||||
if (
|
||||
mode === 'first' &&
|
||||
this.sorter(x, this.val !== null ? this.val : x) <= 0
|
||||
) {
|
||||
this.val = x;
|
||||
}
|
||||
if (
|
||||
mode === 'last' &&
|
||||
this.sorter(x, this.val !== null ? this.val : x) >= 0
|
||||
) {
|
||||
this.val = x;
|
||||
}
|
||||
},
|
||||
value() {
|
||||
return this.val;
|
||||
},
|
||||
format(x) {
|
||||
if (Number.isNaN(x)) {
|
||||
return x;
|
||||
}
|
||||
return formatter(x);
|
||||
},
|
||||
numInputs: typeof attr !== 'undefined' ? 0 : 1,
|
||||
};
|
||||
};
|
||||
};
|
||||
},
|
||||
|
||||
quantile(q, formatter = usFmt) {
|
||||
return function ([attr]) {
|
||||
return function () {
|
||||
return {
|
||||
vals: [],
|
||||
push(record) {
|
||||
const x = parseFloat(record[attr]);
|
||||
if (!Number.isNaN(x)) {
|
||||
this.vals.push(x);
|
||||
}
|
||||
},
|
||||
value() {
|
||||
if (this.vals.length === 0) {
|
||||
return null;
|
||||
}
|
||||
this.vals.sort((a, b) => a - b);
|
||||
const i = (this.vals.length - 1) * q;
|
||||
return (this.vals[Math.floor(i)] + this.vals[Math.ceil(i)]) / 2.0;
|
||||
},
|
||||
format: formatter,
|
||||
numInputs: typeof attr !== 'undefined' ? 0 : 1,
|
||||
};
|
||||
};
|
||||
};
|
||||
},
|
||||
|
||||
runningStat(mode = 'mean', ddof = 1, formatter = usFmt) {
|
||||
return function ([attr]) {
|
||||
return function () {
|
||||
return {
|
||||
n: 0.0,
|
||||
m: 0.0,
|
||||
s: 0.0,
|
||||
push(record) {
|
||||
const x = parseFloat(record[attr]);
|
||||
if (Number.isNaN(x)) {
|
||||
return;
|
||||
}
|
||||
this.n += 1.0;
|
||||
if (this.n === 1.0) {
|
||||
this.m = x;
|
||||
}
|
||||
const mNew = this.m + (x - this.m) / this.n;
|
||||
this.s += (x - this.m) * (x - mNew);
|
||||
this.m = mNew;
|
||||
},
|
||||
value() {
|
||||
if (mode === 'mean') {
|
||||
if (this.n === 0) {
|
||||
return 0 / 0;
|
||||
}
|
||||
return this.m;
|
||||
}
|
||||
if (this.n <= ddof) {
|
||||
return 0;
|
||||
}
|
||||
switch (mode) {
|
||||
case 'var':
|
||||
return this.s / (this.n - ddof);
|
||||
case 'stdev':
|
||||
return Math.sqrt(this.s / (this.n - ddof));
|
||||
default:
|
||||
throw new Error('unknown mode for runningStat');
|
||||
}
|
||||
},
|
||||
format: formatter,
|
||||
numInputs: typeof attr !== 'undefined' ? 0 : 1,
|
||||
};
|
||||
};
|
||||
};
|
||||
},
|
||||
|
||||
sumOverSum(formatter = usFmt) {
|
||||
return function ([num, denom]) {
|
||||
return function () {
|
||||
return {
|
||||
sumNum: 0,
|
||||
sumDenom: 0,
|
||||
push(record) {
|
||||
if (!Number.isNaN(parseFloat(record[num]))) {
|
||||
this.sumNum += parseFloat(record[num]);
|
||||
}
|
||||
if (!Number.isNaN(parseFloat(record[denom]))) {
|
||||
this.sumDenom += parseFloat(record[denom]);
|
||||
}
|
||||
},
|
||||
value() {
|
||||
return this.sumNum / this.sumDenom;
|
||||
},
|
||||
format: formatter,
|
||||
numInputs:
|
||||
typeof num !== 'undefined' && typeof denom !== 'undefined' ? 0 : 2,
|
||||
};
|
||||
};
|
||||
};
|
||||
},
|
||||
|
||||
fractionOf(wrapped, type = 'total', formatter = usFmtPct) {
|
||||
return (...x) =>
|
||||
function (data, rowKey, colKey) {
|
||||
return {
|
||||
selector: { total: [[], []], row: [rowKey, []], col: [[], colKey] }[
|
||||
type
|
||||
],
|
||||
inner: wrapped(...Array.from(x || []))(data, rowKey, colKey),
|
||||
push(record) {
|
||||
this.inner.push(record);
|
||||
},
|
||||
format: formatter,
|
||||
value() {
|
||||
return (
|
||||
this.inner.value() /
|
||||
data
|
||||
.getAggregator(...Array.from(this.selector || []))
|
||||
.inner.value()
|
||||
);
|
||||
},
|
||||
numInputs: wrapped(...Array.from(x || []))().numInputs,
|
||||
};
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const extendedAggregatorTemplates = {
|
||||
countUnique(f) {
|
||||
return baseAggregatorTemplates.uniques(x => x.length, f);
|
||||
},
|
||||
listUnique(s, f) {
|
||||
return baseAggregatorTemplates.uniques(x => x.join(s), f || (x => x));
|
||||
},
|
||||
max(f) {
|
||||
return baseAggregatorTemplates.extremes('max', f);
|
||||
},
|
||||
min(f) {
|
||||
return baseAggregatorTemplates.extremes('min', f);
|
||||
},
|
||||
first(f) {
|
||||
return baseAggregatorTemplates.extremes('first', f);
|
||||
},
|
||||
last(f) {
|
||||
return baseAggregatorTemplates.extremes('last', f);
|
||||
},
|
||||
median(f) {
|
||||
return baseAggregatorTemplates.quantile(0.5, f);
|
||||
},
|
||||
average(f) {
|
||||
return baseAggregatorTemplates.runningStat('mean', 1, f);
|
||||
},
|
||||
var(ddof, f) {
|
||||
return baseAggregatorTemplates.runningStat('var', ddof, f);
|
||||
},
|
||||
stdev(ddof, f) {
|
||||
return baseAggregatorTemplates.runningStat('stdev', ddof, f);
|
||||
},
|
||||
};
|
||||
|
||||
const aggregatorTemplates = {
|
||||
...baseAggregatorTemplates,
|
||||
...extendedAggregatorTemplates,
|
||||
};
|
||||
|
||||
// default aggregators & renderers use US naming and number formatting
|
||||
const aggregators = (tpl => ({
|
||||
Count: tpl.count(usFmtInt),
|
||||
'Count Unique Values': tpl.countUnique(usFmtInt),
|
||||
'List Unique Values': tpl.listUnique(', '),
|
||||
Sum: tpl.sum(usFmt),
|
||||
'Integer Sum': tpl.sum(usFmtInt),
|
||||
Average: tpl.average(usFmt),
|
||||
Median: tpl.median(usFmt),
|
||||
'Sample Variance': tpl.var(1, usFmt),
|
||||
'Sample Standard Deviation': tpl.stdev(1, usFmt),
|
||||
Minimum: tpl.min(usFmt),
|
||||
Maximum: tpl.max(usFmt),
|
||||
First: tpl.first(usFmt),
|
||||
Last: tpl.last(usFmt),
|
||||
'Sum over Sum': tpl.sumOverSum(usFmt),
|
||||
'Sum as Fraction of Total': tpl.fractionOf(tpl.sum(), 'total', usFmtPct),
|
||||
'Sum as Fraction of Rows': tpl.fractionOf(tpl.sum(), 'row', usFmtPct),
|
||||
'Sum as Fraction of Columns': tpl.fractionOf(tpl.sum(), 'col', usFmtPct),
|
||||
'Count as Fraction of Total': tpl.fractionOf(tpl.count(), 'total', usFmtPct),
|
||||
'Count as Fraction of Rows': tpl.fractionOf(tpl.count(), 'row', usFmtPct),
|
||||
'Count as Fraction of Columns': tpl.fractionOf(tpl.count(), 'col', usFmtPct),
|
||||
}))(aggregatorTemplates);
|
||||
|
||||
const locales = {
|
||||
en: {
|
||||
aggregators,
|
||||
localeStrings: {
|
||||
renderError: 'An error occurred rendering the PivotTable results.',
|
||||
computeError: 'An error occurred computing the PivotTable results.',
|
||||
uiRenderError: 'An error occurred rendering the PivotTable UI.',
|
||||
selectAll: 'Select All',
|
||||
selectNone: 'Select None',
|
||||
tooMany: '(too many to list)',
|
||||
filterResults: 'Filter values',
|
||||
apply: 'Apply',
|
||||
cancel: 'Cancel',
|
||||
totals: 'Totals',
|
||||
vs: 'vs',
|
||||
by: 'by',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// dateFormat deriver l10n requires month and day names to be passed in directly
|
||||
const mthNamesEn = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
];
|
||||
const dayNamesEn = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
const zeroPad = number => `0${number}`.substr(-2, 2); // eslint-disable-line no-magic-numbers
|
||||
|
||||
const derivers = {
|
||||
bin(col, binWidth) {
|
||||
return record => record[col] - (record[col] % binWidth);
|
||||
},
|
||||
dateFormat(
|
||||
col,
|
||||
formatString,
|
||||
utcOutput = false,
|
||||
mthNames = mthNamesEn,
|
||||
dayNames = dayNamesEn,
|
||||
) {
|
||||
const utc = utcOutput ? 'UTC' : '';
|
||||
return function (record) {
|
||||
const date = new Date(Date.parse(record[col]));
|
||||
if (Number.isNaN(date)) {
|
||||
return '';
|
||||
}
|
||||
return formatString.replace(/%(.)/g, function (m, p) {
|
||||
switch (p) {
|
||||
case 'y':
|
||||
return date[`get${utc}FullYear`]();
|
||||
case 'm':
|
||||
return zeroPad(date[`get${utc}Month`]() + 1);
|
||||
case 'n':
|
||||
return mthNames[date[`get${utc}Month`]()];
|
||||
case 'd':
|
||||
return zeroPad(date[`get${utc}Date`]());
|
||||
case 'w':
|
||||
return dayNames[date[`get${utc}Day`]()];
|
||||
case 'x':
|
||||
return date[`get${utc}Day`]();
|
||||
case 'H':
|
||||
return zeroPad(date[`get${utc}Hours`]());
|
||||
case 'M':
|
||||
return zeroPad(date[`get${utc}Minutes`]());
|
||||
case 'S':
|
||||
return zeroPad(date[`get${utc}Seconds`]());
|
||||
default:
|
||||
return `%${p}`;
|
||||
}
|
||||
});
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Given an array of attribute values, convert to a key that
|
||||
// can be used in objects.
|
||||
const flatKey = attrVals => attrVals.join(String.fromCharCode(0));
|
||||
|
||||
/*
|
||||
Data Model class
|
||||
*/
|
||||
|
||||
class PivotData {
|
||||
constructor(inputProps = {}, subtotals = {}) {
|
||||
this.props = { ...PivotData.defaultProps, ...inputProps };
|
||||
this.processRecord = this.processRecord.bind(this);
|
||||
PropTypes.checkPropTypes(
|
||||
PivotData.propTypes,
|
||||
this.props,
|
||||
'prop',
|
||||
'PivotData',
|
||||
);
|
||||
|
||||
this.aggregator = this.props
|
||||
.aggregatorsFactory(this.props.defaultFormatter)
|
||||
[this.props.aggregatorName](this.props.vals);
|
||||
this.formattedAggregators =
|
||||
this.props.customFormatters &&
|
||||
Object.entries(this.props.customFormatters).reduce(
|
||||
(acc, [key, columnFormatter]) => {
|
||||
acc[key] = {};
|
||||
Object.entries(columnFormatter).forEach(([column, formatter]) => {
|
||||
acc[key][column] = this.props
|
||||
.aggregatorsFactory(formatter)
|
||||
[this.props.aggregatorName](this.props.vals);
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
this.tree = {};
|
||||
this.rowKeys = [];
|
||||
this.colKeys = [];
|
||||
this.rowTotals = {};
|
||||
this.colTotals = {};
|
||||
this.allTotal = this.aggregator(this, [], []);
|
||||
this.subtotals = subtotals;
|
||||
this.sorted = false;
|
||||
|
||||
// iterate through input, accumulating data for cells
|
||||
PivotData.forEachRecord(this.props.data, this.processRecord);
|
||||
}
|
||||
|
||||
getFormattedAggregator(record, totalsKeys) {
|
||||
if (!this.formattedAggregators) {
|
||||
return this.aggregator;
|
||||
}
|
||||
const [groupName, groupValue] =
|
||||
Object.entries(record).find(
|
||||
([name, value]) =>
|
||||
this.formattedAggregators[name] &&
|
||||
this.formattedAggregators[name][value],
|
||||
) || [];
|
||||
if (
|
||||
!groupName ||
|
||||
!groupValue ||
|
||||
(totalsKeys && !totalsKeys.includes(groupValue))
|
||||
) {
|
||||
return this.aggregator;
|
||||
}
|
||||
return this.formattedAggregators[groupName][groupValue] || this.aggregator;
|
||||
}
|
||||
|
||||
arrSort(attrs, partialOnTop, reverse = false) {
|
||||
const sortersArr = attrs.map(a => getSort(this.props.sorters, a));
|
||||
return function (a, b) {
|
||||
const limit = Math.min(a.length, b.length);
|
||||
for (let i = 0; i < limit; i += 1) {
|
||||
const sorter = sortersArr[i];
|
||||
const comparison = reverse ? sorter(b[i], a[i]) : sorter(a[i], b[i]);
|
||||
if (comparison !== 0) {
|
||||
return comparison;
|
||||
}
|
||||
}
|
||||
return partialOnTop ? a.length - b.length : b.length - a.length;
|
||||
};
|
||||
}
|
||||
|
||||
sortKeys() {
|
||||
if (!this.sorted) {
|
||||
this.sorted = true;
|
||||
const v = (r, c) => this.getAggregator(r, c).value();
|
||||
switch (this.props.rowOrder) {
|
||||
case 'key_z_to_a':
|
||||
this.rowKeys.sort(
|
||||
this.arrSort(this.props.rows, this.subtotals.rowPartialOnTop, true),
|
||||
);
|
||||
break;
|
||||
case 'value_a_to_z':
|
||||
this.rowKeys.sort((a, b) => naturalSort(v(a, []), v(b, [])));
|
||||
break;
|
||||
case 'value_z_to_a':
|
||||
this.rowKeys.sort((a, b) => -naturalSort(v(a, []), v(b, [])));
|
||||
break;
|
||||
default:
|
||||
this.rowKeys.sort(
|
||||
this.arrSort(this.props.rows, this.subtotals.rowPartialOnTop),
|
||||
);
|
||||
}
|
||||
switch (this.props.colOrder) {
|
||||
case 'key_z_to_a':
|
||||
this.colKeys.sort(
|
||||
this.arrSort(this.props.cols, this.subtotals.colPartialOnTop, true),
|
||||
);
|
||||
break;
|
||||
case 'value_a_to_z':
|
||||
this.colKeys.sort((a, b) => naturalSort(v([], a), v([], b)));
|
||||
break;
|
||||
case 'value_z_to_a':
|
||||
this.colKeys.sort((a, b) => -naturalSort(v([], a), v([], b)));
|
||||
break;
|
||||
default:
|
||||
this.colKeys.sort(
|
||||
this.arrSort(this.props.cols, this.subtotals.colPartialOnTop),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getColKeys() {
|
||||
this.sortKeys();
|
||||
return this.colKeys;
|
||||
}
|
||||
|
||||
getRowKeys() {
|
||||
this.sortKeys();
|
||||
return this.rowKeys;
|
||||
}
|
||||
|
||||
processRecord(record) {
|
||||
// this code is called in a tight loop
|
||||
const colKey = [];
|
||||
const rowKey = [];
|
||||
this.props.cols.forEach(col => {
|
||||
colKey.push(col in record ? record[col] : 'null');
|
||||
});
|
||||
this.props.rows.forEach(row => {
|
||||
rowKey.push(row in record ? record[row] : 'null');
|
||||
});
|
||||
|
||||
this.allTotal.push(record);
|
||||
|
||||
const rowStart = this.subtotals.rowEnabled ? 1 : Math.max(1, rowKey.length);
|
||||
const colStart = this.subtotals.colEnabled ? 1 : Math.max(1, colKey.length);
|
||||
|
||||
let isRowSubtotal;
|
||||
let isColSubtotal;
|
||||
for (let ri = rowStart; ri <= rowKey.length; ri += 1) {
|
||||
isRowSubtotal = ri < rowKey.length;
|
||||
const fRowKey = rowKey.slice(0, ri);
|
||||
const flatRowKey = flatKey(fRowKey);
|
||||
if (!this.rowTotals[flatRowKey]) {
|
||||
this.rowKeys.push(fRowKey);
|
||||
this.rowTotals[flatRowKey] = this.getFormattedAggregator(
|
||||
record,
|
||||
rowKey,
|
||||
)(this, fRowKey, []);
|
||||
}
|
||||
this.rowTotals[flatRowKey].push(record);
|
||||
this.rowTotals[flatRowKey].isSubtotal = isRowSubtotal;
|
||||
}
|
||||
|
||||
for (let ci = colStart; ci <= colKey.length; ci += 1) {
|
||||
isColSubtotal = ci < colKey.length;
|
||||
const fColKey = colKey.slice(0, ci);
|
||||
const flatColKey = flatKey(fColKey);
|
||||
if (!this.colTotals[flatColKey]) {
|
||||
this.colKeys.push(fColKey);
|
||||
this.colTotals[flatColKey] = this.getFormattedAggregator(
|
||||
record,
|
||||
colKey,
|
||||
)(this, [], fColKey);
|
||||
}
|
||||
this.colTotals[flatColKey].push(record);
|
||||
this.colTotals[flatColKey].isSubtotal = isColSubtotal;
|
||||
}
|
||||
|
||||
// And now fill in for all the sub-cells.
|
||||
for (let ri = rowStart; ri <= rowKey.length; ri += 1) {
|
||||
isRowSubtotal = ri < rowKey.length;
|
||||
const fRowKey = rowKey.slice(0, ri);
|
||||
const flatRowKey = flatKey(fRowKey);
|
||||
if (!this.tree[flatRowKey]) {
|
||||
this.tree[flatRowKey] = {};
|
||||
}
|
||||
for (let ci = colStart; ci <= colKey.length; ci += 1) {
|
||||
isColSubtotal = ci < colKey.length;
|
||||
const fColKey = colKey.slice(0, ci);
|
||||
const flatColKey = flatKey(fColKey);
|
||||
if (!this.tree[flatRowKey][flatColKey]) {
|
||||
this.tree[flatRowKey][flatColKey] = this.getFormattedAggregator(
|
||||
record,
|
||||
)(this, fRowKey, fColKey);
|
||||
}
|
||||
this.tree[flatRowKey][flatColKey].push(record);
|
||||
|
||||
this.tree[flatRowKey][flatColKey].isRowSubtotal = isRowSubtotal;
|
||||
this.tree[flatRowKey][flatColKey].isColSubtotal = isColSubtotal;
|
||||
this.tree[flatRowKey][flatColKey].isSubtotal =
|
||||
isRowSubtotal || isColSubtotal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAggregator(rowKey, colKey) {
|
||||
let agg;
|
||||
const flatRowKey = flatKey(rowKey);
|
||||
const flatColKey = flatKey(colKey);
|
||||
if (rowKey.length === 0 && colKey.length === 0) {
|
||||
agg = this.allTotal;
|
||||
} else if (rowKey.length === 0) {
|
||||
agg = this.colTotals[flatColKey];
|
||||
} else if (colKey.length === 0) {
|
||||
agg = this.rowTotals[flatRowKey];
|
||||
} else {
|
||||
agg = this.tree[flatRowKey][flatColKey];
|
||||
}
|
||||
return (
|
||||
agg || {
|
||||
value() {
|
||||
return null;
|
||||
},
|
||||
format() {
|
||||
return '';
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// can handle arrays or jQuery selections of tables
|
||||
PivotData.forEachRecord = function (input, processRecord) {
|
||||
if (Array.isArray(input)) {
|
||||
// array of objects
|
||||
return input.map(record => processRecord(record));
|
||||
}
|
||||
throw new Error(t('Unknown input format'));
|
||||
};
|
||||
|
||||
PivotData.defaultProps = {
|
||||
aggregators,
|
||||
cols: [],
|
||||
rows: [],
|
||||
vals: [],
|
||||
aggregatorName: 'Count',
|
||||
sorters: {},
|
||||
rowOrder: 'key_a_to_z',
|
||||
colOrder: 'key_a_to_z',
|
||||
};
|
||||
|
||||
PivotData.propTypes = {
|
||||
data: PropTypes.oneOfType([PropTypes.array, PropTypes.object, PropTypes.func])
|
||||
.isRequired,
|
||||
aggregatorName: PropTypes.string,
|
||||
cols: PropTypes.arrayOf(PropTypes.string),
|
||||
rows: PropTypes.arrayOf(PropTypes.string),
|
||||
vals: PropTypes.arrayOf(PropTypes.string),
|
||||
valueFilter: PropTypes.objectOf(PropTypes.objectOf(PropTypes.bool)),
|
||||
sorters: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.objectOf(PropTypes.func),
|
||||
]),
|
||||
derivedAttributes: PropTypes.objectOf(PropTypes.func),
|
||||
rowOrder: PropTypes.oneOf([
|
||||
'key_a_to_z',
|
||||
'key_z_to_a',
|
||||
'value_a_to_z',
|
||||
'value_z_to_a',
|
||||
]),
|
||||
colOrder: PropTypes.oneOf([
|
||||
'key_a_to_z',
|
||||
'key_z_to_a',
|
||||
'value_a_to_z',
|
||||
'value_z_to_a',
|
||||
]),
|
||||
};
|
||||
|
||||
export {
|
||||
aggregatorTemplates,
|
||||
aggregators,
|
||||
derivers,
|
||||
locales,
|
||||
naturalSort,
|
||||
numberFormat,
|
||||
getSort,
|
||||
sortAs,
|
||||
flatKey,
|
||||
PivotData,
|
||||
};
|
||||
Reference in New Issue
Block a user