[TSVB] Add Top Hits Aggregation (#17047)

* [TSVB] Adding Top Hits Aggregation

* Fixing tests
This commit is contained in:
Chris Cowan 2018-03-08 14:49:26 -07:00
parent 0336858ed2
commit 3e5a220740
11 changed files with 404 additions and 163 deletions

View file

@ -2,7 +2,6 @@ import { expect } from 'chai';
import { createOptions, isBasicAgg } from '../agg_lookup';
describe('aggLookup', () => {
describe('isBasicAgg(metric)', () => {
it('returns true for a basic metric (count)', () => {
expect(isBasicAgg({ type: 'count' })).to.equal(true);
@ -13,11 +12,10 @@ describe('aggLookup', () => {
});
describe('createOptions(type, siblings)', () => {
it('returns options for all aggs', () => {
const options = createOptions();
expect(options).to.have.length(28);
options.forEach((option) => {
expect(options).to.have.length(29);
options.forEach(option => {
expect(option).to.have.property('label');
expect(option).to.have.property('value');
expect(option).to.have.property('disabled');
@ -26,20 +24,23 @@ describe('aggLookup', () => {
it('returns options for basic', () => {
const options = createOptions('basic');
expect(options).to.have.length(14);
expect(options.every(opt => isBasicAgg({ type: opt.value }))).to.equal(true);
expect(options).to.have.length(15);
expect(options.every(opt => isBasicAgg({ type: opt.value }))).to.equal(
true
);
});
it('returns options for pipeline', () => {
const options = createOptions('pipeline');
expect(options).to.have.length(14);
expect(options.every(opt => !isBasicAgg({ type: opt.value }))).to.equal(true);
expect(options.every(opt => !isBasicAgg({ type: opt.value }))).to.equal(
true
);
});
it('returns options for all if given unknown key', () => {
const options = createOptions('foo');
expect(options).to.have.length(28);
expect(options).to.have.length(29);
});
});
});

View file

@ -1,33 +1,34 @@
import _ from 'lodash';
const lookup = {
'count': 'Count',
'calculation': 'Calculation',
'std_deviation': 'Std. Deviation',
'variance': 'Variance',
'sum_of_squares': 'Sum of Sq.',
'avg': 'Average',
'max': 'Max',
'min': 'Min',
'sum': 'Sum',
'percentile': 'Percentile',
'percentile_rank': 'Percentile Rank',
'cardinality': 'Cardinality',
'value_count': 'Value Count',
'derivative': 'Derivative',
'cumulative_sum': 'Cumulative Sum',
'moving_average': 'Moving Average',
'avg_bucket': 'Overall Average',
'min_bucket': 'Overall Min',
'max_bucket': 'Overall Max',
'sum_bucket': 'Overall Sum',
'variance_bucket': 'Overall Variance',
'sum_of_squares_bucket': 'Overall Sum of Sq.',
'std_deviation_bucket': 'Overall Std. Deviation',
'series_agg': 'Series Agg',
'serial_diff': 'Serial Difference',
'filter_ratio': 'Filter Ratio',
'positive_only': 'Positive Only',
'static': 'Static Value'
count: 'Count',
calculation: 'Calculation',
std_deviation: 'Std. Deviation',
variance: 'Variance',
sum_of_squares: 'Sum of Sq.',
avg: 'Average',
max: 'Max',
min: 'Min',
sum: 'Sum',
percentile: 'Percentile',
percentile_rank: 'Percentile Rank',
cardinality: 'Cardinality',
value_count: 'Value Count',
derivative: 'Derivative',
cumulative_sum: 'Cumulative Sum',
moving_average: 'Moving Average',
avg_bucket: 'Overall Average',
min_bucket: 'Overall Min',
max_bucket: 'Overall Max',
sum_bucket: 'Overall Sum',
variance_bucket: 'Overall Variance',
sum_of_squares_bucket: 'Overall Sum of Sq.',
std_deviation_bucket: 'Overall Std. Deviation',
series_agg: 'Series Agg',
serial_diff: 'Serial Difference',
filter_ratio: 'Filter Ratio',
positive_only: 'Positive Only',
static: 'Static Value',
top_hit: 'Top Hit',
};
const pipeline = [
@ -44,7 +45,7 @@ const pipeline = [
'std_deviation_bucket',
'series_agg',
'serial_diff',
'positive_only'
'positive_only',
];
const byType = {
@ -58,8 +59,8 @@ const byType = {
'max',
'sum',
'cardinality',
'value_count'
])
'value_count',
]),
};
export function isBasicAgg(item) {
@ -68,16 +69,18 @@ export function isBasicAgg(item) {
export function createOptions(type = '_all', siblings = []) {
let aggs = byType[type];
if (!aggs) aggs = byType._all;
if (!aggs) aggs = byType._all;
let enablePipelines = siblings.some(isBasicAgg);
if (siblings.length <= 1) enablePipelines = false;
return _(aggs)
.map((label, value) => {
const disabled = _.includes(pipeline, value) ? !enablePipelines : false;
return {
label: disabled ? `${label} (use the "+" button to add this pipeline agg)` : label,
label: disabled
? `${label} (use the "+" button to add this pipeline agg)`
: label,
value,
disabled
disabled,
};
})
.value();

View file

@ -12,7 +12,7 @@ const paths = [
'variance_bucket',
'sum_of_squares_bucket',
'serial_diff',
'positive_only'
'positive_only',
];
export default function calculateLabel(metric, metrics) {
if (!metric) return 'Unknown';
@ -28,7 +28,6 @@ export default function calculateLabel(metric, metrics) {
return `${lookup[metric.type]} (${metric.value}) of ${metric.field}`;
}
if (includes(paths, metric.type)) {
let additionalLabel = '';
const targetMetric = metrics.find(m => startsWith(metric.field, m.id));
@ -48,4 +47,3 @@ export default function calculateLabel(metric, metrics) {
return `${lookup[metric.type]} of ${metric.field}`;
}

View file

@ -15,8 +15,9 @@ const metricAggs = [
{ label: 'Std. Deviation', value: 'std_deviation' },
{ label: 'Sum', value: 'sum' },
{ label: 'Sum of Squares', value: 'sum_of_squares' },
{ label: 'Top Hit', value: 'top_hit' },
{ label: 'Value Count', value: 'value_count' },
{ label: 'Variance', value: 'variance' }
{ label: 'Variance', value: 'variance' },
];
const pipelineAggs = [
@ -26,7 +27,7 @@ const pipelineAggs = [
{ label: 'Moving Average', value: 'moving_average' },
{ label: 'Positive Only', value: 'positive_only' },
{ label: 'Serial Difference', value: 'serial_diff' },
{ label: 'Series Agg', value: 'series_agg' }
{ label: 'Series Agg', value: 'series_agg' },
];
const siblingAggs = [
@ -36,11 +37,10 @@ const siblingAggs = [
{ label: 'Overall Std. Deviation', value: 'std_deviation_bucket' },
{ label: 'Overall Sum', value: 'sum_bucket' },
{ label: 'Overall Sum of Squares', value: 'sum_of_squares_bucket' },
{ label: 'Overall Variance', value: 'variance_bucket' }
{ label: 'Overall Variance', value: 'variance_bucket' },
];
class AggSelectOption extends Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
@ -66,7 +66,7 @@ class AggSelectOption extends Component {
render() {
const { label, heading, pipeline } = this.props.option;
const style = {
paddingLeft: heading ? 0 : 10
paddingLeft: heading ? 0 : 10,
};
// We can ignore that the <div> does not have keyboard handlers even though
// it has mouse handlers, since react-select still takes care, that this works
@ -75,7 +75,11 @@ class AggSelectOption extends Component {
if (heading) {
let note;
if (pipeline) {
note = (<span className="vis_editor__agg_select-note">(requires child aggregation)</span>);
note = (
<span className="vis_editor__agg_select-note">
(requires child aggregation)
</span>
);
}
return (
<div
@ -101,13 +105,12 @@ class AggSelectOption extends Component {
aria-label={label}
>
<span className="Select-value-label" style={style}>
{ this.props.children }
{this.props.children}
</span>
</div>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
}
}
AggSelectOption.props = {
@ -131,7 +134,9 @@ function filterByPanelType(panelType) {
function AggSelect(props) {
const { siblings, panelType } = props;
let enablePipelines = siblings.some(s => !!metricAggs.find(m => m.value === s.type));
let enablePipelines = siblings.some(
s => !!metricAggs.find(m => m.value === s.type)
);
if (siblings.length <= 1) enablePipelines = false;
let options;
@ -139,16 +144,35 @@ function AggSelect(props) {
options = metricAggs;
} else {
options = [
{ label: 'Metric Aggregations', value: null, heading: true, disabled: true },
{
label: 'Metric Aggregations',
value: null,
heading: true,
disabled: true,
},
...metricAggs,
{ label: 'Parent Pipeline Aggregations', value: null, pipeline: true, heading: true, disabled: true },
...pipelineAggs.filter(filterByPanelType(panelType)).map(agg => ({ ...agg, disabled: !enablePipelines })),
{ label: 'Sibling Pipeline Aggregations', value: null, pipeline: true, heading: true, disabled: true },
...siblingAggs.map(agg => ({ ...agg, disabled: !enablePipelines }))
{
label: 'Parent Pipeline Aggregations',
value: null,
pipeline: true,
heading: true,
disabled: true,
},
...pipelineAggs
.filter(filterByPanelType(panelType))
.map(agg => ({ ...agg, disabled: !enablePipelines })),
{
label: 'Sibling Pipeline Aggregations',
value: null,
pipeline: true,
heading: true,
disabled: true,
},
...siblingAggs.map(agg => ({ ...agg, disabled: !enablePipelines })),
];
}
const handleChange = (value) => {
const handleChange = value => {
if (!value) return;
if (value.disabled) return;
if (value.value) props.onChange(value);
@ -172,7 +196,7 @@ AggSelect.propTypes = {
onChange: PropTypes.func,
panelType: PropTypes.string,
siblings: PropTypes.array,
value: PropTypes.string
value: PropTypes.string,
};
export default AggSelect;

View file

@ -7,7 +7,7 @@ import calculateLabel from '../../../common/calculate_label';
import basicAggs from '../../../common/basic_aggs';
function createTypeFilter(restrict, exclude) {
return (metric) => {
return metric => {
if (_.includes(exclude, metric.type)) return false;
switch (restrict) {
case 'basic':
@ -18,25 +18,20 @@ function createTypeFilter(restrict, exclude) {
};
}
// This filters out sibling aggs, percentiles, and special aggs (like Series Agg)
export function filterRows(row) {
return !/_bucket$/.test(row.type)
&& !/^series/.test(row.type)
&& !/^percentile/.test(row.type);
return (
!/_bucket$/.test(row.type) &&
!/^series/.test(row.type) &&
!/^percentile/.test(row.type) &&
!/^top_hit/.test(row.type)
);
}
function MetricSelect(props) {
const {
restrict,
metric,
onChange,
value,
exclude
} = props;
const { restrict, metric, onChange, value, exclude } = props;
const metrics = props.metrics
.filter(createTypeFilter(restrict, exclude));
const metrics = props.metrics.filter(createTypeFilter(restrict, exclude));
const siblings = calculateSiblings(metrics, metric);
@ -50,25 +45,25 @@ function MetricSelect(props) {
row.percentiles.forEach(p => {
if (p.value) {
const value = /\./.test(p.value) ? p.value : `${p.value}.0`;
acc.push({ value: `${row.id}[${value}]`, label: `${label} (${value})` });
acc.push({
value: `${row.id}[${value}]`,
label: `${label} (${value})`,
});
}
});
return acc;
}, []);
const options = siblings
.filter(filterRows)
.map(row => {
const label = calculateLabel(row, metrics);
return { value: row.id, label };
});
const options = siblings.filter(filterRows).map(row => {
const label = calculateLabel(row, metrics);
return { value: row.id, label };
});
return (
<Select
aria-label="Select metric"
placeholder="Select metric..."
options={[ ...options, ...props.additionalOptions, ...percentileOptions]}
options={[...options, ...props.additionalOptions, ...percentileOptions]}
value={value}
onChange={onChange}
/>
@ -88,7 +83,7 @@ MetricSelect.propTypes = {
metric: PropTypes.object,
onChange: PropTypes.func,
restrict: PropTypes.string,
value: PropTypes.string
value: PropTypes.string,
};
export default MetricSelect;

View file

@ -0,0 +1,129 @@
import React from 'react';
import AggRow from './agg_row';
import AggSelect from './agg_select';
import FieldSelect from './field_select';
import Select from 'react-select';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
import { htmlIdGenerator } from '@elastic/eui';
export const TopHitAgg = props => {
const { fields, series, panel } = props;
const defaults = {
agg_with: 'avg',
size: 1,
order: 'desc',
};
const model = { ...defaults, ...props.model };
const indexPattern =
(series.override_index_pattern && series.series_index_pattern) ||
panel.index_pattern;
const handleChange = createChangeHandler(props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
const handleTextChange = createTextHandler(handleChange);
const aggWithOptions = [
{ label: 'Avg', value: 'avg' },
{ label: 'Max', value: 'max' },
{ label: 'Min', value: 'min' },
{ label: 'Sum', value: 'sum' },
];
const orderOptions = [
{ label: 'Asc', value: 'asc' },
{ label: 'Desc', value: 'desc' },
];
const htmlId = htmlIdGenerator();
return (
<AggRow
disableDelete={props.disableDelete}
model={props.model}
onAdd={props.onAdd}
onDelete={props.onDelete}
siblings={props.siblings}
>
<div className="vis_editor__row_item">
<div className="vis_editor__agg_row-item">
<div className="vis_editor__row_item">
<div className="vis_editor__label">Aggregation</div>
<AggSelect
panelType={props.panel.type}
siblings={props.siblings}
value={model.type}
onChange={handleSelectChange('type')}
/>
</div>
<div className="vis_editor__row_item">
<label className="vis_editor__label" htmlFor={htmlId('field')}>
Field
</label>
<FieldSelect
id={htmlId('field')}
fields={fields}
type={model.type}
restrict="numeric"
indexPattern={indexPattern}
value={model.field}
onChange={handleSelectChange('field')}
/>
</div>
</div>
<div className="vis_editor__agg_row-item">
<div className="vis_editor__row_item">
<label className="vis_editor__label" htmlFor={htmlId('size')}>
Size
</label>
<input
id={htmlId('size')}
className="vis_editor__input-grows-100"
value={model.size}
onChange={handleTextChange('size')}
/>
</div>
<div className="vis_editor__row_item">
<label className="vis_editor__label" htmlFor={htmlId('agg_with')}>
Aggregate with
</label>
<Select
inputProps={{ id: htmlId('agg_with') }}
clearable={false}
placeholder="Select..."
onChange={handleSelectChange('agg_with')}
value={model.agg_with}
options={aggWithOptions}
/>
</div>
<div className="vis_editor__row_item">
<label className="vis_editor__label" htmlFor={htmlId('order_by')}>
Order by
</label>
<FieldSelect
id={htmlId('order_by')}
restrict="date"
value={model.order_by}
onChange={handleSelectChange('order_by')}
indexPattern={indexPattern}
fields={fields}
/>
</div>
<div className="vis_editor__row_item">
<label className="vis_editor__label" htmlFor={htmlId('order')}>
Order
</label>
<Select
inputProps={{ id: htmlId('order') }}
clearable={false}
placeholder="Select..."
onChange={handleSelectChange('order')}
value={model.order}
options={orderOptions}
/>
</div>
</div>
</div>
</AggRow>
);
};

View file

@ -12,6 +12,7 @@ import { PositiveOnlyAgg } from '../aggs/positive_only';
import { FilterRatioAgg } from '../aggs/filter_ratio';
import { PercentileRankAgg } from '../aggs/percentile_rank';
import { Static } from '../aggs/static';
import { TopHitAgg } from '../aggs/top_hit';
export default {
count: StdAgg,
avg: StdAgg,
@ -40,7 +41,6 @@ export default {
serial_diff: SerialDiffAgg,
filter_ratio: FilterRatioAgg,
positive_only: PositiveOnlyAgg,
static: Static
static: Static,
top_hit: TopHitAgg,
};

View file

@ -5,75 +5,122 @@ function testAgg(row, metric, expected) {
let name = metric.type;
if (metric.mode) name += `(${metric.mode})`;
if (metric.percent) name += `(${metric.percent})`;
it(`it should return ${name}`, () => {
it(`it should return ${name}(${expected})`, () => {
const value = getAggValue(row, metric);
expect(value).to.eql(expected);
});
}
describe('getAggValue', () => {
describe('extended_stats', () => {
const row = {
'test': {
'count': 9,
'min': 72,
'max': 99,
'avg': 86,
'sum': 774,
'sum_of_squares': 67028,
'variance': 51.55555555555556,
'std_deviation': 7.180219742846005,
'std_deviation_bounds': {
'upper': 100.36043948569201,
'lower': 71.63956051430799
}
}
test: {
count: 9,
min: 72,
max: 99,
avg: 86,
sum: 774,
sum_of_squares: 67028,
variance: 51.55555555555556,
std_deviation: 7.180219742846005,
std_deviation_bounds: {
upper: 100.36043948569201,
lower: 71.63956051430799,
},
},
};
testAgg(row, { id: 'test', type: 'std_deviation' }, 7.180219742846005);
testAgg(row, { id: 'test', type: 'variance' }, 51.55555555555556);
testAgg(row, { id: 'test', type: 'sum_of_squares' }, 67028);
testAgg(row, { id: 'test', type: 'std_deviation', mode: 'upper' }, 100.36043948569201);
testAgg(row, { id: 'test', type: 'std_deviation', mode: 'lower' }, 71.63956051430799);
testAgg(
row,
{ id: 'test', type: 'std_deviation', mode: 'upper' },
100.36043948569201
);
testAgg(
row,
{ id: 'test', type: 'std_deviation', mode: 'lower' },
71.63956051430799
);
});
describe('percentile', () => {
const row = {
'test': {
'values': {
test: {
values: {
'1.0': 15,
'5.0': 20,
'25.0': 23,
'50.0': 25,
'75.0': 29,
'95.0': 60,
'99.0': 150
}
}
'99.0': 150,
},
},
};
testAgg(row, { id: 'test', type: 'percentile', percent: '50' }, 25);
testAgg(row, { id: 'test', type: 'percentile', percent: '1.0' }, 15);
});
describe('top hits', () => {
const row = {
test: {
doc_count: 1,
docs: {
hits: {
hits: [
{ _source: { example: { value: 25 } } },
{ _source: { example: { value: 25 } } },
{ _source: { example: { value: 25 } } },
],
},
},
},
};
testAgg(
row,
{ id: 'test', type: 'top_hit', agg_with: 'avg', field: 'example.value' },
25
);
testAgg(
row,
{ id: 'test', type: 'top_hit', agg_with: 'sum', field: 'example.value' },
75
);
testAgg(
row,
{ id: 'test', type: 'top_hit', agg_with: 'max', field: 'example.value' },
25
);
testAgg(
row,
{ id: 'test', type: 'top_hit', agg_with: 'min', field: 'example.value' },
25
);
});
const basicWithDerv = {
'key_as_string': '2015/02/01 00:00:00',
'key': 1422748800000,
'doc_count': 2,
'test': {
'value': 60.0
key_as_string: '2015/02/01 00:00:00',
key: 1422748800000,
doc_count: 2,
test: {
value: 60.0,
},
test_deriv: {
value: -490.0,
normalized_value: -15.806451612903226,
},
'test_deriv': {
'value': -490.0,
'normalized_value': -15.806451612903226
}
};
describe('derivative', () => {
testAgg(basicWithDerv, { id: 'test_deriv', type: 'derivative' }, -15.806451612903226);
testAgg(
basicWithDerv,
{ id: 'test_deriv', type: 'derivative' },
-15.806451612903226
);
});
describe('basic metric', () => {
testAgg(basicWithDerv, { id: 'test', type: 'avg' }, 60.0);
});
});

View file

@ -1,6 +1,7 @@
import parseSettings from './parse_settings';
import getBucketsPath from './get_buckets_path';
import { parseInterval } from './parse_interval';
import { set } from 'lodash';
function checkMetric(metric, fields) {
fields.forEach(field => {
@ -14,7 +15,7 @@ function stdMetric(bucket) {
checkMetric(bucket, ['type', 'field']);
const body = {};
body[bucket.type] = {
field: bucket.field
field: bucket.field,
};
return body;
}
@ -22,7 +23,7 @@ function stdMetric(bucket) {
function extendStats(bucket) {
checkMetric(bucket, ['type', 'field']);
const body = {
extended_stats: { field: bucket.field }
extended_stats: { field: bucket.field },
};
if (bucket.sigma) body.extended_stats.sigma = parseInt(bucket.sigma, 10);
return body;
@ -44,10 +45,10 @@ export default {
buckets_path: { count: '_count' },
script: {
source: 'count * 1',
lang: 'expression'
lang: 'expression',
},
gap_policy: 'skip'
}
gap_policy: 'skip',
},
};
},
static: bucket => {
@ -57,10 +58,10 @@ export default {
buckets_path: { count: '_count' },
script: {
source: bucket.value,
lang: 'painless'
lang: 'painless',
},
gap_policy: 'skip'
}
gap_policy: 'skip',
},
};
},
avg: stdMetric,
@ -73,13 +74,36 @@ export default {
variance: extendStats,
std_deviation: extendStats,
top_hit: bucket => {
checkMetric(bucket, ['type', 'field', 'size']);
const body = {
filter: {
exists: { field: bucket.field },
},
aggs: {
docs: {
top_hits: {
size: bucket.size,
_source: { includes: [bucket.field] },
},
},
},
};
if (bucket.order_by) {
set(body, 'aggs.docs.top_hits.sort', [
{ [bucket.order_by]: { order: bucket.order } },
]);
}
return body;
},
percentile_rank: bucket => {
checkMetric(bucket, ['type', 'field', 'value']);
const body = {
percentile_ranks: {
field: bucket.field,
values: [bucket.value]
}
values: [bucket.value],
},
};
return body;
},
@ -105,8 +129,8 @@ export default {
const agg = {
percentiles: {
field: bucket.field,
percents
}
percents,
},
};
return agg;
},
@ -117,8 +141,8 @@ export default {
derivative: {
buckets_path: getBucketsPath(bucket.field, metrics),
gap_policy: 'skip', // seems sane
unit: bucketSize
}
unit: bucketSize,
},
};
if (bucket.gap_policy) body.derivative.gap_policy = bucket.gap_policy;
if (bucket.unit) {
@ -135,8 +159,8 @@ export default {
serial_diff: {
buckets_path: getBucketsPath(bucket.field, metrics),
gap_policy: 'skip', // seems sane
lag: 1
}
lag: 1,
},
};
if (bucket.gap_policy) body.serial_diff.gap_policy = bucket.gap_policy;
if (bucket.lag) {
@ -149,8 +173,8 @@ export default {
checkMetric(bucket, ['type', 'field']);
return {
cumulative_sum: {
buckets_path: getBucketsPath(bucket.field, metrics)
}
buckets_path: getBucketsPath(bucket.field, metrics),
},
};
},
@ -160,8 +184,8 @@ export default {
moving_avg: {
buckets_path: getBucketsPath(bucket.field, metrics),
model: bucket.model || 'simple',
gap_policy: 'skip' // seems sane
}
gap_policy: 'skip', // seems sane
},
};
if (bucket.gap_policy) body.moving_avg.gap_policy = bucket.gap_policy;
if (bucket.window) body.moving_avg.window = Number(bucket.window);
@ -185,11 +209,11 @@ export default {
source: bucket.script,
lang: 'painless',
params: {
_interval: parseInterval(bucketSize).asMilliseconds()
}
_interval: parseInterval(bucketSize).asMilliseconds(),
},
},
gap_policy: 'skip' // seems sane
}
gap_policy: 'skip', // seems sane
},
};
if (bucket.gap_policy) body.bucket_script.gap_policy = bucket.gap_policy;
return body;
@ -200,15 +224,15 @@ export default {
const body = {
bucket_script: {
buckets_path: {
value: getBucketsPath(bucket.field, metrics)
value: getBucketsPath(bucket.field, metrics),
},
script: {
source: 'params.value > 0 ? params.value : 0',
lang: 'painless'
lang: 'painless',
},
gap_policy: 'skip' // seems sane
}
gap_policy: 'skip', // seems sane
},
};
return body;
}
},
};

View file

@ -1,14 +1,22 @@
import _ from 'lodash';
import { get, includes, max, min, sum } from 'lodash';
import extendStatsTypes from './extended_stats_types';
const aggFns = {
max,
min,
sum,
avg: values => sum(values) / values.length,
};
export default (row, metric) => {
// Extended Stats
if (_.includes(extendStatsTypes, metric.type)) {
if (includes(extendStatsTypes, metric.type)) {
const isStdDeviation = /^std_deviation/.test(metric.type);
const modeIsBounds = ~['upper', 'lower'].indexOf(metric.mode);
if (isStdDeviation && modeIsBounds) {
return _.get(row, `${metric.id}.std_deviation_bounds.${metric.mode}`);
return get(row, `${metric.id}.std_deviation_bounds.${metric.mode}`);
}
return _.get(row, `${metric.id}.${metric.type}`);
return get(row, `${metric.id}.${metric.type}`);
}
// Percentiles
@ -22,14 +30,27 @@ export default (row, metric) => {
if (metric.type === 'percentile_rank') {
const percentileRankKey = `${metric.value}`;
return row[metric.id] && row[metric.id].values && row[metric.id].values[percentileRankKey];
return (
row[metric.id] &&
row[metric.id].values &&
row[metric.id].values[percentileRankKey]
);
}
if (metric.type === 'top_hit') {
if (row[metric.id].doc_count === 0) return null;
const hits = get(row, [metric.id, 'docs', 'hits', 'hits'], []);
const values = hits.map(doc => {
return get(doc, `_source.${metric.field}`, 0);
});
const aggWith = (metric.agg_with && aggFns[metric.agg_with]) || aggFns.avg;
return aggWith(values);
}
// Derivatives
const normalizedValue = _.get(row, `${metric.id}.normalized_value`, null);
const normalizedValue = get(row, `${metric.id}.normalized_value`, null);
// Everything else
const value = _.get(row, `${metric.id}.value`, null);
const value = get(row, `${metric.id}.value`, null);
return normalizedValue || value;
};

View file

@ -13,17 +13,16 @@ export default function stdMetric(resp, panel, series) {
}
if (/_bucket$/.test(metric.type)) return next(results);
const decoration = getDefaultDecoration(series);
getSplits(resp, panel, series).forEach((split) => {
getSplits(resp, panel, series).forEach(split => {
const data = split.timeseries.buckets.map(mapBucket(metric));
results.push({
id: `${split.id}`,
label: split.label,
color: split.color,
data,
...decoration
...decoration,
});
});
return next(results);
};
}