diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss b/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss index 63cb8a57adba6..dc9178f7062f2 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss @@ -131,7 +131,6 @@ stroke-width: 1px; stroke: $euiColorDarkShade; fill: $euiColorLightShade; - pointer-events: none; } .forecast { diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 9a95cf787c70d..8daf2e8f86891 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -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', diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts index d4548a43f3f2b..80169d8914682 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts @@ -148,7 +148,11 @@ export function getFocusData( modelPlotEnabled, functionDescription ); - focusChartData = processScheduledEventsForChart(focusChartData, scheduledEvents); + focusChartData = processScheduledEventsForChart( + focusChartData, + scheduledEvents, + focusAggregationInterval + ); const refreshFocusData: FocusData = { scheduledEvents, diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.d.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.d.ts index 0ff84931a8ba3..11da0acb506f6 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.d.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.d.ts @@ -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; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js index 588d19cfe9a63..5a508cbf4bb41 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js @@ -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).