diff --git a/src/core_plugins/kibana/public/visualize/editor/agg_params.html b/src/core_plugins/kibana/public/visualize/editor/agg_params.html
index 5f822ada4f0e4..270706b91d665 100644
--- a/src/core_plugins/kibana/public/visualize/editor/agg_params.html
+++ b/src/core_plugins/kibana/public/visualize/editor/agg_params.html
@@ -10,6 +10,12 @@
style="display: none;">
+
{{ agg.schema.deprecateMessage }}
diff --git a/src/core_plugins/metric_vis/public/metric_vis.js b/src/core_plugins/metric_vis/public/metric_vis.js
index 6dbcd852d3f51..7b71836dfbb53 100644
--- a/src/core_plugins/metric_vis/public/metric_vis.js
+++ b/src/core_plugins/metric_vis/public/metric_vis.js
@@ -39,6 +39,7 @@ function MetricVisProvider(Private) {
name: 'metric',
title: 'Metric',
min: 1,
+ aggFilter: ['!derivative'],
defaults: [
{ type: 'count', schema: 'metric' }
]
diff --git a/src/core_plugins/tagcloud/public/tag_cloud_vis.js b/src/core_plugins/tagcloud/public/tag_cloud_vis.js
index 0aec53c4ca83b..a36ea8645654e 100644
--- a/src/core_plugins/tagcloud/public/tag_cloud_vis.js
+++ b/src/core_plugins/tagcloud/public/tag_cloud_vis.js
@@ -37,7 +37,7 @@ visTypes.register(function TagCloudProvider(Private) {
title: 'Tag Size',
min: 1,
max: 1,
- aggFilter: ['!std_dev', '!percentiles', '!percentile_ranks'],
+ aggFilter: ['!std_dev', '!percentiles', '!percentile_ranks', '!derivative'],
defaults: [
{ schema: 'metric', type: 'count' }
]
diff --git a/src/ui/public/agg_types/__tests__/metrics/derivative.js b/src/ui/public/agg_types/__tests__/metrics/derivative.js
new file mode 100644
index 0000000000000..dae886a6a5c9f
--- /dev/null
+++ b/src/ui/public/agg_types/__tests__/metrics/derivative.js
@@ -0,0 +1,128 @@
+import _ from 'lodash';
+import expect from 'expect.js';
+import ngMock from 'ng_mock';
+import DerivativeProvider from 'ui/agg_types/metrics/derivative';
+import VisProvider from 'ui/vis';
+import StubbedIndexPattern from 'fixtures/stubbed_logstash_index_pattern';
+
+describe('Derivative metric', function () {
+ let aggDsl;
+ let derivativeMetric;
+ let aggConfig;
+
+ function init(settings) {
+ ngMock.module('kibana');
+ ngMock.inject(function (Private) {
+ const Vis = Private(VisProvider);
+ const indexPattern = Private(StubbedIndexPattern);
+ derivativeMetric = Private(DerivativeProvider);
+
+ const params = settings || {
+ metricAgg: '1',
+ customMetric: null
+ };
+
+ const vis = new Vis(indexPattern, {
+ title: 'New Visualization',
+ type: 'metric',
+ params: {
+ fontSize: 60,
+ handleNoResults: true
+ },
+ aggs: [
+ {
+ id: '1',
+ type: 'count',
+ schema: 'metric'
+ },
+ {
+ id: '2',
+ type: 'derivative',
+ schema: 'metric',
+ params
+ }
+ ],
+ listeners: {}
+ });
+
+ // Grab the aggConfig off the vis (we don't actually use the vis for anything else)
+ aggConfig = vis.aggs[1];
+ aggDsl = aggConfig.toDsl();
+ });
+ }
+
+ it('should return a label prefixed with Derivative of', function () {
+ init();
+ expect(derivativeMetric.makeLabel(aggConfig)).to.eql('Derivative of Count');
+ });
+
+ it('should return a label Derivative of max bytes', function () {
+ init({
+ metricAgg: 'custom',
+ customMetric: {
+ id:'1-orderAgg',
+ type: 'max',
+ params: { field: 'bytes' },
+ schema: 'orderAgg'
+ }
+ });
+ expect(derivativeMetric.makeLabel(aggConfig)).to.eql('Derivative of Max bytes');
+ });
+
+ it('should return a label prefixed with number of derivative', function () {
+ init({
+ metricAgg: 'custom',
+ customMetric: {
+ id:'2-orderAgg',
+ type: 'derivative',
+ params: {
+ buckets_path: 'custom',
+ customMetric: {
+ id:'2-orderAgg-orderAgg',
+ type: 'count',
+ schema: 'orderAgg'
+ }
+ },
+ schema: 'orderAgg'
+ }
+ });
+ expect(derivativeMetric.makeLabel(aggConfig)).to.eql('2. derivative of Count');
+ });
+
+ it('should set parent aggs', function () {
+ init({
+ metricAgg: 'custom',
+ customMetric: {
+ id:'2-metric',
+ type: 'max',
+ params: { field: 'bytes' },
+ schema: 'orderAgg'
+ }
+ });
+ expect(aggDsl.derivative.buckets_path).to.be('2-metric');
+ expect(aggDsl.parentAggs['2-metric'].max.field).to.be('bytes');
+ });
+
+ it('should set nested parent aggs', function () {
+ init({
+ metricAgg: 'custom',
+ customMetric: {
+ id:'2-metric',
+ type: 'derivative',
+ params: {
+ buckets_path: 'custom',
+ customMetric: {
+ id:'2-metric-metric',
+ type: 'max',
+ params: { field: 'bytes' },
+ schema: 'orderAgg'
+ }
+ },
+ schema: 'orderAgg'
+ }
+ });
+ expect(aggDsl.derivative.buckets_path).to.be('2-metric');
+ expect(aggDsl.parentAggs['2-metric'].derivative.buckets_path).to.be('2-metric-metric');
+ });
+
+});
diff --git a/src/ui/public/agg_types/__tests__/metrics/lib/make_nested_label.js b/src/ui/public/agg_types/__tests__/metrics/lib/make_nested_label.js
new file mode 100644
index 0000000000000..f3b97d9352346
--- /dev/null
+++ b/src/ui/public/agg_types/__tests__/metrics/lib/make_nested_label.js
@@ -0,0 +1,34 @@
+import expect from 'expect.js';
+import { makeNestedLabel } from 'ui/agg_types/metrics/lib/make_nested_label';
+
+describe('metric agg make_nested_label', function () {
+
+ function generateAggConfig(metricLabel) {
+ return {
+ params: {
+ customMetric: {
+ makeLabel: () => { return metricLabel; }
+ }
+ }
+ };
+ }
+
+ it('should return a metric label with prefix', function () {
+ const aggConfig = generateAggConfig('Count');
+ const label = makeNestedLabel(aggConfig, 'derivative');
+ expect(label).to.eql('Derivative of Count');
+ });
+
+ it('should return a numbered prefix', function () {
+ const aggConfig = generateAggConfig('Derivative of Count');
+ const label = makeNestedLabel(aggConfig, 'derivative');
+ expect(label).to.eql('2. derivative of Count');
+ });
+
+ it('should return a prefix with correct order', function () {
+ const aggConfig = generateAggConfig('3. derivative of Count');
+ const label = makeNestedLabel(aggConfig, 'derivative');
+ expect(label).to.eql('4. derivative of Count');
+ });
+
+});
diff --git a/src/ui/public/agg_types/buckets/terms.js b/src/ui/public/agg_types/buckets/terms.js
index 46f9900bd9384..abb25ccb9c026 100644
--- a/src/ui/public/agg_types/buckets/terms.js
+++ b/src/ui/public/agg_types/buckets/terms.js
@@ -16,7 +16,7 @@ export default function TermsAggDefinition(Private) {
const createFilter = Private(AggTypesBucketsCreateFilterTermsProvider);
const routeBasedNotifier = Private(routeBasedNotifierProvider);
- const aggFilter = ['!top_hits', '!percentiles', '!median', '!std_dev'];
+ const aggFilter = ['!top_hits', '!percentiles', '!median', '!std_dev', '!derivative'];
const orderAggSchema = (new Schemas([
{
group: 'none',
diff --git a/src/ui/public/agg_types/controls/sub_agg.html b/src/ui/public/agg_types/controls/sub_agg.html
new file mode 100644
index 0000000000000..100cc1ce69153
--- /dev/null
+++ b/src/ui/public/agg_types/controls/sub_agg.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ui/public/agg_types/index.js b/src/ui/public/agg_types/index.js
index 4409b6a1fbda3..25140ba77daf0 100644
--- a/src/ui/public/agg_types/index.js
+++ b/src/ui/public/agg_types/index.js
@@ -11,6 +11,7 @@ import AggTypesMetricsStdDeviationProvider from 'ui/agg_types/metrics/std_deviat
import AggTypesMetricsCardinalityProvider from 'ui/agg_types/metrics/cardinality';
import AggTypesMetricsPercentilesProvider from 'ui/agg_types/metrics/percentiles';
import AggTypesMetricsPercentileRanksProvider from 'ui/agg_types/metrics/percentile_ranks';
+import AggTypesMetricsDerivativeProvider from 'ui/agg_types/metrics/derivative';
import AggTypesBucketsDateHistogramProvider from 'ui/agg_types/buckets/date_histogram';
import AggTypesBucketsHistogramProvider from 'ui/agg_types/buckets/histogram';
import AggTypesBucketsRangeProvider from 'ui/agg_types/buckets/range';
@@ -34,7 +35,8 @@ export default function AggTypeService(Private) {
Private(AggTypesMetricsCardinalityProvider),
Private(AggTypesMetricsPercentilesProvider),
Private(AggTypesMetricsPercentileRanksProvider),
- Private(AggTypesMetricsTopHitProvider)
+ Private(AggTypesMetricsTopHitProvider),
+ Private(AggTypesMetricsDerivativeProvider),
],
buckets: [
Private(AggTypesBucketsDateHistogramProvider),
diff --git a/src/ui/public/agg_types/metrics/derivative.js b/src/ui/public/agg_types/metrics/derivative.js
new file mode 100644
index 0000000000000..0051908ff29b0
--- /dev/null
+++ b/src/ui/public/agg_types/metrics/derivative.js
@@ -0,0 +1,62 @@
+import AggTypesMetricsMetricAggTypeProvider from 'ui/agg_types/metrics/metric_agg_type';
+import metricAggTemplate from 'ui/agg_types/controls/sub_agg.html';
+import _ from 'lodash';
+import VisAggConfigProvider from 'ui/vis/agg_config';
+import VisSchemasProvider from 'ui/vis/schemas';
+import { makeNestedLabel } from './lib/make_nested_label';
+import { parentPipelineAggController } from './lib/parent_pipeline_agg_controller';
+import { parentPipelineAggWritter } from './lib/parent_pipeline_agg_writter';
+
+export default function AggTypeMetricDerivativeProvider(Private) {
+ const MetricAggType = Private(AggTypesMetricsMetricAggTypeProvider);
+ const AggConfig = Private(VisAggConfigProvider);
+ const Schemas = Private(VisSchemasProvider);
+
+ const aggFilter = ['!top_hits', '!percentiles', '!percentile_ranks', '!median', '!std_dev'];
+ const orderAggSchema = (new Schemas([
+ {
+ group: 'none',
+ name: 'orderAgg',
+ title: 'Order Agg',
+ aggFilter: aggFilter
+ }
+ ])).all[0];
+
+ return new MetricAggType({
+ name: 'derivative',
+ title: 'Derivative',
+ makeLabel: agg => makeNestedLabel(agg, 'derivative'),
+ params: [
+ {
+ name: 'customMetric',
+ type: AggConfig,
+ default: null,
+ serialize: function (customMetric) {
+ return customMetric.toJSON();
+ },
+ deserialize: function (state, agg) {
+ return this.makeAgg(agg, state);
+ },
+ makeAgg: function (termsAgg, state) {
+ state = state || { type: 'count' };
+ state.schema = orderAggSchema;
+ const metricAgg = new AggConfig(termsAgg.vis, state);
+ metricAgg.id = termsAgg.id + '-metric';
+ return metricAgg;
+ },
+ write: _.noop
+ },
+ {
+ name: 'buckets_path',
+ write: _.noop
+ },
+ {
+ name: 'metricAgg',
+ editor: metricAggTemplate,
+ default: 'custom',
+ controller: parentPipelineAggController,
+ write: parentPipelineAggWritter
+ }
+ ]
+ });
+}
diff --git a/src/ui/public/agg_types/metrics/lib/make_nested_label.js b/src/ui/public/agg_types/metrics/lib/make_nested_label.js
new file mode 100644
index 0000000000000..85dcebf94dcfa
--- /dev/null
+++ b/src/ui/public/agg_types/metrics/lib/make_nested_label.js
@@ -0,0 +1,23 @@
+import _ from 'lodash';
+
+const makeNestedLabel = function (aggConfig, label) {
+ const uppercaseLabel = _.startCase(label);
+ if (aggConfig.params.customMetric) {
+ let metricLabel = aggConfig.params.customMetric.makeLabel();
+ if (metricLabel.includes(`${uppercaseLabel} of `)) {
+ metricLabel = metricLabel.substring(`${uppercaseLabel} of `.length);
+ metricLabel = `2. ${label} of ${metricLabel}`;
+ }
+ else if (metricLabel.includes(`${label} of `)) {
+ metricLabel = (parseInt(metricLabel.substring(0, 1)) + 1) + metricLabel.substring(1);
+ }
+ else {
+ metricLabel = `${uppercaseLabel} of ${metricLabel}`;
+ }
+ return metricLabel;
+ }
+ const metric = aggConfig.vis.aggs.find(agg => agg.id === aggConfig.params.metricAgg);
+ return `${uppercaseLabel} of ${metric.makeLabel()}`;
+};
+
+export { makeNestedLabel };
diff --git a/src/ui/public/agg_types/metrics/lib/parent_pipeline_agg_controller.js b/src/ui/public/agg_types/metrics/lib/parent_pipeline_agg_controller.js
new file mode 100644
index 0000000000000..8a2ef041a2902
--- /dev/null
+++ b/src/ui/public/agg_types/metrics/lib/parent_pipeline_agg_controller.js
@@ -0,0 +1,66 @@
+import _ from 'lodash';
+import safeMakeLabel from './safe_make_label';
+
+const parentPipelineAggController = function ($scope) {
+
+ $scope.safeMakeLabel = safeMakeLabel;
+
+ $scope.$watch('responseValueAggs', updateOrderAgg);
+ $scope.$watch('agg.params.metricAgg', updateOrderAgg);
+
+ $scope.$on('$destroy', function () {
+ if ($scope.aggForm && $scope.aggForm.agg) {
+ $scope.aggForm.agg.$setValidity('bucket', true);
+ }
+ });
+
+ $scope.isDisabledAgg = function (agg) {
+ const invalidAggs = ['top_hits', 'percentiles', 'percentile_ranks', 'median', 'std_dev'];
+ return Boolean(invalidAggs.find(invalidAgg => invalidAgg === agg.type.name));
+ };
+
+ function checkBuckets() {
+ const lastBucket = _.findLast($scope.vis.aggs, agg => agg.schema.group === 'buckets');
+ const bucketHasType = lastBucket && lastBucket.type;
+ const bucketIsHistogram = bucketHasType && ['date_histogram', 'histogram'].includes(lastBucket.type.name);
+ const canUseAggregation = lastBucket && bucketIsHistogram;
+
+ // remove errors on all buckets
+ _.each($scope.vis.aggs, agg => { if (agg.error) delete agg.error; });
+
+ if ($scope.aggForm.agg) {
+ $scope.aggForm.agg.$setValidity('bucket', canUseAggregation);
+ }
+ if (canUseAggregation) {
+ lastBucket.params.min_doc_count = (lastBucket.type.name === 'histogram') ? 1 : 0;
+ } else {
+ if (lastBucket) {
+ const type = $scope.agg.type.title;
+ lastBucket.error = `Last bucket aggregation must be "Date Histogram" or
+ "Histogram" when using "${type}" metric aggregation!`;
+ }
+ }
+ }
+
+ function updateOrderAgg() {
+ const agg = $scope.agg;
+ const params = agg.params;
+ const metricAgg = params.metricAgg;
+ const paramDef = agg.type.params.byName.customMetric;
+
+ checkBuckets();
+
+ // we aren't creating a custom aggConfig
+ if (metricAgg !== 'custom') {
+ if (!$scope.vis.aggs.find(agg => agg.id === metricAgg)) {
+ params.metricAgg = null;
+ }
+ params.customMetric = null;
+ return;
+ }
+
+ params.customMetric = params.customMetric || paramDef.makeAgg(agg);
+ }
+};
+
+export { parentPipelineAggController };
diff --git a/src/ui/public/agg_types/metrics/lib/parent_pipeline_agg_writter.js b/src/ui/public/agg_types/metrics/lib/parent_pipeline_agg_writter.js
new file mode 100644
index 0000000000000..b5e49b3373720
--- /dev/null
+++ b/src/ui/public/agg_types/metrics/lib/parent_pipeline_agg_writter.js
@@ -0,0 +1,17 @@
+const parentPipelineAggWritter = function (agg, output) {
+ const vis = agg.vis;
+ const selectedMetric = agg.params.customMetric || vis.aggs.getResponseAggById(agg.params.metricAgg);
+
+ if (agg.params.customMetric && agg.params.customMetric.type.name !== 'count') {
+ output.parentAggs = (output.parentAggs || []).concat(selectedMetric);
+ }
+
+ output.params = {};
+ if (selectedMetric.type.name === 'count') {
+ output.params.buckets_path = '_count';
+ } else {
+ output.params.buckets_path = selectedMetric.id;
+ }
+};
+
+export { parentPipelineAggWritter };
diff --git a/src/ui/public/agg_types/metrics/lib/safe_make_label.js b/src/ui/public/agg_types/metrics/lib/safe_make_label.js
new file mode 100644
index 0000000000000..8534de06916a7
--- /dev/null
+++ b/src/ui/public/agg_types/metrics/lib/safe_make_label.js
@@ -0,0 +1,9 @@
+const safeMakeLabel = function (agg) {
+ try {
+ return agg.makeLabel();
+ } catch (e) {
+ return '- agg not valid -';
+ }
+};
+
+export default safeMakeLabel;
diff --git a/src/ui/public/vis/agg_config.js b/src/ui/public/vis/agg_config.js
index d2852f2b2af88..2be2834bd078e 100644
--- a/src/ui/public/vis/agg_config.js
+++ b/src/ui/public/vis/agg_config.js
@@ -217,6 +217,13 @@ export default function AggConfigFactory(Private, fieldTypeFilter) {
});
}
+ if (output.parentAggs) {
+ const subDslLvl = configDsl.parentAggs || (configDsl.parentAggs = {});
+ output.parentAggs.forEach(function nestAdhocSubAggs(subAggConfig) {
+ subDslLvl[subAggConfig.id] = subAggConfig.toDsl();
+ });
+ }
+
return configDsl;
};
diff --git a/src/ui/public/vis/agg_configs.js b/src/ui/public/vis/agg_configs.js
index 08cbc266a0efc..3099a243d0feb 100644
--- a/src/ui/public/vis/agg_configs.js
+++ b/src/ui/public/vis/agg_configs.js
@@ -73,6 +73,22 @@ export default function AggConfigsFactory(Private) {
return true;
};
+ function removeParentAggs(obj) {
+ for(const prop in obj) {
+ if (prop === 'parentAggs') delete obj[prop];
+ else if (typeof obj[prop] === 'object') removeParentAggs(obj[prop]);
+ }
+ }
+
+ function parseParentAggs(dslLvlCursor, dsl) {
+ if (dsl.parentAggs) {
+ _.each(dsl.parentAggs, (agg, key) => {
+ dslLvlCursor[key] = agg;
+ parseParentAggs(dslLvlCursor, agg);
+ });
+ }
+ }
+
AggConfigs.prototype.toDsl = function () {
const dslTopLvl = {};
let dslLvlCursor;
@@ -114,6 +130,8 @@ export default function AggConfigsFactory(Private) {
const dsl = dslLvlCursor[config.id] = config.toDsl();
let subAggs;
+ parseParentAggs(dslLvlCursor, dsl);
+
if (config.schema.group === 'buckets' && i < list.length - 1) {
// buckets that are not the last item in the list accept sub-aggs
subAggs = dsl.aggs || (dsl.aggs = {});
@@ -126,6 +144,8 @@ export default function AggConfigsFactory(Private) {
}
});
+ removeParentAggs(dslTopLvl);
+
return dslTopLvl;
};
diff --git a/src/ui/public/vislib/visualizations/point_series/area_chart.js b/src/ui/public/vislib/visualizations/point_series/area_chart.js
index 9fffb6b711373..fc073de6c5ddf 100644
--- a/src/ui/public/vislib/visualizations/point_series/area_chart.js
+++ b/src/ui/public/vislib/visualizations/point_series/area_chart.js
@@ -96,7 +96,8 @@ export default function AreaChartFactory(Private) {
function y1(d) {
const y0 = d.y0 || 0;
- return yScale(y0 + d.y);
+ const y = d.y || 0;
+ return yScale(y0 + y);
}
function y0(d) {
@@ -125,7 +126,9 @@ export default function AreaChartFactory(Private) {
return !_.isNull(d.y);
})
.interpolate(interpolate);
- return area(data.values);
+ return area(data.values.filter(function (d) {
+ return !_.isNull(d.y);
+ }));
});
return path;
@@ -185,10 +188,11 @@ export default function AreaChartFactory(Private) {
}
function cy(d) {
+ const y = d.y || 0;
if (isOverlapping) {
- return yScale(d.y);
+ return yScale(y);
}
- return yScale(d.y0 + d.y);
+ return yScale(d.y0 + y);
}
// update
diff --git a/src/ui/public/vislib/visualizations/point_series/column_chart.js b/src/ui/public/vislib/visualizations/point_series/column_chart.js
index ba35923d9049c..74b74212e554d 100644
--- a/src/ui/public/vislib/visualizations/point_series/column_chart.js
+++ b/src/ui/public/vislib/visualizations/point_series/column_chart.js
@@ -1,5 +1,4 @@
import _ from 'lodash';
-import moment from 'moment';
import errors from 'ui/errors';
import VislibVisualizationsPointSeriesProvider from './_point_series';
export default function ColumnChartFactory(Private) {
@@ -39,7 +38,9 @@ export default function ColumnChartFactory(Private) {
.attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')');
const bars = layer.selectAll('rect')
- .data(data.values);
+ .data(data.values.filter(function (d) {
+ return !_.isNull(d.y);
+ }));
bars
.exit()
diff --git a/src/ui/public/vislib/visualizations/point_series/line_chart.js b/src/ui/public/vislib/visualizations/point_series/line_chart.js
index 37ddad59f4900..9e629c429c126 100644
--- a/src/ui/public/vislib/visualizations/point_series/line_chart.js
+++ b/src/ui/public/vislib/visualizations/point_series/line_chart.js
@@ -69,7 +69,8 @@ export default function LineChartFactory(Private) {
}
function cy(d) {
- return yScale(d.y);
+ const y = d.y || 0;
+ return yScale(y);
}
function cColor(d) {
@@ -156,7 +157,8 @@ export default function LineChartFactory(Private) {
}
function cy(d) {
- return yScale(d.y);
+ const y = d.y || 0;
+ return yScale(y);
}
line.append('path')
@@ -169,7 +171,9 @@ export default function LineChartFactory(Private) {
.interpolate(interpolate)
.x(isHorizontal ? cx : cy)
.y(isHorizontal ? cy : cx);
- return d3Line(data.values);
+ return d3Line(data.values.filter(function (d) {
+ return !_.isNull(d.y);
+ }));
})
.attr('fill', 'none')
.attr('stroke', () => {