fieldReducer.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. // Libraries
  2. import isNumber from 'lodash/isNumber';
  3. import { NullValueMode, Field } from '../types';
  4. import { Registry, RegistryItem } from './registry';
  5. export enum ReducerID {
  6. sum = 'sum',
  7. max = 'max',
  8. min = 'min',
  9. logmin = 'logmin',
  10. mean = 'mean',
  11. last = 'last',
  12. first = 'first',
  13. count = 'count',
  14. range = 'range',
  15. diff = 'diff',
  16. delta = 'delta',
  17. step = 'step',
  18. firstNotNull = 'firstNotNull',
  19. lastNotNull = 'lastNotNull',
  20. changeCount = 'changeCount',
  21. distinctCount = 'distinctCount',
  22. allIsZero = 'allIsZero',
  23. allIsNull = 'allIsNull',
  24. }
  25. export interface FieldCalcs {
  26. [key: string]: any;
  27. }
  28. // Internal function
  29. type FieldReducer = (field: Field, ignoreNulls: boolean, nullAsZero: boolean) => FieldCalcs;
  30. export interface FieldReducerInfo extends RegistryItem {
  31. // Internal details
  32. emptyInputResult?: any; // typically null, but some things like 'count' & 'sum' should be zero
  33. standard: boolean; // The most common stats can all be calculated in a single pass
  34. reduce?: FieldReducer;
  35. }
  36. interface ReduceFieldOptions {
  37. field: Field;
  38. reducers: string[]; // The stats to calculate
  39. }
  40. /**
  41. * @returns an object with a key for each selected stat
  42. */
  43. export function reduceField(options: ReduceFieldOptions): FieldCalcs {
  44. const { field, reducers } = options;
  45. if (!field || !reducers || reducers.length < 1) {
  46. return {};
  47. }
  48. if (field.calcs) {
  49. // Find the values we need to calculate
  50. const missing: string[] = [];
  51. for (const s of reducers) {
  52. if (!field.calcs.hasOwnProperty(s)) {
  53. missing.push(s);
  54. }
  55. }
  56. if (missing.length < 1) {
  57. return {
  58. ...field.calcs,
  59. };
  60. }
  61. }
  62. const queue = fieldReducers.list(reducers);
  63. // Return early for empty series
  64. // This lets the concrete implementations assume at least one row
  65. const data = field.values;
  66. if (data.length < 1) {
  67. const calcs = { ...field.calcs } as FieldCalcs;
  68. for (const reducer of queue) {
  69. calcs[reducer.id] = reducer.emptyInputResult !== null ? reducer.emptyInputResult : null;
  70. }
  71. return (field.calcs = calcs);
  72. }
  73. const { nullValueMode } = field.config;
  74. const ignoreNulls = nullValueMode === NullValueMode.Ignore;
  75. const nullAsZero = nullValueMode === NullValueMode.AsZero;
  76. // Avoid calculating all the standard stats if possible
  77. if (queue.length === 1 && queue[0].reduce) {
  78. const values = queue[0].reduce(field, ignoreNulls, nullAsZero);
  79. field.calcs = {
  80. ...field.calcs,
  81. ...values,
  82. };
  83. return values;
  84. }
  85. // For now everything can use the standard stats
  86. let values = doStandardCalcs(field, ignoreNulls, nullAsZero);
  87. for (const reducer of queue) {
  88. if (!values.hasOwnProperty(reducer.id) && reducer.reduce) {
  89. values = {
  90. ...values,
  91. ...reducer.reduce(field, ignoreNulls, nullAsZero),
  92. };
  93. }
  94. }
  95. field.calcs = {
  96. ...field.calcs,
  97. ...values,
  98. };
  99. return values;
  100. }
  101. // ------------------------------------------------------------------------------
  102. //
  103. // No Exported symbols below here.
  104. //
  105. // ------------------------------------------------------------------------------
  106. export const fieldReducers = new Registry<FieldReducerInfo>(() => [
  107. {
  108. id: ReducerID.lastNotNull,
  109. name: 'Last (not null)',
  110. description: 'Last non-null value',
  111. standard: true,
  112. aliasIds: ['current'],
  113. reduce: calculateLastNotNull,
  114. },
  115. {
  116. id: ReducerID.last,
  117. name: 'Last',
  118. description: 'Last Value',
  119. standard: true,
  120. reduce: calculateLast,
  121. },
  122. { id: ReducerID.first, name: 'First', description: 'First Value', standard: true, reduce: calculateFirst },
  123. {
  124. id: ReducerID.firstNotNull,
  125. name: 'First (not null)',
  126. description: 'First non-null value',
  127. standard: true,
  128. reduce: calculateFirstNotNull,
  129. },
  130. { id: ReducerID.min, name: 'Min', description: 'Minimum Value', standard: true },
  131. { id: ReducerID.max, name: 'Max', description: 'Maximum Value', standard: true },
  132. { id: ReducerID.mean, name: 'Mean', description: 'Average Value', standard: true, aliasIds: ['avg'] },
  133. {
  134. id: ReducerID.sum,
  135. name: 'Total',
  136. description: 'The sum of all values',
  137. emptyInputResult: 0,
  138. standard: true,
  139. aliasIds: ['total'],
  140. },
  141. {
  142. id: ReducerID.count,
  143. name: 'Count',
  144. description: 'Number of values in response',
  145. emptyInputResult: 0,
  146. standard: true,
  147. },
  148. {
  149. id: ReducerID.range,
  150. name: 'Range',
  151. description: 'Difference between minimum and maximum values',
  152. standard: true,
  153. },
  154. {
  155. id: ReducerID.delta,
  156. name: 'Delta',
  157. description: 'Cumulative change in value',
  158. standard: true,
  159. },
  160. {
  161. id: ReducerID.step,
  162. name: 'Step',
  163. description: 'Minimum interval between values',
  164. standard: true,
  165. },
  166. {
  167. id: ReducerID.diff,
  168. name: 'Difference',
  169. description: 'Difference between first and last values',
  170. standard: true,
  171. },
  172. {
  173. id: ReducerID.logmin,
  174. name: 'Min (above zero)',
  175. description: 'Used for log min scale',
  176. standard: true,
  177. },
  178. {
  179. id: ReducerID.allIsZero,
  180. name: 'All Zeros',
  181. description: 'All values are zero',
  182. emptyInputResult: false,
  183. standard: true,
  184. },
  185. {
  186. id: ReducerID.allIsNull,
  187. name: 'All Nulls',
  188. description: 'All values are null',
  189. emptyInputResult: true,
  190. standard: true,
  191. },
  192. {
  193. id: ReducerID.changeCount,
  194. name: 'Change Count',
  195. description: 'Number of times the value changes',
  196. standard: false,
  197. reduce: calculateChangeCount,
  198. },
  199. {
  200. id: ReducerID.distinctCount,
  201. name: 'Distinct Count',
  202. description: 'Number of distinct values',
  203. standard: false,
  204. reduce: calculateDistinctCount,
  205. },
  206. ]);
  207. function doStandardCalcs(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
  208. const calcs = {
  209. sum: 0,
  210. max: -Number.MAX_VALUE,
  211. min: Number.MAX_VALUE,
  212. logmin: Number.MAX_VALUE,
  213. mean: null,
  214. last: null,
  215. first: null,
  216. lastNotNull: undefined,
  217. firstNotNull: undefined,
  218. count: 0,
  219. nonNullCount: 0,
  220. allIsNull: true,
  221. allIsZero: true,
  222. range: null,
  223. diff: null,
  224. delta: 0,
  225. step: Number.MAX_VALUE,
  226. // Just used for calcutations -- not exposed as a stat
  227. previousDeltaUp: true,
  228. } as FieldCalcs;
  229. const data = field.values;
  230. for (let i = 0; i < data.length; i++) {
  231. let currentValue = data.get(i);
  232. if (i === 0) {
  233. calcs.first = currentValue;
  234. }
  235. calcs.last = currentValue;
  236. if (currentValue === null) {
  237. if (ignoreNulls) {
  238. continue;
  239. }
  240. if (nullAsZero) {
  241. currentValue = 0;
  242. }
  243. }
  244. if (currentValue !== null) {
  245. const isFirst = calcs.firstNotNull === undefined;
  246. if (isFirst) {
  247. calcs.firstNotNull = currentValue;
  248. }
  249. if (isNumber(currentValue)) {
  250. calcs.sum += currentValue;
  251. calcs.allIsNull = false;
  252. calcs.nonNullCount++;
  253. if (!isFirst) {
  254. const step = currentValue - calcs.lastNotNull!;
  255. if (calcs.step > step) {
  256. calcs.step = step; // the minimum interval
  257. }
  258. if (calcs.lastNotNull! > currentValue) {
  259. // counter reset
  260. calcs.previousDeltaUp = false;
  261. if (i === data.length - 1) {
  262. // reset on last
  263. calcs.delta += currentValue;
  264. }
  265. } else {
  266. if (calcs.previousDeltaUp) {
  267. calcs.delta += step; // normal increment
  268. } else {
  269. calcs.delta += currentValue; // account for counter reset
  270. }
  271. calcs.previousDeltaUp = true;
  272. }
  273. }
  274. if (currentValue > calcs.max) {
  275. calcs.max = currentValue;
  276. }
  277. if (currentValue < calcs.min) {
  278. calcs.min = currentValue;
  279. }
  280. if (currentValue < calcs.logmin && currentValue > 0) {
  281. calcs.logmin = currentValue;
  282. }
  283. }
  284. if (currentValue !== 0) {
  285. calcs.allIsZero = false;
  286. }
  287. calcs.lastNotNull = currentValue;
  288. }
  289. }
  290. if (calcs.max === -Number.MAX_VALUE) {
  291. calcs.max = null;
  292. }
  293. if (calcs.min === Number.MAX_VALUE) {
  294. calcs.min = null;
  295. }
  296. if (calcs.step === Number.MAX_VALUE) {
  297. calcs.step = null;
  298. }
  299. if (calcs.nonNullCount > 0) {
  300. calcs.mean = calcs.sum! / calcs.nonNullCount;
  301. }
  302. if (calcs.allIsNull) {
  303. calcs.allIsZero = false;
  304. }
  305. if (calcs.max !== null && calcs.min !== null) {
  306. calcs.range = calcs.max - calcs.min;
  307. }
  308. if (isNumber(calcs.firstNotNull) && isNumber(calcs.lastNotNull)) {
  309. calcs.diff = calcs.lastNotNull - calcs.firstNotNull;
  310. }
  311. return calcs;
  312. }
  313. function calculateFirst(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
  314. return { first: field.values.get(0) };
  315. }
  316. function calculateFirstNotNull(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
  317. const data = field.values;
  318. for (let idx = 0; idx < data.length; idx++) {
  319. const v = data.get(idx);
  320. if (v != null) {
  321. return { firstNotNull: v };
  322. }
  323. }
  324. return { firstNotNull: undefined };
  325. }
  326. function calculateLast(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
  327. const data = field.values;
  328. return { last: data.get(data.length - 1) };
  329. }
  330. function calculateLastNotNull(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
  331. const data = field.values;
  332. let idx = data.length - 1;
  333. while (idx >= 0) {
  334. const v = data.get(idx--);
  335. if (v != null) {
  336. return { lastNotNull: v };
  337. }
  338. }
  339. return { lastNotNull: undefined };
  340. }
  341. function calculateChangeCount(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
  342. const data = field.values;
  343. let count = 0;
  344. let first = true;
  345. let last: any = null;
  346. for (let i = 0; i < data.length; i++) {
  347. let currentValue = data.get(i);
  348. if (currentValue === null) {
  349. if (ignoreNulls) {
  350. continue;
  351. }
  352. if (nullAsZero) {
  353. currentValue = 0;
  354. }
  355. }
  356. if (!first && last !== currentValue) {
  357. count++;
  358. }
  359. first = false;
  360. last = currentValue;
  361. }
  362. return { changeCount: count };
  363. }
  364. function calculateDistinctCount(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
  365. const data = field.values;
  366. const distinct = new Set<any>();
  367. for (let i = 0; i < data.length; i++) {
  368. let currentValue = data.get(i);
  369. if (currentValue === null) {
  370. if (ignoreNulls) {
  371. continue;
  372. }
  373. if (nullAsZero) {
  374. currentValue = 0;
  375. }
  376. }
  377. distinct.add(currentValue);
  378. }
  379. return { distinctCount: distinct.size };
  380. }