data rendering', () => {
- // For the following tests the directive needs to be rendered in the actual DOM,
- // because otherwise there wouldn't be a width available which would
- // trigger SVG errors. We use a fixed width to be able to test for
- // fine grained attributes of the chart.
-
- // basically a parameterized beforeEach
- function init(chartData, tests) {
- // First we create the element including a wrapper which sets the width:
- $element = angular.element('
');
- // Add the element to the body so it gets rendered
- $element.appendTo(document.body);
-
- $scope.seriesConfig = {
- ...seriesConfig,
- chartData,
- chartLimits: chartLimits(chartData)
- };
-
- // Compile the directive and run a $digest()
- $compile($element)($scope);
- $scope.$evalAsync(tests);
- $scope.$digest();
- }
-
- afterEach(function () {
- // remove the element from the DOM
- $element.remove();
- });
-
- it('Anomaly Explorer Chart with multiple data points', (done) => {
- // prepare data for the test case
- const chartData = [
- {
- date: new Date('2017-02-23T08:00:00.000Z'),
- value: 228243469, anomalyScore: 63.32916, numberOfCauses: 1,
- actual: [228243469], typical: [133107.7703441773]
- },
- { date: new Date('2017-02-23T09:00:00.000Z'), value: null },
- { date: new Date('2017-02-23T10:00:00.000Z'), value: null },
- { date: new Date('2017-02-23T11:00:00.000Z'), value: null },
- {
- date: new Date('2017-02-23T12:00:00.000Z'),
- value: 625736376, anomalyScore: 97.32085, numberOfCauses: 1,
- actual: [625736376], typical: [132830.424736973]
- },
- {
- date: new Date('2017-02-23T13:00:00.000Z'),
- value: 201039318, anomalyScore: 59.83488, numberOfCauses: 1,
- actual: [201039318], typical: [132739.5267403542]
- }
- ];
-
- init(chartData, () => {
- // the loading indicator should not be shown
- expect($element.find('ml-loading-indicator .loading-indicator').length).to.be(0);
-
- // test if all expected elements are present
- const svg = $element.find('svg');
- expect(svg.length).to.be(1);
-
- const lineChart = svg.find('g.line-chart');
- expect(lineChart.length).to.be(1);
-
- const rects = lineChart.find('rect');
- expect(rects.length).to.be(2);
-
- const chartBorder = angular.element(rects[0]);
- expect(+chartBorder.attr('x')).to.be(0);
- expect(+chartBorder.attr('y')).to.be(0);
- expect(+chartBorder.attr('height')).to.be(170);
-
- const selectedInterval = angular.element(rects[1]);
- expect(selectedInterval.attr('class')).to.be('selected-interval');
- expect(+selectedInterval.attr('y')).to.be(1);
- expect(+selectedInterval.attr('height')).to.be(169);
-
- // skip this test for now
- // TODO find out why this doesn't work in IE11
- // const xAxisTicks = lineChart.find('.x.axis .tick');
- // expect(xAxisTicks.length).to.be(4);
- const yAxisTicks = lineChart.find('.y.axis .tick');
- expect(yAxisTicks.length).to.be(10);
-
- const paths = lineChart.find('path');
- expect(angular.element(paths[0]).attr('class')).to.be('domain');
- expect(angular.element(paths[1]).attr('class')).to.be('domain');
-
- const line = angular.element(paths[2]);
- expect(line.attr('class')).to.be('values-line');
- // this is not feasible to test because of minimal differences
- // across various browsers
- // expect(line.attr('d'))
- // .to.be('M205.56285511363637,152.3732523349513M215.3515625,7.72727272727272L217.79873934659093,162.27272727272728');
- expect(line.attr('d')).not.to.be(undefined);
-
- const dots = lineChart.find('g.values-dots circle');
- expect(dots.length).to.be(1);
-
- const dot = angular.element(dots[0]);
- expect(dot.attr('r')).to.be('1.5');
-
- const chartMarkers = lineChart.find('g.chart-markers circle');
- expect(chartMarkers.length).to.be(3);
- expect(chartMarkers.toArray().map(d => +angular.element(d).attr('r'))).to.eql([7, 7, 7]);
-
- done();
- });
- });
-
- it('Anomaly Explorer Chart with single data point', (done) => {
- const chartData = [
- {
- date: new Date('2017-02-23T08:00:00.000Z'),
- value: 228243469, anomalyScore: 63.32916, numberOfCauses: 1,
- actual: [228243469], typical: [228243469]
- }
- ];
-
- init(chartData, () => {
- const svg = $element.find('svg');
- const lineChart = svg.find('g.line-chart');
- const yAxisTicks = lineChart.find('.y.axis .tick');
- expect(yAxisTicks.length).to.be(13);
- done();
- });
- });
- });
-});
diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explore_series.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explore_series.js
new file mode 100644
index 0000000000000..48b11391f5eb8
--- /dev/null
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explore_series.js
@@ -0,0 +1,74 @@
+/*
+ * 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 _ from 'lodash';
+import moment from 'moment';
+import rison from 'rison-node';
+
+import chrome from 'ui/chrome';
+import { timefilter } from 'ui/timefilter';
+
+export function exploreSeries(series) {
+ // Open the Single Metric dashboard over the same overall bounds and
+ // zoomed in to the same time as the current chart.
+ const bounds = timefilter.getActiveBounds();
+ const from = bounds.min.toISOString(); // e.g. 2016-02-08T16:00:00.000Z
+ const to = bounds.max.toISOString();
+
+ const zoomFrom = moment(series.plotEarliest).toISOString();
+ const zoomTo = moment(series.plotLatest).toISOString();
+
+ // Pass the detector index and entity fields (i.e. by, over, partition fields)
+ // to identify the particular series to view.
+ // Initially pass them in the mlTimeSeriesExplorer part of the AppState.
+ // TODO - do we want to pass the entities via the filter?
+ const entityCondition = {};
+ _.each(series.entityFields, (entity) => {
+ entityCondition[entity.fieldName] = entity.fieldValue;
+ });
+
+ // Use rison to build the URL .
+ const _g = rison.encode({
+ ml: {
+ jobIds: [series.jobId]
+ },
+ refreshInterval: {
+ display: 'Off',
+ pause: false,
+ value: 0
+ },
+ time: {
+ from: from,
+ to: to,
+ mode: 'absolute'
+ }
+ });
+
+ const _a = rison.encode({
+ mlTimeSeriesExplorer: {
+ zoom: {
+ from: zoomFrom,
+ to: zoomTo
+ },
+ detectorIndex: series.detectorIndex,
+ entities: entityCondition,
+ },
+ filters: [],
+ query: {
+ query_string: {
+ analyze_wildcard: true,
+ query: '*'
+ }
+ }
+ });
+
+ let path = chrome.getBasePath();
+ path += '/app/ml#/timeseriesexplorer';
+ path += '?_g=' + _g;
+ path += '&_a=' + encodeURIComponent(_a);
+ window.open(path, '_blank');
+
+}
diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_directive.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart.js
similarity index 82%
rename from x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_directive.js
rename to x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart.js
index befa1afc4dd3b..5932ba8786d27 100644
--- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_directive.js
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart.js
@@ -4,43 +4,64 @@
* you may not use this file except in compliance with the Elastic License.
*/
-
-
/*
- * AngularJS directive for rendering a chart of anomalies in the raw data in
+ * React component for rendering a chart of anomalies in the raw data in
* the Machine Learning Explorer dashboard.
*/
-import './styles/explorer_chart_directive.less';
+import './styles/explorer_chart.less';
+
+import PropTypes from 'prop-types';
+import React from 'react';
import _ from 'lodash';
import d3 from 'd3';
-import angular from 'angular';
+import $ from 'jquery';
import moment from 'moment';
-import { formatValue } from 'plugins/ml/formatters/format_value';
-import { getSeverityWithLow } from 'plugins/ml/../common/util/anomaly_utils';
-import { drawLineChartDots, numTicksForDateFormat } from 'plugins/ml/util/chart_utils';
+// 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 loadingIndicatorWrapperTemplate from 'plugins/ml/components/loading_indicator/loading_indicator_wrapper.html';
-import { mlEscape } from 'plugins/ml/util/string_utils';
-import { mlFieldFormatService } from 'plugins/ml/services/field_format_service';
+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';
-import { uiModules } from 'ui/modules';
-const module = uiModules.get('apps/ml');
+const CONTENT_WRAPPER_HEIGHT = 215;
-module.directive('mlExplorerChart', function (
- mlChartTooltipService,
- Private,
- mlSelectSeverityService) {
+export class ExplorerChart extends React.Component {
+ static propTypes = {
+ seriesConfig: PropTypes.object,
+ mlSelectSeverityService: PropTypes.object.isRequired
+ }
- function link(scope, element) {
- console.log('ml-explorer-chart directive link series config:', scope.seriesConfig);
- if (typeof scope.seriesConfig === 'undefined') {
+ 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 config = scope.seriesConfig;
+
const fieldFormat = mlFieldFormatService.getFieldFormat(config.jobId, config.detectorIndex);
let vizWidth = 0;
@@ -56,35 +77,22 @@ module.directive('mlExplorerChart', function (
let lineChartGroup;
let lineChartValuesLine = null;
- // create a chart loading placeholder
- scope.isLoading = config.loading;
- if (Array.isArray(config.chartData)) {
- // make sure we wait for the previous digest cycle to finish
- // or the chart's wrapping elements might not have their
- // right widths yet and we need them to define the SVG's width
- scope.$evalAsync(() => {
- init(config.chartLimits);
- drawLineChart(config.chartData);
- });
- }
-
- element.on('$destroy', function () {
- scope.$destroy();
- });
+ init(config.chartLimits);
+ drawLineChart(config.chartData);
function init(chartLimits) {
- const $el = angular.element('ml-explorer-chart');
+ 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.get(0)).select('.content-wrapper');
+ 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('width', svgWidth)
.attr('height', svgHeight);
// Set the size of the left margin according to the width of the largest y axis tick label.
@@ -122,7 +130,7 @@ module.directive('mlExplorerChart', function (
d3.select('.temp-axis-label').remove();
margin.left = (Math.max(maxYAxisLabelWidth, 40));
- vizWidth = svgWidth - margin.left - margin.right;
+ 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
@@ -319,12 +327,37 @@ module.directive('mlExplorerChart', function (
}
}
- return {
- restrict: 'E',
- scope: {
- seriesConfig: '='
- },
- link: link,
- template: loadingIndicatorWrapperTemplate
- };
-});
+ 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 (
+
+ {isLoading && (
+
+ )}
+ {!isLoading && (
+
+ )}
+
+ );
+ }
+}
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.test.js
new file mode 100644
index 0000000000000..ce40308003f5f
--- /dev/null
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart.test.js
@@ -0,0 +1,207 @@
+/*
+ * 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.
+ */
+
+// Mock TimeBuckets and mlFieldFormatService, they don't play well
+// with the jest based test setup yet.
+jest.mock('ui/time_buckets', () => ({
+ TimeBuckets: function () {
+ this.setBounds = jest.fn();
+ this.setInterval = jest.fn();
+ this.getScaledDateFormat = jest.fn();
+ }
+}));
+jest.mock('../../services/field_format_service', () => ({
+ mlFieldFormatService: {
+ getFieldFormat: jest.fn()
+ }
+}));
+
+import { mount } from 'enzyme';
+import React from 'react';
+
+import { ExplorerChart } from './explorer_chart';
+import { chartLimits } from '../../util/chart_utils';
+
+describe('ExplorerChart', () => {
+ const seriesConfig = {
+ jobId: 'population-03',
+ detectorIndex: 0,
+ metricFunction: 'sum',
+ timeField: '@timestamp',
+ interval: '1h',
+ datafeedConfig: {
+ datafeed_id: 'datafeed-population-03',
+ job_id: 'population-03',
+ query_delay: '60s',
+ frequency: '600s',
+ indices: ['filebeat-7.0.0*'],
+ types: ['doc'],
+ query: { match_all: { boost: 1 } },
+ scroll_size: 1000,
+ chunking_config: { mode: 'auto' },
+ state: 'stopped'
+ },
+ metricFieldName: 'nginx.access.body_sent.bytes',
+ functionDescription: 'sum',
+ bucketSpanSeconds: 3600,
+ detectorLabel: 'high_sum(nginx.access.body_sent.bytes) over nginx.access.remote_ip (population-03)',
+ fieldName: 'nginx.access.body_sent.bytes',
+ entityFields: [{
+ fieldName: 'nginx.access.remote_ip',
+ fieldValue: '72.57.0.53',
+ $$hashKey: 'object:813'
+ }],
+ infoTooltip: `job ID: population-03
+ aggregation interval: 1h
chart function: sum nginx.access.body_sent.bytes
+ nginx.access.remote_ip: 72.57.0.53
`,
+ loading: false,
+ plotEarliest: 1487534400000,
+ plotLatest: 1488168000000,
+ selectedEarliest: 1487808000000,
+ selectedLatest: 1487894399999
+ };
+
+ const mlSelectSeverityServiceMock = {
+ state: {
+ get: () => ({
+ val: ''
+ })
+ }
+ };
+
+ const mockedGetBBox = { x: 0, y: -11.5, width: 12.1875, height: 14.5 };
+ const originalGetBBox = SVGElement.prototype.getBBox;
+ beforeEach(() => SVGElement.prototype.getBBox = () => {
+ return mockedGetBBox;
+ });
+ afterEach(() => (SVGElement.prototype.getBBox = originalGetBBox));
+
+ test('Initialize', () => {
+ const wrapper = mount();
+
+ // without setting any attributes and corresponding data
+ // the directive just ends up being empty.
+ expect(wrapper.isEmptyRender()).toBeTruthy();
+ expect(wrapper.find('.content-wrapper')).toHaveLength(0);
+ expect(wrapper.find('.ml-loading-indicator .loading-spinner')).toHaveLength(0);
+ });
+
+ test('Loading status active, no chart', () => {
+ const config = {
+ loading: true
+ };
+
+ const wrapper = mount();
+
+ // test if the loading indicator is shown
+ expect(wrapper.find('.ml-loading-indicator .loading-spinner')).toHaveLength(1);
+ });
+
+ // For the following tests the directive needs to be rendered in the actual DOM,
+ // because otherwise there wouldn't be a width available which would
+ // trigger SVG errors. We use a fixed width to be able to test for
+ // fine grained attributes of the chart.
+
+ // basically a parameterized beforeEach
+ function init(chartData) {
+ const config = {
+ ...seriesConfig,
+ chartData,
+ chartLimits: chartLimits(chartData)
+ };
+
+ // We create the element including a wrapper which sets the width:
+ return mount(
+
+
+
+ );
+ }
+
+ it('Anomaly Explorer Chart with multiple data points', () => {
+ // prepare data for the test case
+ const chartData = [
+ {
+ date: new Date('2017-02-23T08:00:00.000Z'),
+ value: 228243469, anomalyScore: 63.32916, numberOfCauses: 1,
+ actual: [228243469], typical: [133107.7703441773]
+ },
+ { date: new Date('2017-02-23T09:00:00.000Z'), value: null },
+ { date: new Date('2017-02-23T10:00:00.000Z'), value: null },
+ { date: new Date('2017-02-23T11:00:00.000Z'), value: null },
+ {
+ date: new Date('2017-02-23T12:00:00.000Z'),
+ value: 625736376, anomalyScore: 97.32085, numberOfCauses: 1,
+ actual: [625736376], typical: [132830.424736973]
+ },
+ {
+ date: new Date('2017-02-23T13:00:00.000Z'),
+ value: 201039318, anomalyScore: 59.83488, numberOfCauses: 1,
+ actual: [201039318], typical: [132739.5267403542]
+ }
+ ];
+
+ const wrapper = init(chartData);
+
+ // the loading indicator should not be shown
+ expect(wrapper.find('.ml-loading-indicator .loading-spinner')).toHaveLength(0);
+
+ // test if all expected elements are present
+ // need to use getDOMNode() because the chart is not rendered via react itself
+ const svg = wrapper.getDOMNode().getElementsByTagName('svg');
+ expect(svg).toHaveLength(1);
+
+ const lineChart = svg[0].getElementsByClassName('line-chart');
+ expect(lineChart).toHaveLength(1);
+
+ const rects = lineChart[0].getElementsByTagName('rect');
+ expect(rects).toHaveLength(2);
+
+ const chartBorder = rects[0];
+ expect(+chartBorder.getAttribute('x')).toBe(0);
+ expect(+chartBorder.getAttribute('y')).toBe(0);
+ expect(+chartBorder.getAttribute('height')).toBe(170);
+
+ const selectedInterval = rects[1];
+ expect(selectedInterval.getAttribute('class')).toBe('selected-interval');
+ expect(+selectedInterval.getAttribute('y')).toBe(1);
+ expect(+selectedInterval.getAttribute('height')).toBe(169);
+
+ const xAxisTicks = wrapper.getDOMNode().querySelector('.x').querySelectorAll('.tick');
+ expect([...xAxisTicks]).toHaveLength(0);
+ const yAxisTicks = wrapper.getDOMNode().querySelector('.y').querySelectorAll('.tick');
+ expect([...yAxisTicks]).toHaveLength(10);
+
+ const paths = wrapper.getDOMNode().querySelectorAll('path');
+ expect(paths[0].getAttribute('class')).toBe('domain');
+ expect(paths[1].getAttribute('class')).toBe('domain');
+ expect(paths[2].getAttribute('class')).toBe('values-line');
+ expect(paths[2].getAttribute('d')).toBe('MNaN,159.33024504444444MNaN,9.166257955555556LNaN,169.60736875555557');
+
+ const dots = wrapper.getDOMNode().querySelector('.values-dots').querySelectorAll('circle');
+ expect([...dots]).toHaveLength(1);
+ expect(dots[0].getAttribute('r')).toBe('1.5');
+
+ const chartMarkers = wrapper.getDOMNode().querySelector('.chart-markers').querySelectorAll('circle');
+ expect([...chartMarkers]).toHaveLength(3);
+ expect([...chartMarkers].map(d => +d.getAttribute('r'))).toEqual([7, 7, 7]);
+ });
+
+ it('Anomaly Explorer Chart with single data point', () => {
+ const chartData = [
+ {
+ date: new Date('2017-02-23T08:00:00.000Z'),
+ value: 228243469, anomalyScore: 63.32916, numberOfCauses: 1,
+ actual: [228243469], typical: [228243469]
+ }
+ ];
+
+ const wrapper = init(chartData);
+
+ const yAxisTicks = wrapper.getDOMNode().querySelector('.y').querySelectorAll('.tick');
+ expect([...yAxisTicks]).toHaveLength(13);
+ });
+});
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 07a9bc0885e60..ab167cb423127 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
@@ -14,87 +14,79 @@
import _ from 'lodash';
import { parseInterval } from 'ui/utils/parse_interval';
-import { buildConfigFromDetector } from 'plugins/ml/util/chart_config_builder';
-import { mlEscape } from 'plugins/ml/util/string_utils';
-import { mlJobService } from 'plugins/ml/services/job_service';
-
-export function explorerChartConfigBuilder() {
-
- const compiledTooltip = _.template(
- 'job ID: <%= jobId %>
' +
- 'aggregation interval: <%= aggregationInterval %>
' +
- 'chart function: <%= chartFunction %>' +
- '<% for(let i = 0; i < entityFields.length; ++i) { %>' +
- '
<%= entityFields[i].fieldName %>: <%= entityFields[i].fieldValue %>' +
- '<% } %>' +
- '
');
-
- // Builds the chart configuration for the provided anomaly record, returning
- // an object with properties used for the display (series function and field, aggregation interval etc),
- // and properties for the data feed used for the job (index pattern, time field etc).
- function buildConfig(record) {
- const job = mlJobService.getJob(record.job_id);
- const detectorIndex = record.detector_index;
- const config = buildConfigFromDetector(job, detectorIndex);
-
- // Add extra properties used by the explorer dashboard charts.
- config.functionDescription = record.function_description;
- config.bucketSpanSeconds = parseInterval(job.analysis_config.bucket_span).asSeconds();
-
- config.detectorLabel = record.function;
- if ((_.has(mlJobService.detectorsByJob, record.job_id)) &&
- (detectorIndex < mlJobService.detectorsByJob[record.job_id].length)) {
- config.detectorLabel = mlJobService.detectorsByJob[record.job_id][detectorIndex].detector_description;
- } else {
- if (record.field_name !== undefined) {
- config.detectorLabel += ` ${config.fieldName}`;
- }
- }
-
+import { buildConfigFromDetector } from '../../util/chart_config_builder';
+import { mlEscape } from '../../util/string_utils';
+import { mlJobService } from '../../services/job_service';
+
+// Builds the chart configuration for the provided anomaly record, returning
+// an object with properties used for the display (series function and field, aggregation interval etc),
+// and properties for the data feed used for the job (index pattern, time field etc).
+export function buildConfig(record) {
+ const job = mlJobService.getJob(record.job_id);
+ const detectorIndex = record.detector_index;
+ const config = buildConfigFromDetector(job, detectorIndex);
+
+ // Add extra properties used by the explorer dashboard charts.
+ config.functionDescription = record.function_description;
+ config.bucketSpanSeconds = parseInterval(job.analysis_config.bucket_span).asSeconds();
+
+ config.detectorLabel = record.function;
+ if ((_.has(mlJobService.detectorsByJob, record.job_id)) &&
+ (detectorIndex < mlJobService.detectorsByJob[record.job_id].length)) {
+ config.detectorLabel = mlJobService.detectorsByJob[record.job_id][detectorIndex].detector_description;
+ } else {
if (record.field_name !== undefined) {
- config.fieldName = record.field_name;
- config.metricFieldName = record.field_name;
- }
-
- // Add the 'entity_fields' i.e. the partition, by, over fields which
- // define the metric series to be plotted.
- config.entityFields = [];
- if (_.has(record, 'partition_field_name')) {
- config.entityFields.push({ fieldName: record.partition_field_name, fieldValue: record.partition_field_value });
+ config.detectorLabel += ` ${config.fieldName}`;
}
+ }
- if (_.has(record, 'over_field_name')) {
- config.entityFields.push({ fieldName: record.over_field_name, fieldValue: record.over_field_value });
- }
+ if (record.field_name !== undefined) {
+ config.fieldName = record.field_name;
+ config.metricFieldName = record.field_name;
+ }
- // For jobs with by and over fields, don't add the 'by' field as this
- // field will only be added to the top-level fields for record type results
- // if it also an influencer over the bucket.
- if (_.has(record, 'by_field_name') && !(_.has(record, 'over_field_name'))) {
- config.entityFields.push({ fieldName: record.by_field_name, fieldValue: record.by_field_value });
- }
+ // Add the 'entity_fields' i.e. the partition, by, over fields which
+ // define the metric series to be plotted.
+ config.entityFields = [];
+ if (_.has(record, 'partition_field_name')) {
+ config.entityFields.push({
+ fieldName: record.partition_field_name,
+ fieldValue: record.partition_field_value
+ });
+ }
- // Build the tooltip for the chart info icon, showing further details on what is being plotted.
- let functionLabel = config.metricFunction;
- if (config.metricFieldName !== undefined) {
- functionLabel += ` ${mlEscape(config.metricFieldName)}`;
- }
+ if (_.has(record, 'over_field_name')) {
+ config.entityFields.push({
+ fieldName: record.over_field_name,
+ fieldValue: record.over_field_value
+ });
+ }
- config.infoTooltip = compiledTooltip({
- jobId: record.job_id,
- aggregationInterval: config.interval,
- chartFunction: functionLabel,
- entityFields: config.entityFields.map((f) => ({
- fieldName: mlEscape(f.fieldName),
- fieldValue: mlEscape(f.fieldValue),
- }))
+ // For jobs with by and over fields, don't add the 'by' field as this
+ // field will only be added to the top-level fields for record type results
+ // if it also an influencer over the bucket.
+ if (_.has(record, 'by_field_name') && !(_.has(record, 'over_field_name'))) {
+ config.entityFields.push({
+ fieldName: record.by_field_name,
+ fieldValue: record.by_field_value
});
+ }
- return config;
+ // Build the tooltip data for the chart info icon, showing further details on what is being plotted.
+ let functionLabel = config.metricFunction;
+ if (config.metricFieldName !== undefined) {
+ functionLabel += ` ${mlEscape(config.metricFieldName)}`;
}
- return {
- buildConfig
+ config.infoTooltip = {
+ jobId: record.job_id,
+ aggregationInterval: config.interval,
+ chartFunction: functionLabel,
+ entityFields: config.entityFields.map((f) => ({
+ fieldName: mlEscape(f.fieldName),
+ fieldValue: mlEscape(f.fieldValue),
+ }))
};
-}
+ return config;
+}
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
new file mode 100644
index 0000000000000..c0da82233e067
--- /dev/null
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_tooltip.js
@@ -0,0 +1,37 @@
+/*
+ * 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.html b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.html
deleted file mode 100644
index 34e9ac9f9b014..0000000000000
--- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.html
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
- {{series.detectorLabel}} -
- {{series.detectorLabel}}
-
- {{entity.fieldName}} {{entity.fieldValue}}
-
-
-
-
-
- View
-
-
-
-
-
-
-
-
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
new file mode 100644
index 0000000000000..2fb75120fdb44
--- /dev/null
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js
@@ -0,0 +1,85 @@
+/*
+ * 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';
+
+import { EuiIconTip } from '@elastic/eui';
+
+import { ExplorerChart } from './explorer_chart';
+import { ExplorerChartTooltip } from './explorer_chart_tooltip';
+
+export function ExplorerChartsContainer({
+ exploreSeries,
+ seriesToPlot,
+ layoutCellsPerChart,
+ tooManyBuckets,
+ mlSelectSeverityService
+}) {
+ return (
+
+ {(seriesToPlot.length > 0) &&
+ seriesToPlot.map((series) => {
+
+ // create a somewhat unique ID from charts metadata for React's key attribute
+ const {
+ jobId,
+ detectorLabel,
+ entityFields,
+ } = series;
+ const entities = entityFields.map((ef) => `${ef.fieldName}/${ef.fieldValue}`).join(',');
+ const id = `${jobId}_${detectorLabel}_${entities}`;
+
+ return (
+
+
+
+ {(entityFields.length > 0) && (
+ {detectorLabel} -
+ )}
+ {(entityFields.length === 0) && (
+ {detectorLabel}
+ )}
+ {entityFields.map((entity, j) => {
+ return (
+ {entity.fieldName} {entity.fieldValue}
+ );
+ })}
+
+
} position="left" size="s" />
+ {tooManyBuckets && (
+
+ )}
+
exploreSeries(series)}>
+ View
+
+
+
+
+ );
+ })
+ }
+
+ );
+}
+ExplorerChartsContainer.propTypes = {
+ exploreSeries: PropTypes.func.isRequired,
+ 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_directive.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_directive.js
index 3abb50cfff8cb..412831c4d97c8 100644
--- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_directive.js
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_directive.js
@@ -11,23 +11,37 @@
* anomalies in the raw data in the Machine Learning Explorer dashboard.
*/
-import './styles/explorer_charts_container_directive.less';
+import './styles/explorer_charts_container.less';
-import _ from 'lodash';
-import $ from 'jquery';
-import moment from 'moment';
-import rison from 'rison-node';
+import React from 'react';
+import ReactDOM from 'react-dom';
-import chrome from 'ui/chrome';
-import { timefilter } from 'ui/timefilter';
-import template from './explorer_charts_container.html';
+import $ from 'jquery';
+import { ExplorerChartsContainer } from './explorer_charts_container';
+import { exploreSeries } from './explore_series';
+import { explorerChartsContainerServiceFactory } from './explorer_charts_container_service';
+import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tooltip_service';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
-module.directive('mlExplorerChartsContainer', function ($window) {
+module.directive('mlExplorerChartsContainer', function (
+ mlExplorerDashboardService,
+ mlSelectSeverityService
+) {
function link(scope, element) {
+ const anomalyDataChangeListener = explorerChartsContainerServiceFactory(
+ mlSelectSeverityService,
+ updateComponent
+ );
+
+ mlExplorerDashboardService.anomalyDataChange.watch(anomalyDataChangeListener);
+
+ scope.$on('$destroy', () => {
+ mlExplorerDashboardService.anomalyDataChange.unwatch(anomalyDataChangeListener);
+ });
+
// Create a div for the tooltip.
$('.ml-explorer-charts-tooltip').remove();
$('body').append('');
@@ -36,78 +50,28 @@ module.directive('mlExplorerChartsContainer', function ($window) {
scope.$destroy();
});
- scope.exploreSeries = function (series) {
- // Open the Single Metric dashboard over the same overall bounds and
- // zoomed in to the same time as the current chart.
- const bounds = timefilter.getActiveBounds();
- const from = bounds.min.toISOString(); // e.g. 2016-02-08T16:00:00.000Z
- const to = bounds.max.toISOString();
-
- const zoomFrom = moment(series.plotEarliest).toISOString();
- const zoomTo = moment(series.plotLatest).toISOString();
-
- // Pass the detector index and entity fields (i.e. by, over, partition fields)
- // to identify the particular series to view.
- // Initially pass them in the mlTimeSeriesExplorer part of the AppState.
- // TODO - do we want to pass the entities via the filter?
- const entityCondition = {};
- _.each(series.entityFields, (entity) => {
- entityCondition[entity.fieldName] = entity.fieldValue;
- });
-
- // Use rison to build the URL .
- const _g = rison.encode({
- ml: {
- jobIds: [series.jobId]
- },
- refreshInterval: {
- display: 'Off',
- pause: false,
- value: 0
- },
- time: {
- from: from,
- to: to,
- mode: 'absolute'
- }
- });
-
- const _a = rison.encode({
- mlTimeSeriesExplorer: {
- zoom: {
- from: zoomFrom,
- to: zoomTo
- },
- detectorIndex: series.detectorIndex,
- entities: entityCondition,
- },
- filters: [],
- query: {
- query_string: {
- analyze_wildcard: true,
- query: '*'
- }
- }
- });
-
- let path = chrome.getBasePath();
- path += '/app/ml#/timeseriesexplorer';
- path += '?_g=' + _g;
- path += '&_a=' + encodeURIComponent(_a);
- $window.open(path, '_blank');
-
- };
+ function updateComponent(data) {
+ const props = {
+ exploreSeries,
+ seriesToPlot: data.seriesToPlot,
+ layoutCellsPerChart: data.layoutCellsPerChart,
+ // convert truthy/falsy value to Boolean
+ tooManyBuckets: !!data.tooManyBuckets,
+ mlSelectSeverityService,
+ mlChartTooltipService
+ };
+
+ ReactDOM.render(
+ React.createElement(ExplorerChartsContainer, props),
+ element[0]
+ );
+ }
}
return {
- restrict: 'E',
- scope: {
- seriesToPlot: '=',
- chartsPerRow: '=',
- layoutCellsPerChart: '=',
- tooManyBuckets: '='
- },
- link: link,
- template
+ restrict: 'E',
+ replace: false,
+ scope: false,
+ link: link
};
});
diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_controller.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js
similarity index 89%
rename from x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_controller.js
rename to x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js
index a20a77a63bbf8..fa9d764496b75 100644
--- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_controller.js
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js
@@ -16,41 +16,53 @@
import _ from 'lodash';
import $ from 'jquery';
-import { uiModules } from 'ui/modules';
-const module = uiModules.get('apps/ml');
-import { explorerChartConfigBuilder } from './explorer_chart_config_builder';
-import { chartLimits } from 'plugins/ml/util/chart_utils';
-import { isTimeSeriesViewDetector } from 'plugins/ml/../common/util/job_utils';
-import { mlResultsService } from 'plugins/ml/services/results_service';
-import { mlJobService } from 'plugins/ml/services/job_service';
+import { buildConfig } from './explorer_chart_config_builder';
+import { chartLimits } from '../../util/chart_utils';
+import { isTimeSeriesViewDetector } from '../../../common/util/job_utils';
+import { mlResultsService } from '../../services/results_service';
+import { mlJobService } from '../../services/job_service';
-module.controller('MlExplorerChartsContainerController', function ($scope, $injector) {
- const Private = $injector.get('Private');
- const mlExplorerDashboardService = $injector.get('mlExplorerDashboardService');
- const mlSelectSeverityService = $injector.get('mlSelectSeverityService');
-
- $scope.seriesToPlot = [];
+export function explorerChartsContainerServiceFactory(
+ mlSelectSeverityService,
+ callback
+) {
const $chartContainer = $('.explorer-charts');
+
const FUNCTION_DESCRIPTIONS_TO_PLOT = ['mean', 'min', 'max', 'sum', 'count', 'distinct_count', 'median', 'rare'];
const CHART_MAX_POINTS = 500;
const ANOMALIES_MAX_RESULTS = 500;
const MAX_SCHEDULED_EVENTS = 10; // Max number of scheduled events displayed per bucket.
const ML_TIME_FIELD_NAME = 'timestamp';
const USE_OVERALL_CHART_LIMITS = false;
+ const DEFAULT_LAYOUT_CELLS_PER_CHART = 12;
+ const MAX_CHARTS_PER_ROW = 4;
+
+ function getDefaultData() {
+ return {
+ seriesToPlot: [],
+ // default values, will update on every re-render
+ layoutCellsPerChart: DEFAULT_LAYOUT_CELLS_PER_CHART,
+ tooManyBuckets: false,
+ timeFieldName: 'timestamp'
+ };
+ }
+
+ callback(getDefaultData());
const anomalyDataChangeListener = function (anomalyRecords, earliestMs, latestMs) {
+ const data = getDefaultData();
+
const threshold = mlSelectSeverityService.state.get('threshold');
- const filteredRecords = _.filter(anomalyRecords, (record) => {
+ const filteredRecords = anomalyRecords.filter((record) => {
return Number(record.record_score) >= threshold.val;
});
const allSeriesRecords = processRecordsForDisplay(filteredRecords);
// Calculate the number of charts per row, depending on the width available, to a max of 4.
- const chartsContainerWidth = $chartContainer.width();
- const chartsPerRow = Math.min(Math.max(Math.floor(chartsContainerWidth / 550), 1), 4);
+ const chartsContainerWidth = Math.floor($chartContainer.width());
+ const chartsPerRow = Math.min(Math.max(Math.floor(chartsContainerWidth / 550), 1), MAX_CHARTS_PER_ROW);
- $scope.chartsPerRow = chartsPerRow;
- $scope.layoutCellsPerChart = 12 / $scope.chartsPerRow;
+ data.layoutCellsPerChart = DEFAULT_LAYOUT_CELLS_PER_CHART / chartsPerRow;
// Build the data configs of the anomalies to be displayed.
// TODO - implement paging?
@@ -60,17 +72,20 @@ module.controller('MlExplorerChartsContainerController', function ($scope, $inje
const seriesConfigs = buildDataConfigs(recordsToPlot);
// Calculate the time range of the charts, which is a function of the chart width and max job bucket span.
- $scope.tooManyBuckets = false;
- const chartRange = calculateChartRange(seriesConfigs, earliestMs, latestMs,
- Math.floor(chartsContainerWidth / chartsPerRow), recordsToPlot);
+ data.tooManyBuckets = false;
+ const { chartRange, tooManyBuckets } = calculateChartRange(seriesConfigs, earliestMs, latestMs,
+ Math.floor(chartsContainerWidth / chartsPerRow), recordsToPlot, data.timeFieldName);
+ data.tooManyBuckets = tooManyBuckets;
// initialize the charts with loading indicators
- $scope.seriesToPlot = seriesConfigs.map(config => ({
+ data.seriesToPlot = seriesConfigs.map(config => ({
...config,
loading: true,
chartData: null
}));
+ callback(data);
+
// Query 1 - load the raw metric data.
function getMetricData(config, range) {
const datafeedQuery = _.get(config, 'datafeedConfig.query', null);
@@ -250,7 +265,7 @@ module.controller('MlExplorerChartsContainerController', function ($scope, $inje
}, []);
const overallChartLimits = chartLimits(allDataPoints);
- $scope.seriesToPlot = response.map((d, i) => ({
+ data.seriesToPlot = response.map((d, i) => ({
...seriesConfigs[i],
loading: false,
chartData: processedData[i],
@@ -260,18 +275,13 @@ module.controller('MlExplorerChartsContainerController', function ($scope, $inje
selectedLatest: latestMs,
chartLimits: USE_OVERALL_CHART_LIMITS ? overallChartLimits : chartLimits(processedData[i])
}));
+ callback(data);
})
.catch(error => {
console.error(error);
});
};
- mlExplorerDashboardService.anomalyDataChange.watch(anomalyDataChangeListener);
-
- $scope.$on('$destroy', () => {
- mlExplorerDashboardService.anomalyDataChange.unwatch(anomalyDataChangeListener);
- });
-
function processRecordsForDisplay(anomalyRecords) {
// Aggregate the anomaly data by detector, and entity (by/over/partition).
if (anomalyRecords.length === 0) {
@@ -409,11 +419,11 @@ module.controller('MlExplorerChartsContainerController', function ($scope, $inje
function buildDataConfigs(anomalyRecords) {
// Build the chart configuration for each anomaly record.
- const configBuilder = Private(explorerChartConfigBuilder);
- return anomalyRecords.map(configBuilder.buildConfig);
+ return anomalyRecords.map(buildConfig);
}
- function calculateChartRange(seriesConfigs, earliestMs, latestMs, chartWidth, recordsToPlot) {
+ function calculateChartRange(seriesConfigs, earliestMs, latestMs, chartWidth, recordsToPlot, timeFieldName) {
+ let tooManyBuckets = false;
// Calculate the time range for the charts.
// Fit in as many points in the available container width plotted at the job bucket span.
const midpointMs = Math.ceil((earliestMs + latestMs) / 2);
@@ -429,20 +439,22 @@ module.controller('MlExplorerChartsContainerController', function ($scope, $inje
// at optimal point spacing.
const plotPoints = Math.max(optimumNumPoints, pointsToPlotFullSelection);
const halfPoints = Math.ceil(plotPoints / 2);
- let chartRange = { min: midpointMs - (halfPoints * maxBucketSpanMs),
- max: midpointMs + (halfPoints * maxBucketSpanMs) };
+ let chartRange = {
+ min: midpointMs - (halfPoints * maxBucketSpanMs),
+ max: midpointMs + (halfPoints * maxBucketSpanMs)
+ };
if (plotPoints > CHART_MAX_POINTS) {
- $scope.tooManyBuckets = true;
+ tooManyBuckets = true;
// For each series being plotted, display the record with the highest score if possible.
const maxTimeSpan = maxBucketSpanMs * CHART_MAX_POINTS;
- let minMs = recordsToPlot[0][$scope.timeFieldName];
- let maxMs = recordsToPlot[0][$scope.timeFieldName];
+ let minMs = recordsToPlot[0][timeFieldName];
+ let maxMs = recordsToPlot[0][timeFieldName];
_.each(recordsToPlot, (record) => {
const diffMs = maxMs - minMs;
if (diffMs < maxTimeSpan) {
- const recordTime = record[$scope.timeFieldName];
+ const recordTime = record[timeFieldName];
if (recordTime < minMs) {
if (maxMs - recordTime <= maxTimeSpan) {
minMs = recordTime;
@@ -466,7 +478,12 @@ module.controller('MlExplorerChartsContainerController', function ($scope, $inje
chartRange = { min: minMs, max: maxMs };
}
- return chartRange;
+ return {
+ chartRange,
+ tooManyBuckets
+ };
}
-});
+ return anomalyDataChangeListener;
+
+}
diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/index.js b/x-pack/plugins/ml/public/explorer/explorer_charts/index.js
index 0c484f0394720..7dacdb4b5ee29 100644
--- a/x-pack/plugins/ml/public/explorer/explorer_charts/index.js
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/index.js
@@ -6,7 +6,5 @@
-import './explorer_charts_container_controller.js';
import './explorer_charts_container_directive.js';
-import './explorer_chart_directive.js';
import 'plugins/ml/components/chart_tooltip';
diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart_directive.less b/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart.less
similarity index 96%
rename from x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart_directive.less
rename to x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart.less
index 042fe2a44c126..aad07728dd931 100644
--- a/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart_directive.less
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_chart.less
@@ -1,4 +1,5 @@
-ml-explorer-chart {
+ml-explorer-chart,
+.ml-explorer-chart-container {
display: block;
svg {
diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_charts_container_directive.less b/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_charts_container.less
similarity index 91%
rename from x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_charts_container_directive.less
rename to x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_charts_container.less
index b47baa74fd3d1..bc8256b977384 100644
--- a/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_charts_container_directive.less
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/styles/explorer_charts_container.less
@@ -1,6 +1,6 @@
-ml-explorer-charts-container {
+.explorer-charts {
- .explorer-charts {
+ ml-explorer-charts-container {
.row {
padding: 10px;
@@ -114,11 +114,20 @@ ml-explorer-charts-container {
display: inline-block;
}
+ .euiIcon {
+ vertical-align: top;
+ margin: 4px 0 0 4px;
+ }
+
a {
float:right;
padding-left: 5px;
}
}
+
+ .content-wrapper {
+ height: 215px;
+ }
}
}
}
diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js
index c590026ba3e53..3f7bb48f71d7c 100644
--- a/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js
+++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js
@@ -19,11 +19,12 @@ import { getSeverityColor } from 'plugins/ml/../common/util/anomaly_utils';
import { numTicksForDateFormat } from 'plugins/ml/util/chart_utils';
import { IntervalHelperProvider } from 'plugins/ml/util/ml_time_buckets';
import { mlEscape } from 'plugins/ml/util/string_utils';
+import { mlChartTooltipService } from '../components/chart_tooltip/chart_tooltip_service';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
-module.directive('mlExplorerSwimlane', function ($compile, Private, mlExplorerDashboardService, mlChartTooltipService) {
+module.directive('mlExplorerSwimlane', function ($compile, Private, mlExplorerDashboardService) {
function link(scope, element) {
diff --git a/x-pack/plugins/ml/public/explorer/index.js b/x-pack/plugins/ml/public/explorer/index.js
index b517e473b39cc..3fe0b96bf2900 100644
--- a/x-pack/plugins/ml/public/explorer/index.js
+++ b/x-pack/plugins/ml/public/explorer/index.js
@@ -6,9 +6,9 @@
-import 'plugins/ml/explorer/explorer_controller.js';
-import 'plugins/ml/explorer/explorer_dashboard_service.js';
-import 'plugins/ml/explorer/explorer_swimlane_directive.js';
+import 'plugins/ml/explorer/explorer_controller';
+import 'plugins/ml/explorer/explorer_dashboard_service';
+import 'plugins/ml/explorer/explorer_swimlane_directive';
import 'plugins/ml/explorer/styles/main.less';
import 'plugins/ml/explorer/explorer_charts';
import 'plugins/ml/explorer/select_limit';
diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_chart_directive.js b/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_chart_directive.js
index 5ac504a0ad521..27622ceda9dbc 100644
--- a/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_chart_directive.js
+++ b/x-pack/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_chart_directive.js
@@ -16,13 +16,14 @@ import angular from 'angular';
import moment from 'moment';
import { TimeBuckets } from 'ui/time_buckets';
-import { numTicksForDateFormat } from 'plugins/ml/util/chart_utils';
-import { mlEscape } from 'plugins/ml/util/string_utils';
+import { numTicksForDateFormat } from '../../../../../util/chart_utils';
+import { mlEscape } from '../../../../../util/string_utils';
+import { mlChartTooltipService } from '../../../../../components/chart_tooltip/chart_tooltip_service';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
-module.directive('mlPopulationJobChart', function (mlChartTooltipService) {
+module.directive('mlPopulationJobChart', function () {
function link(scope, element) {
diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_chart_directive.js b/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_chart_directive.js
index 069cdb75990e5..ec35d0310945a 100644
--- a/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_chart_directive.js
+++ b/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_chart_directive.js
@@ -33,15 +33,12 @@ import ContextChartMask from 'plugins/ml/timeseriesexplorer/context_chart_mask';
import { findChartPointForAnomalyTime } from 'plugins/ml/timeseriesexplorer/timeseriesexplorer_utils';
import { mlEscape } from 'plugins/ml/util/string_utils';
import { mlFieldFormatService } from 'plugins/ml/services/field_format_service';
+import { mlChartTooltipService } from '../components/chart_tooltip/chart_tooltip_service';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
-module.directive('mlTimeseriesChart', function (
- $compile,
- $timeout,
- Private,
- mlChartTooltipService) {
+module.directive('mlTimeseriesChart', function () {
function link(scope, element) {
diff --git a/x-pack/plugins/ml/public/util/chart_utils.js b/x-pack/plugins/ml/public/util/chart_utils.js
index e9c251db98199..f6227dad1e3ce 100644
--- a/x-pack/plugins/ml/public/util/chart_utils.js
+++ b/x-pack/plugins/ml/public/util/chart_utils.js
@@ -7,7 +7,7 @@
import d3 from 'd3';
-import { calculateTextWidth } from 'plugins/ml/util/string_utils';
+import { calculateTextWidth } from '../util/string_utils';
import moment from 'moment';
const MAX_LABEL_WIDTH = 100;