mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Calculations: Update First * and Last * reducers to exclude NaNs (#77323)
This commit is contained in:
@@ -29,9 +29,9 @@ The following table contains a list of calculations you can perform in Grafana.
|
|||||||
| Difference percent | Percentage change between first and last value of a field |
|
| Difference percent | Percentage change between first and last value of a field |
|
||||||
| Distinct count | Number of unique values in a field |
|
| Distinct count | Number of unique values in a field |
|
||||||
| First | First value in a field |
|
| First | First value in a field |
|
||||||
| First\* (not null) | First, not null value in a field |
|
| First\* (not null) | First, not null value in a field (also excludes NaNs) |
|
||||||
| Last | Last value in a field |
|
| Last | Last value in a field |
|
||||||
| Last\* (not null) | Last, not null value in a field |
|
| Last\* (not null) | Last, not null value in a field (also excludes NaNs) |
|
||||||
| Max | Maximum value of a field |
|
| Max | Maximum value of a field |
|
||||||
| Mean | Mean value of all values in a field |
|
| Mean | Mean value of all values in a field |
|
||||||
| Variance | Variance of all values in a field |
|
| Variance | Variance of all values in a field |
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ describe('Stats Calculators', () => {
|
|||||||
it('should calculate basic stats', () => {
|
it('should calculate basic stats', () => {
|
||||||
const stats = reduceField({
|
const stats = reduceField({
|
||||||
field: basicTable.fields[0],
|
field: basicTable.fields[0],
|
||||||
reducers: ['first', 'last', 'mean', 'count'],
|
reducers: [ReducerID.first, ReducerID.last, ReducerID.mean, ReducerID.count],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(stats.first).toEqual(10);
|
expect(stats.first).toEqual(10);
|
||||||
@@ -67,7 +67,7 @@ describe('Stats Calculators', () => {
|
|||||||
basicTable.fields[0].state = undefined; // clear the cache
|
basicTable.fields[0].state = undefined; // clear the cache
|
||||||
const stats = reduceField({
|
const stats = reduceField({
|
||||||
field: basicTable.fields[0],
|
field: basicTable.fields[0],
|
||||||
reducers: ['first'],
|
reducers: [ReducerID.first],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should do the simple version that just looks up value
|
// Should do the simple version that just looks up value
|
||||||
@@ -172,6 +172,58 @@ describe('Stats Calculators', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('consistent results for first/last value with NaN', () => {
|
||||||
|
const info = [
|
||||||
|
{
|
||||||
|
data: [NaN, 200, NaN], // first/last value is NaN
|
||||||
|
result: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: [NaN, NaN, NaN], // All NaN
|
||||||
|
result: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: [undefined, undefined, undefined], // Empty row
|
||||||
|
result: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const stats = reduceField({
|
||||||
|
field: createField('x', info[0].data),
|
||||||
|
reducers: [ReducerID.first, ReducerID.last, ReducerID.firstNotNull, ReducerID.lastNotNull, ReducerID.diffperc],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stats[ReducerID.first]).toEqual(NaN);
|
||||||
|
expect(stats[ReducerID.last]).toEqual(NaN);
|
||||||
|
expect(stats[ReducerID.firstNotNull]).toEqual(200);
|
||||||
|
expect(stats[ReducerID.lastNotNull]).toEqual(200);
|
||||||
|
expect(stats[ReducerID.diffperc]).toEqual(0);
|
||||||
|
|
||||||
|
const reducers = [ReducerID.lastNotNull, ReducerID.firstNotNull];
|
||||||
|
for (const input of info) {
|
||||||
|
for (const reducer of reducers) {
|
||||||
|
const v1 = reduceField({
|
||||||
|
field: createField('x', input.data),
|
||||||
|
reducers: [reducer, ReducerID.mean], // uses standard path
|
||||||
|
})[reducer];
|
||||||
|
|
||||||
|
const v2 = reduceField({
|
||||||
|
field: createField('x', input.data),
|
||||||
|
reducers: [reducer], // uses optimized path
|
||||||
|
})[reducer];
|
||||||
|
|
||||||
|
if (v1 !== v2 || v1 !== input.result) {
|
||||||
|
const msg =
|
||||||
|
`Invalid ${reducer} result for: ` +
|
||||||
|
input.data.join(', ') +
|
||||||
|
` Expected: ${input.result}` + // configured
|
||||||
|
` Received: Multiple: ${v1}, Single: ${v2}`;
|
||||||
|
expect(msg).toEqual(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('count should ignoreNulls by default', () => {
|
it('count should ignoreNulls by default', () => {
|
||||||
const someNulls = createField('x', [1, null, null, 1]);
|
const someNulls = createField('x', [1, null, null, 1]);
|
||||||
expect(reduce(someNulls, ReducerID.count)).toEqual(2);
|
expect(reduce(someNulls, ReducerID.count)).toEqual(2);
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export const fieldReducers = new Registry<FieldReducerInfo>(() => [
|
|||||||
{
|
{
|
||||||
id: ReducerID.lastNotNull,
|
id: ReducerID.lastNotNull,
|
||||||
name: 'Last *',
|
name: 'Last *',
|
||||||
description: 'Last non-null value',
|
description: 'Last non-null value (also excludes NaNs)',
|
||||||
standard: true,
|
standard: true,
|
||||||
aliasIds: ['current'],
|
aliasIds: ['current'],
|
||||||
reduce: calculateLastNotNull,
|
reduce: calculateLastNotNull,
|
||||||
@@ -152,7 +152,7 @@ export const fieldReducers = new Registry<FieldReducerInfo>(() => [
|
|||||||
{
|
{
|
||||||
id: ReducerID.firstNotNull,
|
id: ReducerID.firstNotNull,
|
||||||
name: 'First *',
|
name: 'First *',
|
||||||
description: 'First non-null value',
|
description: 'First non-null value (also excludes NaNs)',
|
||||||
standard: true,
|
standard: true,
|
||||||
reduce: calculateFirstNotNull,
|
reduce: calculateFirstNotNull,
|
||||||
},
|
},
|
||||||
@@ -320,8 +320,8 @@ export function doStandardCalcs(field: Field, ignoreNulls: boolean, nullAsZero:
|
|||||||
|
|
||||||
calcs.count++;
|
calcs.count++;
|
||||||
|
|
||||||
if (currentValue != null) {
|
if (currentValue != null && !Number.isNaN(currentValue)) {
|
||||||
// null || undefined
|
// null || undefined || NaN
|
||||||
const isFirst = calcs.firstNotNull === null;
|
const isFirst = calcs.firstNotNull === null;
|
||||||
if (isFirst) {
|
if (isFirst) {
|
||||||
calcs.firstNotNull = currentValue;
|
calcs.firstNotNull = currentValue;
|
||||||
@@ -418,7 +418,7 @@ function calculateFirstNotNull(field: Field, ignoreNulls: boolean, nullAsZero: b
|
|||||||
const data = field.values;
|
const data = field.values;
|
||||||
for (let idx = 0; idx < data.length; idx++) {
|
for (let idx = 0; idx < data.length; idx++) {
|
||||||
const v = data[idx];
|
const v = data[idx];
|
||||||
if (v != null && v !== undefined) {
|
if (v != null && !Number.isNaN(v)) {
|
||||||
return { firstNotNull: v };
|
return { firstNotNull: v };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -435,7 +435,7 @@ function calculateLastNotNull(field: Field, ignoreNulls: boolean, nullAsZero: bo
|
|||||||
let idx = data.length - 1;
|
let idx = data.length - 1;
|
||||||
while (idx >= 0) {
|
while (idx >= 0) {
|
||||||
const v = data[idx--];
|
const v = data[idx--];
|
||||||
if (v != null && v !== undefined) {
|
if (v != null && !Number.isNaN(v)) {
|
||||||
return { lastNotNull: v };
|
return { lastNotNull: v };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user