diff --git a/packages/grafana-data/src/transformations/fieldReducer.test.ts b/packages/grafana-data/src/transformations/fieldReducer.test.ts index 120a416d81a..5e528098920 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.test.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.test.ts @@ -254,6 +254,29 @@ describe('Stats Calculators', () => { expect(reduce(someNulls, ReducerID.count)).toEqual(4); }); + it('median should ignoreNulls by default', () => { + const someNulls = createField('y', [3, null, 2, 1, 4]); + expect(reduce(someNulls, ReducerID.median)).toEqual(2.5); + }); + + it('median should use fieldConfig nullValueMode.Ignore and not count nulls', () => { + const someNulls = createField('y', [3, null, 2, 1, 4]); + someNulls.config.nullValueMode = NullValueMode.Ignore; + expect(reduce(someNulls, ReducerID.median)).toEqual(2.5); + }); + + it('median should use fieldConfig nullValueMode.Null and count nulls', () => { + const someNulls = createField('y', [3, null, 2, 1, 4]); + someNulls.config.nullValueMode = NullValueMode.Null; + expect(reduce(someNulls, ReducerID.median)).toEqual(2); + }); + + it('median should use fieldConfig nullValueMode.AsZero and count nulls as zero', () => { + const someNulls = createField('y', [3, null, 2, 1, 4]); + someNulls.config.nullValueMode = NullValueMode.AsZero; + expect(reduce(someNulls, ReducerID.median)).toEqual(2); + }); + it('can reduce to percentiles', () => { // This `Array.from` will build an array of elements from 1 to 99 const percentiles = [...Array.from({ length: 99 }, (_, i) => i + 1)]; diff --git a/packages/grafana-data/src/transformations/fieldReducer.ts b/packages/grafana-data/src/transformations/fieldReducer.ts index a7d1f0c7242..71a3c4c2344 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.ts @@ -283,7 +283,8 @@ export const fieldReducers = new Registry(() => [ id: ReducerID.median, name: 'Median', description: 'Median Value', - standard: true, + standard: false, + reduce: calculateMedian, aliasIds: ['median'], preservesUnits: true, }, @@ -584,6 +585,7 @@ export function doStandardCalcs(field: Field, ignoreNulls: boolean, nullAsZero: if (isNumber(calcs.firstNotNull) && isNumber(calcs.diff)) { calcs.diffperc = (calcs.diff / calcs.firstNotNull) * 100; } + return calcs; } @@ -703,3 +705,32 @@ function calculatePercentile(field: Field, percentile: number, ignoreNulls: bool const index = Math.round((sorted.length - 1) * percentile); return sorted[index]; } + +function calculateMedian(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs { + const numbers: number[] = []; + + for (let i = 0; i < field.values.length; i++) { + let currentValue = field.values[i]; + + if (currentValue == null) { + if (ignoreNulls) { + continue; + } + if (nullAsZero) { + currentValue = 0; + } + } + + numbers.push(currentValue); + } + + numbers.sort((a, b) => a - b); + + const mid = Math.floor(numbers.length / 2); + + if (numbers.length % 2 === 0) { + return { median: (numbers[mid - 1] + numbers[mid]) / 2 }; + } else { + return { median: numbers[mid] }; + } +}