Skip to content

Commit

Permalink
[ML] Anomaly explorer: Show Data Gaps and Connect Anomalous Points on…
Browse files Browse the repository at this point in the history
… Single Metric Charts (elastic#194119)

## Summary

Fix for [elastic#193885](elastic#193885)

Single metric viewer:
![Screenshot 2024-09-26 at 13 31
35](https://github.com/user-attachments/assets/3576758e-9cb1-4c55-bd84-40f2095cc54f)

Anomaly explorer:
| Before | After |
| ------------- | ------------- |
|
![image](https://github.com/user-attachments/assets/89f48abd-26ac-4dd4-ab10-b1c8198f50ff)
| ![Screenshot 2024-09-26 at 13 32
05](https://github.com/user-attachments/assets/e3a6e1e3-6238-4c01-a372-fa0c25475fbd)
|
  • Loading branch information
rbrtj authored Sep 30, 2024
1 parent 9a7e912 commit 32144fc
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -368,14 +368,17 @@ export class ExplorerChartSingleMetric extends React.Component {
// These are used for displaying tooltips on mouseover.
// Don't render dots where value=null (data gaps, with no anomalies)
// or for multi-bucket anomalies.
// Except for scheduled events.
const dots = lineChartGroup
.append('g')
.attr('class', 'chart-markers')
.selectAll('.metric-value')
.data(
data.filter(
(d) =>
(d.value !== null || typeof d.anomalyScore === 'number') &&
(d.value !== null ||
typeof d.anomalyScore === 'number' ||
d.scheduledEvents !== undefined) &&
!showMultiBucketAnomalyMarker(d)
)
);
Expand Down Expand Up @@ -407,7 +410,11 @@ export class ExplorerChartSingleMetric extends React.Component {
// Update all dots to new positions.
dots
.attr('cx', (d) => lineChartXScale(d.date))
.attr('cy', (d) => lineChartYScale(d.value))
// Fallback with domain's min value if value is null
// To ensure event markers are rendered properly at the bottom of the chart
.attr('cy', (d) =>
lineChartYScale(d.value !== null ? d.value : lineChartYScale.domain()[0])
)
.attr('class', (d) => {
let markerClass = 'metric-value';
if (isAnomalyVisible(d)) {
Expand Down Expand Up @@ -470,7 +477,14 @@ export class ExplorerChartSingleMetric extends React.Component {
// 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_SYMBOL_HEIGHT / 2);
.attr(
'y',
(d) =>
// Fallback with domain's min value if value is null
// To ensure event markers are rendered properly at the bottom of the chart
lineChartYScale(d.value !== null ? d.value : lineChartYScale.domain()[0]) -
SCHEDULED_EVENT_SYMBOL_HEIGHT / 2
);
}

function showAnomalyPopover(marker, circle) {
Expand Down Expand Up @@ -596,7 +610,7 @@ export class ExplorerChartSingleMetric extends React.Component {
});
}
}
} else {
} else if (marker.value !== null) {
tooltipData.push({
label: i18n.translate(
'xpack.ml.explorer.singleMetricChart.valueWithoutAnomalyScoreLabel',
Expand Down
26 changes: 21 additions & 5 deletions x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1055,9 +1055,11 @@ export function anomalyChartsDataProvider(mlClient: MlClient, client: IScopedClu
// differently because of how the source data is structured.
// For rare chart values we are only interested wether a value is either `0` or not,
// `0` acts like a flag in the chart whether to display the dot/marker.
// All other charts (single metric, population) are metric based and with
// For single metric chart, we need to pass null values to display data gaps.
// All other charts are distribution based and with
// those a value of `null` acts as the flag to hide a data point.
if (
chartType === CHART_TYPE.SINGLE_METRIC ||
(chartType === CHART_TYPE.EVENT_DISTRIBUTION && value > 0) ||
(chartType !== CHART_TYPE.EVENT_DISTRIBUTION && value !== null)
) {
Expand All @@ -1079,6 +1081,7 @@ export function anomalyChartsDataProvider(mlClient: MlClient, client: IScopedClu
// Iterate through the anomaly records, adding anomalyScore properties
// to the chartData entries for anomalous buckets.
const chartDataForPointSearch = getChartDataForPointSearch(chartData, records[0], chartType);
let shouldSortChartData = false;
each(records, (record) => {
// Look for a chart point with the same time as the record.
// If none found, insert a point for anomalies due to a gap in the data.
Expand All @@ -1087,10 +1090,17 @@ export function anomalyChartsDataProvider(mlClient: MlClient, client: IScopedClu
if (chartPoint === undefined) {
chartPoint = { date: recordTime, value: null };
chartData.push(chartPoint);
shouldSortChartData = true;
}
if (chartPoint !== undefined) {
chartPoint.anomalyScore = record.record_score;

// If it is an empty chart point, set the value to the actual value
// To properly display the anomaly marker on the chart
if (chartPoint.value === null) {
chartPoint.value = Array.isArray(record.actual) ? record.actual[0] : record.actual;
}

if (record.actual !== undefined) {
chartPoint.actual = record.actual;
chartPoint.typical = record.typical;
Expand Down Expand Up @@ -1119,21 +1129,27 @@ export function anomalyChartsDataProvider(mlClient: MlClient, client: IScopedClu
}
});

// Chart data is sorted by default, but if we added points for anomalies,
// we need to sort again to ensure the points are in the correct order.
if (shouldSortChartData) {
chartData.sort((a, b) => a.date - b.date);
}

// Add a scheduledEvents property to any points in the chart data set
// which correspond to times of scheduled events for the job.
if (scheduledEvents !== undefined) {
each(scheduledEvents, (events, time) => {
const chartPoint = findChartPointForTime(chartDataForPointSearch, Number(time));
if (chartPoint !== undefined) {
chartPoint.scheduledEvents = events;
// We do not want to create additional points for single metric charts
// as it could break the chart.
} else if (chartType !== CHART_TYPE.SINGLE_METRIC) {
} else {
// If there's no underlying metric data point for the scheduled event,
// create a new chart point with a value of 0.
// Except for Single Metric Charts, where we want to create a point at the bottom of the chart.
// Which is not always `0`.
const eventChartPoint: ChartPoint = {
date: Number(time),
value: 0,
value: chartType === CHART_TYPE.SINGLE_METRIC ? null : 0,
entity: SCHEDULE_EVENT_MARKER_ENTITY,
scheduledEvents: events,
};
Expand Down

0 comments on commit 32144fc

Please sign in to comment.