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.error}} +

+
+

{{ 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', () => {