Skip to content

Commit defb31d

Browse files
alexwizpelasticmachinewylieconlonlukeelmers
authored
Add auto interval to histogram AggConfig (#76001) (#76449)
* Add `auto` interval to histogram AggConfig Closes: #75438 * fix JEST * update UI * add tests * some changes * small changes * cleanup code * fix PR comment * fix PR comments * fix PR comment * Update src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts Co-authored-by: Wylie Conlon <wylieconlon@gmail.com> * Change algorithm for auto interval * Update src/plugins/data/common/search/aggs/buckets/histogram.ts Co-authored-by: Luke Elmers <lukeelmers@gmail.com> * added some comments * Update src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts Co-authored-by: Wylie Conlon <wylieconlon@gmail.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Wylie Conlon <wylieconlon@gmail.com> Co-authored-by: Luke Elmers <lukeelmers@gmail.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Wylie Conlon <wylieconlon@gmail.com> Co-authored-by: Luke Elmers <lukeelmers@gmail.com>
1 parent 35b7e11 commit defb31d

File tree

17 files changed

+553
-76
lines changed

17 files changed

+553
-76
lines changed

src/plugins/data/common/search/aggs/buckets/_interval_options.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@
2020
import { i18n } from '@kbn/i18n';
2121
import { IBucketAggConfig } from './bucket_agg_type';
2222

23+
export const autoInterval = 'auto';
24+
export const isAutoInterval = (value: unknown) => value === autoInterval;
25+
2326
export const intervalOptions = [
2427
{
2528
display: i18n.translate('data.search.aggs.buckets.intervalOptions.autoDisplayName', {
2629
defaultMessage: 'Auto',
2730
}),
28-
val: 'auto',
31+
val: autoInterval,
2932
enabled(agg: IBucketAggConfig) {
3033
// not only do we need a time field, but the selected field needs
3134
// to be the time field. (see #3028)

src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
import moment from 'moment';
2121
import { createFilterDateHistogram } from './date_histogram';
22-
import { intervalOptions } from '../_interval_options';
22+
import { intervalOptions, autoInterval } from '../_interval_options';
2323
import { AggConfigs } from '../../agg_configs';
2424
import { mockAggTypesRegistry } from '../../test_helpers';
2525
import { IBucketDateHistogramAggConfig } from '../date_histogram';
@@ -33,7 +33,10 @@ describe('AggConfig Filters', () => {
3333
let bucketStart: any;
3434
let field: any;
3535

36-
const init = (interval: string = 'auto', duration: any = moment.duration(15, 'minutes')) => {
36+
const init = (
37+
interval: string = autoInterval,
38+
duration: any = moment.duration(15, 'minutes')
39+
) => {
3740
field = {
3841
name: 'date',
3942
};

src/plugins/data/common/search/aggs/buckets/date_histogram.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n';
2323

2424
import { KBN_FIELD_TYPES, TimeRange, TimeRangeBounds, UI_SETTINGS } from '../../../../common';
2525

26-
import { intervalOptions } from './_interval_options';
26+
import { intervalOptions, autoInterval, isAutoInterval } from './_interval_options';
2727
import { createFilterDateHistogram } from './create_filter/date_histogram';
2828
import { BucketAggType, IBucketAggConfig } from './bucket_agg_type';
2929
import { BUCKET_TYPES } from './bucket_agg_types';
@@ -44,7 +44,7 @@ const updateTimeBuckets = (
4444
customBuckets?: IBucketDateHistogramAggConfig['buckets']
4545
) => {
4646
const bounds =
47-
agg.params.timeRange && (agg.fieldIsTimeField() || agg.params.interval === 'auto')
47+
agg.params.timeRange && (agg.fieldIsTimeField() || isAutoInterval(agg.params.interval))
4848
? calculateBounds(agg.params.timeRange)
4949
: undefined;
5050
const buckets = customBuckets || agg.buckets;
@@ -149,7 +149,7 @@ export const getDateHistogramBucketAgg = ({
149149
return agg.getIndexPattern().timeFieldName;
150150
},
151151
onChange(agg: IBucketDateHistogramAggConfig) {
152-
if (get(agg, 'params.interval') === 'auto' && !agg.fieldIsTimeField()) {
152+
if (isAutoInterval(get(agg, 'params.interval')) && !agg.fieldIsTimeField()) {
153153
delete agg.params.interval;
154154
}
155155
},
@@ -187,7 +187,7 @@ export const getDateHistogramBucketAgg = ({
187187
}
188188
return state;
189189
},
190-
default: 'auto',
190+
default: autoInterval,
191191
options: intervalOptions,
192192
write(agg, output, aggs) {
193193
updateTimeBuckets(agg, calculateBounds);

src/plugins/data/common/search/aggs/buckets/histogram.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,28 @@ describe('Histogram Agg', () => {
103103
});
104104
});
105105

106+
describe('maxBars', () => {
107+
test('should not be written to the DSL', () => {
108+
const aggConfigs = getAggConfigs({
109+
maxBars: 50,
110+
field: {
111+
name: 'field',
112+
},
113+
});
114+
const { [BUCKET_TYPES.HISTOGRAM]: params } = aggConfigs.aggs[0].toDsl();
115+
116+
expect(params).not.toHaveProperty('maxBars');
117+
});
118+
});
119+
106120
describe('interval', () => {
121+
test('accepts "auto" value', () => {
122+
const params = getParams({
123+
interval: 'auto',
124+
});
125+
126+
expect(params).toHaveProperty('interval', 1);
127+
});
107128
test('accepts a whole number', () => {
108129
const params = getParams({
109130
interval: 100,

src/plugins/data/common/search/aggs/buckets/histogram.ts

Lines changed: 25 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import { BucketAggType, IBucketAggConfig } from './bucket_agg_type';
2828
import { createFilterHistogram } from './create_filter/histogram';
2929
import { BUCKET_TYPES } from './bucket_agg_types';
3030
import { ExtendedBounds } from './lib/extended_bounds';
31+
import { isAutoInterval, autoInterval } from './_interval_options';
32+
import { calculateHistogramInterval } from './lib/histogram_calculate_interval';
3133

3234
export interface AutoBounds {
3335
min: number;
@@ -47,6 +49,7 @@ export interface IBucketHistogramAggConfig extends IBucketAggConfig {
4749
export interface AggParamsHistogram extends BaseAggParams {
4850
field: string;
4951
interval: string;
52+
maxBars?: number;
5053
intervalBase?: number;
5154
min_doc_count?: boolean;
5255
has_extended_bounds?: boolean;
@@ -102,6 +105,7 @@ export const getHistogramBucketAgg = ({
102105
},
103106
{
104107
name: 'interval',
108+
default: autoInterval,
105109
modifyAggConfigOnSearchRequestStart(
106110
aggConfig: IBucketHistogramAggConfig,
107111
searchSource: any,
@@ -127,9 +131,12 @@ export const getHistogramBucketAgg = ({
127131
return childSearchSource
128132
.fetch(options)
129133
.then((resp: any) => {
134+
const min = resp.aggregations?.minAgg?.value ?? 0;
135+
const max = resp.aggregations?.maxAgg?.value ?? 0;
136+
130137
aggConfig.setAutoBounds({
131-
min: get(resp, 'aggregations.minAgg.value'),
132-
max: get(resp, 'aggregations.maxAgg.value'),
138+
min,
139+
max,
133140
});
134141
})
135142
.catch((e: Error) => {
@@ -143,46 +150,24 @@ export const getHistogramBucketAgg = ({
143150
});
144151
},
145152
write(aggConfig, output) {
146-
let interval = parseFloat(aggConfig.params.interval);
147-
if (interval <= 0) {
148-
interval = 1;
149-
}
150-
const autoBounds = aggConfig.getAutoBounds();
151-
152-
// ensure interval does not create too many buckets and crash browser
153-
if (autoBounds) {
154-
const range = autoBounds.max - autoBounds.min;
155-
const bars = range / interval;
156-
157-
if (bars > getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS)) {
158-
const minInterval = range / getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS);
159-
160-
// Round interval by order of magnitude to provide clean intervals
161-
// Always round interval up so there will always be less buckets than histogram:maxBars
162-
const orderOfMagnitude = Math.pow(10, Math.floor(Math.log10(minInterval)));
163-
let roundInterval = orderOfMagnitude;
164-
165-
while (roundInterval < minInterval) {
166-
roundInterval += orderOfMagnitude;
167-
}
168-
interval = roundInterval;
169-
}
170-
}
171-
const base = aggConfig.params.intervalBase;
172-
173-
if (base) {
174-
if (interval < base) {
175-
// In case the specified interval is below the base, just increase it to it's base
176-
interval = base;
177-
} else if (interval % base !== 0) {
178-
// In case the interval is not a multiple of the base round it to the next base
179-
interval = Math.round(interval / base) * base;
180-
}
181-
}
182-
183-
output.params.interval = interval;
153+
const values = aggConfig.getAutoBounds();
154+
155+
output.params.interval = calculateHistogramInterval({
156+
values,
157+
interval: aggConfig.params.interval,
158+
maxBucketsUiSettings: getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS),
159+
maxBucketsUserInput: aggConfig.params.maxBars,
160+
intervalBase: aggConfig.params.intervalBase,
161+
});
184162
},
185163
},
164+
{
165+
name: 'maxBars',
166+
shouldShow(agg) {
167+
return isAutoInterval(get(agg, 'params.interval'));
168+
},
169+
write: () => {},
170+
},
186171
{
187172
name: 'min_doc_count',
188173
default: false,

src/plugins/data/common/search/aggs/buckets/histogram_fn.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ describe('agg_expression_functions', () => {
4343
"interval": "10",
4444
"intervalBase": undefined,
4545
"json": undefined,
46+
"maxBars": undefined,
4647
"min_doc_count": undefined,
4748
},
4849
"schema": undefined,
@@ -55,8 +56,9 @@ describe('agg_expression_functions', () => {
5556
test('includes optional params when they are provided', () => {
5657
const actual = fn({
5758
field: 'field',
58-
interval: '10',
59+
interval: 'auto',
5960
intervalBase: 1,
61+
maxBars: 25,
6062
min_doc_count: false,
6163
has_extended_bounds: false,
6264
extended_bounds: JSON.stringify({
@@ -77,9 +79,10 @@ describe('agg_expression_functions', () => {
7779
},
7880
"field": "field",
7981
"has_extended_bounds": false,
80-
"interval": "10",
82+
"interval": "auto",
8183
"intervalBase": 1,
8284
"json": undefined,
85+
"maxBars": 25,
8386
"min_doc_count": false,
8487
},
8588
"schema": undefined,

src/plugins/data/common/search/aggs/buckets/histogram_fn.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ export const aggHistogram = (): FunctionDefinition => ({
8585
defaultMessage: 'Specifies whether to use min_doc_count for this aggregation',
8686
}),
8787
},
88+
maxBars: {
89+
types: ['number'],
90+
help: i18n.translate('data.search.aggs.buckets.histogram.maxBars.help', {
91+
defaultMessage: 'Calculate interval to get approximately this many bars',
92+
}),
93+
},
8894
has_extended_bounds: {
8995
types: ['boolean'],
9096
help: i18n.translate('data.search.aggs.buckets.histogram.hasExtendedBounds.help', {
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import {
21+
calculateHistogramInterval,
22+
CalculateHistogramIntervalParams,
23+
} from './histogram_calculate_interval';
24+
25+
describe('calculateHistogramInterval', () => {
26+
describe('auto calculating mode', () => {
27+
let params: CalculateHistogramIntervalParams;
28+
29+
beforeEach(() => {
30+
params = {
31+
interval: 'auto',
32+
intervalBase: undefined,
33+
maxBucketsUiSettings: 100,
34+
maxBucketsUserInput: undefined,
35+
values: {
36+
min: 0,
37+
max: 1,
38+
},
39+
};
40+
});
41+
42+
describe('maxBucketsUserInput is defined', () => {
43+
test('should not set interval which more than largest possible', () => {
44+
const p = {
45+
...params,
46+
maxBucketsUserInput: 200,
47+
values: {
48+
min: 150,
49+
max: 250,
50+
},
51+
};
52+
expect(calculateHistogramInterval(p)).toEqual(1);
53+
});
54+
55+
test('should correctly work for float numbers (small numbers)', () => {
56+
expect(
57+
calculateHistogramInterval({
58+
...params,
59+
maxBucketsUserInput: 50,
60+
values: {
61+
min: 0.1,
62+
max: 0.9,
63+
},
64+
})
65+
).toBe(0.02);
66+
});
67+
68+
test('should correctly work for float numbers (big numbers)', () => {
69+
expect(
70+
calculateHistogramInterval({
71+
...params,
72+
maxBucketsUserInput: 10,
73+
values: {
74+
min: 10.45,
75+
max: 1000.05,
76+
},
77+
})
78+
).toBe(100);
79+
});
80+
});
81+
82+
describe('maxBucketsUserInput is not defined', () => {
83+
test('should not set interval which more than largest possible', () => {
84+
expect(
85+
calculateHistogramInterval({
86+
...params,
87+
values: {
88+
min: 0,
89+
max: 100,
90+
},
91+
})
92+
).toEqual(1);
93+
});
94+
95+
test('should set intervals for integer numbers (diff less than maxBucketsUiSettings)', () => {
96+
expect(
97+
calculateHistogramInterval({
98+
...params,
99+
values: {
100+
min: 1,
101+
max: 10,
102+
},
103+
})
104+
).toEqual(0.1);
105+
});
106+
107+
test('should set intervals for integer numbers (diff more than maxBucketsUiSettings)', () => {
108+
// diff === 44445; interval === 500; buckets === 89
109+
expect(
110+
calculateHistogramInterval({
111+
...params,
112+
values: {
113+
min: 45678,
114+
max: 90123,
115+
},
116+
})
117+
).toEqual(500);
118+
});
119+
120+
test('should set intervals the same for the same interval', () => {
121+
// both diffs are the same
122+
// diff === 1.655; interval === 0.02; buckets === 82
123+
expect(
124+
calculateHistogramInterval({
125+
...params,
126+
values: {
127+
min: 1.245,
128+
max: 2.9,
129+
},
130+
})
131+
).toEqual(0.02);
132+
expect(
133+
calculateHistogramInterval({
134+
...params,
135+
values: {
136+
min: 0.5,
137+
max: 2.3,
138+
},
139+
})
140+
).toEqual(0.02);
141+
});
142+
});
143+
});
144+
});

0 commit comments

Comments
 (0)