diff --git a/x-pack/plugins/ml/public/components/messagebar/messagebar_service.js b/x-pack/plugins/ml/public/components/messagebar/messagebar_service.js
index 3bbf7e79c683e..4395be2a9fb4b 100644
--- a/x-pack/plugins/ml/public/components/messagebar/messagebar_service.js
+++ b/x-pack/plugins/ml/public/components/messagebar/messagebar_service.js
@@ -6,7 +6,7 @@
import { notify } from 'ui/notify';
-import { MLRequestFailure } from 'plugins/ml/util/ml_error';
+import { MLRequestFailure } from '../../util/ml_error';
const messages = [];
diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_chart_records.json b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_chart_records.json
new file mode 100644
index 0000000000000..3f106d399f852
--- /dev/null
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_chart_records.json
@@ -0,0 +1,68 @@
+[
+ {
+ "job_id": "mock-job-id",
+ "result_type": "record",
+ "probability": 1.6552181439816634e-32,
+ "record_score": 98.56065708456248,
+ "initial_record_score": 98.56065708456248,
+ "bucket_span": 900,
+ "detector_index": 0,
+ "is_interim": false,
+ "timestamp": 1486656000000,
+ "partition_field_name": "airline",
+ "partition_field_value": "AAL",
+ "function": "mean",
+ "function_description": "mean",
+ "typical": [
+ 99.81123207526203
+ ],
+ "actual": [
+ 242.3568918440077
+ ],
+ "field_name": "responsetime",
+ "influencers": [
+ {
+ "influencer_field_name": "airline",
+ "influencer_field_values": [
+ "AAL"
+ ]
+ }
+ ],
+ "airline": [
+ "AAL"
+ ]
+ },
+ {
+ "job_id": "mock-job-id",
+ "result_type": "record",
+ "probability": 2.6276047868032343e-28,
+ "record_score": 96.93718,
+ "initial_record_score": 92.70812367638732,
+ "bucket_span": 900,
+ "detector_index": 0,
+ "is_interim": false,
+ "timestamp": 1486656900000,
+ "partition_field_name": "airline",
+ "partition_field_value": "AAL",
+ "function": "mean",
+ "function_description": "mean",
+ "typical": [
+ 100.02884159032787
+ ],
+ "actual": [
+ 282.02533259111306
+ ],
+ "field_name": "responsetime",
+ "influencers": [
+ {
+ "influencer_field_name": "airline",
+ "influencer_field_values": [
+ "AAL"
+ ]
+ }
+ ],
+ "airline": [
+ "AAL"
+ ]
+ }
+]
diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_record.json b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_record.json
new file mode 100644
index 0000000000000..ccc13b6a815a4
--- /dev/null
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_record.json
@@ -0,0 +1,33 @@
+{
+ "job_id": "mock-job-id",
+ "result_type": "record",
+ "probability": 0.000374234162864467,
+ "record_score": 1.3677172011743646,
+ "initial_record_score": 1.3677172011743646,
+ "bucket_span": 900,
+ "detector_index": 0,
+ "is_interim": false,
+ "timestamp": 1486743300000,
+ "partition_field_name": "airline",
+ "partition_field_value": "JAL",
+ "function": "mean",
+ "function_description": "mean",
+ "typical": [
+ 499.9850000350266
+ ],
+ "actual": [
+ 511.4997161865235
+ ],
+ "field_name": "responsetime",
+ "influencers": [
+ {
+ "influencer_field_name": "airline",
+ "influencer_field_values": [
+ "JAL"
+ ]
+ }
+ ],
+ "airline": [
+ "JAL"
+ ]
+}
diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_chart_data.js b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_chart_data.js
new file mode 100644
index 0000000000000..e150335114a20
--- /dev/null
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_chart_data.js
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+export 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]
+ }
+];
diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_detectors_by_job.json b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_detectors_by_job.json
new file mode 100644
index 0000000000000..f45a2d5b8f2b9
--- /dev/null
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_detectors_by_job.json
@@ -0,0 +1,11 @@
+{
+ "mock-job-id": [
+ {
+ "detector_description": "mean(responsetime)",
+ "function": "mean",
+ "field_name": "responsetime",
+ "partition_field_name": "airline",
+ "detector_index": 0
+ }
+ ]
+}
diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_job_config.json b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_job_config.json
new file mode 100644
index 0000000000000..2750ad84a8308
--- /dev/null
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_job_config.json
@@ -0,0 +1,88 @@
+{
+ "job_id": "mock-job-id",
+ "job_type": "anomaly_detector",
+ "job_version": "7.0.0-alpha1",
+ "description": "",
+ "create_time": 1532692299663,
+ "finished_time": 1532692304364,
+ "established_model_memory": 560894,
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "mean(responsetime)",
+ "function": "mean",
+ "field_name": "responsetime",
+ "partition_field_name": "airline",
+ "detector_index": 0
+ }
+ ],
+ "influencers": [
+ "airline"
+ ]
+ },
+ "analysis_limits": {
+ "model_memory_limit": "13mb",
+ "categorization_examples_limit": 4
+ },
+ "data_description": {
+ "time_field": "@timestamp",
+ "time_format": "epoch_ms"
+ },
+ "model_snapshot_retention_days": 1,
+ "custom_settings": {
+ "created_by": "multi-metric-wizard"
+ },
+ "model_snapshot_id": "1532692303",
+ "model_snapshot_min_version": "6.4.0",
+ "results_index_name": "shared",
+ "data_counts": {
+ "job_id": "mock-job-id",
+ "processed_record_count": 86274,
+ "processed_field_count": 172548,
+ "input_bytes": 6744642,
+ "input_field_count": 172548,
+ "invalid_date_count": 0,
+ "missing_field_count": 0,
+ "out_of_order_timestamp_count": 0,
+ "empty_bucket_count": 0,
+ "sparse_bucket_count": 0,
+ "bucket_count": 479,
+ "earliest_record_timestamp": 1486425600000,
+ "latest_record_timestamp": 1486857594000,
+ "last_data_time": 1532692303844,
+ "input_record_count": 86274
+ },
+ "model_size_stats": {
+ "job_id": "mock-job-id",
+ "result_type": "model_size_stats",
+ "model_bytes": 560894,
+ "total_by_field_count": 21,
+ "total_over_field_count": 0,
+ "total_partition_field_count": 20,
+ "bucket_allocation_failures_count": 0,
+ "memory_status": "ok",
+ "log_time": 1532692303000,
+ "timestamp": 1486855800000
+ },
+ "datafeed_config": {
+ "datafeed_id": "datafeed-mock-job-id",
+ "job_id": "mock-job-id",
+ "query_delay": "86658ms",
+ "indices": [
+ "farequote-2017"
+ ],
+ "types": [],
+ "query": {
+ "match_all": {
+ "boost": 1
+ }
+ },
+ "scroll_size": 1000,
+ "chunking_config": {
+ "mode": "auto"
+ },
+ "state": "stopped"
+ },
+ "state": "closed"
+}
diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_config_filebeat.json b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_config_filebeat.json
new file mode 100644
index 0000000000000..b2c974e737e48
--- /dev/null
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_config_filebeat.json
@@ -0,0 +1,57 @@
+{
+ "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": {
+ "jobId": "population-03",
+ "aggregationInterval": "1h",
+ "chartFunction": "sum nginx.access.body_sent.bytes",
+ "entityFields": [
+ {
+ "fieldName": "nginx.access.remote_ip",
+ "fieldValue": "72.57.0.53"
+ }
+ ]
+ },
+ "loading": false,
+ "plotEarliest": 1487534400000,
+ "plotLatest": 1488168000000,
+ "selectedEarliest": 1487808000000,
+ "selectedLatest": 1487894399999
+}
diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_promises_response.json b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_promises_response.json
new file mode 100644
index 0000000000000..b2d51e7de713b
--- /dev/null
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_promises_response.json
@@ -0,0 +1,231 @@
+[
+ [
+ {
+ "success": true,
+ "results": {
+ "1486611900000": 95.61584963117328,
+ "1486612800000": 99.34646708170573,
+ "1486613700000": 92.54502330106847,
+ "1486614600000": 98.87258768081665,
+ "1486615500000": 102.82824816022601,
+ "1486616400000": 96.7391939163208,
+ "1486617300000": 99.72634760538737,
+ "1486618200000": 101.08556365966797,
+ "1486619100000": 84.60266517190372,
+ "1486620000000": 105.24246263504028,
+ "1486620900000": 91.86086603800456,
+ "1486621800000": 94.5369130452474,
+ "1486622700000": 97.63843189586292,
+ "1486623600000": 93.79290502211627,
+ "1486624500000": 108.91006604362937,
+ "1486625400000": 107.46900049845378,
+ "1486626300000": 100.03502061631944,
+ "1486627200000": 92.0638559129503,
+ "1486628100000": 96.06356851678146,
+ "1486629000000": 109.89569989372703,
+ "1486629900000": 96.09498441786994,
+ "1486630800000": 105.05972120496962,
+ "1486631700000": 94.53041982650757,
+ "1486632600000": 103.37048240329908,
+ "1486633500000": 105.2058048248291,
+ "1486634400000": 102.06169471740722,
+ "1486635300000": 101.4836499955919,
+ "1486636200000": 96.34219177246094,
+ "1486637100000": 102.81613063812256,
+ "1486638000000": 96.09064518321644,
+ "1486638900000": 104.8488635012978,
+ "1486639800000": 93.45240384056454,
+ "1486640700000": 102.28834065524015,
+ "1486641600000": 104.54204668317523,
+ "1486642500000": 99.85492063823499,
+ "1486643400000": 97.12778260972765,
+ "1486644300000": 103.99638447008635,
+ "1486645200000": 95.34676822863128,
+ "1486646100000": 97.04620517383923,
+ "1486647000000": 104.2849609375,
+ "1486647900000": 97.88982413796818,
+ "1486648800000": 99.03312370300293,
+ "1486649700000": 105.5509593963623,
+ "1486650600000": 100.49496881585372,
+ "1486651500000": 99.06059494018555,
+ "1486652400000": 90.58293914794922,
+ "1486653300000": 92.8633090655009,
+ "1486654200000": 96.12510445004418,
+ "1486655100000": 100.4840145111084,
+ "1486656000000": 242.3568918440077,
+ "1486656900000": 282.02533259111294,
+ "1486657800000": 100.15823459625244,
+ "1486658700000": 97.5446532754337,
+ "1486659600000": 99.53840043809679,
+ "1486660500000": 101.24810005636776,
+ "1486661400000": 101.11400771141052,
+ "1486662300000": 100.70463662398488,
+ "1486663200000": 110.70174247340152,
+ "1486664100000": 96.51030629475912,
+ "1486665000000": 103.92840491400824,
+ "1486665900000": 98.29448418868215,
+ "1486666800000": 98.0272060394287,
+ "1486667700000": 99.63833363850911,
+ "1486668600000": 105.18764642568735,
+ "1486669500000": 97.8544118669298,
+ "1486670400000": 97.99196343672902,
+ "1486671300000": 106.30481338500977,
+ "1486672200000": 99.88215498490767,
+ "1486673100000": 93.50493303934734,
+ "1486674000000": 101.2538422175816,
+ "1486674900000": 102.07398986816406,
+ "1486675800000": 102.66583075890175,
+ "1486676700000": 108.5278158748851,
+ "1486677600000": 103.91436131795247,
+ "1486678500000": 98.55452414119945,
+ "1486679400000": 88.25028387705485,
+ "1486680300000": 93.57433591570172,
+ "1486681200000": 96.70550713172325,
+ "1486682100000": 98.14921424502418,
+ "1486683000000": 96.99264602661133,
+ "1486683900000": 88.23578810691833,
+ "1486684800000": 106.89157305265728,
+ "1486685700000": 101.07822271493765,
+ "1486686600000": 101.77820564718807,
+ "1486687500000": 102.84660829816546,
+ "1486688400000": 103.91598869772518,
+ "1486689300000": 104.73469270978656,
+ "1486690200000": 97.01155325082632,
+ "1486691100000": 104.97890539730297,
+ "1486692000000": 99.66440022786459,
+ "1486692900000": 99.64117607703575,
+ "1486693800000": 87.37038326263428,
+ "1486694700000": 105.95191955566406,
+ "1486695600000": 104.33271111382379,
+ "1486696500000": 101.93921706255745,
+ "1486697400000": 101.11774004422702,
+ "1486698300000": 101.70929403866039,
+ "1486699200000": 102.61243908221905,
+ "1486700100000": 99.16273922390408,
+ "1486701000000": 105.98729952643899,
+ "1486701900000": 114.16951904296874,
+ "1486702800000": 98.25128769874573,
+ "1486703700000": 94.25434192858245,
+ "1486704600000": 99.7759528526893,
+ "1486705500000": 113.10429502788342,
+ "1486706400000": 97.95185834711248,
+ "1486707300000": 114.46214866638184,
+ "1486708200000": 105.51880025863647,
+ "1486709100000": 99.89148930140904,
+ "1486710000000": 90.5253866369074,
+ "1486710900000": 103.66612243652344,
+ "1486711800000": 103.97851837158203,
+ "1486712700000": 92.76053659539474,
+ "1486713600000": 99.99461364746094
+ }
+ },
+ {
+ "success": true,
+ "records": [
+ {
+ "job_id": "mock-job-id",
+ "result_type": "record",
+ "probability": 1.6552181439816634e-32,
+ "record_score": 98.56065708456248,
+ "initial_record_score": 98.56065708456248,
+ "bucket_span": 900,
+ "detector_index": 0,
+ "is_interim": false,
+ "timestamp": 1486656000000,
+ "partition_field_name": "airline",
+ "partition_field_value": "AAL",
+ "function": "mean",
+ "function_description": "mean",
+ "typical": [
+ 99.81123207526203
+ ],
+ "actual": [
+ 242.3568918440077
+ ],
+ "field_name": "responsetime",
+ "influencers": [
+ {
+ "influencer_field_name": "airline",
+ "influencer_field_values": [
+ "AAL"
+ ]
+ }
+ ],
+ "airline": [
+ "AAL"
+ ]
+ },
+ {
+ "job_id": "mock-job-id",
+ "result_type": "record",
+ "probability": 2.6276047868032343e-28,
+ "record_score": 96.93718,
+ "initial_record_score": 92.70812367638732,
+ "bucket_span": 900,
+ "detector_index": 0,
+ "is_interim": false,
+ "timestamp": 1486656900000,
+ "partition_field_name": "airline",
+ "partition_field_value": "AAL",
+ "function": "mean",
+ "function_description": "mean",
+ "typical": [
+ 100.02884159032787
+ ],
+ "actual": [
+ 282.02533259111306
+ ],
+ "field_name": "responsetime",
+ "influencers": [
+ {
+ "influencer_field_name": "airline",
+ "influencer_field_values": [
+ "AAL"
+ ]
+ }
+ ],
+ "airline": [
+ "AAL"
+ ]
+ },
+ {
+ "job_id": "mock-job-id",
+ "result_type": "record",
+ "probability": 0.013283203854072794,
+ "record_score": 0.02716009,
+ "initial_record_score": 0.6110770406098681,
+ "bucket_span": 900,
+ "detector_index": 0,
+ "is_interim": false,
+ "timestamp": 1486619100000,
+ "partition_field_name": "airline",
+ "partition_field_value": "AAL",
+ "function": "mean",
+ "function_description": "mean",
+ "typical": [
+ 99.79426367092864
+ ],
+ "actual": [
+ 84.60266517190372
+ ],
+ "field_name": "responsetime",
+ "influencers": [
+ {
+ "influencer_field_name": "airline",
+ "influencer_field_values": [
+ "AAL"
+ ]
+ }
+ ],
+ "airline": [
+ "AAL"
+ ]
+ }
+ ]
+ },
+ {
+ "success": true,
+ "events": {}
+ }
+ ]
+]
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
new file mode 100644
index 0000000000000..f899ee14003b7
--- /dev/null
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap
@@ -0,0 +1,52 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`buildConfig get dataConfig for anomaly record 1`] = `
+Object {
+ "bucketSpanSeconds": 900,
+ "datafeedConfig": Object {
+ "chunking_config": Object {
+ "mode": "auto",
+ },
+ "datafeed_id": "datafeed-mock-job-id",
+ "indices": Array [
+ "farequote-2017",
+ ],
+ "job_id": "mock-job-id",
+ "query": Object {
+ "match_all": Object {
+ "boost": 1,
+ },
+ },
+ "query_delay": "86658ms",
+ "scroll_size": 1000,
+ "state": "stopped",
+ "types": Array [],
+ },
+ "detectorIndex": 0,
+ "detectorLabel": "mean(responsetime)",
+ "entityFields": Array [
+ Object {
+ "fieldName": "airline",
+ "fieldValue": "JAL",
+ },
+ ],
+ "fieldName": "responsetime",
+ "functionDescription": "mean",
+ "infoTooltip": Object {
+ "aggregationInterval": "15m",
+ "chartFunction": "avg responsetime",
+ "entityFields": Array [
+ Object {
+ "fieldName": "airline",
+ "fieldValue": "JAL",
+ },
+ ],
+ "jobId": "mock-job-id",
+ },
+ "interval": "15m",
+ "jobId": "mock-job-id",
+ "metricFieldName": "responsetime",
+ "metricFunction": "avg",
+ "timeField": "@timestamp",
+}
+`;
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
new file mode 100644
index 0000000000000..c602bc0373c51
--- /dev/null
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_tooltip.test.js.snap
@@ -0,0 +1,24 @@
+// 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.test.js.snap b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container.test.js.snap
new file mode 100644
index 0000000000000..087558cfa4ed4
--- /dev/null
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container.test.js.snap
@@ -0,0 +1,54 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ExplorerChartsContainer Initialization with chart data 1`] = `
+
+
+
+ high_sum(nginx.access.body_sent.bytes) over nginx.access.remote_ip (population-03)
+ -
+
+
+ nginx.access.remote_ip
+
+ 72.57.0.53
+
+
+
+ }
+ position="left"
+ size="s"
+ type="questionInCircle"
+ />
+
+ View
+
+
+
+`;
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
new file mode 100644
index 0000000000000..e7b6cfb8ed9b3
--- /dev/null
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap
@@ -0,0 +1,616 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`explorerChartsContainerService call anomalyChangeListener with actual series config 1`] = `
+Object {
+ "layoutCellsPerChart": 12,
+ "seriesToPlot": Array [],
+ "timeFieldName": "timestamp",
+ "tooManyBuckets": false,
+}
+`;
+
+exports[`explorerChartsContainerService call anomalyChangeListener with actual series config 2`] = `
+Object {
+ "layoutCellsPerChart": 6,
+ "seriesToPlot": Array [
+ Object {
+ "bucketSpanSeconds": 900,
+ "chartData": null,
+ "datafeedConfig": Object {
+ "chunking_config": Object {
+ "mode": "auto",
+ },
+ "datafeed_id": "datafeed-mock-job-id",
+ "indices": Array [
+ "farequote-2017",
+ ],
+ "job_id": "mock-job-id",
+ "query": Object {
+ "match_all": Object {
+ "boost": 1,
+ },
+ },
+ "query_delay": "86658ms",
+ "scroll_size": 1000,
+ "state": "stopped",
+ "types": Array [],
+ },
+ "detectorIndex": 0,
+ "detectorLabel": "mean(responsetime)",
+ "entityFields": Array [
+ Object {
+ "fieldName": "airline",
+ "fieldValue": "AAL",
+ },
+ ],
+ "fieldName": "responsetime",
+ "functionDescription": "mean",
+ "infoTooltip": Object {
+ "aggregationInterval": "15m",
+ "chartFunction": "avg responsetime",
+ "entityFields": Array [
+ Object {
+ "fieldName": "airline",
+ "fieldValue": "AAL",
+ },
+ ],
+ "jobId": "mock-job-id",
+ },
+ "interval": "15m",
+ "jobId": "mock-job-id",
+ "loading": true,
+ "metricFieldName": "responsetime",
+ "metricFunction": "avg",
+ "timeField": "@timestamp",
+ },
+ ],
+ "timeFieldName": "timestamp",
+ "tooManyBuckets": false,
+}
+`;
+
+exports[`explorerChartsContainerService call anomalyChangeListener with actual series config 3`] = `
+Object {
+ "layoutCellsPerChart": 6,
+ "seriesToPlot": Array [
+ Object {
+ "bucketSpanSeconds": 900,
+ "chartData": Array [
+ Object {
+ "date": 1486611900000,
+ "value": 95.61584963117328,
+ },
+ Object {
+ "date": 1486612800000,
+ "value": 99.34646708170573,
+ },
+ Object {
+ "date": 1486613700000,
+ "value": 92.54502330106847,
+ },
+ Object {
+ "date": 1486614600000,
+ "value": 98.87258768081665,
+ },
+ Object {
+ "date": 1486615500000,
+ "value": 102.82824816022601,
+ },
+ Object {
+ "date": 1486616400000,
+ "value": 96.7391939163208,
+ },
+ Object {
+ "date": 1486617300000,
+ "value": 99.72634760538737,
+ },
+ Object {
+ "date": 1486618200000,
+ "value": 101.08556365966797,
+ },
+ Object {
+ "actual": Array [
+ 84.60266517190372,
+ ],
+ "anomalyScore": 0.02716009,
+ "date": 1486619100000,
+ "typical": Array [
+ 99.79426367092864,
+ ],
+ "value": 84.60266517190372,
+ },
+ Object {
+ "date": 1486620000000,
+ "value": 105.24246263504028,
+ },
+ Object {
+ "date": 1486620900000,
+ "value": 91.86086603800456,
+ },
+ Object {
+ "date": 1486621800000,
+ "value": 94.5369130452474,
+ },
+ Object {
+ "date": 1486622700000,
+ "value": 97.63843189586292,
+ },
+ Object {
+ "date": 1486623600000,
+ "value": 93.79290502211627,
+ },
+ Object {
+ "date": 1486624500000,
+ "value": 108.91006604362937,
+ },
+ Object {
+ "date": 1486625400000,
+ "value": 107.46900049845378,
+ },
+ Object {
+ "date": 1486626300000,
+ "value": 100.03502061631944,
+ },
+ Object {
+ "date": 1486627200000,
+ "value": 92.0638559129503,
+ },
+ Object {
+ "date": 1486628100000,
+ "value": 96.06356851678146,
+ },
+ Object {
+ "date": 1486629000000,
+ "value": 109.89569989372703,
+ },
+ Object {
+ "date": 1486629900000,
+ "value": 96.09498441786994,
+ },
+ Object {
+ "date": 1486630800000,
+ "value": 105.05972120496962,
+ },
+ Object {
+ "date": 1486631700000,
+ "value": 94.53041982650757,
+ },
+ Object {
+ "date": 1486632600000,
+ "value": 103.37048240329908,
+ },
+ Object {
+ "date": 1486633500000,
+ "value": 105.2058048248291,
+ },
+ Object {
+ "date": 1486634400000,
+ "value": 102.06169471740722,
+ },
+ Object {
+ "date": 1486635300000,
+ "value": 101.4836499955919,
+ },
+ Object {
+ "date": 1486636200000,
+ "value": 96.34219177246094,
+ },
+ Object {
+ "date": 1486637100000,
+ "value": 102.81613063812256,
+ },
+ Object {
+ "date": 1486638000000,
+ "value": 96.09064518321644,
+ },
+ Object {
+ "date": 1486638900000,
+ "value": 104.8488635012978,
+ },
+ Object {
+ "date": 1486639800000,
+ "value": 93.45240384056454,
+ },
+ Object {
+ "date": 1486640700000,
+ "value": 102.28834065524015,
+ },
+ Object {
+ "date": 1486641600000,
+ "value": 104.54204668317523,
+ },
+ Object {
+ "date": 1486642500000,
+ "value": 99.85492063823499,
+ },
+ Object {
+ "date": 1486643400000,
+ "value": 97.12778260972765,
+ },
+ Object {
+ "date": 1486644300000,
+ "value": 103.99638447008635,
+ },
+ Object {
+ "date": 1486645200000,
+ "value": 95.34676822863128,
+ },
+ Object {
+ "date": 1486646100000,
+ "value": 97.04620517383923,
+ },
+ Object {
+ "date": 1486647000000,
+ "value": 104.2849609375,
+ },
+ Object {
+ "date": 1486647900000,
+ "value": 97.88982413796818,
+ },
+ Object {
+ "date": 1486648800000,
+ "value": 99.03312370300293,
+ },
+ Object {
+ "date": 1486649700000,
+ "value": 105.5509593963623,
+ },
+ Object {
+ "date": 1486650600000,
+ "value": 100.49496881585372,
+ },
+ Object {
+ "date": 1486651500000,
+ "value": 99.06059494018555,
+ },
+ Object {
+ "date": 1486652400000,
+ "value": 90.58293914794922,
+ },
+ Object {
+ "date": 1486653300000,
+ "value": 92.8633090655009,
+ },
+ Object {
+ "date": 1486654200000,
+ "value": 96.12510445004418,
+ },
+ Object {
+ "date": 1486655100000,
+ "value": 100.4840145111084,
+ },
+ Object {
+ "actual": Array [
+ 242.3568918440077,
+ ],
+ "anomalyScore": 98.56065708456248,
+ "date": 1486656000000,
+ "typical": Array [
+ 99.81123207526203,
+ ],
+ "value": 242.3568918440077,
+ },
+ Object {
+ "actual": Array [
+ 282.02533259111306,
+ ],
+ "anomalyScore": 96.93718,
+ "date": 1486656900000,
+ "typical": Array [
+ 100.02884159032787,
+ ],
+ "value": 282.02533259111294,
+ },
+ Object {
+ "date": 1486657800000,
+ "value": 100.15823459625244,
+ },
+ Object {
+ "date": 1486658700000,
+ "value": 97.5446532754337,
+ },
+ Object {
+ "date": 1486659600000,
+ "value": 99.53840043809679,
+ },
+ Object {
+ "date": 1486660500000,
+ "value": 101.24810005636776,
+ },
+ Object {
+ "date": 1486661400000,
+ "value": 101.11400771141052,
+ },
+ Object {
+ "date": 1486662300000,
+ "value": 100.70463662398488,
+ },
+ Object {
+ "date": 1486663200000,
+ "value": 110.70174247340152,
+ },
+ Object {
+ "date": 1486664100000,
+ "value": 96.51030629475912,
+ },
+ Object {
+ "date": 1486665000000,
+ "value": 103.92840491400824,
+ },
+ Object {
+ "date": 1486665900000,
+ "value": 98.29448418868215,
+ },
+ Object {
+ "date": 1486666800000,
+ "value": 98.0272060394287,
+ },
+ Object {
+ "date": 1486667700000,
+ "value": 99.63833363850911,
+ },
+ Object {
+ "date": 1486668600000,
+ "value": 105.18764642568735,
+ },
+ Object {
+ "date": 1486669500000,
+ "value": 97.8544118669298,
+ },
+ Object {
+ "date": 1486670400000,
+ "value": 97.99196343672902,
+ },
+ Object {
+ "date": 1486671300000,
+ "value": 106.30481338500977,
+ },
+ Object {
+ "date": 1486672200000,
+ "value": 99.88215498490767,
+ },
+ Object {
+ "date": 1486673100000,
+ "value": 93.50493303934734,
+ },
+ Object {
+ "date": 1486674000000,
+ "value": 101.2538422175816,
+ },
+ Object {
+ "date": 1486674900000,
+ "value": 102.07398986816406,
+ },
+ Object {
+ "date": 1486675800000,
+ "value": 102.66583075890175,
+ },
+ Object {
+ "date": 1486676700000,
+ "value": 108.5278158748851,
+ },
+ Object {
+ "date": 1486677600000,
+ "value": 103.91436131795247,
+ },
+ Object {
+ "date": 1486678500000,
+ "value": 98.55452414119945,
+ },
+ Object {
+ "date": 1486679400000,
+ "value": 88.25028387705485,
+ },
+ Object {
+ "date": 1486680300000,
+ "value": 93.57433591570172,
+ },
+ Object {
+ "date": 1486681200000,
+ "value": 96.70550713172325,
+ },
+ Object {
+ "date": 1486682100000,
+ "value": 98.14921424502418,
+ },
+ Object {
+ "date": 1486683000000,
+ "value": 96.99264602661133,
+ },
+ Object {
+ "date": 1486683900000,
+ "value": 88.23578810691833,
+ },
+ Object {
+ "date": 1486684800000,
+ "value": 106.89157305265728,
+ },
+ Object {
+ "date": 1486685700000,
+ "value": 101.07822271493765,
+ },
+ Object {
+ "date": 1486686600000,
+ "value": 101.77820564718807,
+ },
+ Object {
+ "date": 1486687500000,
+ "value": 102.84660829816546,
+ },
+ Object {
+ "date": 1486688400000,
+ "value": 103.91598869772518,
+ },
+ Object {
+ "date": 1486689300000,
+ "value": 104.73469270978656,
+ },
+ Object {
+ "date": 1486690200000,
+ "value": 97.01155325082632,
+ },
+ Object {
+ "date": 1486691100000,
+ "value": 104.97890539730297,
+ },
+ Object {
+ "date": 1486692000000,
+ "value": 99.66440022786459,
+ },
+ Object {
+ "date": 1486692900000,
+ "value": 99.64117607703575,
+ },
+ Object {
+ "date": 1486693800000,
+ "value": 87.37038326263428,
+ },
+ Object {
+ "date": 1486694700000,
+ "value": 105.95191955566406,
+ },
+ Object {
+ "date": 1486695600000,
+ "value": 104.33271111382379,
+ },
+ Object {
+ "date": 1486696500000,
+ "value": 101.93921706255745,
+ },
+ Object {
+ "date": 1486697400000,
+ "value": 101.11774004422702,
+ },
+ Object {
+ "date": 1486698300000,
+ "value": 101.70929403866039,
+ },
+ Object {
+ "date": 1486699200000,
+ "value": 102.61243908221905,
+ },
+ Object {
+ "date": 1486700100000,
+ "value": 99.16273922390408,
+ },
+ Object {
+ "date": 1486701000000,
+ "value": 105.98729952643899,
+ },
+ Object {
+ "date": 1486701900000,
+ "value": 114.16951904296874,
+ },
+ Object {
+ "date": 1486702800000,
+ "value": 98.25128769874573,
+ },
+ Object {
+ "date": 1486703700000,
+ "value": 94.25434192858245,
+ },
+ Object {
+ "date": 1486704600000,
+ "value": 99.7759528526893,
+ },
+ Object {
+ "date": 1486705500000,
+ "value": 113.10429502788342,
+ },
+ Object {
+ "date": 1486706400000,
+ "value": 97.95185834711248,
+ },
+ Object {
+ "date": 1486707300000,
+ "value": 114.46214866638184,
+ },
+ Object {
+ "date": 1486708200000,
+ "value": 105.51880025863647,
+ },
+ Object {
+ "date": 1486709100000,
+ "value": 99.89148930140904,
+ },
+ Object {
+ "date": 1486710000000,
+ "value": 90.5253866369074,
+ },
+ Object {
+ "date": 1486710900000,
+ "value": 103.66612243652344,
+ },
+ Object {
+ "date": 1486711800000,
+ "value": 103.97851837158203,
+ },
+ Object {
+ "date": 1486712700000,
+ "value": 92.76053659539474,
+ },
+ Object {
+ "date": 1486713600000,
+ "value": 99.99461364746094,
+ },
+ ],
+ "chartLimits": Object {
+ "max": 282.02533259111294,
+ "min": 84.60266517190372,
+ },
+ "datafeedConfig": Object {
+ "chunking_config": Object {
+ "mode": "auto",
+ },
+ "datafeed_id": "datafeed-mock-job-id",
+ "indices": Array [
+ "farequote-2017",
+ ],
+ "job_id": "mock-job-id",
+ "query": Object {
+ "match_all": Object {
+ "boost": 1,
+ },
+ },
+ "query_delay": "86658ms",
+ "scroll_size": 1000,
+ "state": "stopped",
+ "types": Array [],
+ },
+ "detectorIndex": 0,
+ "detectorLabel": "mean(responsetime)",
+ "entityFields": Array [
+ Object {
+ "fieldName": "airline",
+ "fieldValue": "AAL",
+ },
+ ],
+ "fieldName": "responsetime",
+ "functionDescription": "mean",
+ "infoTooltip": Object {
+ "aggregationInterval": "15m",
+ "chartFunction": "avg responsetime",
+ "entityFields": Array [
+ Object {
+ "fieldName": "airline",
+ "fieldValue": "AAL",
+ },
+ ],
+ "jobId": "mock-job-id",
+ },
+ "interval": "15m",
+ "jobId": "mock-job-id",
+ "loading": false,
+ "metricFieldName": "responsetime",
+ "metricFunction": "avg",
+ "plotEarliest": 1486611900000,
+ "plotLatest": 1486714500000,
+ "selectedEarliest": 1486656000000,
+ "selectedLatest": 1486670399999,
+ "timeField": "@timestamp",
+ },
+ ],
+ "timeFieldName": "timestamp",
+ "tooManyBuckets": false,
+}
+`;
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
deleted file mode 100644
index 48b11391f5eb8..0000000000000
--- a/x-pack/plugins/ml/public/explorer/explorer_charts/explore_series.js
+++ /dev/null
@@ -1,74 +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 _ 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.test.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart.test.js
index ce40308003f5f..cf28917c47a9a 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.test.js
@@ -4,6 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { chartData as mockChartData } from './__mocks__/mock_chart_data';
+import seriesConfig from './__mocks__/mock_series_config_filebeat.json';
+
// Mock TimeBuckets and mlFieldFormatService, they don't play well
// with the jest based test setup yet.
jest.mock('ui/time_buckets', () => ({
@@ -26,44 +29,6 @@ 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: () => ({
@@ -74,9 +39,7 @@ describe('ExplorerChart', () => {
const mockedGetBBox = { x: 0, y: -11.5, width: 12.1875, height: 14.5 };
const originalGetBBox = SVGElement.prototype.getBBox;
- beforeEach(() => SVGElement.prototype.getBBox = () => {
- return mockedGetBBox;
- });
+ beforeEach(() => SVGElement.prototype.getBBox = () => mockedGetBBox);
afterEach(() => (SVGElement.prototype.getBBox = originalGetBBox));
test('Initialize', () => {
@@ -122,29 +85,7 @@ describe('ExplorerChart', () => {
}
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);
+ const wrapper = init(mockChartData);
// the loading indicator should not be shown
expect(wrapper.find('.ml-loading-indicator .loading-spinner')).toHaveLength(0);
diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.test.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.test.js
new file mode 100644
index 0000000000000..bdfee3c933310
--- /dev/null
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.test.js
@@ -0,0 +1,27 @@
+/*
+ * 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 mockAnomalyRecord from './__mocks__/mock_anomaly_record.json';
+import mockDetectorsByJob from './__mocks__/mock_detectors_by_job.json';
+import mockJobConfig from './__mocks__/mock_job_config.json';
+
+jest.mock('../../util/ml_error', () => (class MLRequestFailure {}));
+
+jest.mock('../../services/job_service', () => ({
+ mlJobService: {
+ getJob() { return mockJobConfig; },
+ detectorsByJob: mockDetectorsByJob
+ }
+}));
+
+import { buildConfig } from './explorer_chart_config_builder';
+
+describe('buildConfig', () => {
+ test('get dataConfig for anomaly record', () => {
+ const dataConfig = buildConfig(mockAnomalyRecord);
+ expect(dataConfig).toMatchSnapshot();
+ });
+});
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_tooltip.test.js
new file mode 100644
index 0000000000000..c83ed6af6333b
--- /dev/null
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_tooltip.test.js
@@ -0,0 +1,28 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import React from 'react';
+
+import { ExplorerChartTooltip } from './explorer_chart_tooltip';
+
+describe('ExplorerChartTooltip', () => {
+ test('Render tooltip based on infoTooltip data.', () => {
+ const infoTooltip = {
+ aggregationInterval: '15m',
+ chartFunction: 'avg responsetime',
+ entityFields: [{
+ fieldName: 'airline',
+ fieldValue: 'JAL',
+ }],
+ jobId: 'mock-job-id'
+ };
+
+ const wrapper = shallow();
+ expect(wrapper).toMatchSnapshot();
+ });
+});
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 2fb75120fdb44..b4ec174a69907 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,11 +9,11 @@ import React from 'react';
import { EuiIconTip } from '@elastic/eui';
+import { getExploreSeriesLink } from '../../util/chart_utils';
import { ExplorerChart } from './explorer_chart';
import { ExplorerChartTooltip } from './explorer_chart_tooltip';
export function ExplorerChartsContainer({
- exploreSeries,
seriesToPlot,
layoutCellsPerChart,
tooManyBuckets,
@@ -60,7 +60,7 @@ export function ExplorerChartsContainer({
color="warning"
/>
)}
- exploreSeries(series)}>
+ window.open(getExploreSeriesLink(series), '_blank')}>
View
@@ -76,7 +76,6 @@ export function ExplorerChartsContainer({
);
}
ExplorerChartsContainer.propTypes = {
- exploreSeries: PropTypes.func.isRequired,
seriesToPlot: PropTypes.array.isRequired,
layoutCellsPerChart: PropTypes.number.isRequired,
tooManyBuckets: PropTypes.bool.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
new file mode 100644
index 0000000000000..073cee8632ebd
--- /dev/null
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.js
@@ -0,0 +1,76 @@
+/*
+ * 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 { chartData } from './__mocks__/mock_chart_data';
+import seriesConfig from './__mocks__/mock_series_config_filebeat.json';
+
+// 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 { shallow } from 'enzyme';
+import React from 'react';
+
+import { chartLimits } from '../../util/chart_utils';
+import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tooltip_service';
+
+import { ExplorerChartsContainer } from './explorer_charts_container';
+
+describe('ExplorerChartsContainer', () => {
+ 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 = () => mockedGetBBox);
+ afterEach(() => (SVGElement.prototype.getBBox = originalGetBBox));
+
+ test('Minimal Initialization', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.html()).toBe('');
+ });
+
+ test('Initialization with chart data', () => {
+ const wrapper = shallow();
+
+ // Only do a snapshot of the label section, the included
+ // ExplorerChart component does that in its own tests anyway.
+ expect(wrapper.find('.explorer-chart-label')).toMatchSnapshot();
+ });
+});
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 412831c4d97c8..58b9994b6a650 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
@@ -18,7 +18,6 @@ import ReactDOM from 'react-dom';
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';
@@ -33,7 +32,8 @@ module.directive('mlExplorerChartsContainer', function (
function link(scope, element) {
const anomalyDataChangeListener = explorerChartsContainerServiceFactory(
mlSelectSeverityService,
- updateComponent
+ updateComponent,
+ $('.explorer-charts')
);
mlExplorerDashboardService.anomalyDataChange.watch(anomalyDataChangeListener);
@@ -52,7 +52,6 @@ module.directive('mlExplorerChartsContainer', function (
function updateComponent(data) {
const props = {
- exploreSeries,
seriesToPlot: data.seriesToPlot,
layoutCellsPerChart: data.layoutCellsPerChart,
// convert truthy/falsy value to Boolean
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 fa9d764496b75..b114cd93fffb3 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
@@ -14,7 +14,6 @@
*/
import _ from 'lodash';
-import $ from 'jquery';
import { buildConfig } from './explorer_chart_config_builder';
import { chartLimits } from '../../util/chart_utils';
@@ -25,10 +24,9 @@ import { mlJobService } from '../../services/job_service';
export function explorerChartsContainerServiceFactory(
mlSelectSeverityService,
- callback
+ callback,
+ $chartContainer
) {
- 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;
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
new file mode 100644
index 0000000000000..6978494e151d2
--- /dev/null
+++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.test.js
@@ -0,0 +1,127 @@
+/*
+ * 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 mockAnomalyChartRecords from './__mocks__/mock_anomaly_chart_records.json';
+import mockDetectorsByJob from './__mocks__/mock_detectors_by_job.json';
+import mockJobConfig from './__mocks__/mock_job_config.json';
+import mockSeriesPromisesResponse from './__mocks__/mock_series_promises_response.json';
+
+jest.mock('../../services/job_service', () => ({
+ mlJobService: {
+ getJob() { return mockJobConfig; },
+ detectorsByJob: mockDetectorsByJob
+ }
+}));
+
+jest.mock('../../services/results_service', () => ({
+ mlResultsService: {
+ getMetricData() {
+ return Promise.resolve(mockSeriesPromisesResponse[0][0]);
+ },
+ getRecordsForCriteria() {
+ return Promise.resolve(mockSeriesPromisesResponse[0][1]);
+ },
+ getScheduledEventsByBucket() {
+ return Promise.resolve(mockSeriesPromisesResponse[0][2]);
+ }
+ }
+}));
+
+jest.mock('../../util/string_utils', () => ({
+ mlEscape(d) { return d; }
+}));
+
+const mockMlSelectSeverityService = {
+ state: {
+ get() { return { display: 'warning', val: 0 }; }
+ }
+};
+
+const mockChartContainer = {
+ width() { return 1140; }
+};
+
+function mockGetDefaultData() {
+ return {
+ seriesToPlot: [],
+ layoutCellsPerChart: 12,
+ tooManyBuckets: false,
+ timeFieldName: 'timestamp'
+ };
+}
+
+import { explorerChartsContainerServiceFactory } from './explorer_charts_container_service';
+
+describe('explorerChartsContainerService', () => {
+ test('Initialize factory', (done) => {
+ explorerChartsContainerServiceFactory(
+ mockMlSelectSeverityService,
+ callback
+ );
+
+ function callback(data) {
+ expect(data).toEqual(mockGetDefaultData());
+ done();
+ }
+ });
+
+ test('call anomalyChangeListener with empty series config', (done) => {
+ // callback will be called multiple times.
+ // the callbackData array contains the expected data values for each consecutive call.
+ const callbackData = [];
+ callbackData.push(mockGetDefaultData());
+ callbackData.push({
+ ...mockGetDefaultData(),
+ layoutCellsPerChart: 6
+ });
+
+ const anomalyDataChangeListener = explorerChartsContainerServiceFactory(
+ mockMlSelectSeverityService,
+ callback,
+ mockChartContainer
+ );
+
+ anomalyDataChangeListener(
+ [],
+ 1486656000000,
+ 1486670399999
+ );
+
+ function callback(data) {
+ if (callbackData.length > 0) {
+ expect(data).toEqual(callbackData.shift());
+ }
+ if (callbackData.length === 0) {
+ done();
+ }
+ }
+ });
+
+ test('call anomalyChangeListener with actual series config', (done) => {
+ let testCount = 0;
+ const expectedTestCount = 3;
+
+ const anomalyDataChangeListener = explorerChartsContainerServiceFactory(
+ mockMlSelectSeverityService,
+ callback,
+ mockChartContainer
+ );
+
+ anomalyDataChangeListener(
+ mockAnomalyChartRecords,
+ 1486656000000,
+ 1486670399999
+ );
+
+ function callback(data) {
+ testCount++;
+ expect(data).toMatchSnapshot();
+ if (testCount === expectedTestCount) {
+ done();
+ }
+ }
+ });
+});
diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_service.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_service.js
index 33149d05d33dc..8386faa1f67fd 100644
--- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_service.js
+++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_service.js
@@ -8,7 +8,7 @@
import chrome from 'ui/chrome';
import _ from 'lodash';
-import { http } from 'plugins/ml/services/http_service';
+import { http } from '../../../../../services/http_service';
import emailBody from './email.html';
import emailInfluencersBody from './email-influencers.html';
@@ -153,4 +153,3 @@ class CreateWatchService {
}
export const mlCreateWatchService = new CreateWatchService();
-
diff --git a/x-pack/plugins/ml/public/services/ml_api_service/filters.js b/x-pack/plugins/ml/public/services/ml_api_service/filters.js
index f603b34916306..b4b66e0145335 100644
--- a/x-pack/plugins/ml/public/services/ml_api_service/filters.js
+++ b/x-pack/plugins/ml/public/services/ml_api_service/filters.js
@@ -9,7 +9,7 @@
import chrome from 'ui/chrome';
-import { http } from 'plugins/ml/services/http_service';
+import { http } from '../../services/http_service';
const basePath = chrome.addBasePath('/api/ml');
diff --git a/x-pack/plugins/ml/public/services/ml_api_service/index.js b/x-pack/plugins/ml/public/services/ml_api_service/index.js
index 34bfb99564d06..aceb9d5b7f366 100644
--- a/x-pack/plugins/ml/public/services/ml_api_service/index.js
+++ b/x-pack/plugins/ml/public/services/ml_api_service/index.js
@@ -9,7 +9,7 @@
import { pick } from 'lodash';
import chrome from 'ui/chrome';
-import { http } from 'plugins/ml/services/http_service';
+import { http } from '../../services/http_service';
import { filters } from './filters';
import { results } from './results';
diff --git a/x-pack/plugins/ml/public/services/ml_api_service/jobs.js b/x-pack/plugins/ml/public/services/ml_api_service/jobs.js
index 003de9ed327d7..f9b482ea0dc8b 100644
--- a/x-pack/plugins/ml/public/services/ml_api_service/jobs.js
+++ b/x-pack/plugins/ml/public/services/ml_api_service/jobs.js
@@ -6,7 +6,7 @@
import chrome from 'ui/chrome';
-import { http } from 'plugins/ml/services/http_service';
+import { http } from '../../services/http_service';
const basePath = chrome.addBasePath('/api/ml');
diff --git a/x-pack/plugins/ml/public/services/ml_api_service/results.js b/x-pack/plugins/ml/public/services/ml_api_service/results.js
index 5f7b76ce26865..b4254fa7b2519 100644
--- a/x-pack/plugins/ml/public/services/ml_api_service/results.js
+++ b/x-pack/plugins/ml/public/services/ml_api_service/results.js
@@ -8,7 +8,7 @@
import chrome from 'ui/chrome';
-import { http } from 'plugins/ml/services/http_service';
+import { http } from '../../services/http_service';
const basePath = chrome.addBasePath('/api/ml');
diff --git a/x-pack/plugins/ml/public/services/results_service.js b/x-pack/plugins/ml/public/services/results_service.js
index fb13ed381ab30..6cd9e8ef6e3d4 100644
--- a/x-pack/plugins/ml/public/services/results_service.js
+++ b/x-pack/plugins/ml/public/services/results_service.js
@@ -10,11 +10,11 @@
// Ml Results dashboards.
import _ from 'lodash';
-import { ML_MEDIAN_PERCENTS } from 'plugins/ml/../common/util/job_utils';
-import { escapeForElasticsearchQuery } from 'plugins/ml/util/string_utils';
-import { ML_RESULTS_INDEX_PATTERN } from 'plugins/ml/../common/constants/index_patterns';
+import { ML_MEDIAN_PERCENTS } from '../../common/util/job_utils';
+import { escapeForElasticsearchQuery } from '../util/string_utils';
+import { ML_RESULTS_INDEX_PATTERN } from '../../common/constants/index_patterns';
-import { ml } from 'plugins/ml/services/ml_api_service';
+import { ml } from '../services/ml_api_service';
// Obtains the maximum bucket anomaly scores by job ID and time.
diff --git a/x-pack/plugins/ml/public/util/chart_config_builder.js b/x-pack/plugins/ml/public/util/chart_config_builder.js
index e4cd9037afbbe..1529ea868d4e6 100644
--- a/x-pack/plugins/ml/public/util/chart_config_builder.js
+++ b/x-pack/plugins/ml/public/util/chart_config_builder.js
@@ -13,7 +13,7 @@
import _ from 'lodash';
-import { mlFunctionToESAggregation } from 'plugins/ml/../common/util/job_utils';
+import { mlFunctionToESAggregation } from '../../common/util/job_utils';
// Builds the basic configuration to plot a chart of the source data
// analyzed by the the detector at the given index from the specified ML job.
diff --git a/x-pack/plugins/ml/public/util/chart_utils.js b/x-pack/plugins/ml/public/util/chart_utils.js
index f6227dad1e3ce..260635ef14163 100644
--- a/x-pack/plugins/ml/public/util/chart_utils.js
+++ b/x-pack/plugins/ml/public/util/chart_utils.js
@@ -9,6 +9,10 @@
import d3 from 'd3';
import { calculateTextWidth } from '../util/string_utils';
import moment from 'moment';
+import rison from 'rison-node';
+
+import chrome from 'ui/chrome';
+import { timefilter } from 'ui/timefilter';
const MAX_LABEL_WIDTH = 100;
@@ -117,6 +121,63 @@ export function filterAxisLabels(selection, chartWidth) {
});
}
+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.
+ 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 = {};
+ series.entityFields.forEach((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: '*'
+ }
+ }
+ });
+
+ return `${chrome.getBasePath()}/app/ml#/timeseriesexplorer?_g=${_g}&_a=${encodeURIComponent(_a)}`;
+}
+
export function numTicks(axisWidth) {
return axisWidth / MAX_LABEL_WIDTH;
}
diff --git a/x-pack/plugins/ml/public/util/chart_utils.test.js b/x-pack/plugins/ml/public/util/chart_utils.test.js
new file mode 100644
index 0000000000000..5bec58b9be0c9
--- /dev/null
+++ b/x-pack/plugins/ml/public/util/chart_utils.test.js
@@ -0,0 +1,63 @@
+/*
+ * 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 seriesConfig from '../explorer/explorer_charts/__mocks__/mock_series_config_filebeat';
+
+jest.mock('ui/chrome',
+ () => ({
+ getBasePath: () => {
+ return '';
+ },
+ getUiSettingsClient: () => {
+ return {
+ get: (key) => {
+ switch (key) {
+ case 'timepicker:timeDefaults':
+ return { from: 'now-15m', to: 'now', mode: 'quick' };
+ case 'timepicker:refreshIntervalDefaults':
+ return { pause: false, value: 0 };
+ default:
+ throw new Error(`Unexpected config key: ${key}`);
+ }
+ }
+ };
+ },
+ }), { virtual: true });
+
+jest.mock('ui/timefilter/lib/parse_querystring',
+ () => ({
+ parseQueryString: () => {
+ return {
+ // Can not access local variable from within a mock
+ forceNow: global.nowTime
+ };
+ },
+ }), { virtual: true });
+
+import moment from 'moment';
+import { timefilter } from 'ui/timefilter';
+
+import { getExploreSeriesLink } from './chart_utils';
+
+timefilter.enableTimeRangeSelector();
+timefilter.enableAutoRefreshSelector();
+timefilter.setTime({
+ from: moment(seriesConfig.selectedEarliest).toISOString(),
+ to: moment(seriesConfig.selectedLatest).toISOString()
+});
+
+describe('getExploreSeriesLink', () => {
+ test('get timeseriesexplorer link', () => {
+ const link = getExploreSeriesLink(seriesConfig);
+ const expectedLink = `/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(population-03)),` +
+ `refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2017-02-23T00:00:00.000Z',mode:absolute,` +
+ `to:'2017-02-23T23:59:59.999Z'))&_a=(filters%3A!()%2CmlTimeSeriesExplorer%3A(detectorIndex%3A0%2Centities%3A` +
+ `(nginx.access.remote_ip%3A'72.57.0.53')%2Czoom%3A(from%3A'2017-02-19T20%3A00%3A00.000Z'%2Cto%3A'2017-02-27T04%3A00%3A00.000Z'))` +
+ `%2Cquery%3A(query_string%3A(analyze_wildcard%3A!t%2Cquery%3A'*')))`;
+
+ expect(link).toBe(expectedLink);
+ });
+});