mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Maps] add unique count metric aggregation (#48961)
* [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:
parent
adc204e7e6
commit
48313f73f6
13 changed files with 94 additions and 56 deletions
|
@ -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.
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 }
|
||||
]
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue