From b099b09db1dd9156f220c870d0b71278dbab7772 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 27 Mar 2020 16:12:35 +0100 Subject: [PATCH 1/9] [ML] WIP support empty partition fields values --- .../entity_control/entity_control.tsx | 26 ++++++++++--------- .../timeseries_search_service.ts | 2 +- .../timeseriesexplorer/timeseriesexplorer.js | 18 +++++++------ 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx index 8911ed53e74d0..251b65ec70b17 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx @@ -29,15 +29,17 @@ interface EntityControlProps { isLoading: boolean; onSearchChange: (entity: Entity, queryTerm: string) => void; forceSelection: boolean; - options: EuiComboBoxOptionOption[]; + options: Array>; } interface EntityControlState { - selectedOptions: EuiComboBoxOptionOption[] | undefined; + selectedOptions: Array> | undefined; isLoading: boolean; - options: EuiComboBoxOptionOption[] | undefined; + options: Array> | undefined; } +export const EMPTY_FIELD_VALUE_LABEL = '""'; + export class EntityControl extends Component { inputRef: any; @@ -53,16 +55,16 @@ export class EntityControl extends Component> | undefined = selectedOptions; if ( - (selectedOptions === undefined && fieldValue.length > 0) || + (selectedOptions === undefined && fieldValue !== null) || (Array.isArray(selectedOptions) && - // @ts-ignore - selectedOptions[0].label !== fieldValue && - fieldValue.length > 0) + // @ts-ignore + selectedOptions[0].value !== fieldValue && // FIXME + fieldValue !== null) ) { - selectedOptionsUpdate = [{ label: fieldValue }]; - } else if (Array.isArray(selectedOptions) && fieldValue.length === 0) { + selectedOptionsUpdate = [{ label: EMPTY_FIELD_VALUE_LABEL, value: fieldValue }]; + } else if (Array.isArray(selectedOptions) && fieldValue === null) { selectedOptionsUpdate = undefined; } @@ -84,14 +86,14 @@ export class EntityControl extends Component { + onChange = (selectedOptions: Array>) => { const options = selectedOptions.length > 0 ? selectedOptions : undefined; this.setState({ selectedOptions: options, }); const fieldValue = - Array.isArray(options) && options[0].label.length > 0 ? options[0].label : ''; + Array.isArray(options) && options[0].value !== null ? options[0].value : null; this.props.entityFieldValueChanged(this.props.entity, fieldValue); }; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts index db5ff2ad91910..f973d41ad7754 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts @@ -128,7 +128,7 @@ function getChartDetails( obj.results.functionLabel = functionLabel; const blankEntityFields = _.filter(entityFields, entity => { - return entity.fieldValue.length === 0; + return entity.fieldValue === null; }); // Look to see if any of the entity fields have defined values diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 1a26540709f34..6856ee4ccd4d7 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -78,6 +78,7 @@ import { processRecordScoreResults, getFocusData, } from './timeseriesexplorer_utils'; +import { EMPTY_FIELD_VALUE_LABEL } from './components/entity_control/entity_control'; // Used to indicate the chart is being plotted across // all partition field values, where the cardinality of the field cannot be @@ -94,7 +95,7 @@ function getEntityControlOptions(fieldValues) { fieldValues.sort(); return fieldValues.map(value => { - return { label: value }; + return { label: value === '' ? EMPTY_FIELD_VALUE_LABEL : value, value }; }); } @@ -192,7 +193,7 @@ export class TimeSeriesExplorer extends React.Component { getFieldNamesWithEmptyValues = () => { const latestEntityControls = this.getControlsForDetector(); return latestEntityControls - .filter(({ fieldValue }) => !fieldValue) + .filter(({ fieldValue }) => fieldValue === null) .map(({ fieldName }) => fieldName); }; @@ -328,6 +329,7 @@ export class TimeSeriesExplorer extends React.Component { }; entityFieldValueChanged = (entity, fieldValue) => { + // console.log(fieldValue, '___fieldValue___'); const { appStateHandler } = this.props; const entityControls = this.getControlsForDetector(); @@ -576,7 +578,7 @@ export class TimeSeriesExplorer extends React.Component { }; const nonBlankEntities = entityControls.filter(entity => { - return entity.fieldValue.length > 0; + return entity.fieldValue !== null; }); if ( @@ -739,7 +741,7 @@ export class TimeSeriesExplorer extends React.Component { const overFieldName = get(detector, 'over_field_name'); const byFieldName = get(detector, 'by_field_name'); if (partitionFieldName !== undefined) { - const partitionFieldValue = get(entitiesState, partitionFieldName, ''); + const partitionFieldValue = get(entitiesState, partitionFieldName, null); entities.push({ fieldType: 'partition_field', fieldName: partitionFieldName, @@ -747,7 +749,7 @@ export class TimeSeriesExplorer extends React.Component { }); } if (overFieldName !== undefined) { - const overFieldValue = get(entitiesState, overFieldName, ''); + const overFieldValue = get(entitiesState, overFieldName, null); entities.push({ fieldType: 'over_field', fieldName: overFieldName, @@ -761,7 +763,7 @@ export class TimeSeriesExplorer extends React.Component { // TODO - metric data can be filtered by this field, so should only exclude // from filter for the anomaly records. if (byFieldName !== undefined && overFieldName === undefined) { - const byFieldValue = get(entitiesState, byFieldName, ''); + const byFieldValue = get(entitiesState, byFieldName, null); entities.push({ fieldType: 'by_field', fieldName: byFieldName, fieldValue: byFieldValue }); } @@ -775,7 +777,7 @@ export class TimeSeriesExplorer extends React.Component { */ getCriteriaFields(detectorIndex, entities) { // Only filter on the entity if the field has a value. - const nonBlankEntities = entities.filter(entity => entity.fieldValue.length > 0); + const nonBlankEntities = entities.filter(entity => entity.fieldValue !== null); return [ { fieldName: 'detector_index', @@ -1150,7 +1152,7 @@ export class TimeSeriesExplorer extends React.Component { {entityControls.map(entity => { const entityKey = `${entity.fieldName}`; - const forceSelection = !hasEmptyFieldValues && !entity.fieldValue; + const forceSelection = !hasEmptyFieldValues && entity.fieldValue === null; hasEmptyFieldValues = !hasEmptyFieldValues && forceSelection; return ( Date: Fri, 27 Mar 2020 16:32:44 +0100 Subject: [PATCH 2/9] [ML] support empty field in anomaly table --- .../application/components/entity_cell/entity_cell.js | 8 +++++++- .../application/timeseriesexplorer/timeseriesexplorer.js | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.js b/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.js index 02a9e569f28a4..2457b67ab9d38 100644 --- a/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.js +++ b/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.js @@ -10,6 +10,8 @@ import React from 'react'; import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { EMPTY_FIELD_VALUE_LABEL } from '../../timeseriesexplorer/components/entity_control/entity_control'; +import { MLCATEGORY } from '../../../../common/constants/field_types'; function getAddFilter({ entityName, entityValue, filter }) { return ( @@ -68,7 +70,11 @@ export const EntityCell = function EntityCell({ filter, wrapText = false, }) { - const valueText = entityName !== 'mlcategory' ? entityValue : `mlcategory ${entityValue}`; + let valueText = entityValue === '' ? EMPTY_FIELD_VALUE_LABEL : entityValue; + if (entityName === MLCATEGORY) { + valueText = `${MLCATEGORY} ${valueText}`; + } + const textStyle = { maxWidth: '100%' }; const textWrapperClass = wrapText ? 'field-value-long' : 'field-value-short'; const shouldDisplayIcons = diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 6856ee4ccd4d7..9b1c55639aaa4 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -250,7 +250,7 @@ export class TimeSeriesExplorer extends React.Component { if (operator === '+' && entity.fieldValue !== value) { resultValue = value; } else if (operator === '-' && entity.fieldValue === value) { - resultValue = ''; + resultValue = null; } else { return; } From d420a7ed8bad22a41cd9951d1a02b62b532510bb Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 27 Mar 2020 16:54:20 +0100 Subject: [PATCH 3/9] [ML] remove comments --- .../components/entity_control/entity_control.tsx | 4 ++-- .../application/timeseriesexplorer/timeseriesexplorer.js | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx index 251b65ec70b17..e31a3cf696288 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx @@ -59,8 +59,8 @@ export class EntityControl extends Component { - // console.log(fieldValue, '___fieldValue___'); const { appStateHandler } = this.props; const entityControls = this.getControlsForDetector(); From 49f2225ce3803e7c88ebf332404d47e66cf8bb41 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 30 Mar 2020 14:19:01 +0200 Subject: [PATCH 4/9] [ML] fix context chart --- .../results_service/results_service.js | 36 +++---------------- .../entity_control/entity_control.tsx | 4 ++- 2 files changed, 8 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js index 4dec066a7f325..b7aa5edc88638 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js @@ -1259,39 +1259,13 @@ export function getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, lates }, { term: { job_id: jobId } }, ]; - const shouldCriteria = []; _.each(criteriaFields, criteria => { - if (criteria.fieldValue.length !== 0) { - mustCriteria.push({ - term: { - [criteria.fieldName]: criteria.fieldValue, - }, - }); - } else { - // Add special handling for blank entity field values, checking for either - // an empty string or the field not existing. - const emptyFieldCondition = { - bool: { - must: [ - { - term: {}, - }, - ], - }, - }; - emptyFieldCondition.bool.must[0].term[criteria.fieldName] = ''; - shouldCriteria.push(emptyFieldCondition); - shouldCriteria.push({ - bool: { - must_not: [ - { - exists: { field: criteria.fieldName }, - }, - ], - }, - }); - } + mustCriteria.push({ + term: { + [criteria.fieldName]: criteria.fieldValue, + }, + }); }); ml.esSearch({ diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx index e31a3cf696288..16b592de2ace3 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx @@ -63,7 +63,9 @@ export class EntityControl extends Component Date: Mon, 30 Mar 2020 16:13:56 +0200 Subject: [PATCH 5/9] [ML] rename empty field label, render as italic --- .../components/entity_cell/entity_cell.js | 2 +- .../components/entity_control/entity_control.tsx | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.js b/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.js index 2457b67ab9d38..d3917412bfb7b 100644 --- a/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.js +++ b/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.js @@ -70,7 +70,7 @@ export const EntityCell = function EntityCell({ filter, wrapText = false, }) { - let valueText = entityValue === '' ? EMPTY_FIELD_VALUE_LABEL : entityValue; + let valueText = entityValue === '' ? {EMPTY_FIELD_VALUE_LABEL} : entityValue; if (entityName === MLCATEGORY) { valueText = `${MLCATEGORY} ${valueText}`; } diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx index 16b592de2ace3..09a1867805106 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx @@ -16,6 +16,7 @@ import { EuiFormRow, EuiToolTip, } from '@elastic/eui'; +import { EuiSelectableOption } from '@elastic/eui/src/components/selectable/selectable_option'; export interface Entity { fieldName: string; @@ -38,7 +39,12 @@ interface EntityControlState { options: Array> | undefined; } -export const EMPTY_FIELD_VALUE_LABEL = '""'; +export const EMPTY_FIELD_VALUE_LABEL = i18n.translate( + 'xpack.ml.timeSeriesExplorer.emptyPartitionFieldLabel.', + { + defaultMessage: 'blank (empty string)', + } +); export class EntityControl extends Component { inputRef: any; @@ -107,6 +113,11 @@ export class EntityControl extends Component { + const { label } = option; + return label === EMPTY_FIELD_VALUE_LABEL ? {label} : label; + }; + render() { const { entity, forceSelection } = this.props; const { isLoading, options, selectedOptions } = this.state; @@ -130,6 +141,7 @@ export class EntityControl extends Component ); From 0832ca88fead7d92cac4a3d04a5cc406712ea7b1 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 30 Mar 2020 16:22:02 +0200 Subject: [PATCH 6/9] [ML] rename empty field label --- .../components/entity_control/entity_control.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx index 09a1867805106..7bb0b27472c88 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx @@ -42,7 +42,7 @@ interface EntityControlState { export const EMPTY_FIELD_VALUE_LABEL = i18n.translate( 'xpack.ml.timeSeriesExplorer.emptyPartitionFieldLabel.', { - defaultMessage: 'blank (empty string)', + defaultMessage: '"" (empty string)', } ); From f5a00e4be71cb30d1ccf37b09d8a0283f1f6950c Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 31 Mar 2020 10:48:28 +0200 Subject: [PATCH 7/9] [ML] fix focus chart --- .../public/application/timeseriesexplorer/timeseriesexplorer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index afa04c2332bf4..5e505757dd2aa 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -303,7 +303,7 @@ export class TimeSeriesExplorer extends React.Component { focusAggregationInterval, selectedForecastId, modelPlotEnabled, - entityControls.filter(entity => entity.fieldValue.length > 0), + entityControls.filter(entity => entity.fieldValue !== null), searchBounds, selectedJob, TIME_FIELD_NAME From f3b1e7bdd1e4cda13d60a1557ce8eac79b047522 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 31 Mar 2020 12:10:20 +0200 Subject: [PATCH 8/9] [ML] add time range capping for fields_service.ts --- .../models/fields_service/fields_service.ts | 58 +++++++++++++------ 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts index 567c5d2afb7de..6558d0d48ded9 100644 --- a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts +++ b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts @@ -6,6 +6,7 @@ import Boom from 'boom'; import { APICaller } from 'kibana/server'; +import { duration } from 'moment'; import { parseInterval } from '../../../common/util/parse_interval'; import { initCardinalityFieldsCache } from './fields_aggs_cache'; @@ -16,6 +17,19 @@ import { initCardinalityFieldsCache } from './fields_aggs_cache'; export function fieldsServiceProvider(callAsCurrentUser: APICaller) { const fieldsAggsCache = initCardinalityFieldsCache(); + /** + * Caps the time range to the last 90 days if necessary + */ + function getSafeTimeRange(earliestMs: number, latestMs: number): { start: number; end: number } { + const capOffsetMs = duration(3, 'months').asMilliseconds(); + const capRangeStart = latestMs - capOffsetMs; + + return { + start: Math.max(earliestMs, capRangeStart), + end: latestMs, + }; + } + /** * Gets aggregatable fields. */ @@ -61,12 +75,14 @@ export function fieldsServiceProvider(callAsCurrentUser: APICaller) { return {}; } + const { start, end } = getSafeTimeRange(earliestMs, latestMs); + const cachedValues = fieldsAggsCache.getValues( index, timeFieldName, - earliestMs, - latestMs, + start, + end, 'overallCardinality', fieldNames ) ?? {}; @@ -84,8 +100,8 @@ export function fieldsServiceProvider(callAsCurrentUser: APICaller) { { range: { [timeFieldName]: { - gte: earliestMs, - lte: latestMs, + gte: start, + lte: end, format: 'epoch_millis', }, }, @@ -130,7 +146,7 @@ export function fieldsServiceProvider(callAsCurrentUser: APICaller) { return obj; }, {} as { [field: string]: number }); - fieldsAggsCache.updateValues(index, timeFieldName, earliestMs, latestMs, { + fieldsAggsCache.updateValues(index, timeFieldName, start, end, { overallCardinality: aggResult, }); @@ -185,15 +201,16 @@ export function fieldsServiceProvider(callAsCurrentUser: APICaller) { } /** - * Caps provided time boundaries based on the interval. - * @param earliestMs - * @param latestMs - * @param interval + * Caps provided time boundaries based on the interval */ - function getSafeTimeRange( + function getSafeTimeRangeForInterval( + interval: string, + ...timeRange: number[] + ): { start: number; end: number }; + function getSafeTimeRangeForInterval( + interval: string, earliestMs: number, - latestMs: number, - interval: string + latestMs: number ): { start: number; end: number } { const maxNumberOfBuckets = 1000; const end = latestMs; @@ -234,7 +251,7 @@ export function fieldsServiceProvider(callAsCurrentUser: APICaller) { interval: string | undefined ): Promise<{ [key: string]: number }> { if (!interval) { - throw new Error('Interval is required to retrieve max bucket cardinalities.'); + throw Boom.badRequest('Interval is required to retrieve max bucket cardinalities.'); } const aggregatableFields = await getAggregatableFields(index, fieldNames); @@ -243,12 +260,17 @@ export function fieldsServiceProvider(callAsCurrentUser: APICaller) { return {}; } + const { start, end } = getSafeTimeRangeForInterval( + interval, + ...Object.values(getSafeTimeRange(earliestMs, latestMs)) + ); + const cachedValues = fieldsAggsCache.getValues( index, timeFieldName, - earliestMs, - latestMs, + start, + end, 'maxBucketCardinality', fieldNames ) ?? {}; @@ -260,8 +282,6 @@ export function fieldsServiceProvider(callAsCurrentUser: APICaller) { return cachedValues; } - const { start, end } = getSafeTimeRange(earliestMs, latestMs, interval); - const mustCriteria = [ { range: { @@ -334,6 +354,10 @@ export function fieldsServiceProvider(callAsCurrentUser: APICaller) { return obj; }, {} as { [field: string]: number }); + fieldsAggsCache.updateValues(index, timeFieldName, start, end, { + maxBucketCardinality: aggResult, + }); + return { ...cachedValues, ...aggResult, From 59c0cef4f7f646b241c5ee8f2a240f6738307987 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 31 Mar 2020 14:02:44 +0200 Subject: [PATCH 9/9] [ML] empty string labels in anomaly explorer --- .../ml/public/application/explorer/explorer_swimlane.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.js index e8cb8377a656d..d7333f00c89cd 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.js @@ -25,6 +25,7 @@ import { mlChartTooltipService } from '../components/chart_tooltip/chart_tooltip import { ALLOW_CELL_RANGE_SELECTION, dragSelect$ } from './explorer_dashboard_service'; import { DRAG_SELECT_ACTION } from './explorer_constants'; import { i18n } from '@kbn/i18n'; +import { EMPTY_FIELD_VALUE_LABEL } from '../timeseriesexplorer/components/entity_control/entity_control'; const SCSS = { mlDragselectDragging: 'mlDragselectDragging', @@ -309,6 +310,7 @@ export class ExplorerSwimlane extends React.Component { return function(lane) { const bucketScore = getBucketScore(lane, time); if (bucketScore !== 0) { + lane = lane === '' ? EMPTY_FIELD_VALUE_LABEL : lane; cellMouseover(this, lane, bucketScore, i, time); } }; @@ -376,7 +378,7 @@ export class ExplorerSwimlane extends React.Component { values: { label: mlEscape(label) }, }); } else { - return mlEscape(label); + return label === '' ? `${EMPTY_FIELD_VALUE_LABEL}` : mlEscape(label); } }) .on('click', () => { @@ -393,7 +395,7 @@ export class ExplorerSwimlane extends React.Component { { skipHeader: true }, { label: swimlaneData.fieldName, - value, + value: value === '' ? EMPTY_FIELD_VALUE_LABEL : value, seriesIdentifier: { key: value }, valueAccessor: 'fieldName', },