[Lens] Pass used histogram interval to chart (#91370)

This commit is contained in:
Joe Reuter 2021-02-19 11:30:40 +01:00 committed by GitHub
parent 494d1decf5
commit bf7fdfc87c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 308 additions and 38 deletions

View file

@ -46,6 +46,7 @@ search: {
boundLabel: string;
intervalLabel: string;
})[];
getNumberHistogramIntervalByDatatableColumn: (column: import("../../expressions").DatatableColumn) => number | undefined;
};
getRequestInspectorStats: typeof getRequestInspectorStats;
getResponseInspectorStats: typeof getResponseInspectorStats;

View file

@ -12,6 +12,7 @@ import { AggTypesDependencies } from '../agg_types';
import { BUCKET_TYPES } from './bucket_agg_types';
import { IBucketHistogramAggConfig, getHistogramBucketAgg, AutoBounds } from './histogram';
import { BucketAggType } from './bucket_agg_type';
import { SerializableState } from 'src/plugins/expressions/common';
describe('Histogram Agg', () => {
let aggTypesDependencies: AggTypesDependencies;
@ -230,6 +231,27 @@ describe('Histogram Agg', () => {
expect(params.interval).toBeNaN();
});
test('will serialize the auto interval along with the actually chosen interval and deserialize correctly', () => {
const aggConfigs = getAggConfigs({
interval: 'auto',
field: {
name: 'field',
},
});
(aggConfigs.aggs[0] as IBucketHistogramAggConfig).setAutoBounds({ min: 0, max: 1000 });
const serializedAgg = aggConfigs.aggs[0].serialize();
const serializedIntervalParam = (serializedAgg.params as SerializableState).used_interval;
expect(serializedIntervalParam).toBe(500);
const freshHistogramAggConfig = getAggConfigs({
interval: 100,
field: {
name: 'field',
},
}).aggs[0];
freshHistogramAggConfig.setParams(serializedAgg.params);
expect(freshHistogramAggConfig.getParam('interval')).toEqual('auto');
});
describe('interval scaling', () => {
const getInterval = (
maxBars: number,

View file

@ -8,6 +8,7 @@
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { IUiSettingsClient } from 'kibana/public';
import { KBN_FIELD_TYPES, UI_SETTINGS } from '../../../../common';
import { AggTypesDependencies } from '../agg_types';
@ -39,6 +40,7 @@ export interface IBucketHistogramAggConfig extends IBucketAggConfig {
export interface AggParamsHistogram extends BaseAggParams {
field: string;
interval: number | string;
used_interval?: number | string;
maxBars?: number;
intervalBase?: number;
min_doc_count?: boolean;
@ -141,18 +143,23 @@ export const getHistogramBucketAgg = ({
});
},
write(aggConfig, output) {
const values = aggConfig.getAutoBounds();
output.params.interval = calculateHistogramInterval({
values,
interval: aggConfig.params.interval,
maxBucketsUiSettings: getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS),
maxBucketsUserInput: aggConfig.params.maxBars,
intervalBase: aggConfig.params.intervalBase,
esTypes: aggConfig.params.field?.spec?.esTypes || [],
});
output.params.interval = calculateInterval(aggConfig, getConfig);
},
},
{
name: 'used_interval',
default: autoInterval,
shouldShow() {
return false;
},
write: () => {},
serialize(val, aggConfig) {
if (!aggConfig) return undefined;
// store actually used auto interval in serialized agg config to be able to read it from the result data table meta information
return calculateInterval(aggConfig, getConfig);
},
toExpressionAst: () => undefined,
},
{
name: 'maxBars',
shouldShow(agg) {
@ -193,3 +200,18 @@ export const getHistogramBucketAgg = ({
},
],
});
function calculateInterval(
aggConfig: IBucketHistogramAggConfig,
getConfig: IUiSettingsClient['get']
): any {
const values = aggConfig.getAutoBounds();
return calculateHistogramInterval({
values,
interval: aggConfig.params.interval,
maxBucketsUiSettings: getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS),
maxBucketsUserInput: aggConfig.params.maxBars,
intervalBase: aggConfig.params.intervalBase,
esTypes: aggConfig.params.field?.spec?.esTypes || [],
});
}

View file

@ -0,0 +1,98 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getNumberHistogramIntervalByDatatableColumn } from '.';
import { BUCKET_TYPES } from '../buckets';
describe('getNumberHistogramIntervalByDatatableColumn', () => {
it('returns nothing on column from other data source', () => {
expect(
getNumberHistogramIntervalByDatatableColumn({
id: 'test',
name: 'test',
meta: {
type: 'date',
source: 'essql',
},
})
).toEqual(undefined);
});
it('returns nothing on non histogram column', () => {
expect(
getNumberHistogramIntervalByDatatableColumn({
id: 'test',
name: 'test',
meta: {
type: 'date',
source: 'esaggs',
sourceParams: {
type: BUCKET_TYPES.TERMS,
},
},
})
).toEqual(undefined);
});
it('returns interval on resolved auto interval', () => {
expect(
getNumberHistogramIntervalByDatatableColumn({
id: 'test',
name: 'test',
meta: {
type: 'date',
source: 'esaggs',
sourceParams: {
type: BUCKET_TYPES.HISTOGRAM,
params: {
interval: 'auto',
used_interval: 20,
},
},
},
})
).toEqual(20);
});
it('returns interval on fixed interval', () => {
expect(
getNumberHistogramIntervalByDatatableColumn({
id: 'test',
name: 'test',
meta: {
type: 'date',
source: 'esaggs',
sourceParams: {
type: BUCKET_TYPES.HISTOGRAM,
params: {
interval: 7,
used_interval: 7,
},
},
},
})
).toEqual(7);
});
it('returns undefined if information is not available', () => {
expect(
getNumberHistogramIntervalByDatatableColumn({
id: 'test',
name: 'test',
meta: {
type: 'date',
source: 'esaggs',
sourceParams: {
type: BUCKET_TYPES.HISTOGRAM,
params: {},
},
},
})
).toEqual(undefined);
});
});

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { DatatableColumn } from 'src/plugins/expressions/common';
import type { AggParamsHistogram } from '../buckets';
import { BUCKET_TYPES } from '../buckets/bucket_agg_types';
/**
* Helper function returning the used interval for data table column created by the histogramm agg type.
* "auto" will get expanded to the actually used interval.
* If the column is not a column created by a histogram aggregation of the esaggs data source,
* this function will return undefined.
*/
export const getNumberHistogramIntervalByDatatableColumn = (column: DatatableColumn) => {
if (column.meta.source !== 'esaggs') return;
if (column.meta.sourceParams?.type !== BUCKET_TYPES.HISTOGRAM) return;
const params = (column.meta.sourceParams.params as unknown) as AggParamsHistogram;
if (!params.used_interval || typeof params.used_interval === 'string') {
return undefined;
}
return params.used_interval;
};

View file

@ -7,6 +7,7 @@
*/
export * from './calculate_auto_time_expression';
export { getNumberHistogramIntervalByDatatableColumn } from './get_number_histogram_interval';
export * from './date_interval_utils';
export * from './get_format_with_aggs';
export * from './ipv4_address';

View file

@ -308,6 +308,7 @@ import {
parseInterval,
toAbsoluteDates,
boundsDescendingRaw,
getNumberHistogramIntervalByDatatableColumn,
// expressions utils
getRequestInspectorStats,
getResponseInspectorStats,
@ -417,6 +418,7 @@ export const search = {
termsAggFilter,
toAbsoluteDates,
boundsDescendingRaw,
getNumberHistogramIntervalByDatatableColumn,
},
getRequestInspectorStats,
getResponseInspectorStats,

View file

@ -2238,6 +2238,7 @@ export const search: {
boundLabel: string;
intervalLabel: string;
})[];
getNumberHistogramIntervalByDatatableColumn: (column: import("../../expressions").DatatableColumn) => number | undefined;
};
getRequestInspectorStats: typeof getRequestInspectorStats;
getResponseInspectorStats: typeof getResponseInspectorStats;
@ -2649,21 +2650,21 @@ export const UI_SETTINGS: {
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:425:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:414:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/search/session/session_service.ts:42:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts

View file

@ -26,6 +26,13 @@ exports[`xy_expression XYChart component it renders area 1`] = `
"headerFormatter": [Function],
}
}
xDomain={
Object {
"max": undefined,
"min": undefined,
"minInterval": 50,
}
}
/>
<Connect(SpecInstance)
gridLine={
@ -222,6 +229,13 @@ exports[`xy_expression XYChart component it renders bar 1`] = `
"headerFormatter": [Function],
}
}
xDomain={
Object {
"max": undefined,
"min": undefined,
"minInterval": 50,
}
}
/>
<Connect(SpecInstance)
gridLine={
@ -426,6 +440,13 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = `
"headerFormatter": [Function],
}
}
xDomain={
Object {
"max": undefined,
"min": undefined,
"minInterval": 50,
}
}
/>
<Connect(SpecInstance)
gridLine={
@ -630,6 +651,13 @@ exports[`xy_expression XYChart component it renders line 1`] = `
"headerFormatter": [Function],
}
}
xDomain={
Object {
"max": undefined,
"min": undefined,
"minInterval": 50,
}
}
/>
<Connect(SpecInstance)
gridLine={
@ -826,6 +854,13 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = `
"headerFormatter": [Function],
}
}
xDomain={
Object {
"max": undefined,
"min": undefined,
"minInterval": 50,
}
}
/>
<Connect(SpecInstance)
gridLine={
@ -1030,6 +1065,13 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = `
"headerFormatter": [Function],
}
}
xDomain={
Object {
"max": undefined,
"min": undefined,
"minInterval": 50,
}
}
/>
<Connect(SpecInstance)
gridLine={
@ -1242,6 +1284,13 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] =
"headerFormatter": [Function],
}
}
xDomain={
Object {
"max": undefined,
"min": undefined,
"minInterval": 50,
}
}
/>
<Connect(SpecInstance)
gridLine={

View file

@ -570,7 +570,30 @@ describe('xy_expression', () => {
}}
/>
);
expect(component.find(Settings).prop('xDomain')).toBeUndefined();
const xDomain = component.find(Settings).prop('xDomain');
expect(xDomain).toEqual(
expect.objectContaining({
min: undefined,
max: undefined,
})
);
});
test('it uses min interval if passed in', () => {
const { data, args } = sampleArgs();
const component = shallow(
<XYChart
{...defaultProps}
minInterval={101}
data={data}
args={{
...args,
layers: [{ ...args.layers[0], seriesType: 'line', xScaleType: 'linear' }],
}}
/>
);
expect(component.find(Settings).prop('xDomain')).toEqual({ minInterval: 101 });
});
test('it renders bar', () => {
@ -1881,6 +1904,24 @@ describe('xy_expression', () => {
expect(result).toEqual(5 * 60 * 1000);
});
it('should return interval of number histogram if available on first x axis columns', async () => {
xyProps.args.layers[0].xScaleType = 'linear';
xyProps.data.tables.first.columns[2].meta = {
source: 'esaggs',
type: 'number',
field: 'someField',
sourceParams: {
type: 'histogram',
params: {
interval: 'auto',
used_interval: 5,
},
},
};
const result = await calculateMinInterval(xyProps, jest.fn().mockResolvedValue(undefined));
expect(result).toEqual(5);
});
it('should return undefined if data table is empty', async () => {
xyProps.data.tables.first.rows = [];
const result = await calculateMinInterval(

View file

@ -199,13 +199,20 @@ export async function calculateMinInterval(
const filteredLayers = getFilteredLayers(layers, data);
if (filteredLayers.length === 0) return;
const isTimeViz = data.dateRange && filteredLayers.every((l) => l.xScaleType === 'time');
if (!isTimeViz) return;
const dateColumn = data.tables[filteredLayers[0].layerId].columns.find(
const xColumn = data.tables[filteredLayers[0].layerId].columns.find(
(column) => column.id === filteredLayers[0].xAccessor
);
if (!dateColumn) return;
const dateMetaData = await getIntervalByColumn(dateColumn);
if (!xColumn) return;
if (!isTimeViz) {
const histogramInterval = search.aggs.getNumberHistogramIntervalByDatatableColumn(xColumn);
if (typeof histogramInterval === 'number') {
return histogramInterval;
} else {
return undefined;
}
}
const dateMetaData = await getIntervalByColumn(xColumn);
if (!dateMetaData) return;
const intervalDuration = search.aggs.parseInterval(dateMetaData.interval);
if (!intervalDuration) return;
@ -381,13 +388,11 @@ export function XYChart({
const isTimeViz = data.dateRange && filteredLayers.every((l) => l.xScaleType === 'time');
const isHistogramViz = filteredLayers.every((l) => l.isHistogram);
const xDomain = isTimeViz
? {
min: data.dateRange?.fromDate.getTime(),
max: data.dateRange?.toDate.getTime(),
minInterval,
}
: undefined;
const xDomain = {
min: isTimeViz ? data.dateRange?.fromDate.getTime() : undefined,
max: isTimeViz ? data.dateRange?.toDate.getTime() : undefined,
minInterval,
};
const getYAxesTitles = (
axisSeries: Array<{ layer: string; accessor: string }>,