[ML] Fix Single Metric Viewer chart failing to load if no points during calendar event (#130000)

* [ML] Fix Single Metric Viewer chart load if no points during calendar event

* [ML] Type fix
This commit is contained in:
Pete Harverson 2022-04-12 17:32:36 +01:00 committed by GitHub
parent b7dc8f0e9c
commit 8afa2c30fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 76 additions and 15 deletions

View file

@ -131,7 +131,6 @@
stroke-width: 1px;
stroke: $euiColorDarkShade;
fill: $euiColorLightShade;
pointer-events: none;
}
.forecast {

View file

@ -667,7 +667,8 @@ class TimeseriesChartIntl extends Component {
return d.lower;
}
}
return metricValue;
// metricValue is undefined for scheduled events when there is no source data.
return metricValue || 0;
});
yMax = d3.max(combinedData, (d) => {
let metricValue = d.value;
@ -675,7 +676,8 @@ class TimeseriesChartIntl extends Component {
// If an anomaly coincides with a gap in the data, use the anomaly actual value.
metricValue = Array.isArray(d.actual) ? d.actual[0] : d.actual;
}
return d.upper !== undefined ? Math.max(metricValue, d.upper) : metricValue;
// metricValue is undefined for scheduled events when there is no source data.
return d.upper !== undefined ? Math.max(metricValue, d.upper) : metricValue || 0;
});
if (yMax === yMin) {
@ -701,6 +703,7 @@ class TimeseriesChartIntl extends Component {
// TODO needs revisiting to be a more robust normalization
yMax += Math.abs(yMax - yMin) * ((maxLevel + 1) / 5);
}
this.focusYScale.domain([yMin, yMax]);
} else {
// Display 10 unlabelled ticks.
@ -835,6 +838,10 @@ class TimeseriesChartIntl extends Component {
scheduledEventMarkers
.enter()
.append('rect')
.on('mouseover', function (d) {
showFocusChartTooltip(d, this);
})
.on('mouseout', () => hideFocusChartTooltip())
.attr('width', LINE_CHART_ANOMALY_RADIUS * 2)
.attr('height', SCHEDULED_EVENT_SYMBOL_HEIGHT)
.attr('class', 'scheduled-event-marker')
@ -844,7 +851,10 @@ class TimeseriesChartIntl extends Component {
// Update all markers to new positions.
scheduledEventMarkers
.attr('x', (d) => this.focusXScale(d.date) - LINE_CHART_ANOMALY_RADIUS)
.attr('y', (d) => this.focusYScale(d.value) - 3);
.attr('y', (d) => {
const focusYValue = this.focusYScale(d.value);
return isNaN(focusYValue) ? -focusHeight - 3 : focusYValue - 3;
});
// Plot any forecast data in scope.
if (focusForecastData !== undefined) {
@ -1652,7 +1662,7 @@ class TimeseriesChartIntl extends Component {
valueAccessor: 'prediction',
});
} else {
if (marker.value !== undefined) {
if (marker.value !== undefined && marker.value !== null) {
tooltipData.push({
label: i18n.translate(
'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScore.valueLabel',

View file

@ -148,7 +148,11 @@ export function getFocusData(
modelPlotEnabled,
functionDescription
);
focusChartData = processScheduledEventsForChart(focusChartData, scheduledEvents);
focusChartData = processScheduledEventsForChart(
focusChartData,
scheduledEvents,
focusAggregationInterval
);
const refreshFocusData: FocusData = {
scheduledEvents,

View file

@ -21,7 +21,11 @@ export function processDataForFocusAnomalies(
functionDescription: any
): any;
export function processScheduledEventsForChart(chartData: any, scheduledEvents: any): any;
export function processScheduledEventsForChart(
chartData: any,
scheduledEvents: any,
aggregationInterval: any
): any;
export function findNearestChartPointToTime(chartData: any, time: any): any;

View file

@ -205,16 +205,44 @@ export function processDataForFocusAnomalies(
// Adds a scheduledEvents property to any points in the chart data set
// which correspond to times of scheduled events for the job.
export function processScheduledEventsForChart(chartData, scheduledEvents) {
export function processScheduledEventsForChart(chartData, scheduledEvents, aggregationInterval) {
if (scheduledEvents !== undefined) {
const timesToAddPointsFor = [];
// Iterate through the scheduled events making sure we have a chart point for each event.
const intervalMs = aggregationInterval.asMilliseconds();
let lastChartDataPointTime = undefined;
if (chartData !== undefined && chartData.length > 0) {
lastChartDataPointTime = chartData[chartData.length - 1].date.getTime();
}
// In case there's no chart data/sparse data during these scheduled events
// ensure we add chart points at every aggregation interval for these scheduled events.
let sortRequired = false;
each(scheduledEvents, (events, time) => {
const chartPoint = findNearestChartPointToTime(chartData, time);
if (chartPoint !== undefined) {
// Note if the scheduled event coincides with an absence of the underlying metric data,
// we don't worry about plotting the event.
chartPoint.scheduledEvents = events;
const exactChartPoint = findChartPointForScheduledEvent(chartData, +time);
if (exactChartPoint !== undefined) {
exactChartPoint.scheduledEvents = events;
} else {
const timeToAdd = Math.floor(time / intervalMs) * intervalMs;
if (timesToAddPointsFor.indexOf(timeToAdd) === -1 && timeToAdd !== lastChartDataPointTime) {
const pointToAdd = {
date: new Date(timeToAdd),
value: null,
scheduledEvents: events,
};
chartData.push(pointToAdd);
sortRequired = true;
}
}
});
// Sort chart data by time if extra points were added at the end of the array for scheduled events.
if (sortRequired === true) {
chartData.sort((a, b) => a.date.getTime() - b.date.getTime());
}
}
return chartData;
@ -240,12 +268,12 @@ export function findNearestChartPointToTime(chartData, time) {
// grab the current and previous items and compare the time differences
let foundItem;
for (let i = 0; i < chartData.length; i++) {
const itemTime = chartData[i].date.getTime();
const itemTime = chartData[i]?.date?.getTime();
if (itemTime > time) {
const item = chartData[i];
const previousItem = chartData[i - 1];
const diff1 = Math.abs(time - previousItem.date.getTime());
const diff1 = Math.abs(time - previousItem?.date?.getTime());
const diff2 = Math.abs(time - itemTime);
// foundItem should be the item with a date closest to bucketTime
@ -300,6 +328,22 @@ export function findChartPointForAnomalyTime(chartData, anomalyTime, aggregation
return chartPoint;
}
export function findChartPointForScheduledEvent(chartData, eventTime) {
let chartPoint;
if (chartData === undefined) {
return chartPoint;
}
for (let i = 0; i < chartData.length; i++) {
if (chartData[i].date.getTime() === eventTime) {
chartPoint = chartData[i];
break;
}
}
return chartPoint;
}
export function calculateAggregationInterval(bounds, bucketsTarget, jobs, selectedJob) {
// Aggregation interval used in queries should be a function of the time span of the chart
// and the bucket span of the selected job(s).