[Maps] add unique count metric aggregation (#48961) (#49252)

* [Maps] add unique count metric aggregation

* do not format unique_count aggregation results

* do not format value in legend for unique count

* update heatmap docs

* one more doc change
This commit is contained in:
Nathan Reese 2019-10-24 20:48:36 -06:00 committed by GitHub
parent cddff2babe
commit 49307e0a61
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 94 additions and 56 deletions

View file

@ -13,6 +13,6 @@ You can create a heat map layer from the following data source:
Set *Show as* to *heat map*.
The index must contain at least one field mapped as {ref}/geo-point.html[geo_point].
NOTE: Only count and sum metric aggregations are available with the grid aggregation source and heat map layers.
Mean, median, min, and max are turned off because the heat map will blend nearby values.
NOTE: Only count, sum, unique count metric aggregations are available with the grid aggregation source and heat map layers.
Average, min, and max are turned off because the heat map will blend nearby values.
Blending two average values would make the cluster more prominent, even though it just might literally mean that these nearby areas are average.

View file

@ -102,3 +102,12 @@ export const DRAW_TYPE = {
BOUNDS: 'BOUNDS',
POLYGON: 'POLYGON'
};
export const METRIC_TYPE = {
AVG: 'avg',
COUNT: 'count',
MAX: 'max',
MIN: 'min',
SUM: 'sum',
UNIQUE_COUNT: 'cardinality',
};

View file

@ -12,6 +12,7 @@ import { EuiFieldText, EuiFormRow } from '@elastic/eui';
import { MetricSelect, METRIC_AGGREGATION_VALUES } from './metric_select';
import { SingleFieldSelect } from './single_field_select';
import { METRIC_TYPE } from '../../common/constants';
export function MetricEditor({ fields, metricsFilter, metric, onChange, removeButton }) {
const onAggChange = metricAggregationType => {
@ -34,10 +35,12 @@ export function MetricEditor({ fields, metricsFilter, metric, onChange, removeBu
};
let fieldSelect;
if (metric.type && metric.type !== 'count') {
const filterNumberFields = field => {
return field.type === 'number';
};
if (metric.type && metric.type !== METRIC_TYPE.COUNT) {
const filterField = metric.type !== METRIC_TYPE.UNIQUE_COUNT
? field => {
return field.type === 'number';
}
: undefined;
fieldSelect = (
<EuiFormRow
label={i18n.translate('xpack.maps.metricsEditor.selectFieldLabel', {
@ -51,7 +54,7 @@ export function MetricEditor({ fields, metricsFilter, metric, onChange, removeBu
})}
value={metric.field}
onChange={onFieldChange}
filterField={filterNumberFields}
filterField={filterField}
fields={fields}
isClearable={false}
compressed

View file

@ -8,37 +8,44 @@ import React from 'react';
import PropTypes from 'prop-types';
import { i18n } from '@kbn/i18n';
import { EuiComboBox } from '@elastic/eui';
import { METRIC_TYPE } from '../../common/constants';
const AGG_OPTIONS = [
{
label: i18n.translate('xpack.maps.metricSelect.averageDropDownOptionLabel', {
defaultMessage: 'Average',
}),
value: 'avg',
value: METRIC_TYPE.AVG,
},
{
label: i18n.translate('xpack.maps.metricSelect.countDropDownOptionLabel', {
defaultMessage: 'Count',
}),
value: 'count',
value: METRIC_TYPE.COUNT,
},
{
label: i18n.translate('xpack.maps.metricSelect.maxDropDownOptionLabel', {
defaultMessage: 'Max',
}),
value: 'max',
value: METRIC_TYPE.MAX,
},
{
label: i18n.translate('xpack.maps.metricSelect.minDropDownOptionLabel', {
defaultMessage: 'Min',
}),
value: 'min',
value: METRIC_TYPE.MIN,
},
{
label: i18n.translate('xpack.maps.metricSelect.sumDropDownOptionLabel', {
defaultMessage: 'Sum',
}),
value: 'sum',
value: METRIC_TYPE.SUM,
},
{
label: i18n.translate('xpack.maps.metricSelect.cardinalityDropDownOptionLabel', {
defaultMessage: 'Unique count',
}),
value: METRIC_TYPE.UNIQUE_COUNT,
},
];

View file

@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButtonEmpty, EuiSpacer, EuiTextAlign } from '@elastic/eui';
import { MetricEditor } from './metric_editor';
import { METRIC_TYPE } from '../../common/constants';
export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics, metricsFilter }) {
function renderMetrics() {
@ -99,6 +100,6 @@ MetricsEditor.propTypes = {
};
MetricsEditor.defaultProps = {
metrics: [{ type: 'count' }],
metrics: [{ type: METRIC_TYPE.COUNT }],
allowMultipleMetrics: true,
};

View file

@ -16,6 +16,8 @@ import {
} from '@elastic/eui';
import { MetricsEditor } from '../../../../components/metrics_editor';
import { FormattedMessage } from '@kbn/i18n/react';
import { METRIC_TYPE } from '../../../../../common/constants';
export class MetricsExpression extends Component {
state = {
@ -58,7 +60,7 @@ export class MetricsExpression extends Component {
render() {
const metricExpressions = this.props.metrics
.filter(({ type, field }) => {
if (type === 'count') {
if (type === METRIC_TYPE.COUNT) {
return true;
}
@ -69,7 +71,7 @@ export class MetricsExpression extends Component {
})
.map(({ type, field }) => {
// do not use metric label so field and aggregation are not obscured.
if (type === 'count') {
if (type === METRIC_TYPE.COUNT) {
return 'count';
}
@ -127,6 +129,6 @@ MetricsExpression.propTypes = {
MetricsExpression.defaultProps = {
metrics: [
{ type: 'count' }
{ type: METRIC_TYPE.COUNT }
]
};

View file

@ -21,7 +21,7 @@ import { RENDER_AS } from './render_as';
import { CreateSourceEditor } from './create_source_editor';
import { UpdateSourceEditor } from './update_source_editor';
import { GRID_RESOLUTION } from '../../grid_resolution';
import { SOURCE_DATA_ID_ORIGIN, ES_GEO_GRID } from '../../../../common/constants';
import { SOURCE_DATA_ID_ORIGIN, ES_GEO_GRID, METRIC_TYPE } from '../../../../common/constants';
import { i18n } from '@kbn/i18n';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
@ -36,9 +36,16 @@ const aggSchemas = new Schemas([
title: 'Value',
min: 1,
max: Infinity,
aggFilter: ['avg', 'count', 'max', 'min', 'sum'],
aggFilter: [
METRIC_TYPE.AVG,
METRIC_TYPE.COUNT,
METRIC_TYPE.MAX,
METRIC_TYPE.MIN,
METRIC_TYPE.SUM,
METRIC_TYPE.UNIQUE_COUNT
],
defaults: [
{ schema: 'metric', type: 'count' }
{ schema: 'metric', type: METRIC_TYPE.COUNT }
]
},
{
@ -215,11 +222,11 @@ export class ESGeoGridSource extends AbstractESSource {
}
_formatMetricKey(metric) {
return metric.type !== 'count' ? `${metric.type}_of_${metric.field}` : COUNT_PROP_NAME;
return metric.type !== METRIC_TYPE.COUNT ? `${metric.type}_of_${metric.field}` : COUNT_PROP_NAME;
}
_formatMetricLabel(metric) {
return metric.type !== 'count' ? `${metric.type} of ${metric.field}` : COUNT_PROP_LABEL;
return metric.type !== METRIC_TYPE.COUNT ? `${metric.type} of ${metric.field}` : COUNT_PROP_LABEL;
}
_makeAggConfigs(precision) {
@ -231,7 +238,7 @@ export class ESGeoGridSource extends AbstractESSource {
schema: 'metric',
params: {}
};
if (metric.type !== 'count') {
if (metric.type !== METRIC_TYPE.COUNT) {
metricAggConfig.params = { field: metric.field };
}
return metricAggConfig;

View file

@ -8,6 +8,7 @@ import React, { Fragment, Component } from 'react';
import { RENDER_AS } from './render_as';
import { MetricsEditor } from '../../../components/metrics_editor';
import { METRIC_TYPE } from '../../../../common/constants';
import { indexPatternService } from '../../../kibana_services';
import { ResolutionEditor } from './resolution_editor';
import { i18n } from '@kbn/i18n';
@ -66,7 +67,7 @@ export class UpdateSourceEditor extends Component {
this.props.renderAs === RENDER_AS.HEATMAP
? metric => {
//these are countable metrics, where blending heatmap color blobs make sense
return ['count', 'sum'].includes(metric.value);
return [METRIC_TYPE.COUNT, METRIC_TYPE.SUM, METRIC_TYPE.UNIQUE_COUNT].includes(metric.value);
}
: null;
const allowMultipleMetrics = this.props.renderAs !== RENDER_AS.HEATMAP;

View file

@ -15,7 +15,7 @@ import { UpdateSourceEditor } from './update_source_editor';
import { VectorStyle } from '../../styles/vector_style';
import { vectorStyles } from '../../styles/vector_style_defaults';
import { i18n } from '@kbn/i18n';
import { SOURCE_DATA_ID_ORIGIN, ES_PEW_PEW } from '../../../../common/constants';
import { SOURCE_DATA_ID_ORIGIN, ES_PEW_PEW, METRIC_TYPE } from '../../../../common/constants';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
import { convertToLines } from './convert_to_lines';
import { Schemas } from 'ui/vis/editors/default/schemas';
@ -32,9 +32,16 @@ const aggSchemas = new Schemas([
title: 'Value',
min: 1,
max: Infinity,
aggFilter: ['avg', 'count', 'max', 'min', 'sum'],
aggFilter: [
METRIC_TYPE.AVG,
METRIC_TYPE.COUNT,
METRIC_TYPE.MAX,
METRIC_TYPE.MIN,
METRIC_TYPE.SUM,
METRIC_TYPE.UNIQUE_COUNT
],
defaults: [
{ schema: 'metric', type: 'count' }
{ schema: 'metric', type: METRIC_TYPE.COUNT }
]
}
]);
@ -193,7 +200,7 @@ export class ESPewPewSource extends AbstractESSource {
schema: 'metric',
params: {}
};
if (metric.type !== 'count') {
if (metric.type !== METRIC_TYPE.COUNT) {
metricAggConfig.params = { field: metric.field };
}
return metricAggConfig;
@ -252,11 +259,11 @@ export class ESPewPewSource extends AbstractESSource {
}
_formatMetricKey(metric) {
return metric.type !== 'count' ? `${metric.type}_of_${metric.field}` : COUNT_PROP_NAME;
return metric.type !== METRIC_TYPE.COUNT ? `${metric.type}_of_${metric.field}` : COUNT_PROP_NAME;
}
_formatMetricLabel(metric) {
return metric.type !== 'count' ? `${metric.type} of ${metric.field}` : COUNT_PROP_LABEL;
return metric.type !== METRIC_TYPE.COUNT ? `${metric.type} of ${metric.field}` : COUNT_PROP_LABEL;
}
async _getGeoField() {

View file

@ -512,9 +512,4 @@ export class ESSearchSource extends AbstractESSource {
path: geoField.name,
};
}
_getRawFieldName(fieldName) {
// fieldName is rawFieldName for documents source since the source uses raw documents instead of aggregated metrics
return fieldName;
}
}

View file

@ -19,7 +19,7 @@ import { ESAggMetricTooltipProperty } from '../tooltips/es_aggmetric_tooltip_pro
import uuid from 'uuid/v4';
import { copyPersistentState } from '../../reducers/util';
import { ES_GEO_FIELD_TYPE } from '../../../common/constants';
import { ES_GEO_FIELD_TYPE, METRIC_TYPE } from '../../../common/constants';
import { DataRequestAbortError } from '../util/data_request';
export class AbstractESSource extends AbstractVectorSource {
@ -59,7 +59,7 @@ export class AbstractESSource extends AbstractVectorSource {
_getValidMetrics() {
const metrics = _.get(this._descriptor, 'metrics', []).filter(({ type, field }) => {
if (type === 'count') {
if (type === METRIC_TYPE.COUNT) {
return true;
}
@ -69,7 +69,7 @@ export class AbstractESSource extends AbstractVectorSource {
return false;
});
if (metrics.length === 0) {
metrics.push({ type: 'count' });
metrics.push({ type: METRIC_TYPE.COUNT });
}
return metrics;
}
@ -300,18 +300,13 @@ export class AbstractESSource extends AbstractVectorSource {
return this._descriptor.id;
}
_getRawFieldName(fieldName) {
async getFieldFormatter(fieldName) {
const metricField = this.getMetricFields().find(({ propertyKey }) => {
return propertyKey === fieldName;
});
return metricField ? metricField.field : null;
}
async getFieldFormatter(fieldName) {
// fieldName could be an aggregation so it needs to be unpacked to expose raw field.
const rawFieldName = this._getRawFieldName(fieldName);
if (!rawFieldName) {
// Do not use field formatters for counting metrics
if (metricField && metricField.type === METRIC_TYPE.COUNT || metricField.type === METRIC_TYPE.UNIQUE_COUNT) {
return null;
}
@ -322,7 +317,10 @@ export class AbstractESSource extends AbstractVectorSource {
return null;
}
const fieldFromIndexPattern = indexPattern.fields.getByName(rawFieldName);
const realFieldName = metricField
? metricField.field
: fieldName;
const fieldFromIndexPattern = indexPattern.fields.getByName(realFieldName);
if (!fieldFromIndexPattern) {
return null;
}

View file

@ -11,7 +11,7 @@ import { Schemas } from 'ui/vis/editors/default/schemas';
import { AggConfigs } from 'ui/agg_types';
import { i18n } from '@kbn/i18n';
import { ESTooltipProperty } from '../tooltips/es_tooltip_property';
import { ES_SIZE_LIMIT } from '../../../common/constants';
import { ES_SIZE_LIMIT, METRIC_TYPE } from '../../../common/constants';
const TERMS_AGG_NAME = 'join';
@ -22,9 +22,16 @@ const aggSchemas = new Schemas([
title: 'Value',
min: 1,
max: Infinity,
aggFilter: ['avg', 'count', 'max', 'min', 'sum'],
aggFilter: [
METRIC_TYPE.AVG,
METRIC_TYPE.COUNT,
METRIC_TYPE.MAX,
METRIC_TYPE.MIN,
METRIC_TYPE.SUM,
METRIC_TYPE.UNIQUE_COUNT
],
defaults: [
{ schema: 'metric', type: 'count' }
{ schema: 'metric', type: METRIC_TYPE.COUNT }
]
},
{
@ -81,12 +88,12 @@ export class ESTermSource extends AbstractESSource {
}
_formatMetricKey(metric) {
const metricKey = metric.type !== 'count' ? `${metric.type}_of_${metric.field}` : metric.type;
const metricKey = metric.type !== METRIC_TYPE.COUNT ? `${metric.type}_of_${metric.field}` : metric.type;
return `__kbnjoin__${metricKey}_groupby_${this._descriptor.indexPatternTitle}.${this._descriptor.term}`;
}
_formatMetricLabel(metric) {
const metricLabel = metric.type !== 'count' ? `${metric.type} ${metric.field}` : 'count';
const metricLabel = metric.type !== METRIC_TYPE.COUNT ? `${metric.type} ${metric.field}` : 'count';
return `${metricLabel} of ${this._descriptor.indexPatternTitle}:${this._descriptor.term}`;
}
@ -108,13 +115,13 @@ export class ESTermSource extends AbstractESSource {
const metricPropertyNames = configStates
.filter(configState => {
return configState.schema === 'metric' && configState.type !== 'count';
return configState.schema === 'metric' && configState.type !== METRIC_TYPE.COUNT;
})
.map(configState => {
return configState.id;
});
const countConfigState = configStates.find(configState => {
return configState.type === 'count';
return configState.type === METRIC_TYPE.COUNT;
});
const countPropertyName = _.get(countConfigState, 'id');
return {
@ -128,7 +135,7 @@ export class ESTermSource extends AbstractESSource {
_getRequestDescription(leftSourceName, leftFieldName) {
const metrics = this._getValidMetrics().map(metric => {
return metric.type !== 'count' ? `${metric.type} ${metric.field}` : 'count';
return metric.type !== METRIC_TYPE.COUNT ? `${metric.type} ${metric.field}` : 'count';
});
const joinStatement = [];
joinStatement.push(i18n.translate('xpack.maps.source.esJoin.joinLeftDescription', {
@ -157,7 +164,7 @@ export class ESTermSource extends AbstractESSource {
schema: 'metric',
params: {}
};
if (metric.type !== 'count') {
if (metric.type !== METRIC_TYPE.COUNT) {
metricAggConfig.params = { field: metric.field };
}
return metricAggConfig;

View file

@ -6,6 +6,7 @@
import { ESTooltipProperty } from './es_tooltip_property';
import { METRIC_TYPE } from '../../../common/constants';
export class ESAggMetricTooltipProperty extends ESTooltipProperty {
@ -21,7 +22,7 @@ export class ESAggMetricTooltipProperty extends ESTooltipProperty {
if (typeof this._rawValue === 'undefined') {
return '-';
}
if (this._metricField.type === 'count') {
if (this._metricField.type === METRIC_TYPE.COUNT || this._metricField.type === METRIC_TYPE.UNIQUE_COUNT) {
return this._rawValue;
}
const indexPatternField = this._indexPattern.fields.getByName(this._metricField.field);