From 2266dd969ec9d238fc994edff22156e8e6946115 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Wed, 3 Jun 2020 20:19:04 -0500 Subject: [PATCH] [ML] Extend population preview chart to show actual and typical value (#67569) Co-authored-by: Elastic Machine --- .../explorer_chart_distribution.js | 23 ++++++----- .../ml/public/application/util/chart_utils.js | 38 +++++++++++++++++++ 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js index 6c7c3e90402161..7a18914957ba98 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js @@ -25,6 +25,7 @@ import { getTickValues, numTicksForDateFormat, removeLabelOverlap, + chartExtendedLimits, } from '../../util/chart_utils'; import { LoadingIndicator } from '../../components/loading_indicator/loading_indicator'; import { getTimeBucketsFromCache } from '../../util/time_buckets'; @@ -98,7 +99,7 @@ export class ExplorerChartDistribution extends React.Component { const filteredChartData = init(config); drawRareChart(filteredChartData); - function init({ chartData }) { + function init({ chartData, functionDescription }) { const $el = $('.ml-explorer-chart'); // Clear any existing elements from the visualization, @@ -137,22 +138,24 @@ export class ExplorerChartDistribution extends React.Component { }); if (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { - const focusData = chartData - .filter((d) => { - return d.entity === highlight; - }) - .map((d) => d.value); - const focusExtent = d3.extent(focusData); - + const focusData = chartData.filter((d) => { + return d.entity === highlight; + }); + // calculate the max y domain based on value, typical, and actual + // also sets the min to be at least 0 if the series function type is `count` + const { min: yScaleDomainMin, max: yScaleDomainMax } = chartExtendedLimits( + focusData, + functionDescription + ); // now again filter chartData to include only the data points within the domain chartData = chartData.filter((d) => { - return d.value <= focusExtent[1]; + return d.value <= yScaleDomainMax; }); lineChartYScale = d3.scale .linear() .range([chartHeight, 0]) - .domain([0, focusExtent[1]]) + .domain([yScaleDomainMin < 0 ? yScaleDomainMin : 0, yScaleDomainMax]) .nice(); } else if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) { // avoid overflowing the border of the highlighted area diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.js b/x-pack/plugins/ml/public/application/util/chart_utils.js index 2caf964cb9774d..4ec7c5cb6d819a 100644 --- a/x-pack/plugins/ml/public/application/util/chart_utils.js +++ b/x-pack/plugins/ml/public/application/util/chart_utils.js @@ -65,6 +65,44 @@ export function chartLimits(data = []) { return limits; } +export function chartExtendedLimits(data = [], functionDescription) { + let _min = Infinity; + let _max = -Infinity; + data.forEach((d) => { + let metricValue = d.value; + const actualValue = Array.isArray(d.actual) ? d.actual[0] : d.actual; + const typicalValue = Array.isArray(d.typical) ? d.typical[0] : d.typical; + + 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 = actualValue; + } + + if (d.anomalyScore !== undefined) { + _min = Math.min(_min, metricValue, actualValue, typicalValue); + _max = Math.max(_max, metricValue, actualValue, typicalValue); + } else { + _min = Math.min(_min, metricValue); + _max = Math.max(_max, metricValue); + } + }); + const limits = { max: _max, min: _min }; + + // add padding of 5% of the difference between max and min + // if we ended up with the same value for both of them + if (limits.max === limits.min) { + const padding = limits.max * 0.05; + limits.max += padding; + limits.min -= padding; + } + + // makes sure the domain starts at 0 if the aggregation is by count + // since the number should always be positive + if (functionDescription === 'count' && limits.min < 0) { + limits.min = 0; + } + return limits; +} export function drawLineChartDots(data, lineChartGroup, lineChartValuesLine, radius = 1.5) { // We need to do this because when creating a line for a chart which has data gaps, // if there are single datapoints without any valid data before and after them,