diff --git a/.betterer.results b/.betterer.results index 6141514e717..b3f421ed784 100644 --- a/.betterer.results +++ b/.betterer.results @@ -177,6 +177,9 @@ exports[`better eslint`] = { "packages/grafana-data/src/themes/createColors.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "packages/grafana-data/src/transformations/fieldReducer.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "packages/grafana-data/src/transformations/matchers/valueMatchers/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] diff --git a/packages/grafana-data/src/transformations/fieldReducer.test.ts b/packages/grafana-data/src/transformations/fieldReducer.test.ts index f96321508b3..7ccda4c1573 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.test.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.test.ts @@ -251,4 +251,18 @@ describe('Stats Calculators', () => { expect(reduce(someNulls, ReducerID.count)).toEqual(4); }); + + for (let i = 1; i < 100; i++) { + it(`can reduce the ${i}th percentile`, () => { + const preciseStats = reduceField({ + field: createField( + 'x', + Array.from({ length: 101 }, (_, index) => index) + ), + reducers: [(ReducerID as Record)[`p${i}`]], + }); + + expect(preciseStats[`p${i}`]).toEqual(i); + }); + } }); diff --git a/packages/grafana-data/src/transformations/fieldReducer.ts b/packages/grafana-data/src/transformations/fieldReducer.ts index b303654b895..a08c7db3c50 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.ts @@ -28,6 +28,105 @@ export enum ReducerID { allIsNull = 'allIsNull', allValues = 'allValues', uniqueValues = 'uniqueValues', + p1 = 'p1', + p2 = 'p2', + p3 = 'p3', + p4 = 'p4', + p5 = 'p5', + p6 = 'p6', + p7 = 'p7', + p8 = 'p8', + p9 = 'p9', + p10 = 'p10', + p11 = 'p11', + p12 = 'p12', + p13 = 'p13', + p14 = 'p14', + p15 = 'p15', + p16 = 'p16', + p17 = 'p17', + p18 = 'p18', + p19 = 'p19', + p20 = 'p20', + p21 = 'p21', + p22 = 'p22', + p23 = 'p23', + p24 = 'p24', + p25 = 'p25', + p26 = 'p26', + p27 = 'p27', + p28 = 'p28', + p29 = 'p29', + p30 = 'p30', + p31 = 'p31', + p32 = 'p32', + p33 = 'p33', + p34 = 'p34', + p35 = 'p35', + p36 = 'p36', + p37 = 'p37', + p38 = 'p38', + p39 = 'p39', + p40 = 'p40', + p41 = 'p41', + p42 = 'p42', + p43 = 'p43', + p44 = 'p44', + p45 = 'p45', + p46 = 'p46', + p47 = 'p47', + p48 = 'p48', + p49 = 'p49', + p50 = 'p50', + p51 = 'p51', + p52 = 'p52', + p53 = 'p53', + p54 = 'p54', + p55 = 'p55', + p56 = 'p56', + p57 = 'p57', + p58 = 'p58', + p59 = 'p59', + p60 = 'p60', + p61 = 'p61', + p62 = 'p62', + p63 = 'p63', + p64 = 'p64', + p65 = 'p65', + p66 = 'p66', + p67 = 'p67', + p68 = 'p68', + p69 = 'p69', + p70 = 'p70', + p71 = 'p71', + p72 = 'p72', + p73 = 'p73', + p74 = 'p74', + p75 = 'p75', + p76 = 'p76', + p77 = 'p77', + p78 = 'p78', + p79 = 'p79', + p80 = 'p80', + p81 = 'p81', + p82 = 'p82', + p83 = 'p83', + p84 = 'p84', + p85 = 'p85', + p86 = 'p86', + p87 = 'p87', + p88 = 'p88', + p89 = 'p89', + p90 = 'p90', + p91 = 'p91', + p92 = 'p92', + p93 = 'p93', + p94 = 'p94', + p95 = 'p95', + p96 = 'p96', + p97 = 'p97', + p98 = 'p98', + p99 = 'p99', } export function isReducerID(id: string): id is ReducerID { @@ -305,6 +404,26 @@ export const fieldReducers = new Registry(() => [ }, ]); +for (let i = 1; i < 100; i++) { + const percentile = i / 100; + const id = `p${i}` as ReducerID; + const nth = (n: number) => + n > 3 && n < 21 ? 'th' : n % 10 === 1 ? 'st' : n % 10 === 2 ? 'nd' : n % 10 === 3 ? 'rd' : 'th'; + const name = `${i}${nth(i)} percentile`; + const description = `${i}${nth(i)} percentile value`; + + fieldReducers.register({ + id: id, + name: name, + description: description, + standard: false, + reduce: (field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs => { + return { [id]: calculatePercentile(field, percentile, ignoreNulls, nullAsZero) }; + }, + preservesUnits: true, + }); +} + // Used for test cases export const defaultCalcs: FieldCalcs = { sum: 0, @@ -325,7 +444,6 @@ export const defaultCalcs: FieldCalcs = { delta: 0, step: Number.MAX_VALUE, diffperc: 0, - // Just used for calculations -- not exposed as a stat previousDeltaUp: true, }; @@ -553,3 +671,18 @@ function calculateDistinctCount(field: Field, ignoreNulls: boolean, nullAsZero: } return { distinctCount: distinct.size }; } + +function calculatePercentile(field: Field, percentile: number, ignoreNulls: boolean, nullAsZero: boolean): number { + let data = field.values; + + if (ignoreNulls) { + data = data.filter((value) => value !== null); + } + if (nullAsZero) { + data = data.map((value) => (value === null ? 0 : value)); + } + + const sorted = data.slice().sort((a, b) => a - b); + const index = Math.round((sorted.length - 1) * percentile); + return sorted[index]; +}