|
@@ -1,7 +1,8 @@
|
|
|
// Libraries
|
|
// Libraries
|
|
|
import isNumber from 'lodash/isNumber';
|
|
import isNumber from 'lodash/isNumber';
|
|
|
|
|
|
|
|
-import { DataFrame, NullValueMode } from '../types/index';
|
|
|
|
|
|
|
+import { DataFrame, NullValueMode } from '../types';
|
|
|
|
|
+import { Registry, RegistryItem } from './registry';
|
|
|
|
|
|
|
|
export enum ReducerID {
|
|
export enum ReducerID {
|
|
|
sum = 'sum',
|
|
sum = 'sum',
|
|
@@ -34,38 +35,13 @@ export interface FieldCalcs {
|
|
|
// Internal function
|
|
// Internal function
|
|
|
type FieldReducer = (data: DataFrame, fieldIndex: number, ignoreNulls: boolean, nullAsZero: boolean) => FieldCalcs;
|
|
type FieldReducer = (data: DataFrame, fieldIndex: number, ignoreNulls: boolean, nullAsZero: boolean) => FieldCalcs;
|
|
|
|
|
|
|
|
-export interface FieldReducerInfo {
|
|
|
|
|
- id: string;
|
|
|
|
|
- name: string;
|
|
|
|
|
- description: string;
|
|
|
|
|
- alias?: string; // optional secondary key. 'avg' vs 'mean', 'total' vs 'sum'
|
|
|
|
|
-
|
|
|
|
|
|
|
+export interface FieldReducerInfo extends RegistryItem {
|
|
|
// Internal details
|
|
// Internal details
|
|
|
emptyInputResult?: any; // typically null, but some things like 'count' & 'sum' should be zero
|
|
emptyInputResult?: any; // typically null, but some things like 'count' & 'sum' should be zero
|
|
|
standard: boolean; // The most common stats can all be calculated in a single pass
|
|
standard: boolean; // The most common stats can all be calculated in a single pass
|
|
|
reduce?: FieldReducer;
|
|
reduce?: FieldReducer;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * @param ids list of stat names or null to get all of them
|
|
|
|
|
- */
|
|
|
|
|
-export function getFieldReducers(ids?: string[]): FieldReducerInfo[] {
|
|
|
|
|
- if (ids === null || ids === undefined) {
|
|
|
|
|
- if (!hasBuiltIndex) {
|
|
|
|
|
- getById(ReducerID.mean);
|
|
|
|
|
- }
|
|
|
|
|
- return listOfStats;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return ids.reduce((list, id) => {
|
|
|
|
|
- const stat = getById(id);
|
|
|
|
|
- if (stat) {
|
|
|
|
|
- list.push(stat);
|
|
|
|
|
- }
|
|
|
|
|
- return list;
|
|
|
|
|
- }, new Array<FieldReducerInfo>());
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
interface ReduceFieldOptions {
|
|
interface ReduceFieldOptions {
|
|
|
series: DataFrame;
|
|
series: DataFrame;
|
|
|
fieldIndex: number;
|
|
fieldIndex: number;
|
|
@@ -83,7 +59,7 @@ export function reduceField(options: ReduceFieldOptions): FieldCalcs {
|
|
|
return {};
|
|
return {};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const queue = getFieldReducers(reducers);
|
|
|
|
|
|
|
+ const queue = fieldReducers.list(reducers);
|
|
|
|
|
|
|
|
// Return early for empty series
|
|
// Return early for empty series
|
|
|
// This lets the concrete implementations assume at least one row
|
|
// This lets the concrete implementations assume at least one row
|
|
@@ -122,122 +98,107 @@ export function reduceField(options: ReduceFieldOptions): FieldCalcs {
|
|
|
//
|
|
//
|
|
|
// ------------------------------------------------------------------------------
|
|
// ------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
-// private registry of all stats
|
|
|
|
|
-interface TableStatIndex {
|
|
|
|
|
- [id: string]: FieldReducerInfo;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const listOfStats: FieldReducerInfo[] = [];
|
|
|
|
|
-const index: TableStatIndex = {};
|
|
|
|
|
-let hasBuiltIndex = false;
|
|
|
|
|
-
|
|
|
|
|
-function getById(id: string): FieldReducerInfo | undefined {
|
|
|
|
|
- if (!hasBuiltIndex) {
|
|
|
|
|
- [
|
|
|
|
|
- {
|
|
|
|
|
- id: ReducerID.lastNotNull,
|
|
|
|
|
- name: 'Last (not null)',
|
|
|
|
|
- description: 'Last non-null value',
|
|
|
|
|
- standard: true,
|
|
|
|
|
- alias: 'current',
|
|
|
|
|
- reduce: calculateLastNotNull,
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: ReducerID.last,
|
|
|
|
|
- name: 'Last',
|
|
|
|
|
- description: 'Last Value',
|
|
|
|
|
- standard: true,
|
|
|
|
|
- reduce: calculateLast,
|
|
|
|
|
- },
|
|
|
|
|
- { id: ReducerID.first, name: 'First', description: 'First Value', standard: true, reduce: calculateFirst },
|
|
|
|
|
- {
|
|
|
|
|
- id: ReducerID.firstNotNull,
|
|
|
|
|
- name: 'First (not null)',
|
|
|
|
|
- description: 'First non-null value',
|
|
|
|
|
- standard: true,
|
|
|
|
|
- reduce: calculateFirstNotNull,
|
|
|
|
|
- },
|
|
|
|
|
- { id: ReducerID.min, name: 'Min', description: 'Minimum Value', standard: true },
|
|
|
|
|
- { id: ReducerID.max, name: 'Max', description: 'Maximum Value', standard: true },
|
|
|
|
|
- { id: ReducerID.mean, name: 'Mean', description: 'Average Value', standard: true, alias: 'avg' },
|
|
|
|
|
- {
|
|
|
|
|
- id: ReducerID.sum,
|
|
|
|
|
- name: 'Total',
|
|
|
|
|
- description: 'The sum of all values',
|
|
|
|
|
- emptyInputResult: 0,
|
|
|
|
|
- standard: true,
|
|
|
|
|
- alias: 'total',
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: ReducerID.count,
|
|
|
|
|
- name: 'Count',
|
|
|
|
|
- description: 'Number of values in response',
|
|
|
|
|
- emptyInputResult: 0,
|
|
|
|
|
- standard: true,
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: ReducerID.range,
|
|
|
|
|
- name: 'Range',
|
|
|
|
|
- description: 'Difference between minimum and maximum values',
|
|
|
|
|
- standard: true,
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: ReducerID.delta,
|
|
|
|
|
- name: 'Delta',
|
|
|
|
|
- description: 'Cumulative change in value',
|
|
|
|
|
- standard: true,
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: ReducerID.step,
|
|
|
|
|
- name: 'Step',
|
|
|
|
|
- description: 'Minimum interval between values',
|
|
|
|
|
- standard: true,
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: ReducerID.diff,
|
|
|
|
|
- name: 'Difference',
|
|
|
|
|
- description: 'Difference between first and last values',
|
|
|
|
|
- standard: true,
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: ReducerID.logmin,
|
|
|
|
|
- name: 'Min (above zero)',
|
|
|
|
|
- description: 'Used for log min scale',
|
|
|
|
|
- standard: true,
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: ReducerID.changeCount,
|
|
|
|
|
- name: 'Change Count',
|
|
|
|
|
- description: 'Number of times the value changes',
|
|
|
|
|
- standard: false,
|
|
|
|
|
- reduce: calculateChangeCount,
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: ReducerID.distinctCount,
|
|
|
|
|
- name: 'Distinct Count',
|
|
|
|
|
- description: 'Number of distinct values',
|
|
|
|
|
- standard: false,
|
|
|
|
|
- reduce: calculateDistinctCount,
|
|
|
|
|
- },
|
|
|
|
|
- ].forEach(info => {
|
|
|
|
|
- const { id, alias } = info;
|
|
|
|
|
- if (index.hasOwnProperty(id)) {
|
|
|
|
|
- console.warn('Duplicate Stat', id, info, index);
|
|
|
|
|
- }
|
|
|
|
|
- index[id] = info;
|
|
|
|
|
- if (alias) {
|
|
|
|
|
- if (index.hasOwnProperty(alias)) {
|
|
|
|
|
- console.warn('Duplicate Stat (alias)', alias, info, index);
|
|
|
|
|
- }
|
|
|
|
|
- index[alias] = info;
|
|
|
|
|
- }
|
|
|
|
|
- listOfStats.push(info);
|
|
|
|
|
- });
|
|
|
|
|
- hasBuiltIndex = true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return index[id];
|
|
|
|
|
-}
|
|
|
|
|
|
|
+export const fieldReducers = new Registry<FieldReducerInfo>(() => [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: ReducerID.lastNotNull,
|
|
|
|
|
+ name: 'Last (not null)',
|
|
|
|
|
+ description: 'Last non-null value',
|
|
|
|
|
+ standard: true,
|
|
|
|
|
+ alias: 'current',
|
|
|
|
|
+ reduce: calculateLastNotNull,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: ReducerID.last,
|
|
|
|
|
+ name: 'Last',
|
|
|
|
|
+ description: 'Last Value',
|
|
|
|
|
+ standard: true,
|
|
|
|
|
+ reduce: calculateLast,
|
|
|
|
|
+ },
|
|
|
|
|
+ { id: ReducerID.first, name: 'First', description: 'First Value', standard: true, reduce: calculateFirst },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: ReducerID.firstNotNull,
|
|
|
|
|
+ name: 'First (not null)',
|
|
|
|
|
+ description: 'First non-null value',
|
|
|
|
|
+ standard: true,
|
|
|
|
|
+ reduce: calculateFirstNotNull,
|
|
|
|
|
+ },
|
|
|
|
|
+ { id: ReducerID.min, name: 'Min', description: 'Minimum Value', standard: true },
|
|
|
|
|
+ { id: ReducerID.max, name: 'Max', description: 'Maximum Value', standard: true },
|
|
|
|
|
+ { id: ReducerID.mean, name: 'Mean', description: 'Average Value', standard: true, alias: 'avg' },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: ReducerID.sum,
|
|
|
|
|
+ name: 'Total',
|
|
|
|
|
+ description: 'The sum of all values',
|
|
|
|
|
+ emptyInputResult: 0,
|
|
|
|
|
+ standard: true,
|
|
|
|
|
+ alias: 'total',
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: ReducerID.count,
|
|
|
|
|
+ name: 'Count',
|
|
|
|
|
+ description: 'Number of values in response',
|
|
|
|
|
+ emptyInputResult: 0,
|
|
|
|
|
+ standard: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: ReducerID.range,
|
|
|
|
|
+ name: 'Range',
|
|
|
|
|
+ description: 'Difference between minimum and maximum values',
|
|
|
|
|
+ standard: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: ReducerID.delta,
|
|
|
|
|
+ name: 'Delta',
|
|
|
|
|
+ description: 'Cumulative change in value',
|
|
|
|
|
+ standard: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: ReducerID.step,
|
|
|
|
|
+ name: 'Step',
|
|
|
|
|
+ description: 'Minimum interval between values',
|
|
|
|
|
+ standard: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: ReducerID.diff,
|
|
|
|
|
+ name: 'Difference',
|
|
|
|
|
+ description: 'Difference between first and last values',
|
|
|
|
|
+ standard: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: ReducerID.logmin,
|
|
|
|
|
+ name: 'Min (above zero)',
|
|
|
|
|
+ description: 'Used for log min scale',
|
|
|
|
|
+ standard: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: ReducerID.allIsZero,
|
|
|
|
|
+ name: 'All Zeros',
|
|
|
|
|
+ description: 'All values are zero',
|
|
|
|
|
+ emptyInputResult: false,
|
|
|
|
|
+ standard: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: ReducerID.allIsNull,
|
|
|
|
|
+ name: 'All Nulls',
|
|
|
|
|
+ description: 'All values are null',
|
|
|
|
|
+ emptyInputResult: true,
|
|
|
|
|
+ standard: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: ReducerID.changeCount,
|
|
|
|
|
+ name: 'Change Count',
|
|
|
|
|
+ description: 'Number of times the value changes',
|
|
|
|
|
+ standard: false,
|
|
|
|
|
+ reduce: calculateChangeCount,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: ReducerID.distinctCount,
|
|
|
|
|
+ name: 'Distinct Count',
|
|
|
|
|
+ description: 'Number of distinct values',
|
|
|
|
|
+ standard: false,
|
|
|
|
|
+ reduce: calculateDistinctCount,
|
|
|
|
|
+ },
|
|
|
|
|
+]);
|
|
|
|
|
|
|
|
function doStandardCalcs(data: DataFrame, fieldIndex: number, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
|
|
function doStandardCalcs(data: DataFrame, fieldIndex: number, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
|
|
|
const calcs = {
|
|
const calcs = {
|
|
@@ -253,7 +214,7 @@ function doStandardCalcs(data: DataFrame, fieldIndex: number, ignoreNulls: boole
|
|
|
count: 0,
|
|
count: 0,
|
|
|
nonNullCount: 0,
|
|
nonNullCount: 0,
|
|
|
allIsNull: true,
|
|
allIsNull: true,
|
|
|
- allIsZero: false,
|
|
|
|
|
|
|
+ allIsZero: true,
|
|
|
range: null,
|
|
range: null,
|
|
|
diff: null,
|
|
diff: null,
|
|
|
delta: 0,
|
|
delta: 0,
|
|
@@ -264,7 +225,7 @@ function doStandardCalcs(data: DataFrame, fieldIndex: number, ignoreNulls: boole
|
|
|
} as FieldCalcs;
|
|
} as FieldCalcs;
|
|
|
|
|
|
|
|
for (let i = 0; i < data.rows.length; i++) {
|
|
for (let i = 0; i < data.rows.length; i++) {
|
|
|
- let currentValue = data.rows[i][fieldIndex];
|
|
|
|
|
|
|
+ let currentValue = data.rows[i] ? data.rows[i][fieldIndex] : null;
|
|
|
if (i === 0) {
|
|
if (i === 0) {
|
|
|
calcs.first = currentValue;
|
|
calcs.first = currentValue;
|
|
|
}
|
|
}
|
|
@@ -350,6 +311,10 @@ function doStandardCalcs(data: DataFrame, fieldIndex: number, ignoreNulls: boole
|
|
|
calcs.mean = calcs.sum! / calcs.nonNullCount;
|
|
calcs.mean = calcs.sum! / calcs.nonNullCount;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (calcs.allIsNull) {
|
|
|
|
|
+ calcs.allIsZero = false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (calcs.max !== null && calcs.min !== null) {
|
|
if (calcs.max !== null && calcs.min !== null) {
|
|
|
calcs.range = calcs.max - calcs.min;
|
|
calcs.range = calcs.max - calcs.min;
|
|
|
}
|
|
}
|