From b7963f8e8755b132f8aeb5279f64edf1f19346d0 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 12 Mar 2019 01:29:01 -0700 Subject: [PATCH 01/13] add table reducer --- .../grafana-ui/src/utils/tableReducer.test.ts | 18 ++ packages/grafana-ui/src/utils/tableReducer.ts | 237 ++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 packages/grafana-ui/src/utils/tableReducer.test.ts create mode 100644 packages/grafana-ui/src/utils/tableReducer.ts diff --git a/packages/grafana-ui/src/utils/tableReducer.test.ts b/packages/grafana-ui/src/utils/tableReducer.test.ts new file mode 100644 index 00000000000..64a467fcd5c --- /dev/null +++ b/packages/grafana-ui/src/utils/tableReducer.test.ts @@ -0,0 +1,18 @@ +import { parseCSV } from './processTableData'; +import { reduceTableData } from './tableReducer'; + +describe('Table Reducer', () => { + it('should calculate average', () => { + const table = parseCSV('a,b,c\n1,2,3\n4,5,6'); + + const reduced = reduceTableData(table, { + stats: ['last'], + }); + + expect(reduced.length).toBe(1); + expect(reduced[0].rows.length).toBe(1); + expect(reduced[0].rows[0]).toEqual(table.rows[1]); + + console.log('REDUCE', reduced[0].rows); + }); +}); diff --git a/packages/grafana-ui/src/utils/tableReducer.ts b/packages/grafana-ui/src/utils/tableReducer.ts new file mode 100644 index 00000000000..3c0317b86f4 --- /dev/null +++ b/packages/grafana-ui/src/utils/tableReducer.ts @@ -0,0 +1,237 @@ +// Libraries +import isNumber from 'lodash/isNumber'; + +import { TableData, NullValueMode } from '../types/index'; + +/** Reduce each column in a table to a single value */ +export type TableReducer = ( + data: TableData, + columnIndexes: number[], + ignoreNulls: boolean, + nullAsZero: boolean +) => any[]; + +/** Information about the reducing(stats) functions */ +export interface TableReducerInfo { + key: string; + name: string; + description: string; + standard: boolean; // The most common stats can all be calculated in a single pass + reducer?: TableReducer; + alias?: string; // optional secondary key. 'avg' vs 'mean' +} + +/** Get a list of the known reducing functions */ +export function getTableReducers(): TableReducerInfo[] { + return reducers; +} + +export interface TableReducerOptions { + columnIndexes?: number[]; + nullValueMode?: NullValueMode; + stats: string[]; // The stats to calculate +} + +export function reduceTableData(data: TableData, options: TableReducerOptions): TableData[] { + if (registry == null) { + registry = new Map(); + reducers.forEach(calc => { + registry!.set(calc.key, calc); + if (calc.alias) { + registry!.set(calc.alias, calc); + } + }); + } + + const indexes = verifyColumns(data, options); + const columns = indexes.map(v => data.columns[v]); + + const ignoreNulls = options.nullValueMode === NullValueMode.Ignore; + const nullAsZero = options.nullValueMode === NullValueMode.AsZero; + + const queue = options.stats.map(key => { + const c = registry!.get(key); + if (!c) { + throw new Error('Unknown stats calculator: ' + key); + } + return c; + }); + + // Avoid the standard calculator if possible + if (queue.length === 1 && queue[0].reducer) { + return [ + { + columns, + rows: [queue[0].reducer(data, indexes, ignoreNulls, nullAsZero)], + type: 'table', + columnMap: {}, + }, + ]; + } + + // For now everything can use the standard stats + const standard = standardStatsReducer(data, indexes, ignoreNulls, nullAsZero); + return queue.map(calc => { + const values = calc.standard + ? standard.map((s: any) => s[calc.key]) + : calc.reducer!(data, indexes, ignoreNulls, nullAsZero); + return { + columns, + rows: [values], + type: 'table', + columnMap: {}, + }; + }); +} + +// ------------------------------------------------------------------------------ +// +// No Exported symbols below here. +// +// ------------------------------------------------------------------------------ + +/** + * This will return an array of valid indexes and throw an error if invalid request + */ +function verifyColumns(data: TableData, options: TableReducerOptions): number[] { + const { columnIndexes } = options; + if (!columnIndexes) { + return data.columns.map((v, idx) => idx); + } + columnIndexes.forEach(v => { + if (v < 0 || v >= data.columns.length) { + throw new Error('Invalid column selection: ' + v); + } + }); + return columnIndexes; +} + +interface StandardStats { + sum: number | null; // total + max: number | null; + min: number | null; + logmin: number; + mean: number | null; // avg + last: any; // current + first: any; + count: number; + nonNullCount: number; + range: number | null; + diff: number | null; + + allIsZero: boolean; + allIsNull: boolean; +} + +const reducers: TableReducerInfo[] = [ + { key: 'sum', alias: 'total', name: 'Total', description: 'The sum of all values', standard: true }, + { key: 'min', name: 'Min', description: 'Minimum Value', standard: true }, + { key: 'max', name: 'Max', description: 'Maximum Value', standard: true }, + { key: 'mean', name: 'Mean', description: 'Average Value', standard: true, alias: 'avg' }, + { key: 'first', name: 'First', description: 'First Value', standard: true }, + { key: 'last', name: 'Last', description: 'Last Value (current)', standard: true, alias: 'current' }, + { key: 'count', name: 'Count', description: 'Value Count', standard: true }, + { key: 'range', name: 'Range', description: 'Difference between minimum and maximum values', standard: true }, + { key: 'diff', name: 'Difference', description: 'Difference between first and last values', standard: true }, +]; + +let registry: Map | null = null; + +function standardStatsReducer( + data: TableData, + columnIndexes: number[], + ignoreNulls: boolean, + nullAsZero: boolean +): StandardStats[] { + const column = columnIndexes.map(idx => { + return { + sum: 0, + max: -Number.MAX_VALUE, + min: Number.MAX_VALUE, + logmin: Number.MAX_VALUE, + mean: null, + last: null, + first: null, + count: 0, + nonNullCount: 0, + allIsNull: true, + allIsZero: false, + range: null, + diff: null, + } as StandardStats; + }); + + for (let i = 0; i < data.rows.length; i++) { + for (let x = 0; x < column.length; x++) { + const stats = column[x]; + let currentValue = data.rows[i][x]; + + if (currentValue === null) { + if (ignoreNulls) { + continue; + } + if (nullAsZero) { + currentValue = 0; + } + } + + if (stats.first === null) { + stats.first = currentValue; + } + + if (currentValue !== null) { + stats.last = currentValue; + + if (isNumber(currentValue)) { + stats.sum! += currentValue; + stats.allIsNull = false; + stats.nonNullCount++; + } + + if (currentValue > stats.max!) { + stats.max = currentValue; + } + + if (currentValue < stats.min!) { + stats.min = currentValue; + } + + if (currentValue < stats.logmin && currentValue > 0) { + stats.logmin = currentValue; + } + + if (currentValue !== 0) { + stats.allIsZero = false; + } + } + } + } + + for (let x = 0; x < column.length; x++) { + const stats = column[x]; + + if (stats.max === -Number.MAX_VALUE) { + stats.max = null; + } + + if (stats.min === Number.MAX_VALUE) { + stats.min = null; + } + + if (stats.nonNullCount > 0) { + stats.mean = stats.sum! / stats.nonNullCount; + } + + if (stats.max !== null && stats.min !== null) { + stats.range = stats.max - stats.min; + } + + if (stats.first !== null && stats.last !== null) { + if (isNumber(stats.first) && isNumber(stats.last)) { + stats.diff = stats.last - stats.first; + } + } + } + + return column; +} From 9016c180884e69a7f11db0384496302906230358 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 12 Mar 2019 10:53:28 -0700 Subject: [PATCH 02/13] adding simple widget to pick the reducer --- .../TableReducePicker.story.tsx | 42 ++++++++++++++ .../TableReducePicker/TableReducePicker.tsx | 57 +++++++++++++++++++ .../grafana-ui/src/utils/tableReducer.test.ts | 48 +++++++++++++--- packages/grafana-ui/src/utils/tableReducer.ts | 54 +++++++++++++----- 4 files changed, 179 insertions(+), 22 deletions(-) create mode 100644 packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.story.tsx create mode 100644 packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.tsx diff --git a/packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.story.tsx b/packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.story.tsx new file mode 100644 index 00000000000..596b7fd444a --- /dev/null +++ b/packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.story.tsx @@ -0,0 +1,42 @@ +import React, { PureComponent } from 'react'; + +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; +import { TableReducePicker } from './TableReducePicker'; + +interface State { + reducers: string[]; +} + +export class WrapperWithState extends PureComponent { + constructor(props: any) { + super(props); + this.state = { + reducers: [], // nothing? + }; + } + + render() { + return ( + { + action('Reduce')(reducers); + this.setState({ reducers }); + }} + /> + ); + } +} + +const story = storiesOf('UI/TableReducePicker', module); +story.addDecorator(withCenteredStory); +story.add('default', () => { + return ( +
+ +
+ ); +}); diff --git a/packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.tsx b/packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.tsx new file mode 100644 index 00000000000..d200658babb --- /dev/null +++ b/packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.tsx @@ -0,0 +1,57 @@ +import React, { PureComponent } from 'react'; + +import isArray from 'lodash/isArray'; + +import { Select } from '../index'; + +import { getTableReducers } from '../../utils/tableReducer'; +import { SelectOptionItem } from '../Select/Select'; + +interface Props { + onChange: (reducers: string[]) => void; + reducers: string[]; + width?: number; +} + +export class TableReducePicker extends PureComponent { + static defaultProps = { + width: 12, + }; + + onSelectionChange = (item: SelectOptionItem) => { + const { onChange } = this.props; + if (isArray(item)) { + onChange(item.map(v => v.value)); + } else { + onChange([item.value]); + } + }; + + render() { + const { width, reducers } = this.props; + + const allReducers = getTableReducers(); + + // Need to transform the data structure to work well with Select + const reducerOptions = allReducers.map(info => { + return { + label: info.name, + value: info.key, + description: info.description, + }; + }); + + const current = reducerOptions.filter(options => reducers.includes(options.value)); + return ( + ); diff --git a/packages/grafana-ui/src/utils/tableReducer.ts b/packages/grafana-ui/src/utils/tableReducer.ts index ca5b00986d1..c9c00d70752 100644 --- a/packages/grafana-ui/src/utils/tableReducer.ts +++ b/packages/grafana-ui/src/utils/tableReducer.ts @@ -3,22 +3,44 @@ import isNumber from 'lodash/isNumber'; import { TableData, NullValueMode } from '../types/index'; -/** Reduce each column in a table to a single value */ -type TableReducer = (data: TableData, columnIndexes: number[], ignoreNulls: boolean, nullAsZero: boolean) => any[]; +export enum TableReducerID { + sum = 'sum', + max = 'max', + min = 'min', + logmin = 'logmin', + mean = 'mean', + last = 'last', + first = 'first', + count = 'count', + range = 'range', + diff = 'diff', -/** Information about the reducing(stats) functions */ + allIsZero = 'allIsZero', + allIsNull = 'allIsNull', +} + +/** Information about the reducing(stats) functions */ export interface TableReducerInfo { - key: string; - name: string; + value: string; // The ID - value maps directly to select component + label: string; // The name - label description: string; + alias?: string; // optional secondary key. 'avg' vs 'mean', 'total' vs 'sum' + + // 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 reducer?: TableReducer; - alias?: string; // optional secondary key. 'avg' vs 'mean' } /** Get a list of the known reducing functions */ -export function getTableReducers(): TableReducerInfo[] { - return reducers; +export function getTableReducers(ids?: string[]): TableReducerInfo[] { + if (!hasBuiltIndex) { + getById(TableReducerID.sum); // Force the registry to load + } + if (ids === null || ids === undefined) { + return listOfReducers; + } + return ids.map(id => getById(id)); } export interface TableReducerOptions { @@ -34,39 +56,22 @@ export function reduceTableData(data: TableData, options: TableReducerOptions): const ignoreNulls = options.nullValueMode === NullValueMode.Ignore; const nullAsZero = options.nullValueMode === NullValueMode.AsZero; + const queue = getTableReducers(options.stats); + // Return early for empty tables + // This lets the concrete implementations assume at least one row if (!data.rows || data.rows.length < 1) { - const val = nullAsZero ? 0 : null; - const rows = [indexes.map(v => val)]; - return options.stats.map(stat => { + return queue.map(stat => { return { columns, - rows, + rows: [indexes.map(v => stat.emptyInputResult)], type: 'table', columnMap: {}, }; }); } - if (registry == null) { - registry = new Map(); - reducers.forEach(calc => { - registry!.set(calc.key, calc); - if (calc.alias) { - registry!.set(calc.alias, calc); - } - }); - } - - const queue = options.stats.map(key => { - const c = registry!.get(key); - if (!c) { - throw new Error('Unknown stats calculator: ' + key); - } - return c; - }); - - // Avoid the standard calculator if possible + // Avoid calculating all the standard stats if possible if (queue.length === 1 && queue[0].reducer) { return [ { @@ -82,7 +87,7 @@ export function reduceTableData(data: TableData, options: TableReducerOptions): const standard = standardStatsReducer(data, indexes, ignoreNulls, nullAsZero); return queue.map(calc => { const values = calc.standard - ? standard.map((s: any) => s[calc.key]) + ? standard.map((s: any) => s[calc.value]) : calc.reducer!(data, indexes, ignoreNulls, nullAsZero); return { columns, @@ -99,6 +104,70 @@ export function reduceTableData(data: TableData, options: TableReducerOptions): // // ------------------------------------------------------------------------------ +type TableReducer = (data: TableData, columnIndexes: number[], ignoreNulls: boolean, nullAsZero: boolean) => any[]; + +// private registry of all reducers +interface TableReducerIndex { + [id: string]: TableReducerInfo; +} +const listOfReducers: TableReducerInfo[] = []; +const index: TableReducerIndex = {}; +let hasBuiltIndex = false; + +function getById(id: string): TableReducerInfo { + if (!hasBuiltIndex) { + [ + { + value: TableReducerID.last, + label: 'Last', + description: 'Last Value (current)', + standard: true, + alias: 'current', + reducer: getLastRow, + }, + { value: TableReducerID.first, label: 'First', description: 'First Value', standard: true, reducer: getFirstRow }, + { value: TableReducerID.min, label: 'Min', description: 'Minimum Value', standard: true }, + { value: TableReducerID.max, label: 'Max', description: 'Maximum Value', standard: true }, + { value: TableReducerID.mean, label: 'Mean', description: 'Average Value', standard: true, alias: 'avg' }, + { + value: TableReducerID.sum, + label: 'Total', + description: 'The sum of all values', + standard: true, + alias: 'total', + }, + { value: TableReducerID.count, label: 'Count', description: 'Value Count', standard: true }, + { + value: TableReducerID.range, + label: 'Range', + description: 'Difference between minimum and maximum values', + standard: true, + }, + { + value: TableReducerID.diff, + label: 'Difference', + description: 'Difference between first and last values', + standard: true, + }, + ].forEach(calc => { + const { value, alias } = calc; + if (index.hasOwnProperty(value)) { + console.warn('Duplicate Reducer', value, calc, index); + } + index[value] = calc; + if (alias) { + if (index.hasOwnProperty(alias)) { + console.warn('Duplicate Reducer (alias)', alias, calc, index); + } + index[alias] = calc; + } + listOfReducers.push(calc); + }); + hasBuiltIndex = true; + } + return index[id]; +} + /** * This will return an array of valid indexes and throw an error if invalid request */ @@ -132,27 +201,6 @@ interface StandardStats { allIsNull: boolean; } -const reducers: TableReducerInfo[] = [ - { key: 'sum', alias: 'total', name: 'Total', description: 'The sum of all values', standard: true }, - { key: 'min', name: 'Min', description: 'Minimum Value', standard: true }, - { key: 'max', name: 'Max', description: 'Maximum Value', standard: true }, - { key: 'mean', name: 'Mean', description: 'Average Value', standard: true, alias: 'avg' }, - { key: 'first', name: 'First', description: 'First Value', standard: true, reducer: getFirstRow }, - { - key: 'last', - name: 'Last', - description: 'Last Value (current)', - standard: true, - alias: 'current', - reducer: getLastRow, - }, - { key: 'count', name: 'Count', description: 'Value Count', standard: true }, - { key: 'range', name: 'Range', description: 'Difference between minimum and maximum values', standard: true }, - { key: 'diff', name: 'Difference', description: 'Difference between first and last values', standard: true }, -]; - -let registry: Map | null = null; - function standardStatsReducer( data: TableData, columnIndexes: number[], From e84ddc8e309c5f36ba112354458c1532ade8c04b Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 12 Mar 2019 14:34:01 -0700 Subject: [PATCH 04/13] more tests --- .../grafana-ui/src/utils/tableReducer.test.ts | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/utils/tableReducer.test.ts b/packages/grafana-ui/src/utils/tableReducer.test.ts index 844a27807c6..befe205a931 100644 --- a/packages/grafana-ui/src/utils/tableReducer.test.ts +++ b/packages/grafana-ui/src/utils/tableReducer.test.ts @@ -1,9 +1,30 @@ import { parseCSV } from './processTableData'; -import { reduceTableData } from './tableReducer'; +import { reduceTableData, getTableReducers, TableReducerID } from './tableReducer'; describe('Table Reducer', () => { const basicTable = parseCSV('a,b,c\n10,20,30\n20,30,40'); + it('should load all standard stats', () => { + const names = [ + TableReducerID.sum, + TableReducerID.max, + TableReducerID.min, + // TableReducerID.logmin, + TableReducerID.mean, + TableReducerID.last, + TableReducerID.first, + TableReducerID.count, + TableReducerID.range, + TableReducerID.diff, + // TableReducerID.allIsZero, + // TableReducerID.allIsNull, + ]; + const reducers = getTableReducers(names); + reducers.forEach( (reducer, index) => { + expect(reducer ? reducer.value : '').toEqual(names[index]); + }); + }); + it('should calculate stats', () => { const reduced = reduceTableData(basicTable, { columnIndexes: [0, 1], From 227d28154b9a088996f1ecc2b2664c08f3bb787b Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 12 Mar 2019 17:02:44 -0700 Subject: [PATCH 05/13] touch --- packages/grafana-ui/src/utils/tableReducer.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/utils/tableReducer.ts b/packages/grafana-ui/src/utils/tableReducer.ts index c9c00d70752..e5ff80c0b44 100644 --- a/packages/grafana-ui/src/utils/tableReducer.ts +++ b/packages/grafana-ui/src/utils/tableReducer.ts @@ -133,10 +133,15 @@ function getById(id: string): TableReducerInfo { value: TableReducerID.sum, label: 'Total', description: 'The sum of all values', + emptyInputResult: 0, standard: true, alias: 'total', }, - { value: TableReducerID.count, label: 'Count', description: 'Value Count', standard: true }, + { value: TableReducerID.count, + label: 'Count', + description: 'Value Count', + emptyInputResult: 0, + standard: true }, { value: TableReducerID.range, label: 'Range', From f165139baeefdbc55e49b3ca1cc4252f60b6ddba Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 12 Mar 2019 17:03:21 -0700 Subject: [PATCH 06/13] touch --- packages/grafana-ui/src/utils/tableReducer.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/grafana-ui/src/utils/tableReducer.ts b/packages/grafana-ui/src/utils/tableReducer.ts index e5ff80c0b44..a94239d3a0f 100644 --- a/packages/grafana-ui/src/utils/tableReducer.ts +++ b/packages/grafana-ui/src/utils/tableReducer.ts @@ -137,11 +137,8 @@ function getById(id: string): TableReducerInfo { standard: true, alias: 'total', }, - { value: TableReducerID.count, - label: 'Count', - description: 'Value Count', - emptyInputResult: 0, - standard: true }, + { value: TableReducerID.count, label: 'Count', description: 'Value Count', emptyInputResult: 0, standard: true }, + { value: TableReducerID.range, label: 'Range', From 46521389870a5d678d4a67af8cd2e1298c7ccb49 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 12 Mar 2019 21:05:33 -0700 Subject: [PATCH 07/13] format --- .../components/TableReducePicker/TableReducePicker.tsx | 2 -- packages/grafana-ui/src/utils/tableReducer.test.ts | 9 +++++---- packages/grafana-ui/src/utils/tableReducer.ts | 1 - 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.tsx b/packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.tsx index ce1d7aeed48..bcbc396acdf 100644 --- a/packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.tsx +++ b/packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.tsx @@ -23,12 +23,10 @@ export class TableReducePicker extends PureComponent { }; componentDidMount() { - console.log('MOUNT!', this); this.checkInput(); } componentDidUpdate(prevProps: Props) { - console.log('UPDATE', this); this.checkInput(); } diff --git a/packages/grafana-ui/src/utils/tableReducer.test.ts b/packages/grafana-ui/src/utils/tableReducer.test.ts index befe205a931..1b5be9ce1a0 100644 --- a/packages/grafana-ui/src/utils/tableReducer.test.ts +++ b/packages/grafana-ui/src/utils/tableReducer.test.ts @@ -9,18 +9,18 @@ describe('Table Reducer', () => { TableReducerID.sum, TableReducerID.max, TableReducerID.min, - // TableReducerID.logmin, + // TableReducerID.logmin, TableReducerID.mean, TableReducerID.last, TableReducerID.first, TableReducerID.count, TableReducerID.range, TableReducerID.diff, - // TableReducerID.allIsZero, - // TableReducerID.allIsNull, + // TableReducerID.allIsZero, + // TableReducerID.allIsNull, ]; const reducers = getTableReducers(names); - reducers.forEach( (reducer, index) => { + reducers.forEach((reducer, index) => { expect(reducer ? reducer.value : '').toEqual(names[index]); }); }); @@ -65,6 +65,7 @@ describe('Table Reducer', () => { columnIndexes: [0, 1], stats: ['mean'], }); + expect(reduced.length).toBe(1); expect(reduced[0].rows[0]).toEqual([15, 25]); }); diff --git a/packages/grafana-ui/src/utils/tableReducer.ts b/packages/grafana-ui/src/utils/tableReducer.ts index a94239d3a0f..09626e802be 100644 --- a/packages/grafana-ui/src/utils/tableReducer.ts +++ b/packages/grafana-ui/src/utils/tableReducer.ts @@ -138,7 +138,6 @@ function getById(id: string): TableReducerInfo { alias: 'total', }, { value: TableReducerID.count, label: 'Count', description: 'Value Count', emptyInputResult: 0, standard: true }, - { value: TableReducerID.range, label: 'Range', From c69059855723622d587a1194ceaa8514b65d3566 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 13 Mar 2019 17:16:20 -0700 Subject: [PATCH 08/13] add error when not found --- .../TableReducePicker/TableReducePicker.tsx | 17 ++- .../grafana-ui/src/utils/tableReducer.test.ts | 16 ++- packages/grafana-ui/src/utils/tableReducer.ts | 109 ++++++++++++++---- 3 files changed, 114 insertions(+), 28 deletions(-) diff --git a/packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.tsx b/packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.tsx index bcbc396acdf..ec3b58582fa 100644 --- a/packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.tsx +++ b/packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.tsx @@ -32,9 +32,22 @@ export class TableReducePicker extends PureComponent { checkInput = () => { const { reducers, allowMultiple, defaultReducer, onChange } = this.props; - if (!allowMultiple && reducers.length > 1) { - onChange(reducers.slice(0, 1)); + + // Check that the selected reducers are all real + const notFound: string[] = []; + const current = getTableReducers(reducers, notFound); + if (notFound.length > 0) { + console.warn('Unknown reducers', notFound, reducers); + onChange(current.map(reducer => reducer.value)); } + + // Make sure there is only one + if (!allowMultiple && reducers.length > 1) { + console.warn('Removing extra reducers', reducers); + onChange([reducers[0]]); + } + + // Set the reducer from callback if (defaultReducer && reducers.length < 1) { onChange([defaultReducer]); } diff --git a/packages/grafana-ui/src/utils/tableReducer.test.ts b/packages/grafana-ui/src/utils/tableReducer.test.ts index 1b5be9ce1a0..bf503050709 100644 --- a/packages/grafana-ui/src/utils/tableReducer.test.ts +++ b/packages/grafana-ui/src/utils/tableReducer.test.ts @@ -9,20 +9,32 @@ describe('Table Reducer', () => { TableReducerID.sum, TableReducerID.max, TableReducerID.min, - // TableReducerID.logmin, + TableReducerID.logmin, TableReducerID.mean, TableReducerID.last, TableReducerID.first, TableReducerID.count, TableReducerID.range, TableReducerID.diff, + TableReducerID.step, + TableReducerID.delta, // TableReducerID.allIsZero, // TableReducerID.allIsNull, ]; - const reducers = getTableReducers(names); + const notFound: string[] = []; + const reducers = getTableReducers(names, notFound); reducers.forEach((reducer, index) => { expect(reducer ? reducer.value : '').toEqual(names[index]); }); + expect(notFound.length).toBe(0); + }); + + it('should fail to load unknown reducers', () => { + const names = ['not a reducer', TableReducerID.max, TableReducerID.min, 'also not a reducer']; + const notFound: string[] = []; + const reducers = getTableReducers(names, notFound); + expect(reducers.length).toBe(2); + expect(notFound.length).toBe(2); }); it('should calculate stats', () => { diff --git a/packages/grafana-ui/src/utils/tableReducer.ts b/packages/grafana-ui/src/utils/tableReducer.ts index 09626e802be..5a7f3d1988a 100644 --- a/packages/grafana-ui/src/utils/tableReducer.ts +++ b/packages/grafana-ui/src/utils/tableReducer.ts @@ -14,6 +14,8 @@ export enum TableReducerID { count = 'count', range = 'range', diff = 'diff', + delta = 'delta', + step = 'step', allIsZero = 'allIsZero', allIsNull = 'allIsNull', @@ -32,15 +34,24 @@ export interface TableReducerInfo { reducer?: TableReducer; } -/** Get a list of the known reducing functions */ -export function getTableReducers(ids?: string[]): TableReducerInfo[] { - if (!hasBuiltIndex) { - getById(TableReducerID.sum); // Force the registry to load - } +/** + * Get a list of the known reducing functions + * @param ids list of reducer names or null to get all of them + * @param notFound optional error object that will be filled with the names on unknown reducers + */ +export function getTableReducers(ids?: string[], notFound?: string[]): TableReducerInfo[] { if (ids === null || ids === undefined) { return listOfReducers; } - return ids.map(id => getById(id)); + return ids.reduce((list, id) => { + const reducer = getById(id); + if (reducer) { + list.push(reducer); + } else if (notFound && id) { + notFound.push(id); + } + return list; + }, new Array()); } export interface TableReducerOptions { @@ -114,7 +125,7 @@ const listOfReducers: TableReducerInfo[] = []; const index: TableReducerIndex = {}; let hasBuiltIndex = false; -function getById(id: string): TableReducerInfo { +function getById(id: string): TableReducerInfo | undefined { if (!hasBuiltIndex) { [ { @@ -144,12 +155,30 @@ function getById(id: string): TableReducerInfo { description: 'Difference between minimum and maximum values', standard: true, }, + { + value: TableReducerID.delta, + label: 'Delta', + description: 'Cumulative change in value', // HELP! not totally sure what this does + standard: true, + }, + { + value: TableReducerID.step, + label: 'Step', + description: 'Minimum interval between values', + standard: true, + }, { value: TableReducerID.diff, label: 'Difference', description: 'Difference between first and last values', standard: true, }, + { + value: TableReducerID.logmin, + label: 'Min (above zero)', + description: 'Used for log min scale', + standard: true, + }, ].forEach(calc => { const { value, alias } = calc; if (index.hasOwnProperty(value)) { @@ -197,7 +226,8 @@ interface StandardStats { nonNullCount: number; range: number | null; diff: number | null; - + delta: number | null; + step: number | null; allIsZero: boolean; allIsNull: boolean; } @@ -223,7 +253,12 @@ function standardStatsReducer( allIsZero: false, range: null, diff: null, - } as StandardStats; + delta: 0, + step: 0, + + // Just used for calcutations -- not exposed as a reducer + previousDeltaUp: true, + }; }); for (let i = 0; i < data.rows.length; i++) { @@ -240,40 +275,66 @@ function standardStatsReducer( } } - if (stats.first === null) { - stats.first = currentValue; - } - if (currentValue !== null) { stats.last = currentValue; + const isFirst = stats.first === null; + if (isFirst) { + stats.first = currentValue; + } + if (isNumber(currentValue)) { - stats.sum! += currentValue; + stats.sum += currentValue; stats.allIsNull = false; stats.nonNullCount++; - } - if (currentValue > stats.max!) { - stats.max = currentValue; - } + if (!isFirst) { + const step = currentValue - stats.last!; + if (stats.step > step) { + stats.step = step; // the minimum interval + } - if (currentValue < stats.min!) { - stats.min = currentValue; - } + if (stats.last! > currentValue) { + // counter reset + stats.previousDeltaUp = false; + if (i === data.rows.length - 1) { + // reset on last + stats.delta += currentValue; + } + } else { + if (stats.previousDeltaUp) { + stats.delta += step; // normal increment + } else { + stats.delta += currentValue; // account for counter reset + } + stats.previousDeltaUp = true; + } + } - if (currentValue < stats.logmin && currentValue > 0) { - stats.logmin = currentValue; + if (currentValue > stats.max) { + stats.max = currentValue; + } + + if (currentValue < stats.min) { + stats.min = currentValue; + } + + if (currentValue < stats.logmin && currentValue > 0) { + stats.logmin = currentValue; + } } if (currentValue !== 0) { stats.allIsZero = false; } + + stats.last = currentValue; } } } for (let x = 0; x < column.length; x++) { - const stats = column[x]; + const stats = column[x] as StandardStats; if (stats.max === -Number.MAX_VALUE) { stats.max = null; From a0fa5698e060d470540f31c61a4c0964dbaa6e79 Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 17 Mar 2019 18:02:09 -0700 Subject: [PATCH 09/13] rename reducer to statsCalculator --- .../StatsPicker.story.tsx} | 34 +- .../StatsPicker.tsx} | 34 +- .../src/utils/statsCalculator.test.ts | 73 ++++ .../grafana-ui/src/utils/statsCalculator.ts | 326 +++++++++++++++ .../grafana-ui/src/utils/tableReducer.test.ts | 84 ---- packages/grafana-ui/src/utils/tableReducer.ts | 373 ------------------ 6 files changed, 433 insertions(+), 491 deletions(-) rename packages/grafana-ui/src/components/{TableReducePicker/TableReducePicker.story.tsx => StatsPicker/StatsPicker.story.tsx} (59%) rename packages/grafana-ui/src/components/{TableReducePicker/TableReducePicker.tsx => StatsPicker/StatsPicker.tsx} (58%) create mode 100644 packages/grafana-ui/src/utils/statsCalculator.test.ts create mode 100644 packages/grafana-ui/src/utils/statsCalculator.ts delete mode 100644 packages/grafana-ui/src/utils/tableReducer.test.ts delete mode 100644 packages/grafana-ui/src/utils/tableReducer.ts diff --git a/packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.story.tsx b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx similarity index 59% rename from packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.story.tsx rename to packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx index 6b0a0ad43ee..d37ac92f583 100644 --- a/packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.story.tsx +++ b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx @@ -3,45 +3,45 @@ import React, { PureComponent } from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; -import { TableReducePicker } from './TableReducePicker'; +import { StatsPicker } from './StatsPicker'; import { text, boolean } from '@storybook/addon-knobs'; interface State { - reducers: string[]; + stats: string[]; } export class WrapperWithState extends PureComponent { constructor(props: any) { super(props); this.state = { - reducers: this.toReducersArray(props.initialReducers), + stats: this.toStatsArray(props.initialReducers), }; } - toReducersArray = (txt: string): string[] => { + toStatsArray = (txt: string): string[] => { return txt.split(',').map(v => v.trim()); }; componentDidUpdate(prevProps: any) { const { initialReducers } = this.props; if (initialReducers !== prevProps.initialReducers) { - this.setState({ reducers: this.toReducersArray(initialReducers) }); + this.setState({ stats: this.toStatsArray(initialReducers) }); } } render() { - const { placeholder, defaultReducer, allowMultiple } = this.props; - const { reducers } = this.state; + const { placeholder, defaultStat, allowMultiple } = this.props; + const { stats } = this.state; return ( - { - action('Picked:')(reducers); - this.setState({ reducers }); + stats={stats} + onChange={(stats: string[]) => { + action('Picked:')(stats); + this.setState({ stats }); }} /> ); @@ -52,16 +52,16 @@ const story = storiesOf('UI/TableReducePicker', module); story.addDecorator(withCenteredStory); story.add('picker', () => { const placeholder = text('Placeholder Text', ''); - const defaultReducer = text('Default Reducer', ''); + const defaultStat = text('Default Stat', ''); const allowMultiple = boolean('Allow Multiple', false); - const initialReducers = text('Initial Reducers', ''); + const initialStats = text('Initial Stats', ''); return (
); diff --git a/packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.tsx b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx similarity index 58% rename from packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.tsx rename to packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx index ec3b58582fa..b1114b7f61f 100644 --- a/packages/grafana-ui/src/components/TableReducePicker/TableReducePicker.tsx +++ b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx @@ -4,19 +4,19 @@ import isArray from 'lodash/isArray'; import { Select } from '../index'; -import { getTableReducers } from '../../utils/tableReducer'; +import { getStatsCalculators } from '../../utils/statsCalculator'; import { SelectOptionItem } from '../Select/Select'; interface Props { placeholder?: string; - onChange: (reducers: string[]) => void; - reducers: string[]; + onChange: (stats: string[]) => void; + stats: string[]; width?: number; allowMultiple?: boolean; - defaultReducer?: string; + defaultStat?: string; } -export class TableReducePicker extends PureComponent { +export class StatsPicker extends PureComponent { static defaultProps = { width: 12, allowMultiple: false, @@ -31,25 +31,25 @@ export class TableReducePicker extends PureComponent { } checkInput = () => { - const { reducers, allowMultiple, defaultReducer, onChange } = this.props; + const { stats, allowMultiple, defaultStat, onChange } = this.props; // Check that the selected reducers are all real const notFound: string[] = []; - const current = getTableReducers(reducers, notFound); + const current = getStatsCalculators(stats, notFound); if (notFound.length > 0) { - console.warn('Unknown reducers', notFound, reducers); + console.warn('Unknown reducers', notFound, stats); onChange(current.map(reducer => reducer.value)); } // Make sure there is only one - if (!allowMultiple && reducers.length > 1) { - console.warn('Removing extra reducers', reducers); - onChange([reducers[0]]); + if (!allowMultiple && stats.length > 1) { + console.warn('Removing extra stat', stats); + onChange([stats[0]]); } // Set the reducer from callback - if (defaultReducer && reducers.length < 1) { - onChange([defaultReducer]); + if (defaultStat && stats.length < 1) { + onChange([defaultStat]); } }; @@ -63,17 +63,17 @@ export class TableReducePicker extends PureComponent { }; render() { - const { width, reducers, allowMultiple, defaultReducer, placeholder } = this.props; - const current = getTableReducers(reducers); + const { width, stats, allowMultiple, defaultStat, placeholder } = this.props; + const current = getStatsCalculators(stats); return ( { const basicTable = parseCSV('a,b,c\n10,20,30\n20,30,40'); @@ -21,20 +23,20 @@ describe('Stats Calculators', () => { // StatID.allIsZero, // StatID.allIsNull, ]; - const notFound: string[] = []; - const stats = getStatsCalculators(names, notFound); - stats.forEach((stat, index) => { - expect(stat ? stat.value : '').toEqual(names[index]); - }); - expect(notFound.length).toBe(0); + const stats = getStatsCalculators(names); + expect(stats.length).toBe(names.length); }); it('should fail to load unknown stats', () => { const names = ['not a stat', StatID.max, StatID.min, 'also not a stat']; - const notFound: string[] = []; - const stats = getStatsCalculators(names, notFound); + const stats = getStatsCalculators(names); expect(stats.length).toBe(2); + + const found = stats.map(v => v.value); + const notFound = _.difference(names, found); expect(notFound.length).toBe(2); + + expect(notFound[0]).toBe('not a stat'); }); it('should calculate stats', () => { diff --git a/packages/grafana-ui/src/utils/statsCalculator.ts b/packages/grafana-ui/src/utils/statsCalculator.ts index 3eef55b795c..85c2881a209 100644 --- a/packages/grafana-ui/src/utils/statsCalculator.ts +++ b/packages/grafana-ui/src/utils/statsCalculator.ts @@ -42,18 +42,18 @@ export interface StatCalculatorInfo { /** * @param ids list of stat names or null to get all of them - * @param notFound optional error object that will be filled with the names on unknown stats */ -export function getStatsCalculators(ids?: string[], notFound?: string[]): StatCalculatorInfo[] { +export function getStatsCalculators(ids?: string[]): StatCalculatorInfo[] { if (ids === null || ids === undefined) { + if (!hasBuiltIndex) { + getById(StatID.mean); + } return listOfStats; } return ids.reduce((list, id) => { const stat = getById(id); if (stat) { list.push(stat); - } else if (notFound && id) { - notFound.push(id); } return list; }, new Array()); @@ -146,7 +146,13 @@ function getById(id: string): StatCalculatorInfo | undefined { standard: true, alias: 'total', }, - { value: StatID.count, label: 'Count', description: 'Value Count', emptyInputResult: 0, standard: true }, + { + value: StatID.count, + label: 'Count', + description: 'Number of values in response', + emptyInputResult: 0, + standard: true, + }, { value: StatID.range, label: 'Range', @@ -156,7 +162,7 @@ function getById(id: string): StatCalculatorInfo | undefined { { value: StatID.delta, label: 'Delta', - description: 'Cumulative change in value', // HELP! not totally sure what this does + description: 'Cumulative change in value (??? help not really sure ???)', standard: true, }, { From 7e56514c5acd57978d653598becf9d9288f4ef80 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 19 Mar 2019 21:19:58 -0700 Subject: [PATCH 11/13] add more functions and tests --- .../components/StatsPicker/StatsPicker.tsx | 26 +++- .../src/utils/statsCalculator.test.ts | 22 ++- .../grafana-ui/src/utils/statsCalculator.ts | 138 +++++++++++++----- 3 files changed, 139 insertions(+), 47 deletions(-) diff --git a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx index 649b98a370a..22ba5423b30 100644 --- a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx +++ b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx @@ -36,10 +36,10 @@ export class StatsPicker extends PureComponent { const current = getStatsCalculators(stats); if (current.length !== stats.length) { - const found = current.map(v => v.value); + const found = current.map(v => v.id); const notFound = difference(stats, found); console.warn('Unknown stats', notFound, stats); - onChange(current.map(stat => stat.value)); + onChange(current.map(stat => stat.id)); } // Make sure there is only one @@ -65,15 +65,31 @@ export class StatsPicker extends PureComponent { render() { const { width, stats, allowMultiple, defaultStat, placeholder } = this.props; - const current = getStatsCalculators(stats); + const options = getStatsCalculators().map(s => { + return { + value: s.id, + label: s.name, + desctipiton: s.description, + }; + }); + + const value: SelectOptionItem[] = []; + stats.forEach(s => { + const o = options.find(v => v.value === s); + if (o) { + value.push(o); + } + }); + + //getStatsCalculators(stats); return ( option.value === stat)} + placeholder="Choose Stat" + defaultStat={StatID.mean} + allowMultiple={false} + stats={[stat]} + onChange={this.onStatsChange} />
diff --git a/public/app/plugins/panel/singlestat2/module.tsx b/public/app/plugins/panel/singlestat2/module.tsx index 283b32802e1..8c86b3497ee 100644 --- a/public/app/plugins/panel/singlestat2/module.tsx +++ b/public/app/plugins/panel/singlestat2/module.tsx @@ -1,4 +1,4 @@ -import { ReactPanelPlugin } from '@grafana/ui'; +import { ReactPanelPlugin, getStatsCalculators } from '@grafana/ui'; import { SingleStatOptions, defaults, SingleStatBaseOptions } from './types'; import { SingleStatPanel } from './SingleStatPanel'; import cloneDeep from 'lodash/cloneDeep'; @@ -21,6 +21,13 @@ export const singleStatBaseOptionsCheck = ( }); } + // 6.1 renamed some stats, This makes sure they are up to date + // avg -> mean, current -> last, total -> sum + const { valueOptions } = options; + if (valueOptions && valueOptions.stat) { + valueOptions.stat = getStatsCalculators([valueOptions.stat]).map(s => s.id)[0]; + console.log('CHANGED', valueOptions); + } return options; }; diff --git a/public/app/plugins/panel/singlestat2/types.ts b/public/app/plugins/panel/singlestat2/types.ts index 1f31783e814..797e0cb0116 100644 --- a/public/app/plugins/panel/singlestat2/types.ts +++ b/public/app/plugins/panel/singlestat2/types.ts @@ -1,4 +1,4 @@ -import { VizOrientation, ValueMapping, Threshold } from '@grafana/ui'; +import { VizOrientation, ValueMapping, Threshold, StatID } from '@grafana/ui'; export interface SingleStatBaseOptions { valueMappings: ValueMapping[]; @@ -24,7 +24,7 @@ export const defaults: SingleStatOptions = { prefix: '', suffix: '', decimals: null, - stat: 'avg', + stat: StatID.mean, unit: 'none', }, valueMappings: [], From 78a9243721bde924fde04a53f45e2039b67a0eff Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 22 Mar 2019 00:21:04 -0700 Subject: [PATCH 13/13] remove logging --- packages/grafana-ui/src/utils/statsCalculator.ts | 4 ++-- public/app/features/dashboard/dashgrid/DashboardPanel.tsx | 1 - .../app/plugins/panel/singlestat2/SingleStatValueEditor.tsx | 3 --- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/grafana-ui/src/utils/statsCalculator.ts b/packages/grafana-ui/src/utils/statsCalculator.ts index 5fb03408c17..34dbd305b57 100644 --- a/packages/grafana-ui/src/utils/statsCalculator.ts +++ b/packages/grafana-ui/src/utils/statsCalculator.ts @@ -86,9 +86,9 @@ export function calculateStats(options: CalculateStatsOptions): ColumnStats { // This lets the concrete implementations assume at least one row if (!table.rows || table.rows.length < 1) { const stats = {} as ColumnStats; - queue.forEach(stat => { + for (const stat of queue) { stats[stat.id] = stat.emptyInputResult !== null ? stat.emptyInputResult : null; - }); + } return stats; } diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index d7745c8b7fd..e554e081239 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -102,7 +102,6 @@ export class DashboardPanel extends PureComponent { const hook = plugin.exports.reactPanel.panelTypeChangedHook; if (hook) { panel.options = hook(panel.options || {}, null, null); - console.log('OPITONS', pluginId, panel); } } diff --git a/public/app/plugins/panel/singlestat2/SingleStatValueEditor.tsx b/public/app/plugins/panel/singlestat2/SingleStatValueEditor.tsx index 72b3b06ba67..acf5dc0a3bc 100644 --- a/public/app/plugins/panel/singlestat2/SingleStatValueEditor.tsx +++ b/public/app/plugins/panel/singlestat2/SingleStatValueEditor.tsx @@ -18,7 +18,6 @@ export class SingleStatValueEditor extends PureComponent { onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value }); onStatsChange = stats => { - console.log('SELECTED', stats); const stat = stats[0] || StatID.mean; this.props.onChange({ ...this.props.options, stat }); }; @@ -48,8 +47,6 @@ export class SingleStatValueEditor extends PureComponent { decimalsString = decimals.toString(); } - console.log('xxx', stat); - return (