From bf7fdfc87cb04ecb5f6081d9056102d707997e9b Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 19 Feb 2021 11:30:40 +0100 Subject: [PATCH] [Lens] Pass used histogram interval to chart (#91370) --- ...ibana-plugin-plugins-data-public.search.md | 1 + .../search/aggs/buckets/histogram.test.ts | 22 +++++ .../common/search/aggs/buckets/histogram.ts | 42 ++++++-- .../get_number_histogram_interval.test.ts | 98 +++++++++++++++++++ .../utils/get_number_histogram_interval.ts | 28 ++++++ .../data/common/search/aggs/utils/index.ts | 1 + src/plugins/data/public/index.ts | 2 + src/plugins/data/public/public.api.md | 31 +++--- .../__snapshots__/expression.test.tsx.snap | 49 ++++++++++ .../xy_visualization/expression.test.tsx | 43 +++++++- .../public/xy_visualization/expression.tsx | 29 +++--- 11 files changed, 308 insertions(+), 38 deletions(-) create mode 100644 src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.test.ts create mode 100644 src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md index 4b3c915b49c2dc..440fd25993d643 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md @@ -46,6 +46,7 @@ search: { boundLabel: string; intervalLabel: string; })[]; + getNumberHistogramIntervalByDatatableColumn: (column: import("../../expressions").DatatableColumn) => number | undefined; }; getRequestInspectorStats: typeof getRequestInspectorStats; getResponseInspectorStats: typeof getResponseInspectorStats; diff --git a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts index bddc7060af4401..23693eaf5fca52 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts @@ -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, diff --git a/src/plugins/data/common/search/aggs/buckets/histogram.ts b/src/plugins/data/common/search/aggs/buckets/histogram.ts index 5d6d7d509f08e8..e04ebfe494ba91 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.ts @@ -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,17 +143,22 @@ 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', @@ -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 || [], + }); +} diff --git a/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.test.ts b/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.test.ts new file mode 100644 index 00000000000000..9b08426b551aa3 --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.test.ts @@ -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); + }); +}); diff --git a/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.ts b/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.ts new file mode 100644 index 00000000000000..e1c0cf2d69c60c --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.ts @@ -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; +}; diff --git a/src/plugins/data/common/search/aggs/utils/index.ts b/src/plugins/data/common/search/aggs/utils/index.ts index 40451a0f66e0c9..f90e8f88546f46 100644 --- a/src/plugins/data/common/search/aggs/utils/index.ts +++ b/src/plugins/data/common/search/aggs/utils/index.ts @@ -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'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index df799ede08a310..00bf0385487d81 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -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, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 67423295dfe5e3..36e34479ad2d1f 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -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 diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap index a2047b7bae669b..9a32f1c3311522 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap @@ -26,6 +26,13 @@ exports[`xy_expression XYChart component it renders area 1`] = ` "headerFormatter": [Function], } } + xDomain={ + Object { + "max": undefined, + "min": undefined, + "minInterval": 50, + } + } /> { }} /> ); - 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( + + ); + 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( diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 7f6414b40cb90e..eda08715b394e9 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -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 }>,