mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'table-reducer' of https://github.com/ryantxu/grafana into ryantxu-table-reducer
This commit is contained in:
commit
e0ecbc4c68
@ -0,0 +1,79 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||||
|
import { StatsPicker } from './StatsPicker';
|
||||||
|
import { text, boolean } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
const getKnobs = () => {
|
||||||
|
return {
|
||||||
|
placeholder: text('Placeholder Text', ''),
|
||||||
|
defaultStat: text('Default Stat', ''),
|
||||||
|
allowMultiple: boolean('Allow Multiple', false),
|
||||||
|
initialStats: text('Initial Stats', ''),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
stats: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WrapperWithState extends PureComponent<any, State> {
|
||||||
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
stats: this.toStatsArray(props.initialReducers),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toStatsArray = (txt: string): string[] => {
|
||||||
|
if (!txt) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return txt.split(',').map(v => v.trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: any) {
|
||||||
|
const { initialReducers } = this.props;
|
||||||
|
if (initialReducers !== prevProps.initialReducers) {
|
||||||
|
console.log('Changing initial reducers');
|
||||||
|
this.setState({ stats: this.toStatsArray(initialReducers) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { placeholder, defaultStat, allowMultiple } = this.props;
|
||||||
|
const { stats } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatsPicker
|
||||||
|
placeholder={placeholder}
|
||||||
|
defaultStat={defaultStat}
|
||||||
|
allowMultiple={allowMultiple}
|
||||||
|
stats={stats}
|
||||||
|
onChange={(stats: string[]) => {
|
||||||
|
action('Picked:')(stats);
|
||||||
|
this.setState({ stats });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const story = storiesOf('UI/StatsPicker', module);
|
||||||
|
story.addDecorator(withCenteredStory);
|
||||||
|
story.add('picker', () => {
|
||||||
|
const { placeholder, defaultStat, allowMultiple, initialStats } = getKnobs();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<WrapperWithState
|
||||||
|
placeholder={placeholder}
|
||||||
|
defaultStat={defaultStat}
|
||||||
|
allowMultiple={allowMultiple}
|
||||||
|
initialStats={initialStats}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,98 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
import isArray from 'lodash/isArray';
|
||||||
|
import difference from 'lodash/difference';
|
||||||
|
|
||||||
|
import { Select } from '../index';
|
||||||
|
|
||||||
|
import { getStatsCalculators } from '../../utils/statsCalculator';
|
||||||
|
import { SelectOptionItem } from '../Select/Select';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
placeholder?: string;
|
||||||
|
onChange: (stats: string[]) => void;
|
||||||
|
stats: string[];
|
||||||
|
width?: number;
|
||||||
|
allowMultiple?: boolean;
|
||||||
|
defaultStat?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StatsPicker extends PureComponent<Props> {
|
||||||
|
static defaultProps = {
|
||||||
|
width: 12,
|
||||||
|
allowMultiple: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.checkInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props) {
|
||||||
|
this.checkInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkInput = () => {
|
||||||
|
const { stats, allowMultiple, defaultStat, onChange } = this.props;
|
||||||
|
|
||||||
|
const current = getStatsCalculators(stats);
|
||||||
|
if (current.length !== stats.length) {
|
||||||
|
const found = current.map(v => v.id);
|
||||||
|
const notFound = difference(stats, found);
|
||||||
|
console.warn('Unknown stats', notFound, stats);
|
||||||
|
onChange(current.map(stat => stat.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure there is only one
|
||||||
|
if (!allowMultiple && stats.length > 1) {
|
||||||
|
console.warn('Removing extra stat', stats);
|
||||||
|
onChange([stats[0]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the reducer from callback
|
||||||
|
if (defaultStat && stats.length < 1) {
|
||||||
|
onChange([defaultStat]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectionChange = (item: SelectOptionItem) => {
|
||||||
|
const { onChange } = this.props;
|
||||||
|
if (isArray(item)) {
|
||||||
|
onChange(item.map(v => v.value));
|
||||||
|
} else {
|
||||||
|
onChange([item.value]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { width, stats, allowMultiple, defaultStat, placeholder } = this.props;
|
||||||
|
const options = getStatsCalculators().map(s => {
|
||||||
|
return {
|
||||||
|
value: s.id,
|
||||||
|
label: s.name,
|
||||||
|
description: s.description,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const value: SelectOptionItem[] = [];
|
||||||
|
stats.forEach(s => {
|
||||||
|
const o = options.find(v => v.value === s);
|
||||||
|
if (o) {
|
||||||
|
value.push(o);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//getStatsCalculators(stats);
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
width={width}
|
||||||
|
value={value}
|
||||||
|
isClearable={!defaultStat}
|
||||||
|
isMulti={allowMultiple}
|
||||||
|
isSearchable={true}
|
||||||
|
options={options}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={this.onSelectionChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,7 @@ export { Switch } from './Switch/Switch';
|
|||||||
export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
|
export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
|
||||||
export { PieChart, PieChartDataPoint, PieChartType } from './PieChart/PieChart';
|
export { PieChart, PieChartDataPoint, PieChartType } from './PieChart/PieChart';
|
||||||
export { UnitPicker } from './UnitPicker/UnitPicker';
|
export { UnitPicker } from './UnitPicker/UnitPicker';
|
||||||
|
export { StatsPicker } from './StatsPicker/StatsPicker';
|
||||||
export { Input, InputStatus } from './Input/Input';
|
export { Input, InputStatus } from './Input/Input';
|
||||||
|
|
||||||
// Visualizations
|
// Visualizations
|
||||||
|
@ -5,6 +5,7 @@ export * from './colors';
|
|||||||
export * from './namedColorsPalette';
|
export * from './namedColorsPalette';
|
||||||
export * from './thresholds';
|
export * from './thresholds';
|
||||||
export * from './string';
|
export * from './string';
|
||||||
|
export * from './statsCalculator';
|
||||||
export * from './displayValue';
|
export * from './displayValue';
|
||||||
export * from './deprecationWarning';
|
export * from './deprecationWarning';
|
||||||
export { getMappedValue } from './valueMappings';
|
export { getMappedValue } from './valueMappings';
|
||||||
|
92
packages/grafana-ui/src/utils/statsCalculator.test.ts
Normal file
92
packages/grafana-ui/src/utils/statsCalculator.test.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { parseCSV } from './processTableData';
|
||||||
|
import { getStatsCalculators, StatID, calculateStats } from './statsCalculator';
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
describe('Stats Calculators', () => {
|
||||||
|
const basicTable = parseCSV('a,b,c\n10,20,30\n20,30,40');
|
||||||
|
|
||||||
|
it('should load all standard stats', () => {
|
||||||
|
const names = [
|
||||||
|
StatID.sum,
|
||||||
|
StatID.max,
|
||||||
|
StatID.min,
|
||||||
|
StatID.logmin,
|
||||||
|
StatID.mean,
|
||||||
|
StatID.last,
|
||||||
|
StatID.first,
|
||||||
|
StatID.count,
|
||||||
|
StatID.range,
|
||||||
|
StatID.diff,
|
||||||
|
StatID.step,
|
||||||
|
StatID.delta,
|
||||||
|
// StatID.allIsZero,
|
||||||
|
// StatID.allIsNull,
|
||||||
|
];
|
||||||
|
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 stats = getStatsCalculators(names);
|
||||||
|
expect(stats.length).toBe(2);
|
||||||
|
|
||||||
|
const found = stats.map(v => v.id);
|
||||||
|
const notFound = _.difference(names, found);
|
||||||
|
expect(notFound.length).toBe(2);
|
||||||
|
|
||||||
|
expect(notFound[0]).toBe('not a stat');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate basic stats', () => {
|
||||||
|
const stats = calculateStats({
|
||||||
|
table: basicTable,
|
||||||
|
columnIndex: 0,
|
||||||
|
stats: ['first', 'last', 'mean'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// First
|
||||||
|
expect(stats.first).toEqual(10);
|
||||||
|
|
||||||
|
// Last
|
||||||
|
expect(stats.last).toEqual(20);
|
||||||
|
|
||||||
|
// Mean
|
||||||
|
expect(stats.mean).toEqual(15);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support a single stat also', () => {
|
||||||
|
const stats = calculateStats({
|
||||||
|
table: basicTable,
|
||||||
|
columnIndex: 0,
|
||||||
|
stats: ['first'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should do the simple version that just looks up value
|
||||||
|
expect(Object.keys(stats).length).toEqual(1);
|
||||||
|
expect(stats.first).toEqual(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get non standard stats', () => {
|
||||||
|
const stats = calculateStats({
|
||||||
|
table: basicTable,
|
||||||
|
columnIndex: 0,
|
||||||
|
stats: [StatID.distinctCount, StatID.changeCount],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stats.distinctCount).toEqual(2);
|
||||||
|
expect(stats.changeCount).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate step', () => {
|
||||||
|
const stats = calculateStats({
|
||||||
|
table: { columns: [{ text: 'A' }], rows: [[100], [200], [300], [400]] },
|
||||||
|
columnIndex: 0,
|
||||||
|
stats: [StatID.step, StatID.delta],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stats.step).toEqual(100);
|
||||||
|
expect(stats.delta).toEqual(300);
|
||||||
|
});
|
||||||
|
});
|
403
packages/grafana-ui/src/utils/statsCalculator.ts
Normal file
403
packages/grafana-ui/src/utils/statsCalculator.ts
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
// Libraries
|
||||||
|
import isNumber from 'lodash/isNumber';
|
||||||
|
|
||||||
|
import { TableData, NullValueMode } from '../types/index';
|
||||||
|
|
||||||
|
export enum StatID {
|
||||||
|
sum = 'sum',
|
||||||
|
max = 'max',
|
||||||
|
min = 'min',
|
||||||
|
logmin = 'logmin',
|
||||||
|
mean = 'mean',
|
||||||
|
last = 'last',
|
||||||
|
first = 'first',
|
||||||
|
count = 'count',
|
||||||
|
range = 'range',
|
||||||
|
diff = 'diff',
|
||||||
|
delta = 'delta',
|
||||||
|
step = 'step',
|
||||||
|
|
||||||
|
changeCount = 'changeCount',
|
||||||
|
distinctCount = 'distinctCount',
|
||||||
|
|
||||||
|
allIsZero = 'allIsZero',
|
||||||
|
allIsNull = 'allIsNull',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColumnStats {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal function
|
||||||
|
type StatCalculator = (table: TableData, columnIndex: number, ignoreNulls: boolean, nullAsZero: boolean) => ColumnStats;
|
||||||
|
|
||||||
|
export interface StatCalculatorInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
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
|
||||||
|
calculator?: StatCalculator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ids list of stat names or null to get all of them
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}, new Array<StatCalculatorInfo>());
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalculateStatsOptions {
|
||||||
|
table: TableData;
|
||||||
|
columnIndex: number;
|
||||||
|
stats: string[]; // The stats to calculate
|
||||||
|
nullValueMode?: NullValueMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns an object with a key for each selected stat
|
||||||
|
*/
|
||||||
|
export function calculateStats(options: CalculateStatsOptions): ColumnStats {
|
||||||
|
const { table, columnIndex, stats, nullValueMode } = options;
|
||||||
|
|
||||||
|
if (!stats || stats.length < 1) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const queue = getStatsCalculators(stats);
|
||||||
|
|
||||||
|
// Return early for empty tables
|
||||||
|
// This lets the concrete implementations assume at least one row
|
||||||
|
if (!table.rows || table.rows.length < 1) {
|
||||||
|
const stats = {} as ColumnStats;
|
||||||
|
for (const stat of queue) {
|
||||||
|
stats[stat.id] = stat.emptyInputResult !== null ? stat.emptyInputResult : null;
|
||||||
|
}
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ignoreNulls = nullValueMode === NullValueMode.Ignore;
|
||||||
|
const nullAsZero = nullValueMode === NullValueMode.AsZero;
|
||||||
|
|
||||||
|
// Avoid calculating all the standard stats if possible
|
||||||
|
if (queue.length === 1 && queue[0].calculator) {
|
||||||
|
return queue[0].calculator(table, columnIndex, ignoreNulls, nullAsZero);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now everything can use the standard stats
|
||||||
|
let values = standardStatsStat(table, columnIndex, ignoreNulls, nullAsZero);
|
||||||
|
for (const calc of queue) {
|
||||||
|
if (!values.hasOwnProperty(calc.id) && calc.calculator) {
|
||||||
|
values = {
|
||||||
|
...values,
|
||||||
|
...calc.calculator(table, columnIndex, ignoreNulls, nullAsZero),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// No Exported symbols below here.
|
||||||
|
//
|
||||||
|
// ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// private registry of all stats
|
||||||
|
interface TableStatIndex {
|
||||||
|
[id: string]: StatCalculatorInfo;
|
||||||
|
}
|
||||||
|
const listOfStats: StatCalculatorInfo[] = [];
|
||||||
|
const index: TableStatIndex = {};
|
||||||
|
let hasBuiltIndex = false;
|
||||||
|
|
||||||
|
function getById(id: string): StatCalculatorInfo | undefined {
|
||||||
|
if (!hasBuiltIndex) {
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: StatID.last,
|
||||||
|
name: 'Last',
|
||||||
|
description: 'Last Value (current)',
|
||||||
|
standard: true,
|
||||||
|
alias: 'current',
|
||||||
|
calculator: calculateLast,
|
||||||
|
},
|
||||||
|
{ id: StatID.first, name: 'First', description: 'First Value', standard: true, calculator: calculateFirst },
|
||||||
|
{ id: StatID.min, name: 'Min', description: 'Minimum Value', standard: true },
|
||||||
|
{ id: StatID.max, name: 'Max', description: 'Maximum Value', standard: true },
|
||||||
|
{ id: StatID.mean, name: 'Mean', description: 'Average Value', standard: true, alias: 'avg' },
|
||||||
|
{
|
||||||
|
id: StatID.sum,
|
||||||
|
name: 'Total',
|
||||||
|
description: 'The sum of all values',
|
||||||
|
emptyInputResult: 0,
|
||||||
|
standard: true,
|
||||||
|
alias: 'total',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: StatID.count,
|
||||||
|
name: 'Count',
|
||||||
|
description: 'Number of values in response',
|
||||||
|
emptyInputResult: 0,
|
||||||
|
standard: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: StatID.range,
|
||||||
|
name: 'Range',
|
||||||
|
description: 'Difference between minimum and maximum values',
|
||||||
|
standard: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: StatID.delta,
|
||||||
|
name: 'Delta',
|
||||||
|
description: 'Cumulative change in value (??? help not really sure ???)',
|
||||||
|
standard: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: StatID.step,
|
||||||
|
name: 'Step',
|
||||||
|
description: 'Minimum interval between values',
|
||||||
|
standard: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: StatID.diff,
|
||||||
|
name: 'Difference',
|
||||||
|
description: 'Difference between first and last values',
|
||||||
|
standard: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: StatID.logmin,
|
||||||
|
name: 'Min (above zero)',
|
||||||
|
description: 'Used for log min scale',
|
||||||
|
standard: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: StatID.changeCount,
|
||||||
|
name: 'Change Count',
|
||||||
|
description: 'Number of times the value changes',
|
||||||
|
standard: false,
|
||||||
|
calculator: calculateChangeCount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: StatID.distinctCount,
|
||||||
|
name: 'Distinct Count',
|
||||||
|
description: 'Number of distinct values',
|
||||||
|
standard: false,
|
||||||
|
calculator: 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];
|
||||||
|
}
|
||||||
|
|
||||||
|
function standardStatsStat(
|
||||||
|
data: TableData,
|
||||||
|
columnIndex: number,
|
||||||
|
ignoreNulls: boolean,
|
||||||
|
nullAsZero: boolean
|
||||||
|
): ColumnStats {
|
||||||
|
const stats = {
|
||||||
|
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,
|
||||||
|
delta: 0,
|
||||||
|
step: Number.MAX_VALUE,
|
||||||
|
|
||||||
|
// Just used for calcutations -- not exposed as a stat
|
||||||
|
previousDeltaUp: true,
|
||||||
|
} as ColumnStats;
|
||||||
|
|
||||||
|
for (let i = 0; i < data.rows.length; i++) {
|
||||||
|
let currentValue = data.rows[i][columnIndex];
|
||||||
|
|
||||||
|
if (currentValue === null) {
|
||||||
|
if (ignoreNulls) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (nullAsZero) {
|
||||||
|
currentValue = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentValue !== null) {
|
||||||
|
const isFirst = stats.first === null;
|
||||||
|
if (isFirst) {
|
||||||
|
stats.first = currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNumber(currentValue)) {
|
||||||
|
stats.sum += currentValue;
|
||||||
|
stats.allIsNull = false;
|
||||||
|
stats.nonNullCount++;
|
||||||
|
|
||||||
|
if (!isFirst) {
|
||||||
|
const step = currentValue - stats.last!;
|
||||||
|
if (stats.step > step) {
|
||||||
|
stats.step = step; // the minimum interval
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.max === -Number.MAX_VALUE) {
|
||||||
|
stats.max = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.min === Number.MAX_VALUE) {
|
||||||
|
stats.min = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.step === Number.MAX_VALUE) {
|
||||||
|
stats.step = 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 stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateFirst(data: TableData, columnIndex: number, ignoreNulls: boolean, nullAsZero: boolean): ColumnStats {
|
||||||
|
return { first: data.rows[0][columnIndex] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateLast(data: TableData, columnIndex: number, ignoreNulls: boolean, nullAsZero: boolean): ColumnStats {
|
||||||
|
return { last: data.rows[data.rows.length - 1][columnIndex] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateChangeCount(
|
||||||
|
data: TableData,
|
||||||
|
columnIndex: number,
|
||||||
|
ignoreNulls: boolean,
|
||||||
|
nullAsZero: boolean
|
||||||
|
): ColumnStats {
|
||||||
|
let count = 0;
|
||||||
|
let first = true;
|
||||||
|
let last: any = null;
|
||||||
|
for (let i = 0; i < data.rows.length; i++) {
|
||||||
|
let currentValue = data.rows[i][columnIndex];
|
||||||
|
if (currentValue === null) {
|
||||||
|
if (ignoreNulls) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (nullAsZero) {
|
||||||
|
currentValue = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!first && last !== currentValue) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
last = currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { changeCount: count };
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateDistinctCount(
|
||||||
|
data: TableData,
|
||||||
|
columnIndex: number,
|
||||||
|
ignoreNulls: boolean,
|
||||||
|
nullAsZero: boolean
|
||||||
|
): ColumnStats {
|
||||||
|
const distinct = new Set<any>();
|
||||||
|
for (let i = 0; i < data.rows.length; i++) {
|
||||||
|
let currentValue = data.rows[i][columnIndex];
|
||||||
|
if (currentValue === null) {
|
||||||
|
if (ignoreNulls) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (nullAsZero) {
|
||||||
|
currentValue = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
distinct.add(currentValue);
|
||||||
|
}
|
||||||
|
return { distinctCount: distinct.size };
|
||||||
|
}
|
@ -98,6 +98,11 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
panel.changeType(pluginId, hook);
|
panel.changeType(pluginId, hook);
|
||||||
}
|
}
|
||||||
|
} else if (plugin.exports && plugin.exports.reactPanel) {
|
||||||
|
const hook = plugin.exports.reactPanel.panelTypeChangedHook;
|
||||||
|
if (hook) {
|
||||||
|
panel.options = hook(panel.options || {}, null, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ plugin, angularPanel: null });
|
this.setState({ plugin, angularPanel: null });
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { VizOrientation, SelectOptionItem } from '@grafana/ui';
|
import { VizOrientation, SelectOptionItem, StatID } from '@grafana/ui';
|
||||||
import { SingleStatBaseOptions } from '../singlestat2/types';
|
import { SingleStatBaseOptions } from '../singlestat2/types';
|
||||||
|
|
||||||
export interface BarGaugeOptions extends SingleStatBaseOptions {
|
export interface BarGaugeOptions extends SingleStatBaseOptions {
|
||||||
@ -25,7 +25,7 @@ export const defaults: BarGaugeOptions = {
|
|||||||
orientation: VizOrientation.Horizontal,
|
orientation: VizOrientation.Horizontal,
|
||||||
valueOptions: {
|
valueOptions: {
|
||||||
unit: 'none',
|
unit: 'none',
|
||||||
stat: 'avg',
|
stat: StatID.mean,
|
||||||
prefix: '',
|
prefix: '',
|
||||||
suffix: '',
|
suffix: '',
|
||||||
decimals: null,
|
decimals: null,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { SingleStatBaseOptions } from '../singlestat2/types';
|
import { SingleStatBaseOptions } from '../singlestat2/types';
|
||||||
import { VizOrientation } from '@grafana/ui';
|
import { VizOrientation, StatID } from '@grafana/ui';
|
||||||
|
|
||||||
export interface GaugeOptions extends SingleStatBaseOptions {
|
export interface GaugeOptions extends SingleStatBaseOptions {
|
||||||
maxValue: number;
|
maxValue: number;
|
||||||
@ -17,7 +17,7 @@ export const defaults: GaugeOptions = {
|
|||||||
prefix: '',
|
prefix: '',
|
||||||
suffix: '',
|
suffix: '',
|
||||||
decimals: null,
|
decimals: null,
|
||||||
stat: 'avg',
|
stat: StatID.mean,
|
||||||
unit: 'none',
|
unit: 'none',
|
||||||
},
|
},
|
||||||
valueMappings: [],
|
valueMappings: [],
|
||||||
|
@ -4,7 +4,7 @@ import React, { PureComponent, CSSProperties } from 'react';
|
|||||||
// Types
|
// Types
|
||||||
import { SingleStatOptions, SingleStatBaseOptions } from './types';
|
import { SingleStatOptions, SingleStatBaseOptions } from './types';
|
||||||
|
|
||||||
import { DisplayValue, PanelProps, processTimeSeries, NullValueMode, ColumnType } from '@grafana/ui';
|
import { DisplayValue, PanelProps, NullValueMode, ColumnType, calculateStats } from '@grafana/ui';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { getDisplayProcessor } from '@grafana/ui';
|
import { getDisplayProcessor } from '@grafana/ui';
|
||||||
import { ProcessedValuesRepeater } from './ProcessedValuesRepeater';
|
import { ProcessedValuesRepeater } from './ProcessedValuesRepeater';
|
||||||
@ -14,7 +14,7 @@ export const getSingleStatValues = (props: PanelProps<SingleStatBaseOptions>): D
|
|||||||
const { valueOptions, valueMappings } = options;
|
const { valueOptions, valueMappings } = options;
|
||||||
const { unit, decimals, stat } = valueOptions;
|
const { unit, decimals, stat } = valueOptions;
|
||||||
|
|
||||||
const processor = getDisplayProcessor({
|
const display = getDisplayProcessor({
|
||||||
unit,
|
unit,
|
||||||
decimals,
|
decimals,
|
||||||
mappings: valueMappings,
|
mappings: valueMappings,
|
||||||
@ -25,21 +25,25 @@ export const getSingleStatValues = (props: PanelProps<SingleStatBaseOptions>): D
|
|||||||
});
|
});
|
||||||
|
|
||||||
const values: DisplayValue[] = [];
|
const values: DisplayValue[] = [];
|
||||||
|
|
||||||
for (const table of data) {
|
for (const table of data) {
|
||||||
|
if (stat === 'name') {
|
||||||
|
values.push(display(table.name));
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < table.columns.length; i++) {
|
for (let i = 0; i < table.columns.length; i++) {
|
||||||
const column = table.columns[i];
|
const column = table.columns[i];
|
||||||
|
|
||||||
// Show all columns that are not 'time'
|
// Show all columns that are not 'time'
|
||||||
if (column.type === ColumnType.number) {
|
if (column.type === ColumnType.number) {
|
||||||
const series = processTimeSeries({
|
const stats = calculateStats({
|
||||||
data: [table],
|
table,
|
||||||
xColumn: i,
|
columnIndex: i, // Hardcoded for now!
|
||||||
yColumn: i,
|
stats: [stat], // The stats to calculate
|
||||||
nullValueMode: NullValueMode.Null,
|
nullValueMode: NullValueMode.Null,
|
||||||
})[0];
|
});
|
||||||
|
const displayValue = display(stats[stat]);
|
||||||
const value = stat !== 'name' ? series.stats[stat] : series.label;
|
values.push(displayValue);
|
||||||
values.push(processor(value));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -47,6 +51,7 @@ export const getSingleStatValues = (props: PanelProps<SingleStatBaseOptions>): D
|
|||||||
if (values.length === 0) {
|
if (values.length === 0) {
|
||||||
throw { message: 'Could not find numeric data' };
|
throw { message: 'Could not find numeric data' };
|
||||||
}
|
}
|
||||||
|
|
||||||
return values;
|
return values;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,25 +2,11 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { FormField, FormLabel, PanelOptionsGroup, Select, UnitPicker } from '@grafana/ui';
|
import { FormField, FormLabel, PanelOptionsGroup, StatsPicker, UnitPicker, StatID } from '@grafana/ui';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { SingleStatValueOptions } from './types';
|
import { SingleStatValueOptions } from './types';
|
||||||
|
|
||||||
const statOptions = [
|
|
||||||
{ value: 'min', label: 'Min' },
|
|
||||||
{ value: 'max', label: 'Max' },
|
|
||||||
{ value: 'avg', label: 'Average' },
|
|
||||||
{ value: 'current', label: 'Current' },
|
|
||||||
{ value: 'total', label: 'Total' },
|
|
||||||
{ value: 'name', label: 'Name' },
|
|
||||||
{ value: 'first', label: 'First' },
|
|
||||||
{ value: 'delta', label: 'Delta' },
|
|
||||||
{ value: 'diff', label: 'Difference' },
|
|
||||||
{ value: 'range', label: 'Range' },
|
|
||||||
{ value: 'last_time', label: 'Time of last point' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const labelWidth = 6;
|
const labelWidth = 6;
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@ -30,7 +16,11 @@ export interface Props {
|
|||||||
|
|
||||||
export class SingleStatValueEditor extends PureComponent<Props> {
|
export class SingleStatValueEditor extends PureComponent<Props> {
|
||||||
onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value });
|
onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value });
|
||||||
onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.value });
|
|
||||||
|
onStatsChange = stats => {
|
||||||
|
const stat = stats[0] || StatID.mean;
|
||||||
|
this.props.onChange({ ...this.props.options, stat });
|
||||||
|
};
|
||||||
|
|
||||||
onDecimalChange = event => {
|
onDecimalChange = event => {
|
||||||
if (!isNaN(event.target.value)) {
|
if (!isNaN(event.target.value)) {
|
||||||
@ -61,11 +51,13 @@ export class SingleStatValueEditor extends PureComponent<Props> {
|
|||||||
<PanelOptionsGroup title="Value">
|
<PanelOptionsGroup title="Value">
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<FormLabel width={labelWidth}>Stat</FormLabel>
|
<FormLabel width={labelWidth}>Stat</FormLabel>
|
||||||
<Select
|
<StatsPicker
|
||||||
width={12}
|
width={12}
|
||||||
options={statOptions}
|
placeholder="Choose Stat"
|
||||||
onChange={this.onStatChange}
|
defaultStat={StatID.mean}
|
||||||
value={statOptions.find(option => option.value === stat)}
|
allowMultiple={false}
|
||||||
|
stats={[stat]}
|
||||||
|
onChange={this.onStatsChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ReactPanelPlugin } from '@grafana/ui';
|
import { ReactPanelPlugin, getStatsCalculators } from '@grafana/ui';
|
||||||
import { SingleStatOptions, defaults, SingleStatBaseOptions } from './types';
|
import { SingleStatOptions, defaults, SingleStatBaseOptions } from './types';
|
||||||
import { SingleStatPanel } from './SingleStatPanel';
|
import { SingleStatPanel } from './SingleStatPanel';
|
||||||
import cloneDeep from 'lodash/cloneDeep';
|
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;
|
return options;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { VizOrientation, ValueMapping, Threshold } from '@grafana/ui';
|
import { VizOrientation, ValueMapping, Threshold, StatID } from '@grafana/ui';
|
||||||
|
|
||||||
export interface SingleStatBaseOptions {
|
export interface SingleStatBaseOptions {
|
||||||
valueMappings: ValueMapping[];
|
valueMappings: ValueMapping[];
|
||||||
@ -24,7 +24,7 @@ export const defaults: SingleStatOptions = {
|
|||||||
prefix: '',
|
prefix: '',
|
||||||
suffix: '',
|
suffix: '',
|
||||||
decimals: null,
|
decimals: null,
|
||||||
stat: 'avg',
|
stat: StatID.mean,
|
||||||
unit: 'none',
|
unit: 'none',
|
||||||
},
|
},
|
||||||
valueMappings: [],
|
valueMappings: [],
|
||||||
|
Loading…
Reference in New Issue
Block a user