Add support for HDR percentiles in TSVB visualizations (#78306)

* Add support for HDR percentiles in TSVB visualizations

Closes: #64238

* remove extra console.log

* fix CI

* fix PR comments

* fix layout

* remove legacy injectI18n

* fix localization issues

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Alexey Antonov 2020-10-07 19:41:37 +03:00 committed by GitHub
parent a8c080be28
commit 59d83e6955
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 254 additions and 156 deletions

View file

@ -104,6 +104,7 @@ export const metricsItems = schema.object({
})
)
),
numberOfSignificantValueDigits: numberOptional,
percentiles: schema.maybe(
schema.arrayOf(
schema.object({

View file

@ -24,10 +24,11 @@ import { FieldSelect } from './field_select';
import { AggRow } from './agg_row';
import { createChangeHandler } from '../lib/create_change_handler';
import { createSelectHandler } from '../lib/create_select_handler';
import { createNumberHandler } from '../lib/create_number_handler';
import {
htmlIdGenerator,
EuiSpacer,
EuiFlexGroup,
EuiFlexGrid,
EuiFlexItem,
EuiFormLabel,
EuiFormRow,
@ -35,6 +36,7 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public';
import { Percentiles, newPercentile } from './percentile_ui';
import { PercentileHdr } from './percentile_hdr';
const RESTRICT_FIELDS = [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM];
@ -46,6 +48,8 @@ export function PercentileAgg(props) {
const handleChange = createChangeHandler(props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
const handleNumberChange = createNumberHandler(handleChange);
const indexPattern =
(series.override_index_pattern && series.series_index_pattern) || panel.index_pattern;
@ -66,7 +70,7 @@ export function PercentileAgg(props) {
siblings={props.siblings}
dragHandleProps={props.dragHandleProps}
>
<EuiFlexGroup gutterSize="s">
<EuiFlexGrid gutterSize="s" columns={2}>
<EuiFlexItem>
<EuiFormLabel htmlFor={htmlId('aggregation')}>
<FormattedMessage
@ -103,11 +107,25 @@ export function PercentileAgg(props) {
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<Percentiles onChange={handleChange} name="percentiles" model={model} panel={panel} />
<EuiFlexItem>
<EuiFormRow
label={
<FormattedMessage
id="visTypeTimeseries.percentile.percents"
defaultMessage="Percents"
/>
}
>
<Percentiles onChange={handleChange} name="percentiles" model={model} panel={panel} />
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<PercentileHdr
value={model.numberOfSignificantValueDigits}
onChange={handleNumberChange('numberOfSignificantValueDigits')}
/>
</EuiFlexItem>
</EuiFlexGrid>
</AggRow>
);
}

View file

@ -0,0 +1,52 @@
/*
* 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 from 'react';
import { EuiFieldNumber, EuiFormRow, EuiIconTip } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
export interface PercentileHdrProps {
value: number | undefined;
onChange: () => void;
}
export const PercentileHdr = ({ value, onChange }: PercentileHdrProps) => (
<EuiFormRow
label={
<>
<FormattedMessage
id="visTypeTimeseries.percentileHdr.numberOfSignificantValueDigits"
defaultMessage="Number of significant value digits (HDR histogram)"
/>{' '}
<EuiIconTip
position="right"
content={
<FormattedMessage
id="visTypeTimeseries.percentileHdr.numberOfSignificantValueDigits.hint"
defaultMessage="HDR Histogram (High Dynamic Range Histogram) is an alternative implementation that can be useful when calculating percentile ranks for latency measurements as it can be faster than the t-digest implementation with the trade-off of a larger memory footprint. Number of significant value digits parameter specifies the resolution of values for the histogram in number of significant digits"
/>
}
type="questionInCircle"
/>
</>
}
>
<EuiFieldNumber min={1} value={value || ''} onChange={onChange} />
</EuiFormRow>
);

View file

@ -18,15 +18,8 @@
*/
import React, { ChangeEvent } from 'react';
import { get } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
import {
htmlIdGenerator,
EuiFieldNumber,
EuiFlexGroup,
EuiFlexItem,
EuiFormLabel,
EuiSpacer,
} from '@elastic/eui';
import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { AddDeleteButtons } from '../../add_delete_buttons';
@ -50,8 +43,6 @@ export const MultiValueRow = ({
disableAdd,
disableDelete,
}: MultiValueRowProps) => {
const htmlId = htmlIdGenerator();
const onFieldNumberChange = (event: ChangeEvent<HTMLInputElement>) =>
onChange({
...model,
@ -59,17 +50,9 @@ export const MultiValueRow = ({
});
return (
<div className="tvbAggRow__multiValueRow">
<EuiFlexGroup responsive={false} alignItems="center">
<EuiFlexItem grow={false}>
<EuiFormLabel htmlFor={htmlId('value')}>
<FormattedMessage
id="visTypeTimeseries.multivalueRow.valueLabel"
defaultMessage="Value:"
/>
</EuiFormLabel>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPanel paddingSize="s" className="tvbAggRow__multiValueRow">
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem>
<EuiFieldNumber
value={model.value === '' ? '' : Number(model.value)}
placeholder="0"
@ -82,11 +65,11 @@ export const MultiValueRow = ({
onDelete={() => onDelete(model)}
disableDelete={disableDelete}
disableAdd={disableAdd}
responsive={false}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
</div>
</EuiPanel>
);
};

View file

@ -20,11 +20,11 @@
import React from 'react';
import {
htmlIdGenerator,
EuiFlexGroup,
EuiFlexItem,
EuiFormLabel,
EuiFormRow,
EuiSpacer,
EuiFlexGrid,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { AggSelect } from '../agg_select';
@ -34,12 +34,16 @@ import { FieldSelect } from '../field_select';
import { createChangeHandler } from '../../lib/create_change_handler';
// @ts-ignore
import { createSelectHandler } from '../../lib/create_select_handler';
// @ts-ignore
import { createNumberHandler } from '../../lib/create_number_handler';
import { AggRow } from '../agg_row';
import { PercentileRankValues } from './percentile_rank_values';
import { IFieldType, KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public';
import { MetricsItemsSchema, PanelSchema, SeriesItemsSchema } from '../../../../../common/types';
import { DragHandleProps } from '../../../../types';
import { PercentileHdr } from '../percentile_hdr';
const RESTRICT_FIELDS = [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM];
@ -67,6 +71,7 @@ export const PercentileRankAgg = (props: PercentileRankAggProps) => {
const isTablePanel = panel.type === 'table';
const handleChange = createChangeHandler(props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
const handleNumberChange = createNumberHandler(handleChange);
const handlePercentileRankValuesChange = (values: MetricsItemsSchema['values']) => {
handleChange({
@ -84,7 +89,7 @@ export const PercentileRankAgg = (props: PercentileRankAggProps) => {
siblings={props.siblings}
dragHandleProps={props.dragHandleProps}
>
<EuiFlexGroup gutterSize="s">
<EuiFlexGrid gutterSize="s" columns={2}>
<EuiFlexItem>
<EuiFormLabel htmlFor={htmlId('aggregation')}>
<FormattedMessage
@ -121,17 +126,32 @@ export const PercentileRankAgg = (props: PercentileRankAggProps) => {
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
{model.values && (
<PercentileRankValues
disableAdd={isTablePanel}
disableDelete={isTablePanel}
showOnlyLastRow={isTablePanel}
model={model.values}
onChange={handlePercentileRankValuesChange}
/>
)}
<EuiFlexItem>
<EuiFormRow
label={
<FormattedMessage
id="visTypeTimeseries.percentileRank.values"
defaultMessage="Values"
/>
}
>
<PercentileRankValues
disableAdd={isTablePanel}
disableDelete={isTablePanel}
showOnlyLastRow={isTablePanel}
model={model.values!}
onChange={handlePercentileRankValuesChange}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<PercentileHdr
value={model.numberOfSignificantValueDigits}
onChange={handleNumberChange('numberOfSignificantValueDigits')}
/>
</EuiFlexItem>
</EuiFlexGrid>
</AggRow>
);
};

View file

@ -19,7 +19,7 @@
import React from 'react';
import { last } from 'lodash';
import { EuiFlexGroup } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { MultiValueRow } from './multi_value_row';
interface PercentileRankValuesProps {
@ -52,19 +52,20 @@ export const PercentileRankValues = (props: PercentileRankValuesProps) => {
disableDeleteRow: boolean;
disableAddRow: boolean;
}) => (
<MultiValueRow
key={`percentileRankValue__item${rowModel.id}`}
onAdd={onAddValue}
onChange={onChangeValue}
onDelete={onDeleteValue}
disableDelete={disableDeleteRow}
disableAdd={disableAddRow}
model={rowModel}
/>
<EuiFlexItem key={`percentileRankValue__item${rowModel.id}`}>
<MultiValueRow
onAdd={onAddValue}
onChange={onChangeValue}
onDelete={onDeleteValue}
disableDelete={disableDeleteRow}
disableAdd={disableAddRow}
model={rowModel}
/>
</EuiFlexItem>
);
return (
<EuiFlexGroup direction="column" responsive={false} gutterSize="xs">
<EuiFlexGroup direction="column" gutterSize="s">
{showOnlyLastRow &&
renderRow({
rowModel: {

View file

@ -19,6 +19,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import { collectionActions } from '../lib/collection_actions';
import { AddDeleteButtons } from '../add_delete_buttons';
@ -27,17 +28,19 @@ import {
htmlIdGenerator,
EuiFlexGroup,
EuiFlexItem,
EuiFormLabel,
EuiComboBox,
EuiFieldNumber,
EuiFormRow,
EuiFlexGrid,
EuiPanel,
} from '@elastic/eui';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import { FormattedMessage } from '@kbn/i18n/react';
export const newPercentile = (opts) => {
return _.assign({ id: uuid.v1(), mode: 'line', shade: 0.2 }, opts);
};
class PercentilesUi extends Component {
export class Percentiles extends Component {
handleTextChange(item, name) {
return (e) => {
const handleChange = collectionActions.handleChange.bind(null, this.props);
@ -50,22 +53,31 @@ class PercentilesUi extends Component {
renderRow = (row, i, items) => {
const defaults = { value: '', percentile: '', shade: '' };
const model = { ...defaults, ...row };
const { intl, panel } = this.props;
const { panel } = this.props;
const flexItemStyle = { minWidth: 100 };
const percentileFieldNumber = (
<EuiFlexItem grow={false}>
<EuiFieldNumber
aria-label={intl.formatMessage({
id: 'visTypeTimeseries.percentile.percentileAriaLabel',
defaultMessage: 'Percentile',
})}
placeholder={0}
max={100}
min={0}
step={1}
onChange={this.handleTextChange(model, 'value')}
value={model.value === '' ? '' : Number(model.value)}
/>
<EuiFlexItem style={flexItemStyle}>
<EuiFormRow
label={
<FormattedMessage
id="visTypeTimeseries.percentile.percentile"
defaultMessage="Percentile"
/>
}
>
<EuiFieldNumber
aria-label={i18n.translate('visTypeTimeseries.percentile.percentileAriaLabel', {
defaultMessage: 'Percentile',
})}
placeholder={0}
max={100}
min={0}
step={1}
onChange={this.handleTextChange(model, 'value')}
value={model.value === '' ? '' : Number(model.value)}
/>
</EuiFormRow>
</EuiFlexItem>
);
@ -77,99 +89,103 @@ class PercentilesUi extends Component {
const handleDelete = collectionActions.handleDelete.bind(null, this.props, model);
const modeOptions = [
{
label: intl.formatMessage({
id: 'visTypeTimeseries.percentile.modeOptions.lineLabel',
label: i18n.translate('visTypeTimeseries.percentile.modeOptions.lineLabel', {
defaultMessage: 'Line',
}),
value: 'line',
},
{
label: intl.formatMessage({
id: 'visTypeTimeseries.percentile.modeOptions.bandLabel',
label: i18n.translate('visTypeTimeseries.percentile.modeOptions.bandLabel', {
defaultMessage: 'Band',
}),
value: 'band',
},
];
const optionsStyle = {};
const optionsStyle = {
...flexItemStyle,
};
if (model.mode === 'line') {
optionsStyle.display = 'none';
}
const labelStyle = { marginBottom: 0 };
const htmlId = htmlIdGenerator(model.id);
const selectedModeOption = modeOptions.find((option) => {
return model.mode === option.value;
});
return (
<EuiFlexItem key={model.id}>
<EuiFlexGroup alignItems="center" responsive={false} gutterSize="s">
{percentileFieldNumber}
<EuiFlexItem grow={false}>
<EuiFormLabel style={labelStyle} htmlFor={htmlId('mode')}>
<FormattedMessage
id="visTypeTimeseries.percentile.modeLabel"
defaultMessage="Mode:"
<EuiPanel>
<EuiFlexGroup key={model.id} alignItems="center">
<EuiFlexItem>
<EuiFlexGrid columns={2}>
{percentileFieldNumber}
<EuiFlexItem style={flexItemStyle}>
<EuiFormRow
label={
<FormattedMessage
id="visTypeTimeseries.percentile.modeLabel"
defaultMessage="Mode:"
/>
}
>
<EuiComboBox
isClearable={false}
id={htmlId('mode')}
options={modeOptions}
selectedOptions={selectedModeOption ? [selectedModeOption] : []}
onChange={this.handleTextChange(model, 'mode')}
singleSelection={{ asPlainText: true }}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem style={optionsStyle}>
<EuiFormRow
label={
<FormattedMessage
id="visTypeTimeseries.percentile.fillToLabel"
defaultMessage="Fill to:"
/>
}
>
<EuiFieldNumber
id={htmlId('fillTo')}
min={0}
max={100}
step={1}
onChange={this.handleTextChange(model, 'percentile')}
value={Number(model.percentile)}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem style={optionsStyle}>
<EuiFormRow
label={
<FormattedMessage
id="visTypeTimeseries.percentile.shadeLabel"
defaultMessage="Shade (0 to 1):"
/>
}
>
<EuiFieldNumber
id={htmlId('shade')}
step={0.1}
onChange={this.handleTextChange(model, 'shade')}
value={Number(model.shade)}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGrid>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<AddDeleteButtons
onAdd={handleAdd}
onDelete={handleDelete}
disableDelete={items.length < 2}
responsive={false}
/>
</EuiFormLabel>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiComboBox
isClearable={false}
id={htmlId('mode')}
options={modeOptions}
selectedOptions={selectedModeOption ? [selectedModeOption] : []}
onChange={this.handleTextChange(model, 'mode')}
singleSelection={{ asPlainText: true }}
/>
</EuiFlexItem>
<EuiFlexItem style={optionsStyle} grow={false}>
<EuiFormLabel style={labelStyle} htmlFor={htmlId('fillTo')}>
<FormattedMessage
id="visTypeTimeseries.percentile.fillToLabel"
defaultMessage="Fill to:"
/>
</EuiFormLabel>
</EuiFlexItem>
<EuiFlexItem style={optionsStyle} grow={false}>
<EuiFieldNumber
id={htmlId('fillTo')}
min={0}
max={100}
step={1}
onChange={this.handleTextChange(model, 'percentile')}
value={Number(model.percentile)}
/>
</EuiFlexItem>
<EuiFlexItem style={optionsStyle} grow={false}>
<EuiFormLabel style={labelStyle} htmlFor={htmlId('shade')}>
<FormattedMessage
id="visTypeTimeseries.percentile.shadeLabel"
defaultMessage="Shade (0 to 1):"
/>
</EuiFormLabel>
</EuiFlexItem>
<EuiFlexItem style={optionsStyle} grow={false}>
<EuiFieldNumber
id={htmlId('shade')}
style={optionsStyle}
step={0.1}
onChange={this.handleTextChange(model, 'shade')}
value={Number(model.shade)}
/>
</EuiFlexItem>
<EuiFlexItem>
<AddDeleteButtons
onAdd={handleAdd}
onDelete={handleDelete}
disableDelete={items.length < 2}
responsive={false}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
);
};
@ -192,15 +208,13 @@ class PercentilesUi extends Component {
}
}
PercentilesUi.defaultProps = {
Percentiles.defaultProps = {
name: 'percentile',
};
PercentilesUi.propTypes = {
Percentiles.propTypes = {
name: PropTypes.string,
model: PropTypes.object,
panel: PropTypes.object,
onChange: PropTypes.func,
};
export const Percentiles = injectI18n(PercentilesUi);

View file

@ -43,7 +43,6 @@ export async function getSeriesData(req, panel) {
(acc, items) => acc.concat(items),
[]
);
const data = await searchStrategy.search(req, searches);
const handleResponseBodyFn = handleResponseBody(panel);

View file

@ -64,6 +64,16 @@ function extendStatsBucket(bucket, metrics) {
return body;
}
function getPercentileHdrParam(bucket) {
return bucket.numberOfSignificantValueDigits
? {
hdr: {
number_of_significant_value_digits: bucket.numberOfSignificantValueDigits,
},
}
: undefined;
}
export const bucketTransform = {
count: () => {
return {
@ -139,13 +149,14 @@ export const bucketTransform = {
bucket.percentiles.filter((p) => p.percentile).map((p) => p.percentile)
);
}
const agg = {
return {
percentiles: {
field: bucket.field,
percents,
...getPercentileHdrParam(bucket),
},
};
return agg;
},
percentile_rank: (bucket) => {
@ -155,6 +166,7 @@ export const bucketTransform = {
percentile_ranks: {
field: bucket.field,
values: (bucket.values || []).map((value) => (isEmpty(value) ? 0 : value)),
...getPercentileHdrParam(bucket),
},
};
},

View file

@ -32,7 +32,7 @@ export function percentileRank(resp, panel, series, meta) {
}
getSplits(resp, panel, series, meta).forEach((split) => {
(metric.values || []).forEach((percentileRank) => {
(metric.values || []).forEach((percentileRank, index) => {
const data = split.timeseries.buckets.map((bucket) => [
bucket.key,
getAggValue(bucket, {
@ -43,7 +43,7 @@ export function percentileRank(resp, panel, series, meta) {
results.push({
data,
id: `${split.id}:${percentileRank}`,
id: `${split.id}:${percentileRank}:${index}`,
label: `${split.label} (${percentileRank || 0})`,
color: split.color,
...getDefaultDecoration(series),

View file

@ -3817,7 +3817,6 @@
"visTypeTimeseries.movingAverage.period": "期間",
"visTypeTimeseries.movingAverage.windowSizeHint": "ウィンドウは、必ず、期間のサイズの 2 倍以上でなければなりません",
"visTypeTimeseries.movingAverage.windowSizeLabel": "ウィンドウサイズ",
"visTypeTimeseries.multivalueRow.valueLabel": "値:",
"visTypeTimeseries.noButtonLabel": "いいえ",
"visTypeTimeseries.noDataDescription": "選択されたメトリックに表示するデータがありません",
"visTypeTimeseries.percentile.aggregationLabel": "集約",

View file

@ -3818,7 +3818,6 @@
"visTypeTimeseries.movingAverage.period": "期间",
"visTypeTimeseries.movingAverage.windowSizeHint": "窗口必须始终至少是期间大小的两倍",
"visTypeTimeseries.movingAverage.windowSizeLabel": "窗口大小",
"visTypeTimeseries.multivalueRow.valueLabel": "值:",
"visTypeTimeseries.noButtonLabel": "否",
"visTypeTimeseries.noDataDescription": "所选指标没有可显示的数据",
"visTypeTimeseries.percentile.aggregationLabel": "聚合",