diff --git a/src/plugins/data/common/search/aggs/buckets/_interval_options.ts b/src/plugins/data/common/search/aggs/buckets/_interval_options.ts index 00cf50c272fa0..f94484a6edc2e 100644 --- a/src/plugins/data/common/search/aggs/buckets/_interval_options.ts +++ b/src/plugins/data/common/search/aggs/buckets/_interval_options.ts @@ -20,12 +20,15 @@ import { i18n } from '@kbn/i18n'; import { IBucketAggConfig } from './bucket_agg_type'; +export const autoInterval = 'auto'; +export const isAutoInterval = (value: unknown) => value === autoInterval; + export const intervalOptions = [ { display: i18n.translate('data.search.aggs.buckets.intervalOptions.autoDisplayName', { defaultMessage: 'Auto', }), - val: 'auto', + val: autoInterval, enabled(agg: IBucketAggConfig) { // not only do we need a time field, but the selected field needs // to be the time field. (see #3028) diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.test.ts index 143d549836900..3d0224b213e8d 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.test.ts @@ -19,7 +19,7 @@ import moment from 'moment'; import { createFilterDateHistogram } from './date_histogram'; -import { intervalOptions } from '../_interval_options'; +import { intervalOptions, autoInterval } from '../_interval_options'; import { AggConfigs } from '../../agg_configs'; import { mockAggTypesRegistry } from '../../test_helpers'; import { IBucketDateHistogramAggConfig } from '../date_histogram'; @@ -33,7 +33,10 @@ describe('AggConfig Filters', () => { let bucketStart: any; let field: any; - const init = (interval: string = 'auto', duration: any = moment.duration(15, 'minutes')) => { + const init = ( + interval: string = autoInterval, + duration: any = moment.duration(15, 'minutes') + ) => { field = { name: 'date', }; diff --git a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts index fdf9c456b3876..c273ca53a5fed 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { KBN_FIELD_TYPES, TimeRange, TimeRangeBounds, UI_SETTINGS } from '../../../../common'; -import { intervalOptions } from './_interval_options'; +import { intervalOptions, autoInterval, isAutoInterval } from './_interval_options'; import { createFilterDateHistogram } from './create_filter/date_histogram'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; @@ -44,7 +44,7 @@ const updateTimeBuckets = ( customBuckets?: IBucketDateHistogramAggConfig['buckets'] ) => { const bounds = - agg.params.timeRange && (agg.fieldIsTimeField() || agg.params.interval === 'auto') + agg.params.timeRange && (agg.fieldIsTimeField() || isAutoInterval(agg.params.interval)) ? calculateBounds(agg.params.timeRange) : undefined; const buckets = customBuckets || agg.buckets; @@ -149,7 +149,7 @@ export const getDateHistogramBucketAgg = ({ return agg.getIndexPattern().timeFieldName; }, onChange(agg: IBucketDateHistogramAggConfig) { - if (get(agg, 'params.interval') === 'auto' && !agg.fieldIsTimeField()) { + if (isAutoInterval(get(agg, 'params.interval')) && !agg.fieldIsTimeField()) { delete agg.params.interval; } }, @@ -187,7 +187,7 @@ export const getDateHistogramBucketAgg = ({ } return state; }, - default: 'auto', + default: autoInterval, options: intervalOptions, write(agg, output, aggs) { updateTimeBuckets(agg, calculateBounds); 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 3727747984d3e..a8ac72c174c72 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts @@ -103,7 +103,28 @@ describe('Histogram Agg', () => { }); }); + describe('maxBars', () => { + test('should not be written to the DSL', () => { + const aggConfigs = getAggConfigs({ + maxBars: 50, + field: { + name: 'field', + }, + }); + const { [BUCKET_TYPES.HISTOGRAM]: params } = aggConfigs.aggs[0].toDsl(); + + expect(params).not.toHaveProperty('maxBars'); + }); + }); + describe('interval', () => { + test('accepts "auto" value', () => { + const params = getParams({ + interval: 'auto', + }); + + expect(params).toHaveProperty('interval', 1); + }); test('accepts a whole number', () => { const params = getParams({ interval: 100, diff --git a/src/plugins/data/common/search/aggs/buckets/histogram.ts b/src/plugins/data/common/search/aggs/buckets/histogram.ts index 2b263013e55a2..4b631e1fd7cd7 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.ts @@ -28,6 +28,8 @@ import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { createFilterHistogram } from './create_filter/histogram'; import { BUCKET_TYPES } from './bucket_agg_types'; import { ExtendedBounds } from './lib/extended_bounds'; +import { isAutoInterval, autoInterval } from './_interval_options'; +import { calculateHistogramInterval } from './lib/histogram_calculate_interval'; export interface AutoBounds { min: number; @@ -47,6 +49,7 @@ export interface IBucketHistogramAggConfig extends IBucketAggConfig { export interface AggParamsHistogram extends BaseAggParams { field: string; interval: string; + maxBars?: number; intervalBase?: number; min_doc_count?: boolean; has_extended_bounds?: boolean; @@ -102,6 +105,7 @@ export const getHistogramBucketAgg = ({ }, { name: 'interval', + default: autoInterval, modifyAggConfigOnSearchRequestStart( aggConfig: IBucketHistogramAggConfig, searchSource: any, @@ -127,9 +131,12 @@ export const getHistogramBucketAgg = ({ return childSearchSource .fetch(options) .then((resp: any) => { + const min = resp.aggregations?.minAgg?.value ?? 0; + const max = resp.aggregations?.maxAgg?.value ?? 0; + aggConfig.setAutoBounds({ - min: get(resp, 'aggregations.minAgg.value'), - max: get(resp, 'aggregations.maxAgg.value'), + min, + max, }); }) .catch((e: Error) => { @@ -143,46 +150,24 @@ export const getHistogramBucketAgg = ({ }); }, write(aggConfig, output) { - let interval = parseFloat(aggConfig.params.interval); - if (interval <= 0) { - interval = 1; - } - const autoBounds = aggConfig.getAutoBounds(); - - // ensure interval does not create too many buckets and crash browser - if (autoBounds) { - const range = autoBounds.max - autoBounds.min; - const bars = range / interval; - - if (bars > getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS)) { - const minInterval = range / getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS); - - // Round interval by order of magnitude to provide clean intervals - // Always round interval up so there will always be less buckets than histogram:maxBars - const orderOfMagnitude = Math.pow(10, Math.floor(Math.log10(minInterval))); - let roundInterval = orderOfMagnitude; - - while (roundInterval < minInterval) { - roundInterval += orderOfMagnitude; - } - interval = roundInterval; - } - } - const base = aggConfig.params.intervalBase; - - if (base) { - if (interval < base) { - // In case the specified interval is below the base, just increase it to it's base - interval = base; - } else if (interval % base !== 0) { - // In case the interval is not a multiple of the base round it to the next base - interval = Math.round(interval / base) * base; - } - } - - output.params.interval = interval; + 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, + }); }, }, + { + name: 'maxBars', + shouldShow(agg) { + return isAutoInterval(get(agg, 'params.interval')); + }, + write: () => {}, + }, { name: 'min_doc_count', default: false, diff --git a/src/plugins/data/common/search/aggs/buckets/histogram_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/histogram_fn.test.ts index 34b6fa1a6dcd6..354946f99a2f5 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram_fn.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram_fn.test.ts @@ -43,6 +43,7 @@ describe('agg_expression_functions', () => { "interval": "10", "intervalBase": undefined, "json": undefined, + "maxBars": undefined, "min_doc_count": undefined, }, "schema": undefined, @@ -55,8 +56,9 @@ describe('agg_expression_functions', () => { test('includes optional params when they are provided', () => { const actual = fn({ field: 'field', - interval: '10', + interval: 'auto', intervalBase: 1, + maxBars: 25, min_doc_count: false, has_extended_bounds: false, extended_bounds: JSON.stringify({ @@ -77,9 +79,10 @@ describe('agg_expression_functions', () => { }, "field": "field", "has_extended_bounds": false, - "interval": "10", + "interval": "auto", "intervalBase": 1, "json": undefined, + "maxBars": 25, "min_doc_count": false, }, "schema": undefined, diff --git a/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts b/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts index 877fd13e59f87..2e833bbe0a3eb 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts @@ -85,6 +85,12 @@ export const aggHistogram = (): FunctionDefinition => ({ defaultMessage: 'Specifies whether to use min_doc_count for this aggregation', }), }, + maxBars: { + types: ['number'], + help: i18n.translate('data.search.aggs.buckets.histogram.maxBars.help', { + defaultMessage: 'Calculate interval to get approximately this many bars', + }), + }, has_extended_bounds: { types: ['boolean'], help: i18n.translate('data.search.aggs.buckets.histogram.hasExtendedBounds.help', { diff --git a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts new file mode 100644 index 0000000000000..fd788d3339295 --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts @@ -0,0 +1,144 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + calculateHistogramInterval, + CalculateHistogramIntervalParams, +} from './histogram_calculate_interval'; + +describe('calculateHistogramInterval', () => { + describe('auto calculating mode', () => { + let params: CalculateHistogramIntervalParams; + + beforeEach(() => { + params = { + interval: 'auto', + intervalBase: undefined, + maxBucketsUiSettings: 100, + maxBucketsUserInput: undefined, + values: { + min: 0, + max: 1, + }, + }; + }); + + describe('maxBucketsUserInput is defined', () => { + test('should not set interval which more than largest possible', () => { + const p = { + ...params, + maxBucketsUserInput: 200, + values: { + min: 150, + max: 250, + }, + }; + expect(calculateHistogramInterval(p)).toEqual(1); + }); + + test('should correctly work for float numbers (small numbers)', () => { + expect( + calculateHistogramInterval({ + ...params, + maxBucketsUserInput: 50, + values: { + min: 0.1, + max: 0.9, + }, + }) + ).toBe(0.02); + }); + + test('should correctly work for float numbers (big numbers)', () => { + expect( + calculateHistogramInterval({ + ...params, + maxBucketsUserInput: 10, + values: { + min: 10.45, + max: 1000.05, + }, + }) + ).toBe(100); + }); + }); + + describe('maxBucketsUserInput is not defined', () => { + test('should not set interval which more than largest possible', () => { + expect( + calculateHistogramInterval({ + ...params, + values: { + min: 0, + max: 100, + }, + }) + ).toEqual(1); + }); + + test('should set intervals for integer numbers (diff less than maxBucketsUiSettings)', () => { + expect( + calculateHistogramInterval({ + ...params, + values: { + min: 1, + max: 10, + }, + }) + ).toEqual(0.1); + }); + + test('should set intervals for integer numbers (diff more than maxBucketsUiSettings)', () => { + // diff === 44445; interval === 500; buckets === 89 + expect( + calculateHistogramInterval({ + ...params, + values: { + min: 45678, + max: 90123, + }, + }) + ).toEqual(500); + }); + + test('should set intervals the same for the same interval', () => { + // both diffs are the same + // diff === 1.655; interval === 0.02; buckets === 82 + expect( + calculateHistogramInterval({ + ...params, + values: { + min: 1.245, + max: 2.9, + }, + }) + ).toEqual(0.02); + expect( + calculateHistogramInterval({ + ...params, + values: { + min: 0.5, + max: 2.3, + }, + }) + ).toEqual(0.02); + }); + }); + }); +}); diff --git a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts new file mode 100644 index 0000000000000..f4e42fa8881ef --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts @@ -0,0 +1,143 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { isAutoInterval } from '../_interval_options'; + +interface IntervalValuesRange { + min: number; + max: number; +} + +export interface CalculateHistogramIntervalParams { + interval: string; + maxBucketsUiSettings: number; + maxBucketsUserInput?: number; + intervalBase?: number; + values?: IntervalValuesRange; +} + +/** + * Round interval by order of magnitude to provide clean intervals + */ +const roundInterval = (minInterval: number) => { + const orderOfMagnitude = Math.pow(10, Math.floor(Math.log10(minInterval))); + let interval = orderOfMagnitude; + + while (interval < minInterval) { + interval += orderOfMagnitude; + } + + return interval; +}; + +const calculateForGivenInterval = ( + diff: number, + interval: number, + maxBucketsUiSettings: CalculateHistogramIntervalParams['maxBucketsUiSettings'] +) => { + const bars = diff / interval; + + if (bars > maxBucketsUiSettings) { + const minInterval = diff / maxBucketsUiSettings; + + return roundInterval(minInterval); + } + + return interval; +}; + +/** + * Algorithm for determining auto-interval + + 1. Define maxBars as Math.min(, ) + 2. Find the min and max values in the data + 3. Subtract the min from max to get diff + 4. Set exactInterval to diff / maxBars + 5. Based on exactInterval, find the power of 10 that's lower and higher + 6. Find the number of expected buckets that lowerPower would create: diff / lowerPower + 7. Find the number of expected buckets that higherPower would create: diff / higherPower + 8. There are three possible final intervals, pick the one that's closest to maxBars: + - The lower power of 10 + - The lower power of 10, times 2 + - The lower power of 10, times 5 + **/ +const calculateAutoInterval = ( + diff: number, + maxBucketsUiSettings: CalculateHistogramIntervalParams['maxBucketsUiSettings'], + maxBucketsUserInput: CalculateHistogramIntervalParams['maxBucketsUserInput'] +) => { + const maxBars = Math.min(maxBucketsUiSettings, maxBucketsUserInput ?? maxBucketsUiSettings); + const exactInterval = diff / maxBars; + + const lowerPower = Math.pow(10, Math.floor(Math.log10(exactInterval))); + + const autoBuckets = diff / lowerPower; + + if (autoBuckets > maxBars) { + if (autoBuckets / 2 <= maxBars) { + return lowerPower * 2; + } else if (autoBuckets / 5 <= maxBars) { + return lowerPower * 5; + } else { + return lowerPower * 10; + } + } + + return lowerPower; +}; + +export const calculateHistogramInterval = ({ + interval, + maxBucketsUiSettings, + maxBucketsUserInput, + intervalBase, + values, +}: CalculateHistogramIntervalParams) => { + const isAuto = isAutoInterval(interval); + let calculatedInterval = isAuto ? 0 : parseFloat(interval); + + // should return NaN on non-numeric or invalid values + if (Number.isNaN(calculatedInterval)) { + return calculatedInterval; + } + + if (values) { + const diff = values.max - values.min; + + if (diff) { + calculatedInterval = isAuto + ? calculateAutoInterval(diff, maxBucketsUiSettings, maxBucketsUserInput) + : calculateForGivenInterval(diff, calculatedInterval, maxBucketsUiSettings); + } + } + + if (intervalBase) { + if (calculatedInterval < intervalBase) { + // In case the specified interval is below the base, just increase it to it's base + calculatedInterval = intervalBase; + } else if (calculatedInterval % intervalBase !== 0) { + // In case the interval is not a multiple of the base round it to the next base + calculatedInterval = Math.round(calculatedInterval / intervalBase) * intervalBase; + } + } + + const defaultValueForUnspecifiedInterval = 1; + + return calculatedInterval || defaultValueForUnspecifiedInterval; +}; diff --git a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts index ae7630ecd3dac..04e64233ce196 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts @@ -20,6 +20,7 @@ import moment from 'moment'; import { TimeBuckets, TimeBucketsConfig } from './time_buckets'; +import { autoInterval } from '../../_interval_options'; describe('TimeBuckets', () => { const timeBucketConfig: TimeBucketsConfig = { @@ -103,7 +104,7 @@ describe('TimeBuckets', () => { test('setInterval/getInterval - intreval is a "auto"', () => { const timeBuckets = new TimeBuckets(timeBucketConfig); - timeBuckets.setInterval('auto'); + timeBuckets.setInterval(autoInterval); const interval = timeBuckets.getInterval(); expect(interval.description).toEqual('0 milliseconds'); diff --git a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts index 6402a6e83ead9..d054df0c9274e 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts @@ -28,6 +28,7 @@ import { convertIntervalToEsInterval, EsInterval, } from './calc_es_interval'; +import { autoInterval } from '../../_interval_options'; interface TimeBucketsInterval extends moment.Duration { // TODO double-check whether all of these are needed @@ -189,8 +190,8 @@ export class TimeBuckets { interval = input.val; } - if (!interval || interval === 'auto') { - this._i = 'auto'; + if (!interval || interval === autoInterval) { + this._i = autoInterval; return; } diff --git a/src/plugins/data/common/search/aggs/utils/calculate_auto_time_expression.ts b/src/plugins/data/common/search/aggs/utils/calculate_auto_time_expression.ts index 622e8101f34ab..3637ded44c50a 100644 --- a/src/plugins/data/common/search/aggs/utils/calculate_auto_time_expression.ts +++ b/src/plugins/data/common/search/aggs/utils/calculate_auto_time_expression.ts @@ -21,6 +21,7 @@ import { UI_SETTINGS } from '../../../../common/constants'; import { TimeRange } from '../../../../common/query'; import { TimeBuckets } from '../buckets/lib/time_buckets'; import { toAbsoluteDates } from './date_interval_utils'; +import { autoInterval } from '../buckets/_interval_options'; export function getCalculateAutoTimeExpression(getConfig: (key: string) => any) { return function calculateAutoTimeExpression(range: TimeRange) { @@ -36,7 +37,7 @@ export function getCalculateAutoTimeExpression(getConfig: (key: string) => any) 'dateFormat:scaled': getConfig('dateFormat:scaled'), }); - buckets.setInterval('auto'); + buckets.setInterval(autoInterval); buckets.setBounds({ min: moment(dates.from), max: moment(dates.to), diff --git a/src/plugins/vis_default_editor/public/components/agg_params_map.ts b/src/plugins/vis_default_editor/public/components/agg_params_map.ts index 9bc3146b9903b..e9019e479f92f 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_map.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_map.ts @@ -43,6 +43,7 @@ const buckets = { }, [BUCKET_TYPES.HISTOGRAM]: { interval: controls.NumberIntervalParamEditor, + maxBars: controls.MaxBarsParamEditor, min_doc_count: controls.MinDocCountParamEditor, has_extended_bounds: controls.HasExtendedBoundsParamEditor, extended_bounds: controls.ExtendedBoundsParamEditor, diff --git a/src/plugins/vis_default_editor/public/components/controls/index.ts b/src/plugins/vis_default_editor/public/components/controls/index.ts index cfb236e5e22e3..26e6609c7711d 100644 --- a/src/plugins/vis_default_editor/public/components/controls/index.ts +++ b/src/plugins/vis_default_editor/public/components/controls/index.ts @@ -52,3 +52,4 @@ export { TopSizeParamEditor } from './top_size'; export { TopSortFieldParamEditor } from './top_sort_field'; export { OrderParamEditor } from './order'; export { UseGeocentroidParamEditor } from './use_geocentroid'; +export { MaxBarsParamEditor } from './max_bars'; diff --git a/src/plugins/vis_default_editor/public/components/controls/max_bars.tsx b/src/plugins/vis_default_editor/public/components/controls/max_bars.tsx new file mode 100644 index 0000000000000..b0d517e0928df --- /dev/null +++ b/src/plugins/vis_default_editor/public/components/controls/max_bars.tsx @@ -0,0 +1,108 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useCallback, useEffect } from 'react'; +import { EuiFormRow, EuiFieldNumber, EuiFieldNumberProps, EuiIconTip } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../../kibana_react/public'; +import { AggParamEditorProps } from '../agg_param_props'; +import { UI_SETTINGS } from '../../../../data/public'; + +export interface SizeParamEditorProps extends AggParamEditorProps { + iconTip?: React.ReactNode; + disabled?: boolean; +} + +const autoPlaceholder = i18n.translate('visDefaultEditor.controls.maxBars.autoPlaceholder', { + defaultMessage: 'Auto', +}); + +const label = ( + <> + {' '} + + } + type="questionInCircle" + /> + +); + +function MaxBarsParamEditor({ + disabled, + iconTip, + value, + setValue, + showValidation, + setValidity, + setTouched, +}: SizeParamEditorProps) { + const { services } = useKibana(); + const uiSettingMaxBars = services.uiSettings?.get(UI_SETTINGS.HISTOGRAM_MAX_BARS); + const isValid = + disabled || + value === undefined || + value === '' || + Number(value) > 0 || + value < uiSettingMaxBars; + + useEffect(() => { + setValidity(isValid); + }, [isValid, setValidity]); + + const onChange: EuiFieldNumberProps['onChange'] = useCallback( + (ev) => setValue(ev.target.value === '' ? '' : parseFloat(ev.target.value)), + [setValue] + ); + + return ( + + + + ); +} + +export { MaxBarsParamEditor }; diff --git a/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx b/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx index f6354027ab01b..8cdc92581cefb 100644 --- a/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/number_interval.tsx @@ -20,7 +20,16 @@ import { get } from 'lodash'; import React, { useEffect, useCallback } from 'react'; -import { EuiFieldNumber, EuiFormRow, EuiIconTip } from '@elastic/eui'; +import { + EuiFieldNumber, + EuiFormRow, + EuiIconTip, + EuiSwitch, + EuiSwitchProps, + EuiFieldNumberProps, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { UI_SETTINGS } from '../../../../data/public'; @@ -47,6 +56,25 @@ const label = ( ); +const autoInterval = 'auto'; +const isAutoInterval = (value: unknown) => value === autoInterval; + +const selectIntervalPlaceholder = i18n.translate( + 'visDefaultEditor.controls.numberInterval.selectIntervalPlaceholder', + { + defaultMessage: 'Enter an interval', + } +); +const autoIntervalIsUsedPlaceholder = i18n.translate( + 'visDefaultEditor.controls.numberInterval.autoInteralIsUsed', + { + defaultMessage: 'Auto interval is used', + } +); +const useAutoIntervalLabel = i18n.translate('visDefaultEditor.controls.useAutoInterval', { + defaultMessage: 'Use auto interval', +}); + function NumberIntervalParamEditor({ agg, editorConfig, @@ -55,18 +83,28 @@ function NumberIntervalParamEditor({ setTouched, setValidity, setValue, -}: AggParamEditorProps) { +}: AggParamEditorProps) { + const isAutoChecked = isAutoInterval(value); const base: number = get(editorConfig, 'interval.base') as number; const min = base || 0; - const isValid = value !== undefined && value >= min; + const isValid = + value !== '' && value !== undefined && (isAutoInterval(value) || Number(value) >= min); useEffect(() => { setValidity(isValid); }, [isValid, setValidity]); - const onChange = useCallback( - ({ target }: React.ChangeEvent) => - setValue(isNaN(target.valueAsNumber) ? undefined : target.valueAsNumber), + const onChange: EuiFieldNumberProps['onChange'] = useCallback( + ({ target }) => setValue(isNaN(target.valueAsNumber) ? '' : target.valueAsNumber), + [setValue] + ); + + const onAutoSwitchChange: EuiSwitchProps['onChange'] = useCallback( + (e) => { + const isAutoSwitchChecked = e.target.checked; + + setValue(isAutoSwitchChecked ? autoInterval : ''); + }, [setValue] ); @@ -78,23 +116,32 @@ function NumberIntervalParamEditor({ isInvalid={showValidation && !isValid} helpText={get(editorConfig, 'interval.help')} > - + + + + + + + + ); } diff --git a/test/functional/page_objects/visualize_editor_page.ts b/test/functional/page_objects/visualize_editor_page.ts index 9fcb38efce0db..8cac43d97317b 100644 --- a/test/functional/page_objects/visualize_editor_page.ts +++ b/test/functional/page_objects/visualize_editor_page.ts @@ -445,6 +445,15 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP } else if (type === 'custom') { await comboBox.setCustom('visEditorInterval', newValue); } else { + if (type === 'numeric') { + const autoMode = await testSubjects.getAttribute( + `visEditorIntervalSwitch${aggNth}`, + 'aria-checked' + ); + if (autoMode === 'true') { + await testSubjects.click(`visEditorIntervalSwitch${aggNth}`); + } + } if (append) { await testSubjects.append(`visEditorInterval${aggNth}`, String(newValue)); } else {