Skip to content

Commit 4e8c0ad

Browse files
[Maps] Add percentile (#85367) (#86021)
1 parent 70fbe91 commit 4e8c0ad

File tree

17 files changed

+528
-42
lines changed

17 files changed

+528
-42
lines changed

x-pack/plugins/maps/common/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ export enum AGG_TYPE {
150150
MIN = 'min',
151151
SUM = 'sum',
152152
TERMS = 'terms',
153+
PERCENTILE = 'percentile',
153154
UNIQUE_COUNT = 'cardinality',
154155
}
155156

@@ -171,6 +172,7 @@ export const GEOTILE_GRID_AGG_NAME = 'gridSplit';
171172
export const GEOCENTROID_AGG_NAME = 'gridCentroid';
172173

173174
export const TOP_TERM_PERCENTAGE_SUFFIX = '__percentage';
175+
export const DEFAULT_PERCENTILE = 50;
174176

175177
export const COUNT_PROP_LABEL = i18n.translate('xpack.maps.aggs.defaultCountLabel', {
176178
defaultMessage: 'count',

x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,13 @@ export type FieldedAggDescriptor = AbstractAggDescriptor & {
6060
field?: string;
6161
};
6262

63-
export type AggDescriptor = CountAggDescriptor | FieldedAggDescriptor;
63+
export type PercentileAggDescriptor = AbstractAggDescriptor & {
64+
type: AGG_TYPE.PERCENTILE;
65+
field?: string;
66+
percentile?: number;
67+
};
68+
69+
export type AggDescriptor = CountAggDescriptor | FieldedAggDescriptor | PercentileAggDescriptor;
6470

6571
export type AbstractESAggSourceDescriptor = AbstractESSourceDescriptor & {
6672
metrics: AggDescriptor[];

x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { i18n } from '@kbn/i18n';
77
import _ from 'lodash';
88
import { IndexPattern, IFieldType } from '../../../../../src/plugins/data/common';
9-
import { TOP_TERM_PERCENTAGE_SUFFIX } from '../constants';
9+
import { AGG_TYPE, JOIN_FIELD_NAME_PREFIX, TOP_TERM_PERCENTAGE_SUFFIX } from '../constants';
1010

1111
export type BucketProperties = Record<string | number, unknown>;
1212
export type PropertiesMap = Map<string, BucketProperties>;
@@ -46,6 +46,7 @@ export function extractPropertiesFromBucket(
4646
continue;
4747
}
4848

49+
// todo: push these implementations in the IAggFields
4950
if (_.has(bucket[key], 'value')) {
5051
properties[key] = bucket[key].value;
5152
} else if (_.has(bucket[key], 'buckets')) {
@@ -63,7 +64,20 @@ export function extractPropertiesFromBucket(
6364
);
6465
}
6566
} else {
66-
properties[key] = bucket[key];
67+
if (
68+
key.startsWith(AGG_TYPE.PERCENTILE) ||
69+
key.startsWith(JOIN_FIELD_NAME_PREFIX + AGG_TYPE.PERCENTILE)
70+
) {
71+
const values = bucket[key].values;
72+
for (const k in values) {
73+
if (values.hasOwnProperty(k)) {
74+
properties[key] = values[k];
75+
break;
76+
}
77+
}
78+
} else {
79+
properties[key] = bucket[key];
80+
}
6781
}
6882
}
6983
return properties;

x-pack/plugins/maps/public/classes/fields/agg/es_agg_factory.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import { esAggFieldsFactory } from './es_agg_factory';
88
import { AGG_TYPE, FIELD_ORIGIN } from '../../../../common/constants';
99
import { IESAggSource } from '../../sources/es_agg_source';
1010

11-
const mockEsAggSource = ({} as unknown) as IESAggSource;
11+
const mockEsAggSource = ({
12+
getAggKey() {
13+
return 'foobar';
14+
},
15+
} as unknown) as IESAggSource;
1216

1317
describe('esAggFieldsFactory', () => {
1418
test('Should only create top terms field when term field is not provided', () => {
@@ -28,4 +32,26 @@ describe('esAggFieldsFactory', () => {
2832
);
2933
expect(fields.length).toBe(2);
3034
});
35+
36+
describe('percentile-fields', () => {
37+
test('Should create percentile agg fields with default', () => {
38+
const fields = esAggFieldsFactory(
39+
{ type: AGG_TYPE.PERCENTILE, field: 'myField' },
40+
mockEsAggSource,
41+
FIELD_ORIGIN.SOURCE
42+
);
43+
expect(fields.length).toBe(1);
44+
expect(fields[0].getName()).toBe('foobar_50');
45+
});
46+
47+
test('Should create percentile agg fields with param', () => {
48+
const fields = esAggFieldsFactory(
49+
{ type: AGG_TYPE.PERCENTILE, field: 'myField', percentile: 90 },
50+
mockEsAggSource,
51+
FIELD_ORIGIN.SOURCE
52+
);
53+
expect(fields.length).toBe(1);
54+
expect(fields[0].getName()).toBe('foobar_90');
55+
});
56+
});
3157
});

x-pack/plugins/maps/public/classes/fields/agg/es_agg_factory.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66

77
import { AggDescriptor } from '../../../../common/descriptor_types';
88
import { IESAggSource } from '../../sources/es_agg_source';
9-
import { AGG_TYPE, FIELD_ORIGIN } from '../../../../common/constants';
9+
import { AGG_TYPE, DEFAULT_PERCENTILE, FIELD_ORIGIN } from '../../../../common/constants';
1010
import { ESDocField } from '../es_doc_field';
1111
import { TopTermPercentageField } from './top_term_percentage_field';
1212
import { CountAggField } from './count_agg_field';
1313
import { IESAggField } from './agg_field_types';
1414
import { AggField } from './agg_field';
15+
import { PercentileAggField } from './percentile_agg_field';
1516

1617
export function esAggFieldsFactory(
1718
aggDescriptor: AggDescriptor,
@@ -27,6 +28,21 @@ export function esAggFieldsFactory(
2728
origin,
2829
canReadFromGeoJson,
2930
});
31+
} else if (aggDescriptor.type === AGG_TYPE.PERCENTILE) {
32+
aggField = new PercentileAggField({
33+
label: aggDescriptor.label,
34+
esDocField:
35+
'field' in aggDescriptor && aggDescriptor.field
36+
? new ESDocField({ fieldName: aggDescriptor.field, source, origin })
37+
: undefined,
38+
percentile:
39+
'percentile' in aggDescriptor && typeof aggDescriptor.percentile === 'number'
40+
? aggDescriptor.percentile
41+
: DEFAULT_PERCENTILE,
42+
source,
43+
origin,
44+
canReadFromGeoJson,
45+
});
3046
} else {
3147
aggField = new AggField({
3248
label: aggDescriptor.label,
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { AGG_TYPE, FIELD_ORIGIN } from '../../../../common/constants';
8+
import { IESAggSource } from '../../sources/es_agg_source';
9+
import { IndexPattern } from 'src/plugins/data/public';
10+
import { PercentileAggField } from './percentile_agg_field';
11+
import { ESDocField } from '../es_doc_field';
12+
13+
const mockFields = [
14+
{
15+
name: 'foo*',
16+
},
17+
];
18+
// @ts-expect-error
19+
mockFields.getByName = (name: string) => {
20+
return {
21+
name,
22+
};
23+
};
24+
25+
const mockIndexPattern = {
26+
title: 'wildIndex',
27+
fields: mockFields,
28+
};
29+
30+
const mockEsAggSource = {
31+
getAggKey: (aggType: AGG_TYPE, fieldName: string) => {
32+
return 'agg_key';
33+
},
34+
getAggLabel: (aggType: AGG_TYPE, fieldName: string) => {
35+
return 'agg_label';
36+
},
37+
getIndexPattern: async () => {
38+
return mockIndexPattern;
39+
},
40+
} as IESAggSource;
41+
42+
const mockEsDocField = {
43+
getName() {
44+
return 'foobar';
45+
},
46+
};
47+
48+
const defaultParams = {
49+
source: mockEsAggSource,
50+
origin: FIELD_ORIGIN.SOURCE,
51+
};
52+
53+
describe('percentile agg field', () => {
54+
test('should include percentile in name', () => {
55+
const field = new PercentileAggField({
56+
...defaultParams,
57+
esDocField: mockEsDocField as ESDocField,
58+
percentile: 80,
59+
});
60+
expect(field.getName()).toEqual('agg_key_80');
61+
});
62+
63+
test('should create percentile dsl', () => {
64+
const field = new PercentileAggField({
65+
...defaultParams,
66+
esDocField: mockEsDocField as ESDocField,
67+
percentile: 80,
68+
});
69+
70+
expect(field.getValueAggDsl(mockIndexPattern as IndexPattern)).toEqual({
71+
percentiles: { field: 'foobar', percents: [80] },
72+
});
73+
});
74+
75+
test('label', async () => {
76+
const field = new PercentileAggField({
77+
...defaultParams,
78+
esDocField: mockEsDocField as ESDocField,
79+
percentile: 80,
80+
});
81+
82+
expect(await field.getLabel()).toEqual('80th agg_label');
83+
});
84+
85+
test('label (median)', async () => {
86+
const field = new PercentileAggField({
87+
...defaultParams,
88+
label: '',
89+
esDocField: mockEsDocField as ESDocField,
90+
percentile: 50,
91+
});
92+
93+
expect(await field.getLabel()).toEqual('median foobar');
94+
});
95+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { IndexPattern } from 'src/plugins/data/common/index_patterns/index_patterns';
8+
import { i18n } from '@kbn/i18n';
9+
import { AGG_TYPE } from '../../../../common/constants';
10+
import { IESAggField, CountAggFieldParams } from './agg_field_types';
11+
import { addFieldToDSL, getField } from '../../../../common/elasticsearch_util';
12+
import { ESDocField } from '../es_doc_field';
13+
import { getOrdinalSuffix } from '../../util/ordinal_suffix';
14+
import { AggField } from './agg_field';
15+
16+
interface PercentileAggParams extends CountAggFieldParams {
17+
esDocField?: ESDocField;
18+
percentile: number;
19+
}
20+
21+
export class PercentileAggField extends AggField implements IESAggField {
22+
private readonly _percentile: number;
23+
constructor(params: PercentileAggParams) {
24+
super({
25+
...params,
26+
...{
27+
aggType: AGG_TYPE.PERCENTILE,
28+
},
29+
});
30+
this._percentile = params.percentile;
31+
}
32+
33+
supportsFieldMeta(): boolean {
34+
return true;
35+
}
36+
37+
canValueBeFormatted(): boolean {
38+
return true;
39+
}
40+
41+
async getLabel(): Promise<string> {
42+
if (this._label) {
43+
return this._label;
44+
}
45+
46+
if (this._percentile === 50) {
47+
const median = i18n.translate('xpack.maps.fields.percentileMedianLabek', {
48+
defaultMessage: 'median',
49+
});
50+
return `${median} ${this.getRootName()}`;
51+
}
52+
53+
const suffix = getOrdinalSuffix(this._percentile);
54+
return `${this._percentile}${suffix} ${this._source.getAggLabel(
55+
this._getAggType(),
56+
this.getRootName()
57+
)}`;
58+
}
59+
60+
getName() {
61+
return `${super.getName()}_${this._percentile}`;
62+
}
63+
64+
getValueAggDsl(indexPattern: IndexPattern): unknown {
65+
const field = getField(indexPattern, this.getRootName());
66+
const dsl: Record<string, unknown> = addFieldToDSL({}, field);
67+
dsl.percents = [this._percentile];
68+
return {
69+
percentiles: dsl,
70+
};
71+
}
72+
}

x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ interface CountData {
4848
isSyncClustered: boolean;
4949
}
5050

51-
function getAggType(dynamicProperty: IDynamicStyleProperty<DynamicStylePropertyOptions>): AGG_TYPE {
51+
function getAggType(
52+
dynamicProperty: IDynamicStyleProperty<DynamicStylePropertyOptions>
53+
): AGG_TYPE.AVG | AGG_TYPE.TERMS {
5254
return dynamicProperty.isOrdinal() ? AGG_TYPE.AVG : AGG_TYPE.TERMS;
5355
}
5456

x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import {
2121
import { VectorStyle } from '../styles/vector/vector_style';
2222
import { EMSFileSource } from '../sources/ems_file_source';
2323
// @ts-ignore
24-
import { ESGeoGridSource } from '../sources/es_geo_grid_source';
2524
import { VectorLayer } from './vector_layer/vector_layer';
2625
import { getDefaultDynamicProperties } from '../styles/vector/vector_style_defaults';
2726
import { NUMERICAL_COLOR_PALETTES } from '../styles/color_palettes';
@@ -35,9 +34,13 @@ export function createAggDescriptor(metricAgg: string, metricFieldName?: string)
3534
});
3635
const aggType = aggTypeKey ? AGG_TYPE[aggTypeKey as keyof typeof AGG_TYPE] : undefined;
3736

38-
return aggType && metricFieldName
39-
? { type: aggType, field: metricFieldName }
40-
: { type: AGG_TYPE.COUNT };
37+
if (!aggType || aggType === AGG_TYPE.COUNT || !metricFieldName) {
38+
return { type: AGG_TYPE.COUNT };
39+
} else if (aggType === AGG_TYPE.PERCENTILE) {
40+
return { type: aggType, field: metricFieldName, percentile: 50 };
41+
} else {
42+
return { type: aggType, field: metricFieldName };
43+
}
4144
}
4245

4346
export function createRegionMapLayerDescriptor({

x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import {
1515
AGG_TYPE,
1616
COLOR_MAP_TYPE,
17+
DEFAULT_PERCENTILE,
1718
FIELD_ORIGIN,
1819
GRID_RESOLUTION,
1920
RENDER_AS,
@@ -59,9 +60,18 @@ export function createAggDescriptor(
5960
});
6061
const aggType = aggTypeKey ? AGG_TYPE[aggTypeKey as keyof typeof AGG_TYPE] : undefined;
6162

62-
return aggType && metricFieldName && (!isHeatmap(mapType) || isMetricCountable(aggType))
63-
? { type: aggType, field: metricFieldName }
64-
: { type: AGG_TYPE.COUNT };
63+
if (
64+
!aggType ||
65+
aggType === AGG_TYPE.COUNT ||
66+
!metricFieldName ||
67+
(isHeatmap(mapType) && !isMetricCountable(aggType))
68+
) {
69+
return { type: AGG_TYPE.COUNT };
70+
}
71+
72+
return aggType === AGG_TYPE.PERCENTILE
73+
? { type: aggType, field: metricFieldName, percentile: DEFAULT_PERCENTILE }
74+
: { type: aggType, field: metricFieldName };
6575
}
6676

6777
export function createTileMapLayerDescriptor({

0 commit comments

Comments
 (0)