From f6fe839fd9d11452a82cf66879ae8aa50e78ffb2 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 4 Sep 2018 11:00:54 +0200 Subject: [PATCH 01/37] [ML] Explorer charts container supports different chart types. Dummy rare chart created. --- .../explorer_charts/explorer_chart_rare.js | 364 ++++++++++++++++++ ...art.js => explorer_chart_single_metric.js} | 2 +- ...s => explorer_chart_single_metric.test.js} | 8 +- .../explorer_charts_container.js | 30 +- 4 files changed, 393 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js rename x-pack/plugins/ml/public/explorer/explorer_charts/{explorer_chart.js => explorer_chart_single_metric.js} (99%) rename x-pack/plugins/ml/public/explorer/explorer_charts/{explorer_chart.test.js => explorer_chart_single_metric.test.js} (92%) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js new file mode 100644 index 0000000000000..0def2855a3374 --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js @@ -0,0 +1,364 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * React component for rendering a chart of anomalies in the raw data in + * the Machine Learning Explorer dashboard. + */ + +import './styles/explorer_chart.less'; + +import PropTypes from 'prop-types'; +import React from 'react'; + +import _ from 'lodash'; +import d3 from 'd3'; +import $ from 'jquery'; +import moment from 'moment'; + +// don't use something like plugins/ml/../common +// because it won't work with the jest tests +import { formatValue } from '../../formatters/format_value'; +import { getSeverityWithLow } from '../../../common/util/anomaly_utils'; +import { drawLineChartDots, numTicksForDateFormat } from '../../util/chart_utils'; +import { TimeBuckets } from 'ui/time_buckets'; +import { LoadingIndicator } from '../../components/loading_indicator/loading_indicator'; +import { mlEscape } from '../../util/string_utils'; +import { mlFieldFormatService } from '../../services/field_format_service'; +import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tooltip_service'; + +const CONTENT_WRAPPER_HEIGHT = 215; + +export class ExplorerChartRare extends React.Component { + static propTypes = { + seriesConfig: PropTypes.object, + mlSelectSeverityService: PropTypes.object.isRequired + } + + componentDidMount() { + this.renderChart(); + } + + componentDidUpdate() { + this.renderChart(); + } + + renderChart() { + const { + mlSelectSeverityService + } = this.props; + + const element = this.rootNode; + const config = this.props.seriesConfig; + + if ( + typeof config === 'undefined' || + Array.isArray(config.chartData) === false + ) { + // just return so the empty directive renders without an error later on + return; + } + + const fieldFormat = mlFieldFormatService.getFieldFormat(config.jobId, config.detectorIndex); + + let vizWidth = 0; + const chartHeight = 170; + const LINE_CHART_ANOMALY_RADIUS = 7; + const SCHEDULED_EVENT_MARKER_HEIGHT = 5; + + // Left margin is adjusted later for longest y-axis label. + const margin = { top: 10, right: 0, bottom: 30, left: 60 }; + + let lineChartXScale = null; + let lineChartYScale = null; + let lineChartGroup; + let lineChartValuesLine = null; + + init(config.chartLimits); + drawLineChart(config.chartData); + + function init(chartLimits) { + const $el = $('.ml-explorer-chart'); + + // Clear any existing elements from the visualization, + // then build the svg elements for the chart. + const chartElement = d3.select(element).select('.content-wrapper'); + chartElement.select('svg').remove(); + + const svgWidth = $el.width(); + const svgHeight = chartHeight + margin.top + margin.bottom; + + const svg = chartElement.append('svg') + .attr('width', svgWidth) + .attr('height', svgHeight); + + // Set the size of the left margin according to the width of the largest y axis tick label. + lineChartYScale = d3.scale.linear() + .range([chartHeight, 0]) + .domain([ + chartLimits.min, + chartLimits.max + ]) + .nice(); + + const yAxis = d3.svg.axis().scale(lineChartYScale) + .orient('left') + .innerTickSize(0) + .outerTickSize(0) + .tickPadding(10); + + let maxYAxisLabelWidth = 0; + const tempLabelText = svg.append('g') + .attr('class', 'temp-axis-label tick'); + tempLabelText.selectAll('text.temp.axis').data(lineChartYScale.ticks()) + .enter() + .append('text') + .text((d) => { + if (fieldFormat !== undefined) { + return fieldFormat.convert(d, 'text'); + } else { + return lineChartYScale.tickFormat()(d); + } + }) + .each(function () { + maxYAxisLabelWidth = Math.max(this.getBBox().width + yAxis.tickPadding(), maxYAxisLabelWidth); + }) + .remove(); + d3.select('.temp-axis-label').remove(); + + margin.left = (Math.max(maxYAxisLabelWidth, 40)); + vizWidth = svgWidth - margin.left - margin.right; + + // Set the x axis domain to match the request plot range. + // This ensures ranges on different charts will match, even when there aren't + // data points across the full range, and the selected anomalous region is centred. + lineChartXScale = d3.time.scale() + .range([0, vizWidth]) + .domain([config.plotEarliest, config.plotLatest]); + + lineChartValuesLine = d3.svg.line() + .x(d => lineChartXScale(d.date)) + .y(d => lineChartYScale(d.value)) + .defined(d => d.value !== null); + + lineChartGroup = svg.append('g') + .attr('class', 'line-chart') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + + } + + function drawLineChart(data) { + // Add border round plot area. + lineChartGroup.append('rect') + .attr('x', 0) + .attr('y', 0) + .attr('height', chartHeight) + .attr('width', vizWidth) + .style('stroke', '#cccccc') + .style('fill', 'none') + .style('stroke-width', 1); + + drawLineChartAxes(); + drawLineChartHighlightedSpan(); + drawLineChartPaths(data); + drawLineChartDots(data, lineChartGroup, lineChartValuesLine); + drawLineChartMarkers(data); + } + + function drawLineChartAxes() { + // Get the scaled date format to use for x axis tick labels. + const timeBuckets = new TimeBuckets(); + const bounds = { min: moment(config.plotEarliest), max: moment(config.plotLatest) }; + timeBuckets.setBounds(bounds); + timeBuckets.setInterval('auto'); + const xAxisTickFormat = timeBuckets.getScaledDateFormat(); + + const xAxis = d3.svg.axis().scale(lineChartXScale) + .orient('bottom') + .innerTickSize(-chartHeight) + .outerTickSize(0) + .tickPadding(10) + .ticks(numTicksForDateFormat(vizWidth, xAxisTickFormat)) + .tickFormat(d => moment(d).format(xAxisTickFormat)); + + const yAxis = d3.svg.axis().scale(lineChartYScale) + .orient('left') + .innerTickSize(0) + .outerTickSize(0) + .tickPadding(10); + + if (fieldFormat !== undefined) { + yAxis.tickFormat(d => fieldFormat.convert(d, 'text')); + } + + const axes = lineChartGroup.append('g'); + + axes.append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0,' + chartHeight + ')') + .call(xAxis); + + axes.append('g') + .attr('class', 'y axis') + .call(yAxis); + } + + function drawLineChartHighlightedSpan() { + // Draws a rectangle which highlights the time span that has been selected for view. + // Note depending on the overall time range and the bucket span, the selected time + // span may be longer than the range actually being plotted. + const rectStart = Math.max(config.selectedEarliest, config.plotEarliest); + const rectEnd = Math.min(config.selectedLatest, config.plotLatest); + const rectWidth = lineChartXScale(rectEnd) - lineChartXScale(rectStart); + + lineChartGroup.append('rect') + .attr('class', 'selected-interval') + .attr('x', lineChartXScale(new Date(rectStart))) + .attr('y', 1) + .attr('width', rectWidth) + .attr('height', chartHeight - 1); + } + + function drawLineChartPaths(data) { + lineChartGroup.append('path') + .attr('class', 'values-line') + .attr('d', lineChartValuesLine(data)); + } + + function drawLineChartMarkers(data) { + // Render circle markers for the points. + // These are used for displaying tooltips on mouseover. + // Don't render dots where value=null (data gaps) + const dots = lineChartGroup.append('g') + .attr('class', 'chart-markers') + .selectAll('.metric-value') + .data(data.filter(d => d.value !== null)); + + // Remove dots that are no longer needed i.e. if number of chart points has decreased. + dots.exit().remove(); + // Create any new dots that are needed i.e. if number of chart points has increased. + dots.enter().append('circle') + .attr('r', LINE_CHART_ANOMALY_RADIUS) + .on('mouseover', function (d) { + showLineChartTooltip(d, this); + }) + .on('mouseout', () => mlChartTooltipService.hide()); + + // Update all dots to new positions. + const threshold = mlSelectSeverityService.state.get('threshold'); + dots.attr('cx', function (d) { return lineChartXScale(d.date); }) + .attr('cy', function (d) { return lineChartYScale(d.value); }) + .attr('class', function (d) { + let markerClass = 'metric-value'; + if (_.has(d, 'anomalyScore') && Number(d.anomalyScore) >= threshold.val) { + markerClass += ' anomaly-marker '; + markerClass += getSeverityWithLow(d.anomalyScore); + } + return markerClass; + }); + + // Add rectangular markers for any scheduled events. + const scheduledEventMarkers = lineChartGroup.select('.chart-markers').selectAll('.scheduled-event-marker') + .data(data.filter(d => d.scheduledEvents !== undefined)); + + // Remove markers that are no longer needed i.e. if number of chart points has decreased. + scheduledEventMarkers.exit().remove(); + // Create any new markers that are needed i.e. if number of chart points has increased. + scheduledEventMarkers.enter().append('rect') + .attr('width', LINE_CHART_ANOMALY_RADIUS * 2) + .attr('height', SCHEDULED_EVENT_MARKER_HEIGHT) + .attr('class', 'scheduled-event-marker') + .attr('rx', 1) + .attr('ry', 1); + + // 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_MARKER_HEIGHT / 2)); + + } + + function showLineChartTooltip(marker, circle) { + // Show the time and metric values in the tooltip. + // Uses date, value, upper, lower and anomalyScore (optional) marker properties. + const formattedDate = moment(marker.date).format('MMMM Do YYYY, HH:mm'); + let contents = formattedDate + '

'; + + if (_.has(marker, 'anomalyScore')) { + const score = parseInt(marker.anomalyScore); + const displayScore = (score > 0 ? score : '< 1'); + contents += ('anomaly score: ' + displayScore); + // Show actual/typical when available except for rare detectors. + // Rare detectors always have 1 as actual and the probability as typical. + // Exposing those values in the tooltip with actual/typical labels might irritate users. + if (_.has(marker, 'actual') && config.functionDescription !== 'rare') { + // Display the record actual in preference to the chart value, which may be + // different depending on the aggregation interval of the chart. + contents += (`
actual: ${formatValue(marker.actual, config.functionDescription, fieldFormat)}`); + contents += (`
typical: ${formatValue(marker.typical, config.functionDescription, fieldFormat)}`); + } else { + contents += (`
value: ${formatValue(marker.value, config.functionDescription, fieldFormat)}`); + if (_.has(marker, 'byFieldName') && _.has(marker, 'numberOfCauses')) { + const numberOfCauses = marker.numberOfCauses; + const byFieldName = mlEscape(marker.byFieldName); + if (numberOfCauses < 10) { + // If numberOfCauses === 1, won't go into this block as actual/typical copied to top level fields. + contents += `
${numberOfCauses} unusual ${byFieldName} values`; + } else { + // Maximum of 10 causes are stored in the record, so '10' may mean more than 10. + contents += `
${numberOfCauses}+ unusual ${byFieldName} values`; + } + } + } + } else { + contents += `value: ${formatValue(marker.value, config.functionDescription, fieldFormat)}`; + } + + if (_.has(marker, 'scheduledEvents')) { + contents += `

Scheduled events:
${marker.scheduledEvents.map(mlEscape).join('
')}`; + } + + mlChartTooltipService.show(contents, circle, { + x: LINE_CHART_ANOMALY_RADIUS * 2, + y: 0 + }); + } + } + + shouldComponentUpdate() { + // Prevents component re-rendering + return true; + } + + setRef(componentNode) { + this.rootNode = componentNode; + } + + render() { + const { + seriesConfig + } = this.props; + + if (typeof seriesConfig === 'undefined') { + // just return so the empty directive renders without an error later on + return null; + } + + // create a chart loading placeholder + const isLoading = seriesConfig.loading; + + return ( +
+ Rare Chart + {isLoading && ( + + )} + {!isLoading && ( +
+ )} +
+ ); + } +} diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js similarity index 99% rename from x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart.js rename to x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js index 6f1950e8c09c3..02f99054c7717 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js @@ -38,7 +38,7 @@ import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tool const CONTENT_WRAPPER_HEIGHT = 215; const CONTENT_WRAPPER_CLASS = 'ml-explorer-chart-content-wrapper'; -export class ExplorerChart extends React.Component { +export class ExplorerChartSingleMetric extends React.Component { static propTypes = { tooManyBuckets: PropTypes.bool, seriesConfig: PropTypes.object, diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart.test.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.test.js similarity index 92% rename from x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart.test.js rename to x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.test.js index 9a845cd148ac6..fd922c9805cb8 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart.test.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.test.js @@ -31,7 +31,7 @@ jest.mock('ui/chrome', () => ({ import { mount } from 'enzyme'; import React from 'react'; -import { ExplorerChart } from './explorer_chart'; +import { ExplorerChartSingleMetric } from './explorer_chart_single_metric'; import { chartLimits } from '../../util/chart_utils'; describe('ExplorerChart', () => { @@ -49,7 +49,7 @@ describe('ExplorerChart', () => { afterEach(() => (SVGElement.prototype.getBBox = originalGetBBox)); test('Initialize', () => { - const wrapper = mount(); + const wrapper = mount(); // without setting any attributes and corresponding data // the directive just ends up being empty. @@ -63,7 +63,7 @@ describe('ExplorerChart', () => { loading: true }; - const wrapper = mount(); + const wrapper = mount(); // test if the loading indicator is shown expect(wrapper.find('.ml-loading-indicator .loading-spinner')).toHaveLength(1); @@ -85,7 +85,7 @@ describe('ExplorerChart', () => { // We create the element including a wrapper which sets the width: return mount(
- +
); } diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js index 3b305b54343a4..53cb45a83fcca 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js @@ -17,7 +17,8 @@ import { getExploreSeriesLink, isLabelLengthAboveThreshold } from '../../util/chart_utils'; -import { ExplorerChart } from './explorer_chart'; +import { ExplorerChartRare } from './explorer_chart_rare'; +import { ExplorerChartSingleMetric } from './explorer_chart_single_metric'; import { ExplorerChartLabel } from './components/explorer_chart_label'; const textTooManyBuckets = `This selection contains too many buckets to be displayed. @@ -42,6 +43,7 @@ export function ExplorerChartsContainer({ jobId, detectorLabel, entityFields, + functionDescription } = series; const entities = entityFields.map((ef) => `${ef.fieldName}/${ef.fieldValue}`).join(','); const id = `${jobId}_${detectorLabel}_${entities}`; @@ -80,11 +82,27 @@ export function ExplorerChartsContainer({ infoTooltip={series.infoTooltip} wrapLabel={wrapLabel} /> - + {(() => { + console.warn('detectorLabel', detectorLabel); + switch (functionDescription) { + case 'rare': + return ( + + ); + default: + return ( + + ); + } + })()}
); }) From 47cdce6fdaf7c8bb2a3b2dd9872edb7a23697516 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 4 Sep 2018 15:27:19 +0200 Subject: [PATCH 02/37] [ML] Initial working rare chart. --- .../explorer_charts/explorer_chart_rare.js | 66 ++++++--- .../explorer_charts_container_service.js | 42 +++++- .../ml/public/services/results_service.js | 138 ++++++++++++++++++ 3 files changed, 220 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js index 0def2855a3374..6e3ebb6d5475c 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js @@ -23,7 +23,7 @@ import moment from 'moment'; // because it won't work with the jest tests import { formatValue } from '../../formatters/format_value'; import { getSeverityWithLow } from '../../../common/util/anomaly_utils'; -import { drawLineChartDots, numTicksForDateFormat } from '../../util/chart_utils'; +import { numTicksForDateFormat } from '../../util/chart_utils'; import { TimeBuckets } from 'ui/time_buckets'; import { LoadingIndicator } from '../../components/loading_indicator/loading_indicator'; import { mlEscape } from '../../util/string_utils'; @@ -70,7 +70,7 @@ export class ExplorerChartRare extends React.Component { const SCHEDULED_EVENT_MARKER_HEIGHT = 5; // Left margin is adjusted later for longest y-axis label. - const margin = { top: 10, right: 0, bottom: 30, left: 60 }; + const margin = { top: 10, right: 0, bottom: 30, left: 0 }; let lineChartXScale = null; let lineChartYScale = null; @@ -78,7 +78,7 @@ export class ExplorerChartRare extends React.Component { let lineChartValuesLine = null; init(config.chartLimits); - drawLineChart(config.chartData); + drawRareChart(config.chartData); function init(chartLimits) { const $el = $('.ml-explorer-chart'); @@ -95,7 +95,6 @@ export class ExplorerChartRare extends React.Component { .attr('width', svgWidth) .attr('height', svgHeight); - // Set the size of the left margin according to the width of the largest y axis tick label. lineChartYScale = d3.scale.linear() .range([chartHeight, 0]) .domain([ @@ -129,7 +128,8 @@ export class ExplorerChartRare extends React.Component { .remove(); d3.select('.temp-axis-label').remove(); - margin.left = (Math.max(maxYAxisLabelWidth, 40)); + // Set the size of the left margin according to the width of the largest y axis tick label. + // margin.left = (Math.max(maxYAxisLabelWidth, 40)); vizWidth = svgWidth - margin.left - margin.right; // Set the x axis domain to match the request plot range. @@ -150,7 +150,7 @@ export class ExplorerChartRare extends React.Component { } - function drawLineChart(data) { + function drawRareChart(data) { // Add border round plot area. lineChartGroup.append('rect') .attr('x', 0) @@ -161,14 +161,13 @@ export class ExplorerChartRare extends React.Component { .style('fill', 'none') .style('stroke-width', 1); - drawLineChartAxes(); - drawLineChartHighlightedSpan(); - drawLineChartPaths(data); - drawLineChartDots(data, lineChartGroup, lineChartValuesLine); - drawLineChartMarkers(data); + drawRareChartAxes(); + drawRareChartHighlightedSpan(); + drawRareChartDots(data, lineChartGroup, lineChartValuesLine); + drawRareChartMarkers(data); } - function drawLineChartAxes() { + function drawRareChartAxes() { // Get the scaled date format to use for x axis tick labels. const timeBuckets = new TimeBuckets(); const bounds = { min: moment(config.plotEarliest), max: moment(config.plotLatest) }; @@ -206,7 +205,35 @@ export class ExplorerChartRare extends React.Component { .call(yAxis); } - function drawLineChartHighlightedSpan() { + function drawRareChartDots(data, rareChartGroup, rareChartValuesLine, 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, + // the lines created by using d3...defined() do not contain these data points. + // So this function adds additional circle elements to display the single + // datapoints in additional to the line created for the chart. + + const dotsData = data; + + // check if `g.values-dots` already exists, if not create it + // in both cases assign the element to `dotGroup` + const dotGroup = (rareChartGroup.select('.values-dots').empty()) + ? rareChartGroup.append('g').classed('values-dots', true) + : rareChartGroup.select('.values-dots'); + + // use d3's enter/update/exit pattern to render the dots + const dots = dotGroup.selectAll('circle').data(dotsData); + + dots.enter().append('circle') + .attr('r', radius); + + dots + .attr('cx', rareChartValuesLine.x()) + .attr('cy', rareChartValuesLine.y()); + + dots.exit().remove(); + } + + function drawRareChartHighlightedSpan() { // Draws a rectangle which highlights the time span that has been selected for view. // Note depending on the overall time range and the bucket span, the selected time // span may be longer than the range actually being plotted. @@ -222,13 +249,7 @@ export class ExplorerChartRare extends React.Component { .attr('height', chartHeight - 1); } - function drawLineChartPaths(data) { - lineChartGroup.append('path') - .attr('class', 'values-line') - .attr('d', lineChartValuesLine(data)); - } - - function drawLineChartMarkers(data) { + function drawRareChartMarkers(data) { // Render circle markers for the points. // These are used for displaying tooltips on mouseover. // Don't render dots where value=null (data gaps) @@ -316,6 +337,10 @@ export class ExplorerChartRare extends React.Component { contents += `value: ${formatValue(marker.value, config.functionDescription, fieldFormat)}`; } + if (_.has(marker, 'entity')) { + contents += `
fieldName: ${marker.entity}`; + } + if (_.has(marker, 'scheduledEvents')) { contents += `

Scheduled events:
${marker.scheduledEvents.map(mlEscape).join('
')}`; } @@ -351,7 +376,6 @@ export class ExplorerChartRare extends React.Component { return (
- Rare Chart {isLoading && ( )} diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js index c23e75abab0fa..1aa2b2900b0e6 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js @@ -140,6 +140,23 @@ export function explorerChartsContainerServiceFactory( ); } + // Query 4 - load the rare chart's event distribution + function getEventDistribution(config, range) { + const datafeedQuery = _.get(config, 'datafeedConfig.query', null); + return mlResultsService.getEventDistributionData( + config.datafeedConfig.indices, + config.datafeedConfig.types, + config.entityFields, + datafeedQuery, + config.metricFunction, + config.metricFieldName, + config.timeField, + range.min, + range.max, + config.interval + ); + } + // first load and wait for required data, // only after that trigger data processing and page render. // TODO - if query returns no results e.g. source data has been deleted, @@ -147,7 +164,8 @@ export function explorerChartsContainerServiceFactory( const seriesPromises = seriesConfigs.map(seriesConfig => Promise.all([ getMetricData(seriesConfig, chartRange), getRecordsForCriteria(seriesConfig, chartRange), - getScheduledEvents(seriesConfig, chartRange) + getScheduledEvents(seriesConfig, chartRange), + getEventDistribution(seriesConfig, chartRange) ])); function processChartData(response, seriesIndex) { @@ -155,6 +173,7 @@ export function explorerChartsContainerServiceFactory( const records = response[1].records; const jobId = seriesConfigs[seriesIndex].jobId; const scheduledEvents = response[2].events[jobId]; + const eventDistribution = response[3]; // Return dataset in format used by the chart. // i.e. array of Objects with keys date (timestamp), value, @@ -163,18 +182,23 @@ export function explorerChartsContainerServiceFactory( return []; } - const chartData = _.map(metricData, (value, time) => ({ + let chartData = _.map(metricData, (value, time) => ({ date: +time, value: value })); + + if (eventDistribution.length > 0) { + chartData = eventDistribution; + } + + // Iterate through the anomaly records, adding anomalyScore properties // to the chartData entries for anomalous buckets. _.each(records, (record) => { // Look for a chart point with the same time as the record. // If none found, find closest time in chartData set. - const recordTime = record[ML_TIME_FIELD_NAME]; - let chartPoint = findNearestChartPointToTime(chartData, recordTime); + let chartPoint = findNearestChartPointToTime(chartData, record); if (chartPoint === undefined) { // In case there is a record with a time after that of the last chart point, set the score @@ -224,7 +248,15 @@ export function explorerChartsContainerServiceFactory( return chartData; } - function findNearestChartPointToTime(chartData, time) { + function findNearestChartPointToTime(chartData, record) { + const time = record[ML_TIME_FIELD_NAME]; + + if (record.function === 'rare') { + chartData = chartData.filter((d) => { + return d.entity === record.by_field_value; + }); + } + let chartPoint; for (let i = 0; i < chartData.length; i++) { if (chartData[i].date === time) { diff --git a/x-pack/plugins/ml/public/services/results_service.js b/x-pack/plugins/ml/public/services/results_service.js index 6cd9e8ef6e3d4..efa5f615de015 100644 --- a/x-pack/plugins/ml/public/services/results_service.js +++ b/x-pack/plugins/ml/public/services/results_service.js @@ -9,6 +9,7 @@ // Service for carrying out Elasticsearch queries to obtain data for the // Ml Results dashboards. import _ from 'lodash'; +// import d3 from 'd3'; import { ML_MEDIAN_PERCENTS } from '../../common/util/job_utils'; import { escapeForElasticsearchQuery } from '../util/string_utils'; @@ -1391,6 +1392,142 @@ function getEventRateData( }); } +// Queries Elasticsearch to obtain event distribution i.e. the count +// of entities over time. +// index can be a String, or String[], of index names to search. +// Extra query object can be supplied, or pass null if no additional query. +// Returned response contains a results property, which is an object +// of document counts against time (epoch millis). +function getEventDistributionData( + index, + types, + entityFields, + query, + metricFunction, + metricFieldName, + timeFieldName, + earliestMs, + latestMs, + interval) { + return new Promise((resolve, reject) => { + // only get this data for count (used by rare chart) + if (metricFunction !== 'count') { + return resolve([]); + } + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the types, time range, entity fields, + // plus any additional supplied query. + const mustCriteria = []; + const shouldCriteria = []; + + if (types && types.length) { + mustCriteria.push({ terms: { _type: types } }); + } + + mustCriteria.push({ + range: { + [timeFieldName]: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis' + } + } + }); + + if (query) { + mustCriteria.push(query); + } + + + const entityFieldName = entityFields[0].fieldName; + + const body = { + query: { + bool: { + must: mustCriteria + } + }, + size: 0, + _source: { + excludes: [] + }, + aggs: { + byTime: { + date_histogram: { + field: timeFieldName, + interval: interval, + min_doc_count: 0 + }, + aggs: { + entities: { + terms: { + field: entityFieldName, + size: 1000 + } + } + } + } + } + }; + + if (shouldCriteria.length > 0) { + body.query.bool.should = shouldCriteria; + body.query.bool.minimum_should_match = shouldCriteria.length / 2; + } + + if (metricFieldName !== undefined && metricFieldName !== '') { + body.aggs.byTime.aggs = {}; + + const metricAgg = { + [metricFunction]: { + field: metricFieldName + } + }; + + if (metricFunction === 'percentiles') { + metricAgg[metricFunction].percents = [ML_MEDIAN_PERCENTS]; + } + body.aggs.byTime.aggs.metric = metricAgg; + } + + ml.esSearch({ + index, + body + }) + .then((resp) => { + + const dataByTime = _.get(resp, ['aggregations', 'byTime', 'buckets'], []); + + // normalize data + const data = []; + dataByTime.forEach((dataForTime) => { + const date = +dataForTime.key; + const entities = _.get(dataForTime, ['entities', 'buckets'], []); + entities.forEach((entity) => { + data.push({ + date, + entity: entity.key, + value: entity.doc_count + }); + }); + }); + + // group data by entity + /* + const nestedData = d3.nest() + .key(d => d.entity) + .entries(data); + */ + + resolve(data); + }) + .catch((resp) => { + reject(resp); + }); + }); +} + function getModelPlotOutput( jobId, detectorIndex, @@ -1663,6 +1800,7 @@ export const mlResultsService = { getRecordsForCriteria, getMetricData, getEventRateData, + getEventDistributionData, getModelPlotOutput, getRecordMaxScoreByTime }; From 12998c2bfe0c417acac04b15b48dc7f5eff3f71f Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 4 Sep 2018 16:00:34 +0200 Subject: [PATCH 03/37] [ML] Working categorical event distribution chart --- .../explorer_charts/explorer_chart_rare.js | 46 +++++++++++++------ .../explorer_charts_container.js | 1 - 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js index 6e3ebb6d5475c..a8cf019a35b42 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js @@ -31,6 +31,8 @@ import { mlFieldFormatService } from '../../services/field_format_service'; import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tooltip_service'; const CONTENT_WRAPPER_HEIGHT = 215; +const CHART_TYPE = 'rare'; // one of ['rare', 'population'] +const CHART_Y_ATTRIBUTE = 'entity'; // on of ['entity', 'value'] export class ExplorerChartRare extends React.Component { static propTypes = { @@ -77,10 +79,10 @@ export class ExplorerChartRare extends React.Component { let lineChartGroup; let lineChartValuesLine = null; - init(config.chartLimits); + init(config); drawRareChart(config.chartData); - function init(chartLimits) { + function init({ chartLimits, chartData }) { const $el = $('.ml-explorer-chart'); // Clear any existing elements from the visualization, @@ -95,13 +97,23 @@ export class ExplorerChartRare extends React.Component { .attr('width', svgWidth) .attr('height', svgHeight); - lineChartYScale = d3.scale.linear() - .range([chartHeight, 0]) - .domain([ - chartLimits.min, - chartLimits.max - ]) - .nice(); + const scaleCategories = _.uniq(chartData.map(d => d.entity)); + + if (CHART_TYPE === 'population') { + lineChartYScale = d3.scale.linear() + .range([chartHeight, 0]) + .domain([ + chartLimits.min, + chartLimits.max + ]) + .nice(); + } else if (CHART_TYPE === 'rare') { + lineChartYScale = d3.scale.ordinal() + .rangePoints([0, chartHeight]) + .domain(scaleCategories); + } else { + throw `CHART_TYPE '${CHART_TYPE}' not supported`; + } const yAxis = d3.svg.axis().scale(lineChartYScale) .orient('left') @@ -112,14 +124,18 @@ export class ExplorerChartRare extends React.Component { let maxYAxisLabelWidth = 0; const tempLabelText = svg.append('g') .attr('class', 'temp-axis-label tick'); - tempLabelText.selectAll('text.temp.axis').data(lineChartYScale.ticks()) + const tempLabelTextData = (CHART_TYPE === 'population') ? lineChartYScale.ticks() : scaleCategories; + tempLabelText.selectAll('text.temp.axis').data(tempLabelTextData) .enter() .append('text') .text((d) => { if (fieldFormat !== undefined) { return fieldFormat.convert(d, 'text'); } else { - return lineChartYScale.tickFormat()(d); + if (CHART_TYPE === 'population') { + return lineChartYScale.tickFormat()(d); + } + return d; } }) .each(function () { @@ -141,7 +157,7 @@ export class ExplorerChartRare extends React.Component { lineChartValuesLine = d3.svg.line() .x(d => lineChartXScale(d.date)) - .y(d => lineChartYScale(d.value)) + .y(d => lineChartYScale(d[CHART_Y_ATTRIBUTE])) .defined(d => d.value !== null); lineChartGroup = svg.append('g') @@ -271,7 +287,7 @@ export class ExplorerChartRare extends React.Component { // Update all dots to new positions. const threshold = mlSelectSeverityService.state.get('threshold'); dots.attr('cx', function (d) { return lineChartXScale(d.date); }) - .attr('cy', function (d) { return lineChartYScale(d.value); }) + .attr('cy', function (d) { return lineChartYScale(d[CHART_Y_ATTRIBUTE]); }) .attr('class', function (d) { let markerClass = 'metric-value'; if (_.has(d, 'anomalyScore') && Number(d.anomalyScore) >= threshold.val) { @@ -297,7 +313,7 @@ export class ExplorerChartRare 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_MARKER_HEIGHT / 2)); + .attr('y', (d) => lineChartYScale(d[CHART_Y_ATTRIBUTE]) - (SCHEDULED_EVENT_MARKER_HEIGHT / 2)); } @@ -314,7 +330,7 @@ export class ExplorerChartRare extends React.Component { // Show actual/typical when available except for rare detectors. // Rare detectors always have 1 as actual and the probability as typical. // Exposing those values in the tooltip with actual/typical labels might irritate users. - if (_.has(marker, 'actual') && config.functionDescription !== 'rare') { + if (_.has(marker, 'actual') && config.function !== 'rare') { // Display the record actual in preference to the chart value, which may be // different depending on the aggregation interval of the chart. contents += (`
actual: ${formatValue(marker.actual, config.functionDescription, fieldFormat)}`); diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js index 53cb45a83fcca..faacd697ca7b1 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js @@ -83,7 +83,6 @@ export function ExplorerChartsContainer({ wrapLabel={wrapLabel} /> {(() => { - console.warn('detectorLabel', detectorLabel); switch (functionDescription) { case 'rare': return ( From 9ef4b6b510679730ba1e6f4074f9c8a4f1a2884c Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 4 Sep 2018 16:14:10 +0200 Subject: [PATCH 04/37] [ML] Initial population distribution chart. --- .../explorer_charts/explorer_chart_rare.js | 7 ++-- .../explorer_charts_container.js | 41 +++++++++++-------- .../explorer_charts_container_service.js | 2 +- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js index a8cf019a35b42..025bb016f639c 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js @@ -31,8 +31,6 @@ import { mlFieldFormatService } from '../../services/field_format_service'; import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tooltip_service'; const CONTENT_WRAPPER_HEIGHT = 215; -const CHART_TYPE = 'rare'; // one of ['rare', 'population'] -const CHART_Y_ATTRIBUTE = 'entity'; // on of ['entity', 'value'] export class ExplorerChartRare extends React.Component { static propTypes = { @@ -79,6 +77,9 @@ export class ExplorerChartRare extends React.Component { let lineChartGroup; let lineChartValuesLine = null; + const CHART_TYPE = (config.functionDescription === 'rare') ? 'rare' : 'population'; + const CHART_Y_ATTRIBUTE = (config.functionDescription === 'rare') ? 'entity' : 'value'; + init(config); drawRareChart(config.chartData); @@ -330,7 +331,7 @@ export class ExplorerChartRare extends React.Component { // Show actual/typical when available except for rare detectors. // Rare detectors always have 1 as actual and the probability as typical. // Exposing those values in the tooltip with actual/typical labels might irritate users. - if (_.has(marker, 'actual') && config.function !== 'rare') { + if (_.has(marker, 'actual') && config.functionDescription !== 'rare') { // Display the record actual in preference to the chart value, which may be // different depending on the aggregation interval of the chart. contents += (`
actual: ${formatValue(marker.actual, config.functionDescription, fieldFormat)}`); diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js index faacd697ca7b1..9ad3fc0dbaf01 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js @@ -83,24 +83,31 @@ export function ExplorerChartsContainer({ wrapLabel={wrapLabel} /> {(() => { - switch (functionDescription) { - case 'rare': - return ( - - ); - default: - return ( - - ); + if (functionDescription === 'rare') { + return ( + + ); + } + if (functionDescription === 'count') { + return ( + + ); } + return ( + + ); })()}
); diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js index 1aa2b2900b0e6..4290d63ca9fc5 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js @@ -251,7 +251,7 @@ export function explorerChartsContainerServiceFactory( function findNearestChartPointToTime(chartData, record) { const time = record[ML_TIME_FIELD_NAME]; - if (record.function === 'rare') { + if (record.function === 'rare' || record.function === 'count') { chartData = chartData.filter((d) => { return d.entity === record.by_field_value; }); From 23c53caac697f947a0c3feb3c6193ad67b825ffe Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 17 Sep 2018 17:59:19 +0200 Subject: [PATCH 05/37] [ML] Fixes rare chart styles. Implements dedicated split and filter field for rare chart. --- .../explorer_chart_config_builder.js | 9 ++- .../explorer_charts/explorer_chart_rare.js | 62 +++++++++++++------ .../explorer_charts_container_service.js | 10 ++- .../ml/public/services/results_service.js | 36 +++++------ 4 files changed, 75 insertions(+), 42 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.js index ab167cb423127..f3dfb2bea62a6 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.js @@ -51,14 +51,16 @@ export function buildConfig(record) { if (_.has(record, 'partition_field_name')) { config.entityFields.push({ fieldName: record.partition_field_name, - fieldValue: record.partition_field_value + fieldValue: record.partition_field_value, + fieldType: 'partition' }); } if (_.has(record, 'over_field_name')) { config.entityFields.push({ fieldName: record.over_field_name, - fieldValue: record.over_field_value + fieldValue: record.over_field_value, + fieldType: 'over' }); } @@ -68,7 +70,8 @@ export function buildConfig(record) { if (_.has(record, 'by_field_name') && !(_.has(record, 'over_field_name'))) { config.entityFields.push({ fieldName: record.by_field_name, - fieldValue: record.by_field_value + fieldValue: record.by_field_value, + fieldType: 'by' }); } diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js index 025bb016f639c..6fb93856111e6 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js @@ -23,7 +23,11 @@ import moment from 'moment'; // because it won't work with the jest tests import { formatValue } from '../../formatters/format_value'; import { getSeverityWithLow } from '../../../common/util/anomaly_utils'; -import { numTicksForDateFormat } from '../../util/chart_utils'; +import { + getTickValues, + numTicksForDateFormat, + removeLabelOverlap +} from '../../util/chart_utils'; import { TimeBuckets } from 'ui/time_buckets'; import { LoadingIndicator } from '../../components/loading_indicator/loading_indicator'; import { mlEscape } from '../../util/string_utils'; @@ -48,6 +52,7 @@ export class ExplorerChartRare extends React.Component { renderChart() { const { + tooManyBuckets, mlSelectSeverityService } = this.props; @@ -98,7 +103,13 @@ export class ExplorerChartRare extends React.Component { .attr('width', svgWidth) .attr('height', svgHeight); - const scaleCategories = _.uniq(chartData.map(d => d.entity)); + const scaleCategories = d3.nest() + .key(d => d.entity) + .entries(chartData) + .sort((a, b) => { + return b.values.length - a.values.length; + }) + .map(d => d.key); if (CHART_TYPE === 'population') { lineChartYScale = d3.scale.linear() @@ -109,8 +120,10 @@ export class ExplorerChartRare extends React.Component { ]) .nice(); } else if (CHART_TYPE === 'rare') { + // avoid overflowing the border of the highlighted area + const rowMargin = 5; lineChartYScale = d3.scale.ordinal() - .rangePoints([0, chartHeight]) + .rangePoints([rowMargin, chartHeight - rowMargin]) .domain(scaleCategories); } else { throw `CHART_TYPE '${CHART_TYPE}' not supported`; @@ -192,14 +205,29 @@ export class ExplorerChartRare extends React.Component { timeBuckets.setInterval('auto'); const xAxisTickFormat = timeBuckets.getScaledDateFormat(); + const emphasisStart = Math.max(config.selectedEarliest, config.plotEarliest); + const emphasisEnd = Math.min(config.selectedLatest, config.plotLatest); + // +1 ms to account for the ms that was substracted for query aggregations. + const interval = emphasisEnd - emphasisStart + 1; + const tickValues = getTickValues(emphasisStart, interval, config.plotEarliest, config.plotLatest); + const xAxis = d3.svg.axis().scale(lineChartXScale) .orient('bottom') .innerTickSize(-chartHeight) .outerTickSize(0) .tickPadding(10) - .ticks(numTicksForDateFormat(vizWidth, xAxisTickFormat)) .tickFormat(d => moment(d).format(xAxisTickFormat)); + // With tooManyBuckets the chart would end up with no x-axis labels + // because the ticks are based on the span of the emphasis section, + // and the highlighted area spans the whole chart. + if (tooManyBuckets === false) { + xAxis.tickValues(tickValues); + } else { + xAxis.ticks(numTicksForDateFormat(vizWidth, xAxisTickFormat)); + } + + const yAxis = d3.svg.axis().scale(lineChartYScale) .orient('left') .innerTickSize(0) @@ -212,7 +240,7 @@ export class ExplorerChartRare extends React.Component { const axes = lineChartGroup.append('g'); - axes.append('g') + const gAxis = axes.append('g') .attr('class', 'x axis') .attr('transform', 'translate(0,' + chartHeight + ')') .call(xAxis); @@ -220,17 +248,13 @@ export class ExplorerChartRare extends React.Component { axes.append('g') .attr('class', 'y axis') .call(yAxis); - } - - function drawRareChartDots(data, rareChartGroup, rareChartValuesLine, 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, - // the lines created by using d3...defined() do not contain these data points. - // So this function adds additional circle elements to display the single - // datapoints in additional to the line created for the chart. - const dotsData = data; + if (tooManyBuckets === false) { + removeLabelOverlap(gAxis, emphasisStart, interval, vizWidth); + } + } + function drawRareChartDots(dotsData, rareChartGroup, rareChartValuesLine, radius = 1.5) { // check if `g.values-dots` already exists, if not create it // in both cases assign the element to `dotGroup` const dotGroup = (rareChartGroup.select('.values-dots').empty()) @@ -260,10 +284,12 @@ export class ExplorerChartRare extends React.Component { lineChartGroup.append('rect') .attr('class', 'selected-interval') - .attr('x', lineChartXScale(new Date(rectStart))) - .attr('y', 1) - .attr('width', rectWidth) - .attr('height', chartHeight - 1); + .attr('x', lineChartXScale(new Date(rectStart)) + 2) + .attr('y', 2) + .attr('rx', 3) + .attr('ry', 3) + .attr('width', rectWidth - 4) + .attr('height', chartHeight - 4); } function drawRareChartMarkers(data) { diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js index 4290d63ca9fc5..6508820c6272b 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js @@ -142,11 +142,19 @@ export function explorerChartsContainerServiceFactory( // Query 4 - load the rare chart's event distribution function getEventDistribution(config, range) { + let splitField = config.entityFields.find(f => f.fiedType === 'partition'); + let filterField = null; + if (config.functionDescription === 'rare') { + splitField = config.entityFields.find(f => f.fieldType === 'by'); + filterField = config.entityFields.find(f => f.fieldType === 'partition'); + } + const datafeedQuery = _.get(config, 'datafeedConfig.query', null); return mlResultsService.getEventDistributionData( config.datafeedConfig.indices, config.datafeedConfig.types, - config.entityFields, + splitField, + filterField, datafeedQuery, config.metricFunction, config.metricFieldName, diff --git a/x-pack/plugins/ml/public/services/results_service.js b/x-pack/plugins/ml/public/services/results_service.js index efa5f615de015..80b7d86a2572a 100644 --- a/x-pack/plugins/ml/public/services/results_service.js +++ b/x-pack/plugins/ml/public/services/results_service.js @@ -1401,7 +1401,8 @@ function getEventRateData( function getEventDistributionData( index, types, - entityFields, + splitField, + filterField = null, query, metricFunction, metricFieldName, @@ -1439,8 +1440,13 @@ function getEventDistributionData( mustCriteria.push(query); } - - const entityFieldName = entityFields[0].fieldName; + if (filterField !== null) { + mustCriteria.push({ + term: { + [filterField.fieldName]: filterField.fieldValue + } + }); + } const body = { query: { @@ -1462,8 +1468,8 @@ function getEventDistributionData( aggs: { entities: { terms: { - field: entityFieldName, - size: 1000 + field: splitField.fieldName, + size: 10 } } } @@ -1496,30 +1502,20 @@ function getEventDistributionData( body }) .then((resp) => { - - const dataByTime = _.get(resp, ['aggregations', 'byTime', 'buckets'], []); - // normalize data - const data = []; - dataByTime.forEach((dataForTime) => { + const dataByTime = _.get(resp, ['aggregations', 'byTime', 'buckets'], []); + const data = dataByTime.reduce((d, dataForTime) => { const date = +dataForTime.key; const entities = _.get(dataForTime, ['entities', 'buckets'], []); entities.forEach((entity) => { - data.push({ + d.push({ date, entity: entity.key, value: entity.doc_count }); }); - }); - - // group data by entity - /* - const nestedData = d3.nest() - .key(d => d.entity) - .entries(data); - */ - + return d; + }, []); resolve(data); }) .catch((resp) => { From a498ad314fdd0eef149f0e0bd259e869fc57a3bf Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 21 Sep 2018 10:48:55 +0200 Subject: [PATCH 06/37] [ML] Make sure rare chart shows selected entity. --- .../explorer_charts_container_service.js | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js index 6508820c6272b..8babccd02545f 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js @@ -142,7 +142,7 @@ export function explorerChartsContainerServiceFactory( // Query 4 - load the rare chart's event distribution function getEventDistribution(config, range) { - let splitField = config.entityFields.find(f => f.fiedType === 'partition'); + let splitField = config.entityFields.find(f => f.fieldType === 'partition'); let filterField = null; if (config.functionDescription === 'rare') { splitField = config.entityFields.find(f => f.fieldType === 'by'); @@ -190,16 +190,26 @@ export function explorerChartsContainerServiceFactory( return []; } - let chartData = _.map(metricData, (value, time) => ({ - date: +time, - value: value - })); - + let chartData; if (eventDistribution.length > 0) { - chartData = eventDistribution; + const filterField = records[0].by_field_value; + chartData = eventDistribution.filter(d => (d.entity !== filterField)); + _.map(metricData, (value, time) => { + if (value > 0) { + chartData.push({ + date: +time, + value: value, + entity: filterField + }); + } + }); + } else { + chartData = _.map(metricData, (value, time) => ({ + date: +time, + value: value + })); } - // Iterate through the anomaly records, adding anomalyScore properties // to the chartData entries for anomalous buckets. _.each(records, (record) => { From 0cad955c8a0d7363914cd3126863dfb9b71e57e6 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Sat, 22 Sep 2018 10:50:40 +0200 Subject: [PATCH 07/37] [ML] Introduces sampling to rare context data. Limits rare chart to 30 categories. --- .../explorer_charts/explorer_chart_rare.js | 15 +++++++++++++-- .../ml/public/services/results_service.js | 19 ++++++++++++++----- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js index 6fb93856111e6..5be5c74a37e63 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js @@ -85,8 +85,8 @@ export class ExplorerChartRare extends React.Component { const CHART_TYPE = (config.functionDescription === 'rare') ? 'rare' : 'population'; const CHART_Y_ATTRIBUTE = (config.functionDescription === 'rare') ? 'entity' : 'value'; - init(config); - drawRareChart(config.chartData); + const filteredChartData = init(config); + drawRareChart(filteredChartData); function init({ chartLimits, chartData }) { const $el = $('.ml-explorer-chart'); @@ -103,14 +103,24 @@ export class ExplorerChartRare extends React.Component { .attr('width', svgWidth) .attr('height', svgHeight); + let highlight = chartData.find(d => (d.anomalyScore !== undefined)); + highlight = highlight && highlight.entity; + const categoryLimit = 30; const scaleCategories = d3.nest() .key(d => d.entity) .entries(chartData) .sort((a, b) => { return b.values.length - a.values.length; }) + .filter((d, i) => { + return (i < categoryLimit || d.key === highlight); + }) .map(d => d.key); + chartData = chartData.filter((d) => { + return (scaleCategories.findIndex(s => (s === d.entity)) > -1); + }); + if (CHART_TYPE === 'population') { lineChartYScale = d3.scale.linear() .range([chartHeight, 0]) @@ -178,6 +188,7 @@ export class ExplorerChartRare extends React.Component { .attr('class', 'line-chart') .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + return chartData; } function drawRareChart(data) { diff --git a/x-pack/plugins/ml/public/services/results_service.js b/x-pack/plugins/ml/public/services/results_service.js index 80b7d86a2572a..1640253882656 100644 --- a/x-pack/plugins/ml/public/services/results_service.js +++ b/x-pack/plugins/ml/public/services/results_service.js @@ -1398,6 +1398,8 @@ function getEventRateData( // Extra query object can be supplied, or pass null if no additional query. // Returned response contains a results property, which is an object // of document counts against time (epoch millis). +// const SAMPLER_TOP_TERMS_THRESHOLD = 100000; +const SAMPLER_TOP_TERMS_SHARD_SIZE = 200; function getEventDistributionData( index, types, @@ -1466,10 +1468,17 @@ function getEventDistributionData( min_doc_count: 0 }, aggs: { - entities: { - terms: { - field: splitField.fieldName, - size: 10 + sample: { + sampler: { + shard_size: SAMPLER_TOP_TERMS_SHARD_SIZE + }, + aggs: { + entities: { + terms: { + field: splitField.fieldName, + size: 10 + } + } } } } @@ -1506,7 +1515,7 @@ function getEventDistributionData( const dataByTime = _.get(resp, ['aggregations', 'byTime', 'buckets'], []); const data = dataByTime.reduce((d, dataForTime) => { const date = +dataForTime.key; - const entities = _.get(dataForTime, ['entities', 'buckets'], []); + const entities = _.get(dataForTime, ['sample', 'entities', 'buckets'], []); entities.forEach((entity) => { d.push({ date, From df6aa4efa432f8d0a91cbb30e42a008e03f44c07 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 24 Sep 2018 19:08:19 +0200 Subject: [PATCH 08/37] [ML] Use EUI flex grid instead of some bootstrap classes. --- ...explorer_chart_config_builder.test.js.snap | 1 + ...orer_charts_container_service.test.js.snap | 4 +- .../explorer_charts_container.js | 194 ++++++++++-------- .../explorer_charts_container.test.js | 10 +- .../explorer_charts_container_directive.js | 2 +- .../explorer_charts_container_service.js | 4 +- .../styles/explorer_chart.less | 2 - 7 files changed, 121 insertions(+), 96 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap index f899ee14003b7..ca27cd1065623 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap @@ -27,6 +27,7 @@ Object { "entityFields": Array [ Object { "fieldName": "airline", + "fieldType": "partition", "fieldValue": "JAL", }, ], diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap index f93ba62f6c04c..fbd5e08e0482a 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap @@ -2,7 +2,6 @@ exports[`explorerChartsContainerService call anomalyChangeListener with actual series config 1`] = ` Object { - "layoutCellsPerChart": 12, "seriesToPlot": Array [], "timeFieldName": "timestamp", "tooManyBuckets": false, @@ -11,7 +10,7 @@ Object { exports[`explorerChartsContainerService call anomalyChangeListener with actual series config 2`] = ` Object { - "layoutCellsPerChart": 12, + "chartsPerRow": 1, "seriesToPlot": Array [ Object { "bucketSpanSeconds": 900, @@ -40,6 +39,7 @@ Object { "entityFields": Array [ Object { "fieldName": "airline", + "fieldType": "partition", "fieldValue": "AAL", }, ], diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js index 9ad3fc0dbaf01..efe6fa47e87e5 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js @@ -9,6 +9,8 @@ import React from 'react'; import { EuiButtonEmpty, + EuiFlexGrid, + EuiFlexItem, EuiIconTip, EuiToolTip } from '@elastic/eui'; @@ -25,100 +27,126 @@ const textTooManyBuckets = `This selection contains too many buckets to be displ The dashboard is best viewed over a shorter time range.`; const textViewButton = 'Open in Single Metric Viewer'; -export function ExplorerChartsContainer({ - seriesToPlot, - layoutCellsPerChart, +// create a somewhat unique ID +// from charts metadata for React's key attribute +function getChartId(series) { + const { + jobId, + detectorLabel, + entityFields + } = series; + const entities = entityFields.map((ef) => `${ef.fieldName}/${ef.fieldValue}`).join(','); + const id = `${jobId}_${detectorLabel}_${entities}`; + return id; +} + +// Wrapper for a single explorer chart +function ExplorerChartContainer({ + series, tooManyBuckets, mlSelectSeverityService }) { - const wrapLabel = seriesToPlot.some((series) => isLabelLengthAboveThreshold(series)); - - return ( -
- {(seriesToPlot.length > 0) && - seriesToPlot.map((series) => { + const wrapLabel = isLabelLengthAboveThreshold(series); - // create a somewhat unique ID from charts metadata for React's key attribute - const { - jobId, - detectorLabel, - entityFields, - functionDescription - } = series; - const entities = entityFields.map((ef) => `${ef.fieldName}/${ef.fieldValue}`).join(','); - const id = `${jobId}_${detectorLabel}_${entities}`; + const { + detectorLabel, + entityFields, + functionDescription + } = series; + return ( + +
+ {tooManyBuckets && ( + + + + )} + + window.open(getExploreSeriesLink(series), '_blank')} + > + View + + +
+ + {(() => { + if (functionDescription === 'rare') { return ( -
-
- {tooManyBuckets && ( - - - - )} - - window.open(getExploreSeriesLink(series), '_blank')} - > - View - - -
- - {(() => { - if (functionDescription === 'rare') { - return ( - - ); - } - if (functionDescription === 'count') { - return ( - - ); - } - return ( - - ); - })()} -
+ ); - }) - } -
+ } + if (functionDescription === 'count') { + return ( + + ); + } + return ( + + ); + })()} + + ); +} + +// Flex layout wrapper for all explorer charts +export function ExplorerChartsContainer({ + chartsPerRow, + seriesToPlot, + tooManyBuckets, + mlSelectSeverityService +}) { + // doesn't allow a setting of `columns={1}` when chartsPerRow would be 1. + // If that's the case we trick it doing that with the following settings: + const chartsWidth = (chartsPerRow === 1) ? 'calc(100% - 20px)' : 'auto'; + const chartsColumns = (chartsPerRow === 1) ? 0 : chartsPerRow; + + return ( + + {(seriesToPlot.length > 0) && seriesToPlot.map((series) => ( + + + + ))} + ); } ExplorerChartsContainer.propTypes = { seriesToPlot: PropTypes.array.isRequired, - layoutCellsPerChart: PropTypes.number.isRequired, tooManyBuckets: PropTypes.bool.isRequired, mlSelectSeverityService: PropTypes.object.isRequired, mlChartTooltipService: PropTypes.object.isRequired diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.js index 1036caccfe168..3b8836debf506 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.js @@ -61,7 +61,7 @@ timefilter.setTime({ to: moment(seriesConfig.selectedLatest).toISOString() }); -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; import React from 'react'; import { chartLimits } from '../../util/chart_utils'; @@ -86,23 +86,23 @@ describe('ExplorerChartsContainer', () => { test('Minimal Initialization', () => { const wrapper = shallow(); - expect(wrapper.html()).toBe('
'); + expect(wrapper.html()).toBe('
'); }); test('Initialization with chart data', () => { - const wrapper = shallow( Date: Fri, 28 Sep 2018 11:08:08 +0200 Subject: [PATCH 09/37] [ML] Fixes rebase regressions. --- .../explorer/explorer_charts/explorer_chart_rare.js | 1 + .../explorer_charts/explorer_charts_container.js | 13 ++++++++----- .../explorer_charts/styles/explorer_chart.less | 1 - 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js index 5be5c74a37e63..333226ba15fb8 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js @@ -100,6 +100,7 @@ export class ExplorerChartRare extends React.Component { const svgHeight = chartHeight + margin.top + margin.bottom; const svg = chartElement.append('svg') + .classed('ml-explorer-chart-svg', true) .attr('width', svgWidth) .attr('height', svgHeight); diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js index efe6fa47e87e5..c2c98a19a461a 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js @@ -44,10 +44,9 @@ function getChartId(series) { function ExplorerChartContainer({ series, tooManyBuckets, - mlSelectSeverityService + mlSelectSeverityService, + wrapLabel }) { - const wrapLabel = isLabelLengthAboveThreshold(series); - const { detectorLabel, entityFields, @@ -55,7 +54,8 @@ function ExplorerChartContainer({ } = series; return ( - + // Needs to be a div, Using React.Fragment would break the Flex Layout +
{tooManyBuckets && ( @@ -115,7 +115,7 @@ function ExplorerChartContainer({ /> ); })()} - +
); } @@ -131,6 +131,8 @@ export function ExplorerChartsContainer({ const chartsWidth = (chartsPerRow === 1) ? 'calc(100% - 20px)' : 'auto'; const chartsColumns = (chartsPerRow === 1) ? 0 : chartsPerRow; + const wrapLabel = seriesToPlot.some((series) => isLabelLengthAboveThreshold(series)); + return ( {(seriesToPlot.length > 0) && seriesToPlot.map((series) => ( @@ -139,6 +141,7 @@ export function ExplorerChartsContainer({ series={series} tooManyBuckets={tooManyBuckets} mlSelectSeverityService={mlSelectSeverityService} + wrapLabel={wrapLabel} /> ))} diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart.less b/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart.less index aca0ff57f5767..c4089212e522c 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart.less +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart.less @@ -1,4 +1,3 @@ -ml-explorer-chart, .ml-explorer-chart-container { .ml-explorer-chart-svg { From 829016efaea457e81f7022903b294e437ebcfe9f Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 3 Oct 2018 07:40:02 +0200 Subject: [PATCH 10/37] [ML] Use EuiFlexGroup for Explorer Charts header. --- .../explorer_chart_label.js | 4 +- .../explorer_chart_label_badge.js | 2 +- .../styles/explorer_chart_label.less | 33 -------- .../styles/explorer_chart_label_badge.less | 3 +- .../explorer_charts_container.js | 76 ++++++++++--------- .../styles/explorer_charts_container.less | 1 - 6 files changed, 44 insertions(+), 75 deletions(-) delete mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label.less diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js index 218f270f806c5..d1632e235d936 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './styles/explorer_chart_label.less'; - import PropTypes from 'prop-types'; import React from 'react'; @@ -62,7 +60,7 @@ export function ExplorerChartLabel({ detectorLabel, entityFields, infoTooltip, w )} {wrapLabel && ( -
{entityFieldBadges}
+
{entityFieldBadges}
)} ); diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.js b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.js index 6a0d34991d895..133005c2226f4 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.js @@ -16,7 +16,7 @@ import { export function ExplorerChartLabelBadge({ entity }) { return ( - + {entity.fieldName} {entity.fieldValue} diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label.less b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label.less deleted file mode 100644 index c9065f9404f0e..0000000000000 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label.less +++ /dev/null @@ -1,33 +0,0 @@ -.ml-explorer-chart-label { - font-weight: normal; - /* account 80px for the "View" link and potential alert icon */ - max-width: calc(~"100% - 80px"); - overflow: hidden; -} - -.ml-explorer-chart-label-detector { - vertical-align: middle; - /* account 100px for the "View" link and info icon */ - max-width: calc(~"100% - 100px"); - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - display: inline-block; -} - -.ml-explorer-chart-info-icon { - margin: 0; - vertical-align: middle; - display: inline-block; -} - -/* only used when the field badges get wrapped to a new line */ -.ml-explorer-chart-label-fields { - width: 100%; - /* use a fixed height to avoid layout issues when - only some charts don't have entity fields */ - height: 22px; - overflow: hidden; - white-space: nowrap; - line-height: 0; -} diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label_badge.less b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label_badge.less index 69b13432f05c9..80a8faa3af65e 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label_badge.less +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label_badge.less @@ -4,7 +4,6 @@ Used in the Explorer Chart label badge to display an entity's field_name as `normal` and field_value as `strong`. */ -.ml-explorer-chart-label-badge { +.ml-reset-font-weight { font-weight: normal; - vertical-align: middle; } diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js index c2c98a19a461a..8a91e405dc3fe 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js @@ -10,6 +10,7 @@ import React from 'react'; import { EuiButtonEmpty, EuiFlexGrid, + EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiToolTip @@ -54,40 +55,45 @@ function ExplorerChartContainer({ } = series; return ( - // Needs to be a div, Using React.Fragment would break the Flex Layout -
-
- {tooManyBuckets && ( - - + + + + + +
+ {tooManyBuckets && ( + + + + )} + - - )} - - window.open(getExploreSeriesLink(series), '_blank')} - > - View - - -
- + content={textViewButton} + > + window.open(getExploreSeriesLink(series), '_blank')} + > + View + + +
+ + {(() => { if (functionDescription === 'rare') { return ( @@ -98,7 +104,7 @@ function ExplorerChartContainer({ /> ); } - if (functionDescription === 'count') { + if (functionDescription === 'count' && series.entityFields.find(f => f.fieldType === 'over')) { return ( ); })()} -
+ ); } diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_charts_container.less b/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_charts_container.less index 04a55fb0bc316..36f23bff87bf4 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_charts_container.less +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_charts_container.less @@ -104,7 +104,6 @@ /* wrapper class for the top right alert icon and view button */ .ml-explorer-chart-icons { - float:right; padding-left: 5px; /* counter-margin for EuiButtonEmpty's padding */ margin: 2px -8px 0 0; From 21df55b1020d6cf1fd1a2ed0d47aa9b40ca7012c Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 3 Oct 2018 07:41:41 +0200 Subject: [PATCH 11/37] [ML] Data transformation fixes for rare/population chart. --- .../explorer_charts/explorer_chart_rare.js | 38 ++++++++++++++----- .../explorer_charts_container_service.js | 23 ++++++++--- .../styles/explorer_chart.less | 7 +++- .../ml/public/services/results_service.js | 2 +- 4 files changed, 53 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js index 333226ba15fb8..78903b111f0f6 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js @@ -76,6 +76,9 @@ export class ExplorerChartRare extends React.Component { // Left margin is adjusted later for longest y-axis label. const margin = { top: 10, right: 0, bottom: 30, left: 0 }; + if (config.functionDescription === 'count') { + margin.left = 60; + } let lineChartXScale = null; let lineChartYScale = null; @@ -85,10 +88,13 @@ export class ExplorerChartRare extends React.Component { const CHART_TYPE = (config.functionDescription === 'rare') ? 'rare' : 'population'; const CHART_Y_ATTRIBUTE = (config.functionDescription === 'rare') ? 'entity' : 'value'; + let highlight = config.chartData.find(d => (d.anomalyScore !== undefined)); + highlight = highlight && highlight.entity; + const filteredChartData = init(config); drawRareChart(filteredChartData); - function init({ chartLimits, chartData }) { + function init({ chartData }) { const $el = $('.ml-explorer-chart'); // Clear any existing elements from the visualization, @@ -104,8 +110,6 @@ export class ExplorerChartRare extends React.Component { .attr('width', svgWidth) .attr('height', svgHeight); - let highlight = chartData.find(d => (d.anomalyScore !== undefined)); - highlight = highlight && highlight.entity; const categoryLimit = 30; const scaleCategories = d3.nest() .key(d => d.entity) @@ -114,21 +118,32 @@ export class ExplorerChartRare extends React.Component { return b.values.length - a.values.length; }) .filter((d, i) => { + // only filter for rare charts + if (CHART_TYPE === 'rare') { + return true; + } return (i < categoryLimit || d.key === highlight); }) .map(d => d.key); chartData = chartData.filter((d) => { - return (scaleCategories.findIndex(s => (s === d.entity)) > -1); + return (scaleCategories.includes(d.entity)); }); if (CHART_TYPE === 'population') { + const focusData = chartData.filter((d) => { + return d.entity === highlight; + }).map(d => d.value); + const focusExtent = d3.extent(focusData); + + // now again filter chartData to include only the data points within the domain + chartData = chartData.filter((d) => { + return (d.value <= focusExtent[1]); + }); + lineChartYScale = d3.scale.linear() .range([chartHeight, 0]) - .domain([ - chartLimits.min, - chartLimits.max - ]) + .domain([0, focusExtent[1]]) .nice(); } else if (CHART_TYPE === 'rare') { // avoid overflowing the border of the highlighted area @@ -239,7 +254,6 @@ export class ExplorerChartRare extends React.Component { xAxis.ticks(numTicksForDateFormat(vizWidth, xAxisTickFormat)); } - const yAxis = d3.svg.axis().scale(lineChartYScale) .orient('left') .innerTickSize(0) @@ -277,7 +291,11 @@ export class ExplorerChartRare extends React.Component { const dots = dotGroup.selectAll('circle').data(dotsData); dots.enter().append('circle') - .attr('r', radius); + .classed('values-dots-circle', true) + .classed('values-dots-circle-blur', (d) => { + return (d.entity !== highlight); + }) + .attr('r', d => ((d.entity === highlight) ? (radius * 1.5) : radius)); dots .attr('cx', rareChartValuesLine.x()) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js index 7da510c45855c..9a9948396aac5 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js @@ -138,15 +138,28 @@ export function explorerChartsContainerServiceFactory( ); } - // Query 4 - load the rare chart's event distribution + // Query 4 - load context data distribution function getEventDistribution(config, range) { - let splitField = config.entityFields.find(f => f.fieldType === 'partition'); + // Defaults to use the "partition" field to group the data + let splitField; let filterField = null; + + // If this is a detector using the rare function, + // use the "by" field to split the data and filter by the selection partition. if (config.functionDescription === 'rare') { splitField = config.entityFields.find(f => f.fieldType === 'by'); filterField = config.entityFields.find(f => f.fieldType === 'partition'); } + // Logic to consider an over field: If the above didn't result in choosing a split Field, + // check if an over field is part of the detector, if yes, use that to split and + // filter by a possible partition field. + const overField = config.entityFields.find(f => f.fieldType === 'over'); + if (splitField === undefined && overField !== undefined) { + splitField = overField; + filterField = config.entityFields.find(f => f.fieldType === 'partition'); + } + const datafeedQuery = _.get(config, 'datafeedConfig.query', null); return mlResultsService.getEventDistributionData( config.datafeedConfig.indices, @@ -190,7 +203,7 @@ export function explorerChartsContainerServiceFactory( let chartData; if (eventDistribution.length > 0) { - const filterField = records[0].by_field_value; + const filterField = records[0].by_field_value || records[0].over_field_value; chartData = eventDistribution.filter(d => (d.entity !== filterField)); _.map(metricData, (value, time) => { if (value > 0) { @@ -267,9 +280,9 @@ export function explorerChartsContainerServiceFactory( function findNearestChartPointToTime(chartData, record) { const time = record[ML_TIME_FIELD_NAME]; - if (record.function === 'rare' || record.function === 'count') { + if (record.function === 'rare' || record.over_field_value !== 'undefined') { chartData = chartData.filter((d) => { - return d.entity === record.by_field_value; + return d.entity === (record.by_field_value || record.over_field_value); }); } diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart.less b/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart.less index c4089212e522c..9f29781ef078c 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart.less +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart.less @@ -51,11 +51,16 @@ stroke-width: 2; } - .values-dots circle { + .values-dots circle, + .values-dots-circle { fill: #32a7c2; stroke-width: 0; } + .values-dots circle.values-dots-circle-blur { + fill: #aaa; + } + .metric-value { opacity: 1; fill: #32a7c2; diff --git a/x-pack/plugins/ml/public/services/results_service.js b/x-pack/plugins/ml/public/services/results_service.js index 1640253882656..e1e4778d77288 100644 --- a/x-pack/plugins/ml/public/services/results_service.js +++ b/x-pack/plugins/ml/public/services/results_service.js @@ -1414,7 +1414,7 @@ function getEventDistributionData( interval) { return new Promise((resolve, reject) => { // only get this data for count (used by rare chart) - if (metricFunction !== 'count') { + if (metricFunction !== 'count' || splitField === undefined) { return resolve([]); } From de2a31152b3c25531ac6291561580fb0a1aeebfd Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 3 Oct 2018 08:15:33 +0200 Subject: [PATCH 12/37] [ML] Consolidate chart type selection. --- .../explorer_charts/explorer_chart_rare.js | 21 +++++++++++-------- .../explorer_charts_container.js | 19 +++++++---------- .../ml/public/explorer/explorer_constants.js | 6 ++++++ x-pack/plugins/ml/public/util/chart_utils.js | 14 +++++++++++++ 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js index 78903b111f0f6..8de7279ffe7a7 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js @@ -24,6 +24,7 @@ import moment from 'moment'; import { formatValue } from '../../formatters/format_value'; import { getSeverityWithLow } from '../../../common/util/anomaly_utils'; import { + getChartType, getTickValues, numTicksForDateFormat, removeLabelOverlap @@ -34,6 +35,8 @@ import { mlEscape } from '../../util/string_utils'; import { mlFieldFormatService } from '../../services/field_format_service'; import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tooltip_service'; +import { CHART_TYPE } from '../explorer_constants'; + const CONTENT_WRAPPER_HEIGHT = 215; export class ExplorerChartRare extends React.Component { @@ -85,8 +88,8 @@ export class ExplorerChartRare extends React.Component { let lineChartGroup; let lineChartValuesLine = null; - const CHART_TYPE = (config.functionDescription === 'rare') ? 'rare' : 'population'; - const CHART_Y_ATTRIBUTE = (config.functionDescription === 'rare') ? 'entity' : 'value'; + const chartType = getChartType(config); + const CHART_Y_ATTRIBUTE = (chartType === CHART_TYPE.EVENT_DISTRIBUTION) ? 'entity' : 'value'; let highlight = config.chartData.find(d => (d.anomalyScore !== undefined)); highlight = highlight && highlight.entity; @@ -119,7 +122,7 @@ export class ExplorerChartRare extends React.Component { }) .filter((d, i) => { // only filter for rare charts - if (CHART_TYPE === 'rare') { + if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) { return true; } return (i < categoryLimit || d.key === highlight); @@ -130,7 +133,7 @@ export class ExplorerChartRare extends React.Component { return (scaleCategories.includes(d.entity)); }); - if (CHART_TYPE === 'population') { + if (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { const focusData = chartData.filter((d) => { return d.entity === highlight; }).map(d => d.value); @@ -145,14 +148,14 @@ export class ExplorerChartRare extends React.Component { .range([chartHeight, 0]) .domain([0, focusExtent[1]]) .nice(); - } else if (CHART_TYPE === 'rare') { + } else if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) { // avoid overflowing the border of the highlighted area const rowMargin = 5; lineChartYScale = d3.scale.ordinal() .rangePoints([rowMargin, chartHeight - rowMargin]) .domain(scaleCategories); } else { - throw `CHART_TYPE '${CHART_TYPE}' not supported`; + throw `chartType '${chartType}' not supported`; } const yAxis = d3.svg.axis().scale(lineChartYScale) @@ -164,7 +167,7 @@ export class ExplorerChartRare extends React.Component { let maxYAxisLabelWidth = 0; const tempLabelText = svg.append('g') .attr('class', 'temp-axis-label tick'); - const tempLabelTextData = (CHART_TYPE === 'population') ? lineChartYScale.ticks() : scaleCategories; + const tempLabelTextData = (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) ? lineChartYScale.ticks() : scaleCategories; tempLabelText.selectAll('text.temp.axis').data(tempLabelTextData) .enter() .append('text') @@ -172,7 +175,7 @@ export class ExplorerChartRare extends React.Component { if (fieldFormat !== undefined) { return fieldFormat.convert(d, 'text'); } else { - if (CHART_TYPE === 'population') { + if (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { return lineChartYScale.tickFormat()(d); } return d; @@ -387,7 +390,7 @@ export class ExplorerChartRare extends React.Component { // Show actual/typical when available except for rare detectors. // Rare detectors always have 1 as actual and the probability as typical. // Exposing those values in the tooltip with actual/typical labels might irritate users. - if (_.has(marker, 'actual') && config.functionDescription !== 'rare') { + if (_.has(marker, 'actual') && chartType === CHART_TYPE.EVENT_DISTRIBUTION) { // Display the record actual in preference to the chart value, which may be // different depending on the aggregation interval of the chart. contents += (`
actual: ${formatValue(marker.actual, config.functionDescription, fieldFormat)}`); diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js index 8a91e405dc3fe..7c6e7e9a2f7b9 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js @@ -17,6 +17,7 @@ import { } from '@elastic/eui'; import { + getChartType, getExploreSeriesLink, isLabelLengthAboveThreshold } from '../../util/chart_utils'; @@ -24,6 +25,8 @@ import { ExplorerChartRare } from './explorer_chart_rare'; import { ExplorerChartSingleMetric } from './explorer_chart_single_metric'; import { ExplorerChartLabel } from './components/explorer_chart_label'; +import { CHART_TYPE } from '../explorer_constants'; + const textTooManyBuckets = `This selection contains too many buckets to be displayed. The dashboard is best viewed over a shorter time range.`; const textViewButton = 'Open in Single Metric Viewer'; @@ -50,10 +53,11 @@ function ExplorerChartContainer({ }) { const { detectorLabel, - entityFields, - functionDescription + entityFields } = series; + const chartType = getChartType(series); + return ( @@ -95,16 +99,7 @@ function ExplorerChartContainer({ {(() => { - if (functionDescription === 'rare') { - return ( - - ); - } - if (functionDescription === 'count' && series.entityFields.find(f => f.fieldType === 'over')) { + if (chartType === CHART_TYPE.EVENT_DISTRIBUTION || chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { return ( f.fieldType === 'over')) { + return CHART_TYPE.POPULATION_DISTRIBUTION; + } + + return CHART_TYPE.SINGLE_METRIC; +} + export function getExploreSeriesLink(series) { // Open the Single Metric dashboard over the same overall bounds and // zoomed in to the same time as the current chart. From 832e21b6c1f6f25750c8e3d1a1044366a19aae65 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 3 Oct 2018 08:30:58 +0200 Subject: [PATCH 13/37] [ML] Support for chart type feature flags. --- .../explorer_charts_container_service.js | 24 +++++++++---------- x-pack/plugins/ml/public/util/chart_utils.js | 18 ++++++++++---- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js index 9a9948396aac5..1a3269cba5495 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js @@ -16,11 +16,15 @@ import _ from 'lodash'; import { buildConfig } from './explorer_chart_config_builder'; -import { chartLimits } from '../../util/chart_utils'; +import { + chartLimits, + getChartType +} from '../../util/chart_utils'; import { isTimeSeriesViewDetector } from '../../../common/util/job_utils'; import { mlResultsService } from '../../services/results_service'; import { mlJobService } from '../../services/job_service'; +import { CHART_TYPE } from '../explorer_constants'; export function explorerChartsContainerServiceFactory( mlSelectSeverityService, @@ -140,23 +144,17 @@ export function explorerChartsContainerServiceFactory( // Query 4 - load context data distribution function getEventDistribution(config, range) { - // Defaults to use the "partition" field to group the data + const chartType = getChartType(config); + let splitField; let filterField = null; - // If this is a detector using the rare function, - // use the "by" field to split the data and filter by the selection partition. - if (config.functionDescription === 'rare') { + // Define splitField and filterField based on chartType + if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) { splitField = config.entityFields.find(f => f.fieldType === 'by'); filterField = config.entityFields.find(f => f.fieldType === 'partition'); - } - - // Logic to consider an over field: If the above didn't result in choosing a split Field, - // check if an over field is part of the detector, if yes, use that to split and - // filter by a possible partition field. - const overField = config.entityFields.find(f => f.fieldType === 'over'); - if (splitField === undefined && overField !== undefined) { - splitField = overField; + } else if (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { + splitField = config.entityFields.find(f => f.fieldType === 'over'); filterField = config.entityFields.find(f => f.fieldType === 'partition'); } diff --git a/x-pack/plugins/ml/public/util/chart_utils.js b/x-pack/plugins/ml/public/util/chart_utils.js index 728a19717aa34..f814a54d641e9 100644 --- a/x-pack/plugins/ml/public/util/chart_utils.js +++ b/x-pack/plugins/ml/public/util/chart_utils.js @@ -123,12 +123,22 @@ export function filterAxisLabels(selection, chartWidth) { }); } +// feature flags for chart types +const EVENT_DISTRIBUTION_ENABLED = true; +const POPULATION_DISTRIBUTION_ENABLED = true; + +// get the chart type based on its configuration export function getChartType(config) { - if (config.functionDescription === 'rare') { + if ( + EVENT_DISTRIBUTION_ENABLED && + config.functionDescription === 'rare' + ) { return CHART_TYPE.EVENT_DISTRIBUTION; - } - - if (config.functionDescription === 'count' && config.entityFields.find(f => f.fieldType === 'over')) { + } else if ( + POPULATION_DISTRIBUTION_ENABLED && + config.functionDescription === 'count' && + config.entityFields.find(f => f.fieldType === 'over') + ) { return CHART_TYPE.POPULATION_DISTRIBUTION; } From fa02dff3e6f435ad06c9dd5de9899c2b30b460bc Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 3 Oct 2018 08:41:03 +0200 Subject: [PATCH 14/37] [ML] Fix left margin for population chart. --- .../explorer/explorer_charts/explorer_chart_rare.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js index 8de7279ffe7a7..280abd5cab456 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js @@ -77,9 +77,11 @@ export class ExplorerChartRare extends React.Component { const LINE_CHART_ANOMALY_RADIUS = 7; const SCHEDULED_EVENT_MARKER_HEIGHT = 5; + const chartType = getChartType(config); + // Left margin is adjusted later for longest y-axis label. const margin = { top: 10, right: 0, bottom: 30, left: 0 }; - if (config.functionDescription === 'count') { + if (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { margin.left = 60; } @@ -88,7 +90,6 @@ export class ExplorerChartRare extends React.Component { let lineChartGroup; let lineChartValuesLine = null; - const chartType = getChartType(config); const CHART_Y_ATTRIBUTE = (chartType === CHART_TYPE.EVENT_DISTRIBUTION) ? 'entity' : 'value'; let highlight = config.chartData.find(d => (d.anomalyScore !== undefined)); @@ -188,7 +189,9 @@ export class ExplorerChartRare extends React.Component { d3.select('.temp-axis-label').remove(); // Set the size of the left margin according to the width of the largest y axis tick label. - // margin.left = (Math.max(maxYAxisLabelWidth, 40)); + if (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { + margin.left = (Math.max(maxYAxisLabelWidth, 40)); + } vizWidth = svgWidth - margin.left - margin.right; // Set the x axis domain to match the request plot range. From f4667fde0d8b58366471aaa20ab66d9833a1ce67 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 3 Oct 2018 09:19:19 +0200 Subject: [PATCH 15/37] [ML] Fixes data transformation when population chart is disabled. --- .../explorer_charts_container_service.js | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js index 1a3269cba5495..cd2676e81b4cd 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js @@ -191,6 +191,7 @@ export function explorerChartsContainerServiceFactory( const jobId = seriesConfigs[seriesIndex].jobId; const scheduledEvents = response[2].events[jobId]; const eventDistribution = response[3]; + const chartType = getChartType(seriesConfigs[seriesIndex]); // Return dataset in format used by the chart. // i.e. array of Objects with keys date (timestamp), value, @@ -219,13 +220,24 @@ export function explorerChartsContainerServiceFactory( })); } + let chartDataForPointSearch = chartData; + // Iterate through the anomaly records, adding anomalyScore properties // to the chartData entries for anomalous buckets. _.each(records, (record) => { + if ( + chartType === CHART_TYPE.EVENT_DISTRIBUTION || + chartType === CHART_TYPE.POPULATION_DISTRIBUTION + ) { + chartDataForPointSearch = chartData.filter((d) => { + return d.entity === (record.by_field_value || record.over_field_value); + }); + } + // Look for a chart point with the same time as the record. // If none found, find closest time in chartData set. - let chartPoint = findNearestChartPointToTime(chartData, record); + let chartPoint = findNearestChartPointToTime(chartDataForPointSearch, record); if (chartPoint === undefined) { // In case there is a record with a time after that of the last chart point, set the score @@ -278,12 +290,6 @@ export function explorerChartsContainerServiceFactory( function findNearestChartPointToTime(chartData, record) { const time = record[ML_TIME_FIELD_NAME]; - if (record.function === 'rare' || record.over_field_value !== 'undefined') { - chartData = chartData.filter((d) => { - return d.entity === (record.by_field_value || record.over_field_value); - }); - } - let chartPoint; for (let i = 0; i < chartData.length; i++) { if (chartData[i].date === time) { From 3d7839e4e80c5f8391d5c43e35e8e3139f7cb9e8 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 3 Oct 2018 10:37:06 +0200 Subject: [PATCH 16/37] [ML] Fixes scheduled event markers for population chart. --- .../explorer_charts_container_service.js | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js index cd2676e81b4cd..0126875ca0429 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js @@ -220,24 +220,13 @@ export function explorerChartsContainerServiceFactory( })); } - let chartDataForPointSearch = chartData; - // Iterate through the anomaly records, adding anomalyScore properties // to the chartData entries for anomalous buckets. _.each(records, (record) => { - - if ( - chartType === CHART_TYPE.EVENT_DISTRIBUTION || - chartType === CHART_TYPE.POPULATION_DISTRIBUTION - ) { - chartDataForPointSearch = chartData.filter((d) => { - return d.entity === (record.by_field_value || record.over_field_value); - }); - } - // Look for a chart point with the same time as the record. // If none found, find closest time in chartData set. - let chartPoint = findNearestChartPointToTime(chartDataForPointSearch, record); + const recordTime = record[ML_TIME_FIELD_NAME]; + let chartPoint = findNearestChartPointToTime(getChartDataForPointSearch(chartData, record), recordTime, chartType); if (chartPoint === undefined) { // In case there is a record with a time after that of the last chart point, set the score @@ -275,7 +264,7 @@ export function explorerChartsContainerServiceFactory( // which correspond to times of scheduled events for the job. if (scheduledEvents !== undefined) { _.each(scheduledEvents, (events, time) => { - const chartPoint = findNearestChartPointToTime(chartData, time); + const chartPoint = findNearestChartPointToTime(getChartDataForPointSearch(chartData, records[0]), Number(time), chartType); 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. @@ -287,9 +276,20 @@ export function explorerChartsContainerServiceFactory( return chartData; } - function findNearestChartPointToTime(chartData, record) { - const time = record[ML_TIME_FIELD_NAME]; + function getChartDataForPointSearch(chartData, record, chartType) { + if ( + chartType === CHART_TYPE.EVENT_DISTRIBUTION || + chartType === CHART_TYPE.POPULATION_DISTRIBUTION + ) { + return chartData.filter((d) => { + return d.entity === (record.by_field_value || record.over_field_value); + }); + } + + return chartData; + } + function findNearestChartPointToTime(chartData, time) { let chartPoint; for (let i = 0; i < chartData.length; i++) { if (chartData[i].date === time) { From 651bc4890e4bf729650a06c11e4ddc8b013e35cc Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 3 Oct 2018 11:26:26 +0200 Subject: [PATCH 17/37] [ML] Unify chartdata filtering for markers and scheduled events. --- .../explorer_charts/explorer_charts_container_service.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js index 0126875ca0429..18442eb85d294 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js @@ -222,11 +222,12 @@ export function explorerChartsContainerServiceFactory( // Iterate through the anomaly records, adding anomalyScore properties // to the chartData entries for anomalous buckets. + const chartDataForPointSearch = getChartDataForPointSearch(chartData, records[0], chartType); _.each(records, (record) => { // Look for a chart point with the same time as the record. // If none found, find closest time in chartData set. const recordTime = record[ML_TIME_FIELD_NAME]; - let chartPoint = findNearestChartPointToTime(getChartDataForPointSearch(chartData, record), recordTime, chartType); + let chartPoint = findNearestChartPointToTime(chartDataForPointSearch, recordTime); if (chartPoint === undefined) { // In case there is a record with a time after that of the last chart point, set the score @@ -264,7 +265,7 @@ export function explorerChartsContainerServiceFactory( // which correspond to times of scheduled events for the job. if (scheduledEvents !== undefined) { _.each(scheduledEvents, (events, time) => { - const chartPoint = findNearestChartPointToTime(getChartDataForPointSearch(chartData, records[0]), Number(time), chartType); + const chartPoint = findNearestChartPointToTime(chartDataForPointSearch, Number(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. From 0625fcdfab430ce0cd1cc064903a4feec0ad98be Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 3 Oct 2018 16:05:07 +0200 Subject: [PATCH 18/37] [ML] Chart info tooltips include a descriptive text for rare and population charts. --- .../explorer_chart_label.js | 7 +- .../styles/explorer_chart_label.less | 3 + .../explorer_chart_info_tooltip.js | 81 +++++++++++++++++++ ...js => explorer_chart_info_tooltip.test.js} | 4 +- .../explorer_charts/explorer_chart_tooltip.js | 37 --------- .../explorer_charts_container.js | 2 +- .../styles/explorer_chart_info_tooltip.less | 34 ++++++++ 7 files changed, 126 insertions(+), 42 deletions(-) create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label.less create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js rename x-pack/plugins/ml/public/explorer/explorer_charts/{explorer_chart_tooltip.test.js => explorer_chart_info_tooltip.test.js} (81%) delete mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_tooltip.js create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart_info_tooltip.less diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js index d1632e235d936..a8f5e84ba4523 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import './styles/explorer_chart_label.less'; + import PropTypes from 'prop-types'; import React from 'react'; @@ -12,7 +14,7 @@ import { } from '@elastic/eui'; import { ExplorerChartLabelBadge } from './explorer_chart_label_badge'; -import { ExplorerChartTooltip } from '../../explorer_chart_tooltip'; +import { ExplorerChartInfoTooltip } from '../../explorer_chart_info_tooltip'; export function ExplorerChartLabel({ detectorLabel, entityFields, infoTooltip, wrapLabel = false }) { // Depending on whether we wrap the entityField badges to a new line, we render this differently: @@ -41,7 +43,8 @@ export function ExplorerChartLabel({ detectorLabel, entityFields, infoTooltip, w const infoIcon = ( } + className="ml-explorer-chart-eui-icon-tip" + content={} position="top" size="s" /> diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label.less b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label.less new file mode 100644 index 0000000000000..58692cf0a6f24 --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label.less @@ -0,0 +1,3 @@ +.ml-explorer-chart-eui-icon-tip { + max-width: none; +} diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js new file mode 100644 index 0000000000000..f60f7b491cf15 --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import './styles/explorer_chart_info_tooltip.less'; + +import PropTypes from 'prop-types'; +import React from 'react'; + +import { CHART_TYPE } from '../explorer_constants'; + +const CHART_DESCRIPTION = { + [CHART_TYPE.EVENT_DISTRIBUTION]: 'The gray dots depict the distribution of occurences over time for a sample of by_field_values with \ +more frequent event types at the top and more rare ones at the bottom.', + [CHART_TYPE.POPULATION_DISTRIBUTION]: 'The gray dots depict the distribution of values over time for a sample of over_field_values.' +}; + +function TooltipDefinitionList({ toolTipData }) { + return ( +
+ {toolTipData.map(({ title, description }) => { + return ( + +
{title}
+
{description}
+
+ ); + })} +
+ ); +} + +export function ExplorerChartInfoTooltip({ + jobId, + aggregationInterval, + chartFunction, + chartType, + entityFields = [], +}) { + const chartDescription = CHART_DESCRIPTION[chartType]; + + const toolTipData = [ + { + title: 'job ID', + description: jobId, + }, + { + title: 'aggregation interval', + description: aggregationInterval, + }, + { + title: 'chart function', + description: chartFunction, + }, + ]; + + entityFields.forEach((entityField) => { + toolTipData.push({ + title: entityField.fieldName, + description: entityField.fieldValue + }); + }); + + return ( +
+ + {chartDescription && ( + {chartDescription} + )} +
+ ); +} +ExplorerChartInfoTooltip.propTypes = { + jobId: PropTypes.string.isRequired, + aggregationInterval: PropTypes.string, + chartFunction: PropTypes.string, + entityFields: PropTypes.array +}; diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_tooltip.test.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.test.js similarity index 81% rename from x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_tooltip.test.js rename to x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.test.js index c83ed6af6333b..6db4360fa7f20 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_tooltip.test.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.test.js @@ -8,7 +8,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { ExplorerChartTooltip } from './explorer_chart_tooltip'; +import { ExplorerChartInfoTooltip } from './explorer_chart_info_tooltip'; describe('ExplorerChartTooltip', () => { test('Render tooltip based on infoTooltip data.', () => { @@ -22,7 +22,7 @@ describe('ExplorerChartTooltip', () => { jobId: 'mock-job-id' }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_tooltip.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_tooltip.js deleted file mode 100644 index c0da82233e067..0000000000000 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_tooltip.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - -import PropTypes from 'prop-types'; -import React from 'react'; - -export function ExplorerChartTooltip({ - jobId, - aggregationInterval, - chartFunction, - entityFields = [], -}) { - return ( -
- job ID: {jobId}
- aggregation interval: {aggregationInterval}
- chart function: {chartFunction} - {entityFields.map((entityField, i) => { - return ( - -
{entityField.fieldName}: {entityField.fieldValue} -
- ); - })} -
- ); -} -ExplorerChartTooltip.propTypes = { - jobId: PropTypes.string.isRequired, - aggregationInterval: PropTypes.string, - chartFunction: PropTypes.string, - entityFields: PropTypes.array -}; diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js index 7c6e7e9a2f7b9..5fd49b20f601b 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js @@ -65,7 +65,7 @@ function ExplorerChartContainer({ diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart_info_tooltip.less b/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart_info_tooltip.less new file mode 100644 index 0000000000000..0139ae1259478 --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart_info_tooltip.less @@ -0,0 +1,34 @@ +.ml-explorer-chart-info-tooltip { + max-width: 384px; +} + +.ml-explorer-chart-description { + font-size: 12px; + font-style: italic; +} + +.ml-explorer-chart-info-tooltip .mlDescriptionList > * { + margin-top: 3px; +} + +.ml-explorer-chart-info-tooltip .mlDescriptionList { + display: grid; + grid-template-columns: max-content auto; + + .mlDescriptionList__title { + color: #fff; + font-size: 12px; + font-weight: normal; + white-space: nowrap; + grid-column-start: 1; + } + + .mlDescriptionList__description { + color: #fff; + font-size: 12px; + font-weight: bold; + padding-left: 8px; + max-width: 256px; + grid-column-start: 2; + } +} From 6f3895a05004a871a48dac1bbc8f65faefd2a289 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 3 Oct 2018 16:33:26 +0200 Subject: [PATCH 19/37] [ML] Fixes tests to reflect new structure. --- .../explorer_chart_info_tooltip.test.js.snap | 30 +++++++++++++++++++ .../explorer_chart_tooltip.test.js.snap | 24 --------------- ...orer_charts_container_service.test.js.snap | 3 +- .../explorer_chart_label.test.js.snap | 10 +++---- .../explorer_chart_label_badge.test.js.snap | 1 + .../explorer_charts_container.test.js | 2 +- .../explorer_charts_container_service.test.js | 10 +++++-- 7 files changed, 46 insertions(+), 34 deletions(-) create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_info_tooltip.test.js.snap delete mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_tooltip.test.js.snap diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_info_tooltip.test.js.snap b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_info_tooltip.test.js.snap new file mode 100644 index 0000000000000..b331c5e496c26 --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_info_tooltip.test.js.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ExplorerChartTooltip Render tooltip based on infoTooltip data. 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_tooltip.test.js.snap b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_tooltip.test.js.snap deleted file mode 100644 index c602bc0373c51..0000000000000 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_tooltip.test.js.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ExplorerChartTooltip Render tooltip based on infoTooltip data. 1`] = ` -
- job ID: - mock-job-id -
- aggregation interval: - 15m -
- chart function: - avg responsetime - -
- airline - : - JAL -
-
-`; diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap index fbd5e08e0482a..e9292ed69d6c5 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap @@ -71,7 +71,7 @@ Object { exports[`explorerChartsContainerService call anomalyChangeListener with actual series config 3`] = ` Object { - "layoutCellsPerChart": 12, + "chartsPerRow": 1, "seriesToPlot": Array [ Object { "bucketSpanSeconds": 900, @@ -582,6 +582,7 @@ Object { "entityFields": Array [ Object { "fieldName": "airline", + "fieldType": "partition", "fieldValue": "AAL", }, ], diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap index 0654b24dcf60a..f69905408eefd 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap @@ -34,8 +34,9 @@ exports[`ExplorerChartLabelBadge Render the chart label in one line. 1`] = ` >
-
+
diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label_badge.test.js.snap b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label_badge.test.js.snap index 9853aaa7907d9..56ad3c24a885a 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label_badge.test.js.snap +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label_badge.test.js.snap @@ -5,6 +5,7 @@ exports[`ExplorerChartLabelBadge Render entity label badge. 1`] = ` className="ml-explorer-chart-label-badge" > diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.js index 3b8836debf506..415e277417cff 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.js @@ -110,6 +110,6 @@ describe('ExplorerChartsContainer', () => { // We test child components with snapshots separately // so we just do some high level sanity check here. - expect(wrapper.find('.ml-explorer-chart-container').children()).toHaveLength(3); + expect(wrapper.find('.ml-explorer-chart-container').children()).toHaveLength(2); }); }); diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.test.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.test.js index df97ff35bd07b..c966bb11721d0 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.test.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.test.js @@ -26,6 +26,9 @@ jest.mock('../../services/results_service', () => ({ }, getScheduledEventsByBucket() { return Promise.resolve(mockSeriesPromisesResponse[0][2]); + }, + getEventDistributionData() { + return Promise.resolve([]); } } })); @@ -54,7 +57,6 @@ const mockChartContainer = { function mockGetDefaultData() { return { seriesToPlot: [], - layoutCellsPerChart: 12, tooManyBuckets: false, timeFieldName: 'timestamp' }; @@ -82,7 +84,7 @@ describe('explorerChartsContainerService', () => { callbackData.push(mockGetDefaultData()); callbackData.push({ ...mockGetDefaultData(), - layoutCellsPerChart: 6 + chartsPerRow: 2 }); const anomalyDataChangeListener = explorerChartsContainerServiceFactory( @@ -99,7 +101,9 @@ describe('explorerChartsContainerService', () => { function callback(data) { if (callbackData.length > 0) { - expect(data).toEqual(callbackData.shift()); + expect(data).toEqual({ + ...callbackData.shift() + }); } if (callbackData.length === 0) { done(); From 9d010709341640937d26514eac3d04306a3066ed Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 3 Oct 2018 16:47:59 +0200 Subject: [PATCH 20/37] [ML] Rename to distribution chart. --- ...{explorer_chart_rare.js => explorer_chart_distribution.js} | 2 +- .../explorer/explorer_charts/explorer_charts_container.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename x-pack/plugins/ml/public/explorer/explorer_charts/{explorer_chart_rare.js => explorer_chart_distribution.js} (99%) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js similarity index 99% rename from x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js rename to x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js index 280abd5cab456..478b17af8dd17 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_rare.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js @@ -39,7 +39,7 @@ import { CHART_TYPE } from '../explorer_constants'; const CONTENT_WRAPPER_HEIGHT = 215; -export class ExplorerChartRare extends React.Component { +export class ExplorerChartDistribution extends React.Component { static propTypes = { seriesConfig: PropTypes.object, mlSelectSeverityService: PropTypes.object.isRequired diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js index 5fd49b20f601b..1e4a335abf583 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js @@ -21,7 +21,7 @@ import { getExploreSeriesLink, isLabelLengthAboveThreshold } from '../../util/chart_utils'; -import { ExplorerChartRare } from './explorer_chart_rare'; +import { ExplorerChartDistribution } from './explorer_chart_distribution'; import { ExplorerChartSingleMetric } from './explorer_chart_single_metric'; import { ExplorerChartLabel } from './components/explorer_chart_label'; @@ -101,7 +101,7 @@ function ExplorerChartContainer({ {(() => { if (chartType === CHART_TYPE.EVENT_DISTRIBUTION || chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { return ( - Date: Wed, 3 Oct 2018 17:16:19 +0200 Subject: [PATCH 21/37] [ML] Add missing key attribute. --- .../explorer/explorer_charts/explorer_chart_info_tooltip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js index f60f7b491cf15..7464ec41edd1e 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js @@ -23,7 +23,7 @@ function TooltipDefinitionList({ toolTipData }) {
{toolTipData.map(({ title, description }) => { return ( - +
{title}
{description}
From 378e99f1f011460de0d539c97a0208a084f46c02 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 4 Oct 2018 09:46:32 +0200 Subject: [PATCH 22/37] [ML] Include a check if there are any records. --- .../explorer_charts/explorer_charts_container_service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js index 18442eb85d294..fd6562cdb62cb 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js @@ -201,7 +201,7 @@ export function explorerChartsContainerServiceFactory( } let chartData; - if (eventDistribution.length > 0) { + if (eventDistribution.length > 0 && records.length > 0) { const filterField = records[0].by_field_value || records[0].over_field_value; chartData = eventDistribution.filter(d => (d.entity !== filterField)); _.map(metricData, (value, time) => { From 3e4e37e30bc79873e0c08be32414e8f36a1250dd Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 4 Oct 2018 10:11:31 +0200 Subject: [PATCH 23/37] [ML] Add another check if record isn't undefined. --- .../explorer_charts/explorer_charts_container_service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js index 0372bcb3723a6..e3ff022ba7706 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js @@ -291,7 +291,7 @@ export function explorerChartsContainerServiceFactory( chartType === CHART_TYPE.POPULATION_DISTRIBUTION ) { return chartData.filter((d) => { - return d.entity === (record.by_field_value || record.over_field_value); + return d.entity === (record && (record.by_field_value || record.over_field_value)); }); } From 9537d87976f47714601aed0fcf1e16b9c9dbd131 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 4 Oct 2018 10:54:28 +0200 Subject: [PATCH 24/37] [ML] Fixes chart header flex layout overflows. --- .../components/explorer_chart_label/explorer_chart_label.js | 4 ++-- .../explorer_chart_label/styles/explorer_chart_label.less | 6 ++++++ .../explorer/explorer_charts/styles/explorer_chart.less | 5 +++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js index a8f5e84ba4523..a64457f0a44c8 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js @@ -35,7 +35,7 @@ export function ExplorerChartLabel({ detectorLabel, entityFields, infoTooltip, w const entityFieldBadges = entityFields.map((entity) => { return ( -   + ); }); @@ -63,7 +63,7 @@ export function ExplorerChartLabel({ detectorLabel, entityFields, infoTooltip, w )} {wrapLabel && ( -
{entityFieldBadges}
+ {entityFieldBadges} )}
); diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label.less b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label.less index 58692cf0a6f24..ecafa9bd4356a 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label.less +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/styles/explorer_chart_label.less @@ -1,3 +1,9 @@ .ml-explorer-chart-eui-icon-tip { max-width: none; } + +.ml-explorer-chart-label-badges { + margin-top: 3px; + /* let this overflow but not interfere with the flex layout */ + width: 0; +} diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart.less b/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart.less index ace3165e09e36..6ab1382384078 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart.less +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart.less @@ -1,4 +1,5 @@ .ml-explorer-chart-container { + overflow: hidden; .ml-explorer-chart-svg { font-size: 12px; @@ -109,6 +110,10 @@ } } +.ml-explorer-chart { + overflow: hidden; +} + .ml-explorer-chart-content-wrapper { height: 215px; } From 0ac6237dc33b3804988f0d477723f2cad6760d91 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 4 Oct 2018 10:59:42 +0200 Subject: [PATCH 25/37] [ML] Tweaks the chart info tooltip layout. --- .../explorer_charts/explorer_chart_info_tooltip.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js index 7464ec41edd1e..fe285e7febc6e 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js @@ -18,6 +18,8 @@ more frequent event types at the top and more rare ones at the bottom.', [CHART_TYPE.POPULATION_DISTRIBUTION]: 'The gray dots depict the distribution of values over time for a sample of over_field_values.' }; +import { EuiSpacer } from '@elastic/eui'; + function TooltipDefinitionList({ toolTipData }) { return (
@@ -68,7 +70,10 @@ export function ExplorerChartInfoTooltip({
{chartDescription && ( - {chartDescription} + + +
{chartDescription}
+
)}
); From 45b38a4e19761cc52bfef65ba266a9800ee24338 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 4 Oct 2018 13:25:48 +0200 Subject: [PATCH 26/37] [ML] Update jest snapshots. --- .../__snapshots__/explorer_chart_label.test.js.snap | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap index f69905408eefd..140ab03e37522 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap @@ -26,7 +26,6 @@ exports[`ExplorerChartLabelBadge Render the chart label in one line. 1`] = ` } } /> -   -
+ @@ -113,8 +114,7 @@ exports[`ExplorerChartLabelBadge Render the chart label in two lines. 1`] = ` } } /> -   -
+ `; From 84d85a9862ed04c750acc01b02eda1aa99dd1c37 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 4 Oct 2018 14:57:55 +0200 Subject: [PATCH 27/37] [ML] Show rare chart only if no over field is used in the detector for now. --- x-pack/plugins/ml/public/util/chart_utils.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/util/chart_utils.js b/x-pack/plugins/ml/public/util/chart_utils.js index f0849daabf97b..4cf3293db01fd 100644 --- a/x-pack/plugins/ml/public/util/chart_utils.js +++ b/x-pack/plugins/ml/public/util/chart_utils.js @@ -136,13 +136,14 @@ const POPULATION_DISTRIBUTION_ENABLED = true; export function getChartType(config) { if ( EVENT_DISTRIBUTION_ENABLED && - config.functionDescription === 'rare' + config.functionDescription === 'rare' && + (config.entityFields.some(f => f.fieldType === 'over') === false) ) { return CHART_TYPE.EVENT_DISTRIBUTION; } else if ( POPULATION_DISTRIBUTION_ENABLED && config.functionDescription === 'count' && - config.entityFields.find(f => f.fieldType === 'over') + config.entityFields.some(f => f.fieldType === 'over') ) { return CHART_TYPE.POPULATION_DISTRIBUTION; } From 2f75c7fba7a46d35cad45f1a4481702375656423 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 4 Oct 2018 15:32:48 +0200 Subject: [PATCH 28/37] [ML] Fixes expanding time span. --- .../explorer_charts/explorer_charts_container_service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js index e3ff022ba7706..11b1f14bec36e 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js @@ -558,8 +558,8 @@ export function explorerChartsContainerServiceFactory( if ((maxMs - minMs) < maxTimeSpan) { // Expand out to cover as much as the requested time span as possible. - minMs = Math.max(earliestMs, maxMs - maxTimeSpan); - maxMs = Math.min(latestMs, minMs + maxTimeSpan); + minMs = Math.max(earliestMs, minMs - maxTimeSpan); + maxMs = Math.min(latestMs, maxMs + maxTimeSpan); } chartRange = { min: minMs, max: maxMs }; From a1acf52ddba2bb1dd8ee4559f04a562af871d718 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 4 Oct 2018 15:38:27 +0200 Subject: [PATCH 29/37] [ML] Say 'rarer' instead of 'more rare'. --- .../explorer/explorer_charts/explorer_chart_info_tooltip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js index fe285e7febc6e..c938da5100ad3 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js @@ -14,7 +14,7 @@ import { CHART_TYPE } from '../explorer_constants'; const CHART_DESCRIPTION = { [CHART_TYPE.EVENT_DISTRIBUTION]: 'The gray dots depict the distribution of occurences over time for a sample of by_field_values with \ -more frequent event types at the top and more rare ones at the bottom.', +more frequent event types at the top and rarer ones at the bottom.', [CHART_TYPE.POPULATION_DISTRIBUTION]: 'The gray dots depict the distribution of values over time for a sample of over_field_values.' }; From e16f71bd5e9c01d0f6c11b566d233c0103b59842 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 4 Oct 2018 16:06:26 +0200 Subject: [PATCH 30/37] [ML] Remove commented code. --- x-pack/plugins/ml/public/services/results_service.js | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/ml/public/services/results_service.js b/x-pack/plugins/ml/public/services/results_service.js index e1e4778d77288..5973d94e724be 100644 --- a/x-pack/plugins/ml/public/services/results_service.js +++ b/x-pack/plugins/ml/public/services/results_service.js @@ -1398,7 +1398,6 @@ function getEventRateData( // Extra query object can be supplied, or pass null if no additional query. // Returned response contains a results property, which is an object // of document counts against time (epoch millis). -// const SAMPLER_TOP_TERMS_THRESHOLD = 100000; const SAMPLER_TOP_TERMS_SHARD_SIZE = 200; function getEventDistributionData( index, From 5f5ae7a63efed11e51749b79673df12d724c2594 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 4 Oct 2018 16:18:29 +0200 Subject: [PATCH 31/37] [ML] More strict check for marker.byFieldName. --- .../explorer/explorer_charts/explorer_chart_distribution.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js index 478b17af8dd17..50e55fc0b30ce 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js @@ -400,7 +400,7 @@ export class ExplorerChartDistribution extends React.Component { contents += (`
typical: ${formatValue(marker.typical, config.functionDescription, fieldFormat)}`); } else { contents += (`
value: ${formatValue(marker.value, config.functionDescription, fieldFormat)}`); - if (_.has(marker, 'byFieldName') && _.has(marker, 'numberOfCauses')) { + if (typeof marker.byFieldName !== 'undefined' && _.has(marker, 'numberOfCauses')) { const numberOfCauses = marker.numberOfCauses; const byFieldName = mlEscape(marker.byFieldName); if (numberOfCauses < 10) { From 4e2b0155e9012a9e4d795fc955babc479124c61b Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 4 Oct 2018 16:26:41 +0200 Subject: [PATCH 32/37] [ML] Show typical value in tooltips for anomalies with over but no by field or 1 numberOfCauses. --- .../explorer/explorer_charts/explorer_chart_distribution.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js index 50e55fc0b30ce..6264bd26fbdbd 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js @@ -400,6 +400,9 @@ export class ExplorerChartDistribution extends React.Component { contents += (`
typical: ${formatValue(marker.typical, config.functionDescription, fieldFormat)}`); } else { contents += (`
value: ${formatValue(marker.value, config.functionDescription, fieldFormat)}`); + if (typeof marker.numberOfCauses === 'undefined' || marker.numberOfCauses === 1) { + contents += (`
typical: ${formatValue(marker.typical, config.functionDescription, fieldFormat)}`); + } if (typeof marker.byFieldName !== 'undefined' && _.has(marker, 'numberOfCauses')) { const numberOfCauses = marker.numberOfCauses; const byFieldName = mlEscape(marker.byFieldName); From 2684e1f3632199f4d12d0102efe7213e070fc891 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 4 Oct 2018 17:11:40 +0200 Subject: [PATCH 33/37] [ML] consider numberOfCauses === 1. --- .../explorer_charts/explorer_chart_distribution.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js index 6264bd26fbdbd..5329d73d8c0fb 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js @@ -390,9 +390,6 @@ export class ExplorerChartDistribution extends React.Component { const score = parseInt(marker.anomalyScore); const displayScore = (score > 0 ? score : '< 1'); contents += ('anomaly score: ' + displayScore); - // Show actual/typical when available except for rare detectors. - // Rare detectors always have 1 as actual and the probability as typical. - // Exposing those values in the tooltip with actual/typical labels might irritate users. if (_.has(marker, 'actual') && chartType === CHART_TYPE.EVENT_DISTRIBUTION) { // Display the record actual in preference to the chart value, which may be // different depending on the aggregation interval of the chart. @@ -406,8 +403,9 @@ export class ExplorerChartDistribution extends React.Component { if (typeof marker.byFieldName !== 'undefined' && _.has(marker, 'numberOfCauses')) { const numberOfCauses = marker.numberOfCauses; const byFieldName = mlEscape(marker.byFieldName); - if (numberOfCauses < 10) { - // If numberOfCauses === 1, won't go into this block as actual/typical copied to top level fields. + if (numberOfCauses === 1) { + contents += `
1 unusual ${byFieldName} value`; + } else if (numberOfCauses < 10) { contents += `
${numberOfCauses} unusual ${byFieldName} values`; } else { // Maximum of 10 causes are stored in the record, so '10' may mean more than 10. From 2f798a7e6477ec05e63b72f139784038368a9b75 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 4 Oct 2018 18:52:12 +0200 Subject: [PATCH 34/37] [ML] Fix rare chart tooltip info. --- .../explorer_chart_distribution.js | 17 ++++++----------- .../explorer_chart_single_metric.js | 5 +++-- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js index 5329d73d8c0fb..83c915a6906c9 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js @@ -384,18 +384,13 @@ export class ExplorerChartDistribution extends React.Component { // Show the time and metric values in the tooltip. // Uses date, value, upper, lower and anomalyScore (optional) marker properties. const formattedDate = moment(marker.date).format('MMMM Do YYYY, HH:mm'); - let contents = formattedDate + '

'; + let contents = `${formattedDate}

`; if (_.has(marker, 'anomalyScore')) { const score = parseInt(marker.anomalyScore); const displayScore = (score > 0 ? score : '< 1'); - contents += ('anomaly score: ' + displayScore); - if (_.has(marker, 'actual') && chartType === CHART_TYPE.EVENT_DISTRIBUTION) { - // Display the record actual in preference to the chart value, which may be - // different depending on the aggregation interval of the chart. - contents += (`
actual: ${formatValue(marker.actual, config.functionDescription, fieldFormat)}`); - contents += (`
typical: ${formatValue(marker.typical, config.functionDescription, fieldFormat)}`); - } else { + contents += `anomaly score: ${displayScore}`; + if (chartType !== CHART_TYPE.EVENT_DISTRIBUTION) { contents += (`
value: ${formatValue(marker.value, config.functionDescription, fieldFormat)}`); if (typeof marker.numberOfCauses === 'undefined' || marker.numberOfCauses === 1) { contents += (`
typical: ${formatValue(marker.typical, config.functionDescription, fieldFormat)}`); @@ -413,16 +408,16 @@ export class ExplorerChartDistribution extends React.Component { } } } - } else { + } else if (chartType !== CHART_TYPE.EVENT_DISTRIBUTION) { contents += `value: ${formatValue(marker.value, config.functionDescription, fieldFormat)}`; } if (_.has(marker, 'entity')) { - contents += `
fieldName: ${marker.entity}`; + contents += `
${marker.entity}
`; } if (_.has(marker, 'scheduledEvents')) { - contents += `

Scheduled events:
${marker.scheduledEvents.map(mlEscape).join('
')}`; + contents += `

Scheduled events:
${marker.scheduledEvents.map(mlEscape).join('
')}
`; } mlChartTooltipService.show(contents, circle, { diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js index d80574dc794fc..8cb0b72015406 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js @@ -359,8 +359,9 @@ export class ExplorerChartSingleMetric extends React.Component { if (_.has(marker, 'byFieldName') && _.has(marker, 'numberOfCauses')) { const numberOfCauses = marker.numberOfCauses; const byFieldName = mlEscape(marker.byFieldName); - if (numberOfCauses < 10) { - // If numberOfCauses === 1, won't go into this block as actual/typical copied to top level fields. + if (numberOfCauses === 1) { + contents += `
1 unusual ${byFieldName} value`; + } else if (numberOfCauses < 10) { contents += `
${numberOfCauses} unusual ${byFieldName} values`; } else { // Maximum of 10 causes are stored in the record, so '10' may mean more than 10. From 001f328dc6826a0957bf899b086b03aeadefa4d2 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 5 Oct 2018 11:15:47 +0200 Subject: [PATCH 35/37] [ML] Move entity value just below date above other info. --- .../explorer_charts/explorer_chart_distribution.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js index 83c915a6906c9..544b027229038 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js @@ -386,6 +386,10 @@ export class ExplorerChartDistribution extends React.Component { const formattedDate = moment(marker.date).format('MMMM Do YYYY, HH:mm'); let contents = `${formattedDate}

`; + if (_.has(marker, 'entity')) { + contents += `
${marker.entity}
`; + } + if (_.has(marker, 'anomalyScore')) { const score = parseInt(marker.anomalyScore); const displayScore = (score > 0 ? score : '< 1'); @@ -412,10 +416,6 @@ export class ExplorerChartDistribution extends React.Component { contents += `value: ${formatValue(marker.value, config.functionDescription, fieldFormat)}`; } - if (_.has(marker, 'entity')) { - contents += `
${marker.entity}
`; - } - if (_.has(marker, 'scheduledEvents')) { contents += `

Scheduled events:
${marker.scheduledEvents.map(mlEscape).join('
')}
`; } From 901c1e356757e81973a76731f54cabc2cb45f520 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 5 Oct 2018 12:41:54 +0200 Subject: [PATCH 36/37] [ML] Simplify JSX for entityFieldBadges. --- .../explorer_chart_label/explorer_chart_label.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js index a64457f0a44c8..c6dd2cc357aca 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js @@ -32,13 +32,9 @@ export function ExplorerChartLabel({ detectorLabel, entityFields, infoTooltip, w (entityFields.length === 0 || detectorLabel.length === 0) ) ? ( ) : ( – ); - const entityFieldBadges = entityFields.map((entity) => { - return ( - - - - ); - }); + const entityFieldBadges = entityFields.map((entity) => ( + + )); const infoIcon = ( From 23cfe6f02543fd2df8342ea531db6be55e38b12e Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 5 Oct 2018 15:24:16 +0200 Subject: [PATCH 37/37] [ML] Updated snapshots. Review feedback. --- .../explorer_chart_label.test.js.snap | 42 ++++++++----------- .../explorer_chart_distribution.js | 2 +- .../explorer_chart_info_tooltip.js | 14 +++---- .../explorer_chart_single_metric.js | 2 +- 4 files changed, 26 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap index 140ab03e37522..3054cc4cf3957 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap @@ -14,19 +14,16 @@ exports[`ExplorerChartLabelBadge Render the chart label in one line. 1`] = ` - - - + } + key="nginx.access.remote_ip 72.57.0.53" + /> - - - + } + key="nginx.access.remote_ip 72.57.0.53" + /> `; diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js index 544b027229038..1ffa0f9ed3757 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js @@ -428,7 +428,7 @@ export class ExplorerChartDistribution extends React.Component { } shouldComponentUpdate() { - // Prevents component re-rendering + // Always return true, d3 will take care of appropriate re-rendering. return true; } diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js index c938da5100ad3..5cd4551f68af2 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js @@ -23,14 +23,12 @@ import { EuiSpacer } from '@elastic/eui'; function TooltipDefinitionList({ toolTipData }) { return (
- {toolTipData.map(({ title, description }) => { - return ( - -
{title}
-
{description}
-
- ); - })} + {toolTipData.map(({ title, description }) => ( + +
{title}
+
{description}
+
+ ))}
); } diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js index 8cb0b72015406..2b04b3de05723 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js @@ -385,7 +385,7 @@ export class ExplorerChartSingleMetric extends React.Component { } shouldComponentUpdate() { - // Prevents component re-rendering + // Always return true, d3 will take care of appropriate re-rendering. return true; }