mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ML] Indicate multi-bucket anomalies in results dashboards (#23746)
This commit is contained in:
parent
c993ad3996
commit
557fc7a66f
12 changed files with 263 additions and 65 deletions
15
x-pack/plugins/ml/common/constants/multi_bucket_impact.js
Normal file
15
x-pack/plugins/ml/common/constants/multi_bucket_impact.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
// Thresholds for indicating the impact of multi-bucket features in an anomaly.
|
||||
export const MULTI_BUCKET_IMPACT = {
|
||||
HIGH: 4,
|
||||
MEDIUM: 3,
|
||||
LOW: 1,
|
||||
NONE: -5
|
||||
};
|
|
@ -11,6 +11,7 @@ import {
|
|||
getSeverity,
|
||||
getSeverityWithLow,
|
||||
getSeverityColor,
|
||||
getMultiBucketImpactLabel,
|
||||
getEntityFieldName,
|
||||
getEntityFieldValue,
|
||||
showActualForFunction,
|
||||
|
@ -281,6 +282,35 @@ describe('ML - anomaly utils', () => {
|
|||
|
||||
});
|
||||
|
||||
describe('getMultiBucketImpactLabel', () => {
|
||||
|
||||
it('returns high for 4 <= score <= 5', () => {
|
||||
expect(getMultiBucketImpactLabel(4)).to.be('high');
|
||||
expect(getMultiBucketImpactLabel(5)).to.be('high');
|
||||
});
|
||||
|
||||
it('returns medium for 3 <= score < 4', () => {
|
||||
expect(getMultiBucketImpactLabel(3)).to.be('medium');
|
||||
expect(getMultiBucketImpactLabel(3.99)).to.be('medium');
|
||||
});
|
||||
|
||||
it('returns low for 1 <= score < 3', () => {
|
||||
expect(getMultiBucketImpactLabel(1)).to.be('low');
|
||||
expect(getMultiBucketImpactLabel(2.99)).to.be('low');
|
||||
});
|
||||
|
||||
it('returns none for -5 <= score < 1', () => {
|
||||
expect(getMultiBucketImpactLabel(-5)).to.be('none');
|
||||
expect(getMultiBucketImpactLabel(0.99)).to.be('none');
|
||||
});
|
||||
|
||||
it('returns expected label when impact outside normal bounds', () => {
|
||||
expect(getMultiBucketImpactLabel(10)).to.be('high');
|
||||
expect(getMultiBucketImpactLabel(-10)).to.be('none');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('getEntityFieldName', () => {
|
||||
it('returns the by field name', () => {
|
||||
expect(getEntityFieldName(byEntityRecord)).to.be('airline');
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
import { CONDITIONS_NOT_SUPPORTED_FUNCTIONS } from '../constants/detector_rule';
|
||||
import { MULTI_BUCKET_IMPACT } from '../constants/multi_bucket_impact';
|
||||
|
||||
// List of function descriptions for which actual values from record level results should be displayed.
|
||||
const DISPLAY_ACTUAL_FUNCTIONS = ['count', 'distinct_count', 'lat_long', 'mean', 'max', 'min', 'sum',
|
||||
|
@ -75,6 +76,21 @@ export function getSeverityColor(normalizedScore) {
|
|||
}
|
||||
}
|
||||
|
||||
// Returns a label to use for the multi-bucket impact of an anomaly
|
||||
// according to the value of the multi_bucket_impact field of a record,
|
||||
// which ranges from -5 to +5.
|
||||
export function getMultiBucketImpactLabel(multiBucketImpact) {
|
||||
if (multiBucketImpact >= MULTI_BUCKET_IMPACT.HIGH) {
|
||||
return 'high';
|
||||
} else if (multiBucketImpact >= MULTI_BUCKET_IMPACT.MEDIUM) {
|
||||
return 'medium';
|
||||
} else if (multiBucketImpact >= MULTI_BUCKET_IMPACT.LOW) {
|
||||
return 'low';
|
||||
} else {
|
||||
return 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the name of the field to use as the entity name from the source record
|
||||
// obtained from Elasticsearch. The function looks first for a by_field, then over_field,
|
||||
// then partition_field, returning undefined if none of these fields are present.
|
||||
|
|
|
@ -25,11 +25,13 @@ import { formatDate } from '@elastic/eui/lib/services/format';
|
|||
|
||||
import { EntityCell } from './entity_cell';
|
||||
import {
|
||||
getMultiBucketImpactLabel,
|
||||
getSeverity,
|
||||
showActualForFunction,
|
||||
showTypicalForFunction
|
||||
} from 'plugins/ml/../common/util/anomaly_utils';
|
||||
import { formatValue } from 'plugins/ml/formatters/format_value';
|
||||
showTypicalForFunction,
|
||||
} from '../../../common/util/anomaly_utils';
|
||||
import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impact';
|
||||
import { formatValue } from '../../formatters/format_value';
|
||||
|
||||
const TIME_FIELD_NAME = 'timestamp';
|
||||
|
||||
|
@ -152,6 +154,14 @@ function getDetailsItems(anomaly, examples, filter) {
|
|||
description: anomaly.jobId
|
||||
});
|
||||
|
||||
if (source.multi_bucket_impact !== undefined &&
|
||||
source.multi_bucket_impact >= MULTI_BUCKET_IMPACT.LOW) {
|
||||
items.push({
|
||||
title: 'multi-bucket impact',
|
||||
description: getMultiBucketImpactLabel(source.multi_bucket_impact)
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
title: 'probability',
|
||||
description: source.probability
|
||||
|
|
|
@ -22,12 +22,20 @@ import moment from 'moment';
|
|||
// don't use something like plugins/ml/../common
|
||||
// because it won't work with the jest tests
|
||||
import { formatValue } from '../../formatters/format_value';
|
||||
import { getSeverityWithLow } from '../../../common/util/anomaly_utils';
|
||||
import {
|
||||
getSeverityWithLow,
|
||||
getMultiBucketImpactLabel,
|
||||
} from '../../../common/util/anomaly_utils';
|
||||
import {
|
||||
LINE_CHART_ANOMALY_RADIUS,
|
||||
MULTI_BUCKET_SYMBOL_SIZE,
|
||||
SCHEDULED_EVENT_SYMBOL_HEIGHT,
|
||||
drawLineChartDots,
|
||||
getTickValues,
|
||||
numTicksForDateFormat,
|
||||
removeLabelOverlap
|
||||
removeLabelOverlap,
|
||||
showMultiBucketAnomalyMarker,
|
||||
showMultiBucketAnomalyTooltip,
|
||||
} from '../../util/chart_utils';
|
||||
import { TimeBuckets } from 'ui/time_buckets';
|
||||
import { LoadingIndicator } from '../../components/loading_indicator/loading_indicator';
|
||||
|
@ -74,8 +82,6 @@ export class ExplorerChart extends React.Component {
|
|||
|
||||
let vizWidth = 0;
|
||||
const chartHeight = 170;
|
||||
const LINE_CHART_ANOMALY_RADIUS = 7;
|
||||
const SCHEDULED_EVENT_MARKER_HEIGHT = 5;
|
||||
|
||||
// Left margin is adjusted later for longest y-axis label.
|
||||
const margin = { top: 10, right: 0, bottom: 30, left: 60 };
|
||||
|
@ -260,11 +266,11 @@ export class ExplorerChart extends React.Component {
|
|||
function drawLineChartMarkers(data) {
|
||||
// Render circle markers for the points.
|
||||
// These are used for displaying tooltips on mouseover.
|
||||
// Don't render dots where value=null (data gaps)
|
||||
// Don't render dots where value=null (data gaps) or for multi-bucket anomalies.
|
||||
const dots = lineChartGroup.append('g')
|
||||
.attr('class', 'chart-markers')
|
||||
.selectAll('.metric-value')
|
||||
.data(data.filter(d => d.value !== null));
|
||||
.data(data.filter(d => (d.value !== null && !showMultiBucketAnomalyMarker(d))));
|
||||
|
||||
// Remove dots that are no longer needed i.e. if number of chart points has decreased.
|
||||
dots.exit().remove();
|
||||
|
@ -283,12 +289,28 @@ export class ExplorerChart extends React.Component {
|
|||
.attr('class', function (d) {
|
||||
let markerClass = 'metric-value';
|
||||
if (_.has(d, 'anomalyScore') && Number(d.anomalyScore) >= threshold.val) {
|
||||
markerClass += ' anomaly-marker ';
|
||||
markerClass += getSeverityWithLow(d.anomalyScore);
|
||||
markerClass += ` anomaly-marker ${getSeverityWithLow(d.anomalyScore)}`;
|
||||
}
|
||||
return markerClass;
|
||||
});
|
||||
|
||||
// Render cross symbols for any multi-bucket anomalies.
|
||||
const multiBucketMarkers = lineChartGroup.select('.chart-markers').selectAll('.multi-bucket')
|
||||
.data(data.filter(d => (d.anomalyScore !== null && showMultiBucketAnomalyMarker(d) === true)));
|
||||
|
||||
// Remove multi-bucket markers that are no longer needed
|
||||
multiBucketMarkers.exit().remove();
|
||||
|
||||
// Update markers to new positions.
|
||||
multiBucketMarkers.enter().append('path')
|
||||
.attr('d', d3.svg.symbol().size(MULTI_BUCKET_SYMBOL_SIZE).type('cross'))
|
||||
.attr('transform', d => `translate(${lineChartXScale(d.date)}, ${lineChartYScale(d.value)})`)
|
||||
.attr('class', d => `metric-value anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore)}`)
|
||||
.on('mouseover', function (d) {
|
||||
showLineChartTooltip(d, this);
|
||||
})
|
||||
.on('mouseout', () => mlChartTooltipService.hide());
|
||||
|
||||
// Add rectangular markers for any scheduled events.
|
||||
const scheduledEventMarkers = lineChartGroup.select('.chart-markers').selectAll('.scheduled-event-marker')
|
||||
.data(data.filter(d => d.scheduledEvents !== undefined));
|
||||
|
@ -298,14 +320,14 @@ export class ExplorerChart extends React.Component {
|
|||
// Create any new markers that are needed i.e. if number of chart points has increased.
|
||||
scheduledEventMarkers.enter().append('rect')
|
||||
.attr('width', LINE_CHART_ANOMALY_RADIUS * 2)
|
||||
.attr('height', SCHEDULED_EVENT_MARKER_HEIGHT)
|
||||
.attr('height', SCHEDULED_EVENT_SYMBOL_HEIGHT)
|
||||
.attr('class', 'scheduled-event-marker')
|
||||
.attr('rx', 1)
|
||||
.attr('ry', 1);
|
||||
|
||||
// Update all markers to new positions.
|
||||
scheduledEventMarkers.attr('x', (d) => lineChartXScale(d.date) - LINE_CHART_ANOMALY_RADIUS)
|
||||
.attr('y', (d) => lineChartYScale(d.value) - (SCHEDULED_EVENT_MARKER_HEIGHT / 2));
|
||||
.attr('y', (d) => lineChartYScale(d.value) - (SCHEDULED_EVENT_SYMBOL_HEIGHT / 2));
|
||||
|
||||
}
|
||||
|
||||
|
@ -319,6 +341,11 @@ export class ExplorerChart extends React.Component {
|
|||
const score = parseInt(marker.anomalyScore);
|
||||
const displayScore = (score > 0 ? score : '< 1');
|
||||
contents += ('anomaly score: ' + displayScore);
|
||||
|
||||
if (showMultiBucketAnomalyTooltip(marker) === true) {
|
||||
contents += `<br/>multi-bucket impact: ${getMultiBucketImpactLabel(marker.multiBucketImpact)}`;
|
||||
}
|
||||
|
||||
// Show actual/typical when available except for rare detectors.
|
||||
// Rare detectors always have 1 as actual and the probability as typical.
|
||||
// Exposing those values in the tooltip with actual/typical labels might irritate users.
|
||||
|
|
|
@ -209,6 +209,10 @@ export function explorerChartsContainerServiceFactory(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_.has(record, 'multi_bucket_impact')) {
|
||||
chartPoint.multiBucketImpact = record.multi_bucket_impact;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -64,44 +64,44 @@ ml-explorer-chart,
|
|||
fill: #32a7c2;
|
||||
}
|
||||
|
||||
circle.metric-value {
|
||||
.metric-value {
|
||||
opacity: 1;
|
||||
fill: transparent;
|
||||
stroke: #32a7c2;
|
||||
stroke-width: 0px;
|
||||
}
|
||||
|
||||
circle.anomaly-marker {
|
||||
.anomaly-marker {
|
||||
stroke-width: 1px;
|
||||
stroke: #aaaaaa;
|
||||
}
|
||||
|
||||
circle.anomaly-marker:hover {
|
||||
.anomaly-marker:hover {
|
||||
stroke-width: 6px;
|
||||
stroke: #32a7c2;
|
||||
}
|
||||
|
||||
circle.metric-value.critical {
|
||||
.metric-value.critical {
|
||||
fill: #fe5050;
|
||||
}
|
||||
|
||||
circle.metric-value.major {
|
||||
.metric-value.major {
|
||||
fill: #ff7e00;
|
||||
}
|
||||
|
||||
circle.metric-value.minor {
|
||||
.metric-value.minor {
|
||||
fill: #ffdd00;
|
||||
}
|
||||
|
||||
circle.metric-value.warning {
|
||||
.metric-value.warning {
|
||||
fill: #8bc8fb;
|
||||
}
|
||||
|
||||
circle.metric-value.low {
|
||||
.metric-value.low {
|
||||
fill: #d2e9f7;
|
||||
}
|
||||
|
||||
circle.metric-value:hover {
|
||||
.metric-value:hover {
|
||||
stroke-width: 6px;
|
||||
stroke-opacity: 0.65;
|
||||
}
|
||||
|
|
|
@ -151,40 +151,47 @@
|
|||
fill: rgba(204, 163, 0, 0.25);
|
||||
}
|
||||
|
||||
circle.metric-value {
|
||||
.metric-value {
|
||||
opacity: 1;
|
||||
fill: transparent;
|
||||
stroke: #32a7c2;
|
||||
stroke-width: 0px;
|
||||
}
|
||||
|
||||
circle.anomaly-marker {
|
||||
.anomaly-marker {
|
||||
stroke-width: 1px;
|
||||
stroke: #aaaaaa;
|
||||
}
|
||||
|
||||
circle.metric-value.critical {
|
||||
.metric-value.critical {
|
||||
fill: #fe5050;
|
||||
}
|
||||
|
||||
circle.metric-value.major {
|
||||
.metric-value.major {
|
||||
fill: #ff7e00;
|
||||
}
|
||||
|
||||
circle.metric-value.minor {
|
||||
.metric-value.minor {
|
||||
fill: #ffdd00;
|
||||
}
|
||||
|
||||
circle.metric-value.warning {
|
||||
.metric-value.warning {
|
||||
fill: #8bc8fb;
|
||||
}
|
||||
|
||||
circle.metric-value.low {
|
||||
.metric-value.low {
|
||||
fill: #d2e9f7;
|
||||
}
|
||||
|
||||
circle.metric-value:hover,
|
||||
circle.anomaly-marker.highlighted {
|
||||
.metric-value:hover,
|
||||
.anomaly-marker.highlighted {
|
||||
stroke-width: 6px;
|
||||
stroke-opacity: 0.65;
|
||||
stroke: #32a7c2;
|
||||
}
|
||||
|
||||
.metric-value:hover,
|
||||
.anomaly-marker.highlighted {
|
||||
stroke-width: 6px;
|
||||
stroke-opacity: 0.65;
|
||||
stroke: #32a7c2;
|
||||
|
@ -198,8 +205,8 @@
|
|||
}
|
||||
|
||||
.forecast {
|
||||
circle.metric-value,
|
||||
circle.metric-value:hover {
|
||||
.metric-value,
|
||||
.metric-value:hover {
|
||||
stroke: #cca300;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,12 +20,20 @@ import { timefilter } from 'ui/timefilter';
|
|||
|
||||
import { ResizeChecker } from 'ui/resize_checker';
|
||||
|
||||
import { getSeverityWithLow } from 'plugins/ml/../common/util/anomaly_utils';
|
||||
import {
|
||||
getSeverityWithLow,
|
||||
getMultiBucketImpactLabel,
|
||||
} from 'plugins/ml/../common/util/anomaly_utils';
|
||||
import { formatValue } from 'plugins/ml/formatters/format_value';
|
||||
import {
|
||||
LINE_CHART_ANOMALY_RADIUS,
|
||||
MULTI_BUCKET_SYMBOL_SIZE,
|
||||
SCHEDULED_EVENT_SYMBOL_HEIGHT,
|
||||
drawLineChartDots,
|
||||
filterAxisLabels,
|
||||
numTicksForDateFormat
|
||||
numTicksForDateFormat,
|
||||
showMultiBucketAnomalyMarker,
|
||||
showMultiBucketAnomalyTooltip,
|
||||
} from 'plugins/ml/util/chart_utils';
|
||||
import { TimeBuckets } from 'ui/time_buckets';
|
||||
import { mlAnomaliesTableService } from 'plugins/ml/components/anomalies_table/anomalies_table_service';
|
||||
|
@ -55,9 +63,6 @@ module.directive('mlTimeseriesChart', function () {
|
|||
const svgHeight = focusHeight + contextChartHeight + swimlaneHeight + chartSpacing + margin.top + margin.bottom;
|
||||
let vizWidth = svgWidth - margin.left - margin.right;
|
||||
|
||||
const FOCUS_CHART_ANOMALY_RADIUS = 7;
|
||||
const SCHEDULED_EVENT_MARKER_HEIGHT = 5;
|
||||
|
||||
const ZOOM_INTERVAL_OPTIONS = [
|
||||
{ duration: moment.duration(1, 'h'), label: '1h' },
|
||||
{ duration: moment.duration(12, 'h'), label: '12h' },
|
||||
|
@ -459,15 +464,15 @@ module.directive('mlTimeseriesChart', function () {
|
|||
|
||||
// Render circle markers for the points.
|
||||
// These are used for displaying tooltips on mouseover.
|
||||
// Don't render dots where value=null (data gaps)
|
||||
// Don't render dots where value=null (data gaps) or for multi-bucket anomalies.
|
||||
const dots = d3.select('.focus-chart-markers').selectAll('.metric-value')
|
||||
.data(data.filter(d => d.value !== null));
|
||||
.data(data.filter(d => (d.value !== null && !showMultiBucketAnomalyMarker(d))));
|
||||
|
||||
// Remove dots that are no longer needed i.e. if number of chart points has decreased.
|
||||
dots.exit().remove();
|
||||
// Create any new dots that are needed i.e. if number of chart points has increased.
|
||||
dots.enter().append('circle')
|
||||
.attr('r', FOCUS_CHART_ANOMALY_RADIUS)
|
||||
.attr('r', LINE_CHART_ANOMALY_RADIUS)
|
||||
.on('mouseover', function (d) {
|
||||
showFocusChartTooltip(d, this);
|
||||
})
|
||||
|
@ -479,12 +484,29 @@ module.directive('mlTimeseriesChart', function () {
|
|||
.attr('class', (d) => {
|
||||
let markerClass = 'metric-value';
|
||||
if (_.has(d, 'anomalyScore')) {
|
||||
markerClass += ' anomaly-marker ';
|
||||
markerClass += getSeverityWithLow(d.anomalyScore);
|
||||
markerClass += ` anomaly-marker ${getSeverityWithLow(d.anomalyScore)}`;
|
||||
}
|
||||
return markerClass;
|
||||
});
|
||||
|
||||
// Render cross symbols for any multi-bucket anomalies.
|
||||
const multiBucketMarkers = d3.select('.focus-chart-markers').selectAll('.multi-bucket')
|
||||
.data(data.filter(d => (d.anomalyScore !== null && showMultiBucketAnomalyMarker(d) === true)));
|
||||
|
||||
// Remove multi-bucket markers that are no longer needed
|
||||
multiBucketMarkers.exit().remove();
|
||||
|
||||
// Update markers to new positions.
|
||||
multiBucketMarkers.enter().append('path')
|
||||
.attr('d', d3.svg.symbol().size(MULTI_BUCKET_SYMBOL_SIZE).type('cross'))
|
||||
.attr('transform', d => `translate(${focusXScale(d.date)}, ${focusYScale(d.value)})`)
|
||||
.attr('class', d => `metric-value anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore)}`)
|
||||
.on('mouseover', function (d) {
|
||||
showFocusChartTooltip(d, this);
|
||||
})
|
||||
.on('mouseout', () => mlChartTooltipService.hide());
|
||||
|
||||
|
||||
// Add rectangular markers for any scheduled events.
|
||||
const scheduledEventMarkers = d3.select('.focus-chart-markers').selectAll('.scheduled-event-marker')
|
||||
.data(data.filter(d => d.scheduledEvents !== undefined));
|
||||
|
@ -494,14 +516,14 @@ module.directive('mlTimeseriesChart', function () {
|
|||
|
||||
// Create any new markers that are needed i.e. if number of chart points has increased.
|
||||
scheduledEventMarkers.enter().append('rect')
|
||||
.attr('width', FOCUS_CHART_ANOMALY_RADIUS * 2)
|
||||
.attr('height', SCHEDULED_EVENT_MARKER_HEIGHT)
|
||||
.attr('width', LINE_CHART_ANOMALY_RADIUS * 2)
|
||||
.attr('height', SCHEDULED_EVENT_SYMBOL_HEIGHT)
|
||||
.attr('class', 'scheduled-event-marker')
|
||||
.attr('rx', 1)
|
||||
.attr('ry', 1);
|
||||
|
||||
// Update all markers to new positions.
|
||||
scheduledEventMarkers.attr('x', (d) => focusXScale(d.date) - FOCUS_CHART_ANOMALY_RADIUS)
|
||||
scheduledEventMarkers.attr('x', (d) => focusXScale(d.date) - LINE_CHART_ANOMALY_RADIUS)
|
||||
.attr('y', (d) => focusYScale(d.value) - 3);
|
||||
|
||||
// Plot any forecast data in scope.
|
||||
|
@ -520,7 +542,7 @@ module.directive('mlTimeseriesChart', function () {
|
|||
forecastDots.exit().remove();
|
||||
// Create any new dots that are needed i.e. if number of forecast points has increased.
|
||||
forecastDots.enter().append('circle')
|
||||
.attr('r', FOCUS_CHART_ANOMALY_RADIUS)
|
||||
.attr('r', LINE_CHART_ANOMALY_RADIUS)
|
||||
.on('mouseover', function (d) {
|
||||
showFocusChartTooltip(d, this);
|
||||
})
|
||||
|
@ -959,6 +981,10 @@ module.directive('mlTimeseriesChart', function () {
|
|||
const displayScore = (score > 0 ? score : '< 1');
|
||||
contents += `anomaly score: ${displayScore}<br/>`;
|
||||
|
||||
if (showMultiBucketAnomalyTooltip(marker) === true) {
|
||||
contents += `multi-bucket impact: ${getMultiBucketImpactLabel(marker.multiBucketImpact)}<br/>`;
|
||||
}
|
||||
|
||||
if (scope.modelPlotEnabled === false) {
|
||||
// Show actual/typical when available except for rare detectors.
|
||||
// Rare detectors always have 1 as actual and the probability as typical.
|
||||
|
@ -1006,7 +1032,7 @@ module.directive('mlTimeseriesChart', function () {
|
|||
}
|
||||
|
||||
mlChartTooltipService.show(contents, circle, {
|
||||
x: FOCUS_CHART_ANOMALY_RADIUS * 2,
|
||||
x: LINE_CHART_ANOMALY_RADIUS * 2,
|
||||
y: 0
|
||||
});
|
||||
}
|
||||
|
@ -1024,17 +1050,21 @@ module.directive('mlTimeseriesChart', function () {
|
|||
// TODO - plot anomaly markers for cases where there is an anomaly due
|
||||
// to the absence of data and model plot is enabled.
|
||||
if (markerToSelect !== undefined) {
|
||||
const selectedMarker = d3.select('.focus-chart-markers').selectAll('.focus-chart-highlighted-marker')
|
||||
const selectedMarker = d3.select('.focus-chart-markers')
|
||||
.selectAll('.focus-chart-highlighted-marker')
|
||||
.data([markerToSelect]);
|
||||
selectedMarker.enter().append('circle')
|
||||
.attr('r', FOCUS_CHART_ANOMALY_RADIUS);
|
||||
selectedMarker.attr('cx', (d) => { return focusXScale(d.date); })
|
||||
.attr('cy', (d) => { return focusYScale(d.value); })
|
||||
.attr('class', (d) => {
|
||||
let markerClass = 'metric-value anomaly-marker highlighted ';
|
||||
markerClass += getSeverityWithLow(d.anomalyScore);
|
||||
return markerClass;
|
||||
});
|
||||
if (showMultiBucketAnomalyMarker(markerToSelect) === true) {
|
||||
selectedMarker.enter().append('path')
|
||||
.attr('d', d3.svg.symbol().size(MULTI_BUCKET_SYMBOL_SIZE).type('cross'))
|
||||
.attr('transform', d => `translate(${focusXScale(d.date)}, ${focusYScale(d.value)})`);
|
||||
} else {
|
||||
selectedMarker.enter().append('circle')
|
||||
.attr('r', LINE_CHART_ANOMALY_RADIUS)
|
||||
.attr('cx', d => focusXScale(d.date))
|
||||
.attr('cy', d => focusYScale(d.value));
|
||||
}
|
||||
selectedMarker.attr('class',
|
||||
d => `metric-value anomaly-marker ${getSeverityWithLow(d.anomalyScore)} highlighted`);
|
||||
|
||||
// Display the chart tooltip for this marker.
|
||||
// Note the values of the record and marker may differ depending on the levels of aggregation.
|
||||
|
|
|
@ -91,17 +91,17 @@ export function processRecordScoreResults(scoreData) {
|
|||
return bucketScoreData;
|
||||
}
|
||||
|
||||
// Uses data from the list of anomaly records to add anomalyScore properties
|
||||
// to the chartData entries for anomalous buckets.
|
||||
// Uses data from the list of anomaly records to add anomalyScore,
|
||||
// function, actual and typical properties, plus causes and multi-bucket
|
||||
// info if applicable, to the chartData entries for anomalous buckets.
|
||||
export function processDataForFocusAnomalies(
|
||||
chartData,
|
||||
anomalyRecords,
|
||||
timeFieldName) {
|
||||
|
||||
// Iterate through the anomaly records, adding anomalyScore, function,
|
||||
// actual and typical properties, plus causes info if applicable,
|
||||
// to the chartData entries for anomalous buckets.
|
||||
_.each(anomalyRecords, (record) => {
|
||||
// Iterate through the anomaly records adding the
|
||||
// various properties required for display.
|
||||
anomalyRecords.forEach((record) => {
|
||||
|
||||
// Look for a chart point with the same time as the record.
|
||||
// If none found, find closest time in chartData set.
|
||||
|
@ -146,6 +146,10 @@ export function processDataForFocusAnomalies(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_.has(record, 'multi_bucket_impact')) {
|
||||
chartPoint.multiBucketImpact = record.multi_bucket_impact;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,14 @@
|
|||
import $ from 'jquery';
|
||||
import d3 from 'd3';
|
||||
import expect from 'expect.js';
|
||||
import { chartLimits, filterAxisLabels, numTicks } from '../chart_utils';
|
||||
import {
|
||||
chartLimits,
|
||||
filterAxisLabels,
|
||||
numTicks,
|
||||
showMultiBucketAnomalyMarker,
|
||||
showMultiBucketAnomalyTooltip,
|
||||
} from '../chart_utils';
|
||||
import { MULTI_BUCKET_IMPACT } from 'plugins/ml/../common/constants/multi_bucket_impact';
|
||||
|
||||
describe('ML - chart utils', () => {
|
||||
|
||||
|
@ -120,4 +127,34 @@ describe('ML - chart utils', () => {
|
|||
|
||||
});
|
||||
|
||||
describe('showMultiBucketAnomalyMarker', () => {
|
||||
|
||||
it('returns true for points with multiBucketImpact at or above medium impact', () => {
|
||||
expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.HIGH })).to.be(true);
|
||||
expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.MEDIUM })).to.be(true);
|
||||
});
|
||||
|
||||
it('returns false for points with multiBucketImpact missing or below medium impact', () => {
|
||||
expect(showMultiBucketAnomalyMarker({})).to.be(false);
|
||||
expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.LOW })).to.be(false);
|
||||
expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.NONE })).to.be(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('showMultiBucketAnomalyTooltip', () => {
|
||||
|
||||
it('returns true for points with multiBucketImpact at or above low impact', () => {
|
||||
expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.HIGH })).to.be(true);
|
||||
expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.MEDIUM })).to.be(true);
|
||||
expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.LOW })).to.be(true);
|
||||
});
|
||||
|
||||
it('returns false for points with multiBucketImpact missing or below medium impact', () => {
|
||||
expect(showMultiBucketAnomalyTooltip({})).to.be(false);
|
||||
expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.NONE })).to.be(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -8,12 +8,18 @@
|
|||
|
||||
import d3 from 'd3';
|
||||
import { calculateTextWidth } from '../util/string_utils';
|
||||
import { MULTI_BUCKET_IMPACT } from '../../common/constants/multi_bucket_impact';
|
||||
import moment from 'moment';
|
||||
import rison from 'rison-node';
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import { timefilter } from 'ui/timefilter';
|
||||
|
||||
|
||||
export const LINE_CHART_ANOMALY_RADIUS = 7;
|
||||
export const MULTI_BUCKET_SYMBOL_SIZE = 144; // In square pixels for use with d3 symbol.size
|
||||
export const SCHEDULED_EVENT_SYMBOL_HEIGHT = 5;
|
||||
|
||||
const MAX_LABEL_WIDTH = 100;
|
||||
|
||||
export function chartLimits(data = []) {
|
||||
|
@ -178,6 +184,18 @@ export function getExploreSeriesLink(series) {
|
|||
return `${chrome.getBasePath()}/app/ml#/timeseriesexplorer?_g=${_g}&_a=${encodeURIComponent(_a)}`;
|
||||
}
|
||||
|
||||
export function showMultiBucketAnomalyMarker(point) {
|
||||
// TODO - test threshold with real use cases
|
||||
return (point.multiBucketImpact !== undefined &&
|
||||
point.multiBucketImpact >= MULTI_BUCKET_IMPACT.MEDIUM);
|
||||
}
|
||||
|
||||
export function showMultiBucketAnomalyTooltip(point) {
|
||||
// TODO - test threshold with real use cases
|
||||
return (point.multiBucketImpact !== undefined &&
|
||||
point.multiBucketImpact >= MULTI_BUCKET_IMPACT.LOW);
|
||||
}
|
||||
|
||||
export function numTicks(axisWidth) {
|
||||
return axisWidth / MAX_LABEL_WIDTH;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue