| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- // Libraries
- import isNumber from 'lodash/isNumber';
- import { NullValueMode, Field } from '../types';
- import { Registry, RegistryItem } from './registry';
- export enum ReducerID {
- sum = 'sum',
- max = 'max',
- min = 'min',
- logmin = 'logmin',
- mean = 'mean',
- last = 'last',
- first = 'first',
- count = 'count',
- range = 'range',
- diff = 'diff',
- delta = 'delta',
- step = 'step',
- firstNotNull = 'firstNotNull',
- lastNotNull = 'lastNotNull',
- changeCount = 'changeCount',
- distinctCount = 'distinctCount',
- allIsZero = 'allIsZero',
- allIsNull = 'allIsNull',
- }
- export interface FieldCalcs {
- [key: string]: any;
- }
- // Internal function
- type FieldReducer = (field: Field, ignoreNulls: boolean, nullAsZero: boolean) => FieldCalcs;
- export interface FieldReducerInfo extends RegistryItem {
- // Internal details
- 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
- reduce?: FieldReducer;
- }
- interface ReduceFieldOptions {
- field: Field;
- reducers: string[]; // The stats to calculate
- }
- /**
- * @returns an object with a key for each selected stat
- */
- export function reduceField(options: ReduceFieldOptions): FieldCalcs {
- const { field, reducers } = options;
- if (!field || !reducers || reducers.length < 1) {
- return {};
- }
- if (field.calcs) {
- // Find the values we need to calculate
- const missing: string[] = [];
- for (const s of reducers) {
- if (!field.calcs.hasOwnProperty(s)) {
- missing.push(s);
- }
- }
- if (missing.length < 1) {
- return {
- ...field.calcs,
- };
- }
- }
- const queue = fieldReducers.list(reducers);
- // Return early for empty series
- // This lets the concrete implementations assume at least one row
- const data = field.values;
- if (data.length < 1) {
- const calcs = { ...field.calcs } as FieldCalcs;
- for (const reducer of queue) {
- calcs[reducer.id] = reducer.emptyInputResult !== null ? reducer.emptyInputResult : null;
- }
- return (field.calcs = calcs);
- }
- const { nullValueMode } = field.config;
- const ignoreNulls = nullValueMode === NullValueMode.Ignore;
- const nullAsZero = nullValueMode === NullValueMode.AsZero;
- // Avoid calculating all the standard stats if possible
- if (queue.length === 1 && queue[0].reduce) {
- const values = queue[0].reduce(field, ignoreNulls, nullAsZero);
- field.calcs = {
- ...field.calcs,
- ...values,
- };
- return values;
- }
- // For now everything can use the standard stats
- let values = doStandardCalcs(field, ignoreNulls, nullAsZero);
- for (const reducer of queue) {
- if (!values.hasOwnProperty(reducer.id) && reducer.reduce) {
- values = {
- ...values,
- ...reducer.reduce(field, ignoreNulls, nullAsZero),
- };
- }
- }
- field.calcs = {
- ...field.calcs,
- ...values,
- };
- return values;
- }
- // ------------------------------------------------------------------------------
- //
- // No Exported symbols below here.
- //
- // ------------------------------------------------------------------------------
- export const fieldReducers = new Registry<FieldReducerInfo>(() => [
- {
- id: ReducerID.lastNotNull,
- name: 'Last (not null)',
- description: 'Last non-null value',
- standard: true,
- aliasIds: ['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, aliasIds: ['avg'] },
- {
- id: ReducerID.sum,
- name: 'Total',
- description: 'The sum of all values',
- emptyInputResult: 0,
- standard: true,
- aliasIds: ['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(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
- const calcs = {
- sum: 0,
- max: -Number.MAX_VALUE,
- min: Number.MAX_VALUE,
- logmin: Number.MAX_VALUE,
- mean: null,
- last: null,
- first: null,
- lastNotNull: undefined,
- firstNotNull: undefined,
- count: 0,
- nonNullCount: 0,
- allIsNull: true,
- allIsZero: true,
- range: null,
- diff: null,
- delta: 0,
- step: Number.MAX_VALUE,
- // Just used for calcutations -- not exposed as a stat
- previousDeltaUp: true,
- } as FieldCalcs;
- const data = field.values;
- for (let i = 0; i < data.length; i++) {
- let currentValue = data.get(i);
- if (i === 0) {
- calcs.first = currentValue;
- }
- calcs.last = currentValue;
- if (currentValue === null) {
- if (ignoreNulls) {
- continue;
- }
- if (nullAsZero) {
- currentValue = 0;
- }
- }
- if (currentValue !== null) {
- const isFirst = calcs.firstNotNull === undefined;
- if (isFirst) {
- calcs.firstNotNull = currentValue;
- }
- if (isNumber(currentValue)) {
- calcs.sum += currentValue;
- calcs.allIsNull = false;
- calcs.nonNullCount++;
- if (!isFirst) {
- const step = currentValue - calcs.lastNotNull!;
- if (calcs.step > step) {
- calcs.step = step; // the minimum interval
- }
- if (calcs.lastNotNull! > currentValue) {
- // counter reset
- calcs.previousDeltaUp = false;
- if (i === data.length - 1) {
- // reset on last
- calcs.delta += currentValue;
- }
- } else {
- if (calcs.previousDeltaUp) {
- calcs.delta += step; // normal increment
- } else {
- calcs.delta += currentValue; // account for counter reset
- }
- calcs.previousDeltaUp = true;
- }
- }
- if (currentValue > calcs.max) {
- calcs.max = currentValue;
- }
- if (currentValue < calcs.min) {
- calcs.min = currentValue;
- }
- if (currentValue < calcs.logmin && currentValue > 0) {
- calcs.logmin = currentValue;
- }
- }
- if (currentValue !== 0) {
- calcs.allIsZero = false;
- }
- calcs.lastNotNull = currentValue;
- }
- }
- if (calcs.max === -Number.MAX_VALUE) {
- calcs.max = null;
- }
- if (calcs.min === Number.MAX_VALUE) {
- calcs.min = null;
- }
- if (calcs.step === Number.MAX_VALUE) {
- calcs.step = null;
- }
- if (calcs.nonNullCount > 0) {
- calcs.mean = calcs.sum! / calcs.nonNullCount;
- }
- if (calcs.allIsNull) {
- calcs.allIsZero = false;
- }
- if (calcs.max !== null && calcs.min !== null) {
- calcs.range = calcs.max - calcs.min;
- }
- if (isNumber(calcs.firstNotNull) && isNumber(calcs.lastNotNull)) {
- calcs.diff = calcs.lastNotNull - calcs.firstNotNull;
- }
- return calcs;
- }
- function calculateFirst(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
- return { first: field.values.get(0) };
- }
- function calculateFirstNotNull(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
- const data = field.values;
- for (let idx = 0; idx < data.length; idx++) {
- const v = data.get(idx);
- if (v != null) {
- return { firstNotNull: v };
- }
- }
- return { firstNotNull: undefined };
- }
- function calculateLast(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
- const data = field.values;
- return { last: data.get(data.length - 1) };
- }
- function calculateLastNotNull(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
- const data = field.values;
- let idx = data.length - 1;
- while (idx >= 0) {
- const v = data.get(idx--);
- if (v != null) {
- return { lastNotNull: v };
- }
- }
- return { lastNotNull: undefined };
- }
- function calculateChangeCount(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
- const data = field.values;
- let count = 0;
- let first = true;
- let last: any = null;
- for (let i = 0; i < data.length; i++) {
- let currentValue = data.get(i);
- if (currentValue === null) {
- if (ignoreNulls) {
- continue;
- }
- if (nullAsZero) {
- currentValue = 0;
- }
- }
- if (!first && last !== currentValue) {
- count++;
- }
- first = false;
- last = currentValue;
- }
- return { changeCount: count };
- }
- function calculateDistinctCount(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
- const data = field.values;
- const distinct = new Set<any>();
- for (let i = 0; i < data.length; i++) {
- let currentValue = data.get(i);
- if (currentValue === null) {
- if (ignoreNulls) {
- continue;
- }
- if (nullAsZero) {
- currentValue = 0;
- }
- }
- distinct.add(currentValue);
- }
- return { distinctCount: distinct.size };
- }
|