Skip to content

Commit

Permalink
[ML] Fix Single Metric Viewer chart failing to load if no points duri…
Browse files Browse the repository at this point in the history
…ng calendar event (#130000)

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

* [ML] Type fix
  • Loading branch information
peteharverson authored Apr 12, 2022
1 parent b7dc8f0 commit 8afa2c3
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@
stroke-width: 1px;
stroke: $euiColorDarkShade;
fill: $euiColorLightShade;
pointer-events: none;
}

.forecast {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -667,15 +667,17 @@ 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;
if (metricValue === null && d.anomalyScore !== undefined && d.actual !== undefined) {
// 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) {
Expand All @@ -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.
Expand Down Expand Up @@ -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')
Expand All @@ -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) {
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,11 @@ export function getFocusData(
modelPlotEnabled,
functionDescription
);
focusChartData = processScheduledEventsForChart(focusChartData, scheduledEvents);
focusChartData = processScheduledEventsForChart(
focusChartData,
scheduledEvents,
focusAggregationInterval
);

const refreshFocusData: FocusData = {
scheduledEvents,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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).
Expand Down

0 comments on commit 8afa2c3

Please sign in to comment.