@adaptabletools/adaptable
Version:
Powerful data-agnostic HTML5 AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements
1,053 lines • 69.3 kB
JavaScript
import { ExpressionEvaluationError } from '../../parser/src/ExpressionEvaluationError';
import { SumArray } from '../Extensions/ArrayExtensions';
import { getTypedKeys } from '../Extensions/TypeExtensions';
import { extractColumnParameter, extractColumnParameters, extractParameter, getNumericValue, handleColumnFunction, validateColumnType, } from './expressionFunctionUtils';
import { isAfter } from 'date-fns';
import { parseDateValue } from '../Helpers/DateHelper';
export const aggregatedExpressionFunctions = [
'SUM',
'PERCENTAGE',
'MIN',
'MAX',
'AVG',
'COUNT',
'WEIGHT',
'COL',
'GROUP_BY',
'MEDIAN',
'MODE',
'DISTINCT',
'ONLY',
'STD_DEVIATION',
'PREVIOUS',
'NEXT',
'FIRST',
'LAST',
'ABSOLUTE_DIFF',
'PERCENT_DIFF',
];
export const cumulativeAggregatedExpressionFunctions = [
'CUMUL',
'OVER',
'SUM',
'PERCENTAGE',
'MIN',
'MAX',
'QUANT',
'PREVIOUS',
'NEXT',
'FIRST',
'LAST',
'AVG',
'WEIGHT',
'COL',
'GROUP_BY',
'ABSOLUTE_DIFF',
'PERCENT_DIFF',
];
export const quantileAggregatedExpressionFunctions = [
'QUANT',
'QUARTILE',
'PERCENTILE',
'COL',
'GROUP_BY',
];
export const aggregatedScalarExpressionFunctions = {
CUMUL: {
handler(args, context) {
const aggregationParameter = extractParameter('CUMUL', 'aggregationScalar', ['SUM', 'PERCENTAGE', 'AVG', 'MIN', 'MAX', 'COUNT'], args);
const overColumnParameter = extractParameter('CUMUL', 'operand', ['OVER'], args);
const groupByParameter = extractParameter(aggregationParameter.name, 'operand', ['GROUP_BY'], args, {
isOptional: true,
});
if (!!groupByParameter) {
// ensure the GROUP_BY is NOT on the CUMUL function
// it should be on the AGGREGATION function
throw new ExpressionEvaluationError('GROUP_BY', `should be passed as an argument to the '${aggregationParameter.name}' aggregation function!`);
}
const cumulationExpressionEvaluation = mapAggregationToCumulation(aggregationParameter, overColumnParameter, context);
const result = {
name: 'CUMUL',
type: 'aggregationScalar',
value: cumulationExpressionEvaluation,
};
return result;
},
description: 'Performs a cumulative aggregation (running total) with the given aggregation operation over a provided column',
signatures: ['CUMUL( aggregationOp: SUM|MIN|MAX, OVER( [colNameB] ))'],
examples: [
'CUMUL( SUM([colNameA]), OVER( [colNameB] ))',
'CUMUL( MIN([colNameA]), OVER( [colNameB] ))',
'CUMUL( MAX([colNameA]), OVER( [colNameB] ))',
],
category: 'cumulative',
},
SUM: {
handler(args, context) {
const sumColumnParameter = extractColumnParameter('SUM', args);
const sumColumnName = sumColumnParameter.value;
const columnType = validateColumnType(sumColumnName, ['number'], 'SUM', context.adaptableApi);
const groupByParameter = extractParameter('SUM', 'operand', ['GROUP_BY'], args, {
isOptional: true,
});
const aggregationExpressionEvaluation = {
aggregationParams: {
reducers: {
SUM: {
name: 'SUM',
field: sumColumnName,
initialValue: 0,
reducer: (totalSum, rowValue) => {
if (isUndefinedValue(rowValue)) {
return totalSum;
}
return totalSum + getNumericValue(rowValue);
},
},
},
},
rowFilterFn: context.filterFn,
getRowNodes: context.getRowNodes,
columnType,
};
addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
const result = {
name: 'SUM',
type: 'aggregationScalar',
value: aggregationExpressionEvaluation,
};
return result;
},
description: 'Aggregates a column over multiple rows by summing up the column values\nOptionally the aggregation may be computed within provided individual groups',
signatures: [
'SUM( [colName] )',
'SUM( COL(name: string))',
'SUM( [colNameA], GROUP_BY( [colNameB] ))',
'SUM( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
],
examples: ['SUM([colA])', 'SUM([colA], GROUP_BY([colB]))'],
category: 'aggregation',
inputs: ['number'],
},
PERCENTAGE: {
handler(args, context) {
const percentageColumnParameter = extractColumnParameter('PERCENTAGE', args);
const percentageColumnName = percentageColumnParameter.value;
const columnType = validateColumnType(percentageColumnName, ['number'], 'PERCENTAGE', context.adaptableApi);
const sumOperand = extractParameter('PERCENTAGE', 'aggregationScalar', ['SUM'], args, {
isOptional: true,
});
const groupByOperand = extractParameter('PERCENTAGE', 'operand', ['GROUP_BY'], args, {
isOptional: true,
});
if (sumOperand && groupByOperand) {
throw new ExpressionEvaluationError('PERCENTAGE', `expects either a SUM or a GROUP_BY argument`);
}
const sumAggregationColumnName = sumOperand
? sumOperand.value.aggregationParams.reducers['SUM'].field
: percentageColumnName;
const groupByColumnNames = groupByOperand
? groupByOperand?.value
: sumOperand?.value.aggregationParams.groupBy?.map((param) => param.field);
const aggregationExpressionEvaluation = {
aggregationParams: {
reducers: {
PERCENTAGE: {
name: 'PERCENTAGE',
field: sumAggregationColumnName,
initialValue: 0,
reducer: (totalSum, rowValue) => {
if (isUndefinedValue(rowValue)) {
return totalSum;
}
return totalSum + getNumericValue(rowValue);
},
},
},
},
rowValueGetter: (rowNode, aggregationValue) => {
return ((getNumericValue(context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, percentageColumnName)) /
aggregationValue) *
100);
},
rowFilterFn: context.filterFn,
getRowNodes: context.getRowNodes,
columnType,
};
addGroupByParams(groupByColumnNames, aggregationExpressionEvaluation);
const result = {
name: 'PERCENTAGE',
type: 'aggregationScalar',
value: aggregationExpressionEvaluation,
};
return result;
},
description: 'Divides each column value by the aggregated sum of a column\nThe column with the aggregated value and the column with the percentage value do not necessarily have to be identical',
signatures: [
'PERCENTAGE( [colName] )',
'PERCENTAGE( [colNameA], GROUP_BY( [colNameB] ))',
'PERCENTAGE( [colNameA], SUM ( [colNameB] ))',
'PERCENTAGE( [colNameA], SUM ( [colNameB], GROUP_BY( [colNameC] )))',
],
examples: [
'PERCENTAGE( [colName] )',
'PERCENTAGE( [colNameA], GROUP_BY( [colNameB] ))',
'PERCENTAGE( [colNameA], SUM ( [colNameB] ))',
'PERCENTAGE( [colNameA], SUM ( [colNameB], GROUP_BY( [colNameC] )))',
],
category: 'aggregation',
},
AVG: {
handler(args, context) {
const avgColumnParameter = extractColumnParameter('AVG', args);
const avgColumnName = avgColumnParameter.value;
const columnType = validateColumnType(avgColumnName, ['number'], 'AVG', context.adaptableApi);
const groupByParameter = extractParameter('AVG', 'operand', ['GROUP_BY'], args, {
isOptional: true,
});
const weightParameter = extractParameter('AVG', 'operand', ['WEIGHT'], args, {
isOptional: true,
});
const aggregationExpressionEvaluation = {
aggregationParams: {
reducers: {
AVG: {
name: 'AVG',
field: avgColumnName,
initialValue: {
sumOfValues: 0,
numberOfValues: 0,
},
reducer: (aggregatedValue, rowValue, rowNode) => {
if (isUndefinedValue(rowValue)) {
return aggregatedValue;
}
const numericRowValue = getNumericValue(rowValue);
if (weightParameter) {
// weighted average
const weightValue = getNumericValue(context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, weightParameter.value)) ?? 0;
aggregatedValue.sumOfValues =
aggregatedValue.sumOfValues + numericRowValue * weightValue;
aggregatedValue.numberOfValues = aggregatedValue.numberOfValues + weightValue;
}
else {
aggregatedValue.sumOfValues = aggregatedValue.sumOfValues + numericRowValue;
aggregatedValue.numberOfValues = aggregatedValue.numberOfValues + 1;
}
return aggregatedValue;
},
done: (aggregatedValue) => {
if (aggregatedValue.numberOfValues === 0) {
return 0;
}
return aggregatedValue.sumOfValues / aggregatedValue.numberOfValues;
},
},
},
},
rowFilterFn: context.filterFn,
getRowNodes: context.getRowNodes,
columnType,
};
addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
if (weightParameter) {
aggregationExpressionEvaluation.context = {
weightParam: weightParameter,
};
}
const result = {
name: 'AVG',
type: 'aggregationScalar',
value: aggregationExpressionEvaluation,
};
return result;
},
description: 'Aggregates a column over multiple rows by computing the average value (arithmetic mean) of the column values\nOptionally the aggregation may be weighted and/or grouped.',
signatures: [
'AVG( [colName] )',
'AVG( COL(name: string))',
'AVG( [colNameA], GROUP_BY( [colNameB] ))',
'AVG( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
],
examples: ['AVG([colA])', 'AVG([colA], GROUP_BY([colB]))', 'AVG([colA], WEIGHT([colB]))'],
category: 'aggregation',
inputs: ['number'],
},
MEDIAN: {
handler(args, context) {
const medianColumnParameter = extractColumnParameter('MEDIAN', args);
const medianColumnName = medianColumnParameter.value;
const columnType = validateColumnType(medianColumnName, ['number'], 'MEDIAN', context.adaptableApi);
const groupByParameter = extractParameter('MEDIAN', 'operand', ['GROUP_BY'], args, {
isOptional: true,
});
const aggregationExpressionEvaluation = {
aggregationParams: {
reducers: {
MEDIAN: {
name: 'MEDIAN',
field: medianColumnName,
initialValue: [],
reducer: (aggregatedValue, cellValue) => {
aggregatedValue.push(getNumericValue(cellValue));
return aggregatedValue;
},
done: (aggregatedValue) => {
if (aggregatedValue.length === 0) {
return null;
}
if (aggregatedValue.length === 1) {
return aggregatedValue[0];
}
const length = aggregatedValue.length;
const middle = Math.floor((length - 1) / 2);
if (length % 2) {
return aggregatedValue[middle];
}
else {
return (aggregatedValue[middle] + aggregatedValue[middle + 1]) / 2.0;
}
},
},
},
},
rowFilterFn: context.filterFn,
getRowNodes: context.getRowNodes,
columnType,
};
addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
const result = {
name: 'MEDIAN',
type: 'aggregationScalar',
value: aggregationExpressionEvaluation,
};
return result;
},
description: 'Aggregates a column over multiple rows by computing the median of the column values\nOptionally the aggregation may be grouped.',
signatures: [
'MEDIAN( [colName] )',
'MEDIAN( COL(name: string))',
'MEDIAN( [colNameA], GROUP_BY( [colNameB] ))',
'MEDIAN( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
],
examples: ['MEDIAN([colA])', 'MEDIAN([colA], GROUP_BY([colB]))'],
category: 'aggregation',
inputs: ['number'],
},
MODE: {
handler(args, context) {
const modeColumnParameter = extractColumnParameter('mode', args);
const modeColumnName = modeColumnParameter.value;
const columnType = validateColumnType(modeColumnName, ['number', 'text', 'date'], 'mode', context.adaptableApi);
const groupByParameter = extractParameter('MODE', 'operand', ['GROUP_BY'], args, {
isOptional: true,
});
const aggregationExpressionEvaluation = {
aggregationParams: {
reducers: {
MODE: {
name: 'MODE',
field: modeColumnName,
initialValue: new Map(),
reducer: (aggregatedValue, rowValue) => {
if (typeof rowValue !== 'number' && typeof rowValue !== 'string') {
return aggregatedValue;
}
if (!aggregatedValue) {
return new Map();
}
aggregatedValue.set(rowValue, (aggregatedValue.get(rowValue) ?? 0) + 1);
return aggregatedValue;
},
done: (aggregatedValue) => {
const sorted = [...aggregatedValue.entries()].sort(([aVal, aFreq], [bVal, bFreq]) => {
if (aFreq < bFreq) {
return 1;
}
else if (aFreq > bFreq || aVal < bVal) {
return -1;
}
else {
return aVal === bVal ? 0 : 1;
}
});
return sorted?.[0]?.[0];
},
},
},
},
rowFilterFn: context.filterFn,
getRowNodes: context.getRowNodes,
columnType,
};
addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
const result = {
name: 'MODE',
type: 'aggregationScalar',
value: aggregationExpressionEvaluation,
};
return result;
},
description: 'Aggregates a column over multiple rows by computing the mode of the column values\nOptionally the aggregation may be grouped.',
signatures: [
'MODE( [colName] )',
'MODE( COL(name: string))',
'MODE( [colNameA], GROUP_BY( [colNameB] ))',
'MODE( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
],
examples: ['MODE([colA])', 'MODE([colA], GROUP_BY([colB]))'],
category: 'aggregation',
inputs: [['number'], ['text'], ['date']],
},
DISTINCT: {
handler(args, context) {
const distinctColumnParameter = extractColumnParameter('DISTINCT', args);
const distinctColumnName = distinctColumnParameter.value;
const groupByParameter = extractParameter('MODE', 'operand', ['GROUP_BY'], args, {
isOptional: true,
});
const aggregationExpressionEvaluation = {
aggregationParams: {
reducers: {
DISTINCT: {
name: 'DISTINCT',
field: distinctColumnName,
initialValue: new Set(),
reducer: (aggregatedValue, rowValue, rowNode) => {
if (typeof rowValue !== 'number' && typeof rowValue !== 'string') {
return aggregatedValue;
}
aggregatedValue.add(rowValue);
return aggregatedValue;
},
done: (aggregatedValue) => {
return aggregatedValue.size;
},
},
},
},
getRowNodes: context.getRowNodes,
};
addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
const result = {
name: 'DISTINCT',
type: 'aggregationScalar',
value: aggregationExpressionEvaluation,
};
return result;
},
description: 'Aggregates a column over multiple rows by computing the distinct number of the column values\nOptionally the aggregation may be grouped.',
signatures: [
'DISTINCT( [colName] )',
'DISTINCT( COL(name: string))',
'DISTINCT( [colNameA], GROUP_BY( [colNameB] ))',
'DISTINCT( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
],
examples: ['DISTINCT([colA])', 'DISTINCT([colA], GROUP_BY([colB]))'],
category: 'aggregation',
inputs: [['number'], ['text'], ['date']],
},
ONLY: {
handler(args, context) {
const distinctColumnParameter = extractColumnParameter('ONLY', args);
const onlyColumnName = distinctColumnParameter.value;
const columnType = validateColumnType(onlyColumnName, ['number'], 'ONLY', context.adaptableApi);
const groupByParameter = extractParameter('ONLY', 'operand', ['GROUP_BY'], args, {
isOptional: true,
});
const aggregationExpressionEvaluation = {
aggregationParams: {
reducers: {
ONLY: {
name: 'ONLY',
field: onlyColumnName,
initialValue: new Set(),
reducer: (aggregatedValue, rowValue, rowNode) => {
if (typeof rowValue !== 'number' && typeof rowValue !== 'string') {
return aggregatedValue;
}
aggregatedValue.add(rowValue);
return aggregatedValue;
},
done: (aggregatedValue) => {
return aggregatedValue.size === 1 ? aggregatedValue.values().next().value : null;
},
},
},
},
rowFilterFn: context.filterFn,
getRowNodes: context.getRowNodes,
columnType,
};
addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
const result = {
name: 'ONLY',
type: 'aggregationScalar',
value: aggregationExpressionEvaluation,
};
return result;
},
description: 'Aggregates a column over multiple rows by computing to a value that is common to all rows. \nOptionally the aggregation may be grouped.',
signatures: [
'ONLY( [colName] )',
'ONLY( COL(name: string))',
'ONLY( [colNameA], GROUP_BY( [colNameB] ))',
'ONLY( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
],
examples: ['ONLY([colA])', 'ONLY([colA], GROUP_BY([colB]))'],
category: 'aggregation',
inputs: [['number'], ['text'], ['date']],
},
STD_DEVIATION: {
handler(args, context) {
const columnParameter = extractColumnParameter('STD_DEVIATION', args);
const columnName = columnParameter.value;
const columnType = validateColumnType(columnName, ['number'], 'STD_DEVIATION', context.adaptableApi);
const groupByParameter = extractParameter('STD_DEVIATION', 'operand', ['GROUP_BY'], args, {
isOptional: true,
});
const aggregationExpressionEvaluation = {
aggregationParams: {
reducers: {
STD_DEVIATION: {
name: 'STD_DEVIATION',
field: columnName,
initialValue: { sumOfValues: 0, values: [] },
reducer: (aggregatedValue, rowValue, rowNode) => {
// TODO: getNumericValue; allow numeric strings
if (typeof rowValue !== 'number' || isNaN(rowValue)) {
return aggregatedValue;
}
aggregatedValue.sumOfValues = aggregatedValue.sumOfValues + rowValue;
aggregatedValue.values.push(rowValue);
return aggregatedValue;
},
done: (aggregatedValue) => {
const { sumOfValues, values } = aggregatedValue;
if (values.length === 0) {
return 0;
}
const mean = sumOfValues / values.length;
const variance = SumArray(values.map((value) => Math.pow(value - mean, 2))) / values.length;
return Math.sqrt(variance);
},
},
},
},
rowFilterFn: context.filterFn,
getRowNodes: context.getRowNodes,
columnType,
};
addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
const result = {
name: 'STD_DEVIATION',
type: 'aggregationScalar',
value: aggregationExpressionEvaluation,
};
return result;
},
description: 'Aggregates a column over multiple rows by computing its standard deviation.\nOptionally the aggregation may be grouped.',
signatures: [
'STD_DEVIATION( [colName] )',
'STD_DEVIATION( COL(name: string))',
'STD_DEVIATION( [colNameA], GROUP_BY( [colNameB] ))',
'STD_DEVIATION( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
],
examples: ['STD_DEVIATION([colA])', 'STD_DEVIATION([colA], GROUP_BY([colB]))'],
category: 'aggregation',
inputs: ['number'],
},
MIN: {
handler(args, context) {
const minColumnParameter = extractColumnParameter('MIN', args);
const minColumnName = minColumnParameter.value;
const columnType = validateColumnType(minColumnName, ['number', 'date'], 'MIN', context.adaptableApi);
const groupByParameter = extractParameter('MIN', 'operand', ['GROUP_BY'], args, {
isOptional: true,
});
const aggregationExpressionEvaluation = {
aggregationParams: {
reducers: {
MIN: {
name: 'MIN',
field: minColumnName,
initialValue: Number.MAX_VALUE,
reducer: (minValue, rowValue) => {
if (isUndefinedValue(rowValue)) {
return minValue;
}
if (minValue === Number.MAX_VALUE) {
return rowValue;
}
if (columnType === 'number') {
const numericRowValue = getNumericValue(rowValue);
return numericRowValue < minValue ? numericRowValue : minValue;
}
else {
// Date
return isAfter(parseDateValue(rowValue), parseDateValue(minValue))
? minValue
: rowValue;
}
},
done: (minValue) => {
if (minValue !== Number.MAX_VALUE) {
return minValue;
}
},
},
},
},
rowFilterFn: context.filterFn,
getRowNodes: context.getRowNodes,
columnType,
};
addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
const result = {
name: 'MIN',
type: 'aggregationScalar',
value: aggregationExpressionEvaluation,
};
return result;
},
description: 'Aggregates a column over multiple rows by computing the minimum of the column values\nOptionally the aggregation may be computed within provided individual groups',
signatures: [
'MIN( [colName] )',
'MIN( COL(name: string))',
'MIN( [colNameA], GROUP_BY( [colNameB] ))',
'MIN( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
],
examples: ['MIN([colA])', 'MIN([colA], GROUP_BY([colB]))'],
category: 'aggregation',
inputs: ['number'],
},
MAX: {
handler(args, context) {
const maxColumnParameter = extractColumnParameter('MAX', args);
const maxColumnName = maxColumnParameter.value;
const columnType = validateColumnType(maxColumnName, ['number', 'date'], 'MAX', context.adaptableApi);
const groupByParameter = extractParameter('MAX', 'operand', ['GROUP_BY'], args, {
isOptional: true,
});
const aggregationExpressionEvaluation = {
aggregationParams: {
reducers: {
MAX: {
name: 'MAX',
field: maxColumnName,
initialValue: Number.MIN_VALUE,
reducer: (maxValue, rowValue) => {
if (isUndefinedValue(rowValue)) {
return maxValue;
}
if (maxValue === Number.MIN_VALUE) {
return rowValue;
}
if (columnType === 'number') {
const numericRowValue = getNumericValue(rowValue);
return numericRowValue > maxValue ? numericRowValue : maxValue;
}
else {
// Date
return isAfter(parseDateValue(rowValue), parseDateValue(maxValue))
? rowValue
: maxValue;
}
},
done: (maxValue) => {
if (maxValue !== Number.MIN_VALUE) {
return maxValue;
}
},
},
},
},
rowFilterFn: context.filterFn,
getRowNodes: context.getRowNodes,
columnType,
};
addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
const result = {
name: 'MAX',
type: 'aggregationScalar',
value: aggregationExpressionEvaluation,
};
return result;
},
description: 'Aggregates a column over multiple rows by computing the maximum of the column values\nOptionally the aggregation may be computed within provided individual groups',
signatures: [
'MAX( [colName] )',
'MAX( COL(name: string))',
'MAX( [colNameA], GROUP_BY( [colNameB] ))',
'MAX( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
],
examples: ['MAX([colA])', 'MAX([colA], GROUP_BY([colB]))'],
category: 'aggregation',
inputs: ['number'],
},
QUANT: {
handler(args, context) {
const quantileColumnParameter = extractColumnParameter('QUANT', args);
const quantileColumnName = quantileColumnParameter.value;
const columnType = validateColumnType(quantileColumnName, ['number'], 'QUANT', context.adaptableApi);
const qNumberCandidates = args.filter((arg) => typeof arg === 'number');
if (qNumberCandidates.length !== 1) {
throw new ExpressionEvaluationError('QUANT', 'expects a single positive numeric argument as q-quantile');
}
const qNumber = qNumberCandidates[0];
const groupByOperand = extractParameter('QUANT', 'operand', ['GROUP_BY'], args, {
isOptional: true,
});
const groupByColumnNames = groupByOperand?.value;
let quantReducer;
if (groupByColumnNames?.length) {
// do the heavy-lifting in done()
quantReducer = {
initialValue: {},
reducer: (nodeValueMap, rowValue, rowNode, rowIndex) => {
if (isUndefinedValue(rowValue)) {
return nodeValueMap;
}
nodeValueMap[rowNode.id] = rowValue;
return nodeValueMap;
},
done: (nodeValueMap, groupItems) => {
const populationSize = groupItems?.filter((rowNode) => {
const rowValue = context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, quantileColumnName);
return isDefinedValue(rowValue);
})?.length ?? 0;
const bucketsMap = new Map();
for (let step = 1; step <= qNumber; step++) {
bucketsMap.set(step, populationSize * (step / qNumber));
}
const indexBucketMap = new Map();
for (let populationIndex = 0; populationIndex < populationSize; populationIndex++) {
for (let quantIndex = 1; quantIndex <= qNumber; quantIndex++) {
if (bucketsMap.get(quantIndex) < populationIndex + 1) {
continue;
}
else {
indexBucketMap.set(populationIndex, quantIndex);
break;
}
}
}
const groupedRowBucketMap = {};
groupItems.forEach((subgroupRowNode, subgroupRowIndex) => {
const subgroupRowValue = nodeValueMap[subgroupRowNode.id];
if (subgroupRowValue) {
groupedRowBucketMap[subgroupRowValue] = indexBucketMap.get(subgroupRowIndex);
}
});
return groupedRowBucketMap;
},
};
}
else {
// in case of non-grouped quantiles, we are able to calculate the buckets beforehand (which is the most efficient)
const populationSize = context.adaptableApi.gridApi.getAllRowNodes()?.filter((rowNode) => {
const rowValue = context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, quantileColumnName);
return isDefinedValue(rowValue);
})?.length ?? 0;
const bucketsMap = new Map();
for (let step = 1; step <= qNumber; step++) {
bucketsMap.set(step, populationSize * (step / qNumber));
}
const indexBucketMap = new Map();
for (let populationIndex = 0; populationIndex < populationSize; populationIndex++) {
for (let quantIndex = 1; quantIndex <= qNumber; quantIndex++) {
if (bucketsMap.get(quantIndex) < populationIndex + 1) {
continue;
}
else {
indexBucketMap.set(populationIndex, quantIndex);
break;
}
}
}
quantReducer = {
initialValue: {},
reducer: (rowBucketMap, rowValue, rowNode, rowIndex) => {
if (isUndefinedValue(rowValue)) {
return rowBucketMap;
}
rowBucketMap[rowValue] = indexBucketMap.get(rowIndex);
return rowBucketMap;
},
};
}
const aggregationExpressionEvaluation = {
sortByColumn: quantileColumnName,
rowFilterFn: (rowNode) => {
const rowValue = context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, quantileColumnName);
return isDefinedValue(rowValue);
},
getRowNodes: context.getRowNodes,
aggregationParams: {
reducers: {
QUANT: {
name: 'QUANT',
field: quantileColumnName,
...quantReducer,
},
},
},
rowValueGetter: (rowNode, rowBucketMap) => {
const rowValue = context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, quantileColumnName);
if (rowValue == null) {
return;
}
return rowBucketMap[rowValue];
},
columnType,
};
addGroupByParams(groupByOperand?.value, aggregationExpressionEvaluation);
const result = {
name: 'QUANT',
type: 'aggregationScalar',
value: aggregationExpressionEvaluation,
};
return result;
},
description: 'Calculates the q-Quantiles of the given group and displays which q-quantile the given value is in\nCommon quantiles are 4-quantile or Quartile, 5-quantile or Quintile, 10-quantile or Decile and 100-quantile or Percentile',
signatures: [
'QUANT( [colName], q: number)',
'QUANT( COL(name: string), q: number)',
'QUANT( [colName1], q: number, GROUP_BY([colName2]) )',
],
examples: [
'QUANT( [col1], 5)',
`QUANT( COL('col1'), 100))`,
'QUANT( [col1], 4, GROUP_BY([col2]) )',
],
category: 'aggregation',
},
QUARTILE: {
handler(args, context) {
const quartileArgs = [...args, 4];
return aggregatedScalarExpressionFunctions.QUANT.handler(quartileArgs, context);
},
description: 'Calculates the Quartiles of the given group and displays which quartile the given value is in',
signatures: ['QUARTILE( [colName])', 'QUARTILE( [colName1], GROUP_BY([colName2]) )'],
examples: ['QUARTILE( [col1], 5)', 'QUARTILE( [col1], 4, GROUP_BY([col2]) )'],
category: 'aggregation',
},
PERCENTILE: {
handler(args, context) {
const quartileArgs = [...args, 100];
return aggregatedScalarExpressionFunctions.QUANT.handler(quartileArgs, context);
},
description: 'Calculates the Percentile of the given group and displays which percentile the given value is in',
signatures: ['PERCENTILE( [colName])', 'PERCENTILE( [colName1], GROUP_BY([colName2]) )'],
examples: ['PERCENTILE( [col1], 5)', 'PERCENTILE( [col1], 4, GROUP_BY([col2]) )'],
category: 'aggregation',
},
COUNT: {
handler(args, context) {
const countColumnParameter = extractColumnParameter('COUNT', args);
const countColumnName = countColumnParameter.value;
const groupByParameter = extractParameter('COUNT', 'operand', ['GROUP_BY'], args, {
isOptional: true,
});
const aggregationExpressionEvaluation = {
aggregationParams: {
reducers: {
COUNT: {
name: 'COUNT',
field: countColumnName,
initialValue: 0,
reducer: (totalSum, rowValue) => {
if (isUndefinedValue(rowValue)) {
return totalSum;
}
return totalSum + 1;
},
},
},
},
rowFilterFn: context.filterFn,
getRowNodes: context.getRowNodes,
};
addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
const result = {
name: 'COUNT',
type: 'aggregationScalar',
value: aggregationExpressionEvaluation,
};
return result;
},
description: 'Counts the number of non-null (i.e. not empty) values in a given column.\nOptionally the aggregation may be computed within provided individual groups',
signatures: [
'COUNT( [colName] )',
'COUNT( COL(name: string))',
'COUNT( [colNameA], GROUP_BY( [colNameB] ))',
'COUNT( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
],
examples: ['COUNT([colA])', 'COUNT([colA], GROUP_BY([colB]))'],
category: 'aggregation',
inputs: [['number'], ['text'], ['date']],
},
OVER: {
handler(args, context) {
const columnParameter = extractColumnParameter('OVER', args);
validateColumnType(columnParameter.value, ['number', 'date'], 'OVER', context.adaptableApi);
const result = {
type: 'operand',
name: 'OVER',
value: columnParameter.value,
};
return result;
},
description: 'Defines an accumulative dimension (order) for the enclosing cumulative aggregation',
signatures: ['OVER( [colName] )', 'OVER( COL(name: string))'],
examples: ['OVER( [colName] )', `OVER( COL('colName'))`, `CUMUL( SUM([colA]), OVER([colB]))`],
category: 'cumulative',
},
WEIGHT: {
handler(args, context) {
const columnParameter = extractColumnParameter('WEIGHT', args);
validateColumnType(columnParameter.value, ['number'], 'WEIGHT', context.adaptableApi);
const result = {
type: 'operand',
name: 'WEIGHT',
value: columnParameter.value,
};
return result;
},
description: 'Defines a weight for the enclosing AVG(Average) aggregation',
signatures: ['WEIGHT( [colName] )', 'WEIGHT( COL(name: string))'],
examples: [
'WEIGHT( [colName] )',
`WEIGHT( COL('colName'))`,
`AVG( [colName1], WEIGHT([colName2]))`,
],
category: 'operand',
},
GROUP_BY: {
handler(args, context) {
const columnParameters = extractColumnParameters('GROUP_BY', args);
const result = {
type: 'operand',
name: 'GROUP_BY',
value: columnParameters.map((columnParam) => columnParam.value),
};
return result;
},
description: 'Groups an aggregation operation within the rows that have the same value in the specified column',
signatures: ['GROUP_BY( [colName] )', 'GROUP_BY( COL(name: string))'],
examples: ['GROUP_BY( [colName] )', `GROUP_BY( COL('colName'))`],
category: 'grouping',
},
COL: {
handler(args, context) {
return handleColumnFunction(args, context);
},
description: 'References a column by its unique identifier',
signatures: ['[colName]', 'COL(name: string)'],
examples: ['[col1]', 'COL("col1")'],
category: 'special',
},
PREVIOUS: {
handler(args, context) {
const prevColumnParameter = extractColumnParameter('PREVIOUS', args);
const prevColumnName = prevColumnParameter.value;
const groupByParameter = extractParameter('PREVIOUS', 'operand', ['GROUP_BY'], args, {
isOptional: true,
});
const overParameter = extractParameter('PREVIOUS', 'operand', ['OVER'], args);
const aggregationExpressionEvaluation = {
aggregationParams: {
reducers: {
PREVIOUS: {
name: 'PREVIOUS',
field: prevColumnName,
initialValue: {
map: {},
previousValue: undefined,
},
reducer: (indexMapAndPrevious, rowValue, rowNode) => {
if (isDefinedValue(indexMapAndPrevious.previousValue)) {
indexMapAndPrevious.map[rowNode.id] = indexMapAndPrevious.previousValue;
}
indexMapAndPrevious.previousValue = rowValue;
return indexMapAndPrevious;
},
done: (indexMapAndPrevious) => {
return indexMapAndPrevious.map;
},
},
},
},
rowValueGetter: (rowNode, indexMap) => {
return indexMap[rowNode.id];
},
rowFilterFn: context.filterFn,
sortByColumn: overParameter.value,
};
addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
const result = {
name: 'PREVIOUS',
type: 'aggregationScalar',
value: aggregationExpressionEvaluation,
};
return result;
},
description: 'Returns the value of the previous element in the given column, when ordered by the second given column.\nOptionally the value may be computed within provided individual groups',
signatures: [
'PREVIOUS( [colNameA], OVER [colNameB] )',
'PREVIOUS( COL(nameA: string), OVER( COL(nameB: string)))',
'PREVIOUS( [colNameA], OVER( [colNameB] ), GROUP_BY( [colNameC] ))',
'PREVIOUS( COL(nameA: string), OVER( COL(nameB: string)), GROUP_BY( COL(nameC: string)))',
],
examples: ['PREVIOUS([colA], OVER[colB])', 'PREVIOUS([colA], OVER([colB]), GROUP_BY([colC]))'],
category: 'refs',
},
NEXT: {
handler(args, context) {
const nextColumnParameter = extractColumnParameter('NEXT', args);
const nextColumnName = nextColumnParameter.value;
const groupByParameter = extractParameter('NEXT', 'operand', ['GROUP_BY'], args, {
isOptional: true,
});
const overParameter = extractParameter('NEXT', 'operand', ['OVER'], args);
const aggregationExpressionEvaluation = {
aggregationParams: {
reducers: {
NEXT: {
name: 'NEXT',
field: nextColumnName,
initialValue: {
map: {},
previousRowNode: undefined,
},
reducer: (indexMapAndPreviousRowNode, rowValue, rowNode) => {
if (isDefinedValue(rowValue) &&
isDefinedValue(indexMapAndPreviousRowNode.previousRowNode)) {
indexMapAndPreviousRowNode.map[indexMapAndPreviousRowNode.previousRowNode.id] =
rowValue;
}
indexMapAndPreviousRowNode.previousRowNode = rowNode;
return indexMapAndPreviousRowNode;
},
done: (indexMapAndPreviousRowNode) => {
return indexMapAndPreviousRowNode.map;
},
},
},
},
rowValueGetter: (rowNode, indexMap) => {
return indexMap[rowNode.id];
},
rowFilterFn: context.filterFn,
sortByColumn: overParameter.value,
};
addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
const result = {
name: 'NEXT',
type: 'aggregationScalar',
value: aggregationExpressionEvaluation,
};
return result;
},
description: 'Returns the value of the next element in the given column, when ordered by the second given column.\nOptionally the value may be computed within provided individual groups',
si