diff --git a/src/fixtures/vislib/mock_data/date_histogram/_dual_axis_series.js b/src/fixtures/vislib/mock_data/date_histogram/_dual_axis_series.js new file mode 100644 index 0000000000000..a3f0354d64a2f --- /dev/null +++ b/src/fixtures/vislib/mock_data/date_histogram/_dual_axis_series.js @@ -0,0 +1,282 @@ +define(function (require) { + let moment = require('moment'); + + return { + 'label': '', + 'xAxisLabel': '@timestamp per 30 sec', + 'ordered': { + 'date': true, + 'min': 1411761457636, + 'max': 1411762357636, + 'interval': 30000 + }, + 'yAxisLabel': 'Count of documents', + 'series': [ + { + 'onSecondaryYAxis': true, + 'values': [ + { + 'x': 1411761450000, + 'y': 4100 + }, + { + 'x': 1411761480000, + 'y': 1800 + }, + { + 'x': 1411761510000, + 'y': 2200 + }, + { + 'x': 1411761540000, + 'y': 1700 + }, + { + 'x': 1411761570000, + 'y': 1700 + }, + { + 'x': 1411761600000, + 'y': 2100 + }, + { + 'x': 1411761630000, + 'y': 1600 + }, + { + 'x': 1411761660000, + 'y': 1700 + }, + { + 'x': 1411761690000, + 'y': 1500 + }, + { + 'x': 1411761720000, + 'y': 1900 + }, + { + 'x': 1411761750000, + 'y': 1100 + }, + { + 'x': 1411761780000, + 'y': 1300 + }, + { + 'x': 1411761810000, + 'y': 2400 + }, + { + 'x': 1411761840000, + 'y': 2000 + }, + { + 'x': 1411761870000, + 'y': 2000 + }, + { + 'x': 1411761900000, + 'y': 2100 + }, + { + 'x': 1411761930000, + 'y': 1700 + }, + { + 'x': 1411761960000, + 'y': 2000 + }, + { + 'x': 1411761990000, + 'y': 1300 + }, + { + 'x': 1411762020000, + 'y': 1400 + }, + { + 'x': 1411762050000, + 'y': 2500 + }, + { + 'x': 1411762080000, + 'y': 1700 + }, + { + 'x': 1411762110000, + 'y': 1400 + }, + { + 'x': 1411762140000, + 'y': 2200 + }, + { + 'x': 1411762170000, + 'y': 1400 + }, + { + 'x': 1411762200000, + 'y': 1900 + }, + { + 'x': 1411762230000, + 'y': 2200 + }, + { + 'x': 1411762260000, + 'y': 1700 + }, + { + 'x': 1411762290000, + 'y': 800 + }, + { + 'x': 1411762320000, + 'y': 1500 + }, + { + 'x': 1411762350000, + 'y': 400 + } + ] + }, + { + 'onSecondaryYAxis': false, + 'values': [ + { + 'x': 1411761450000, + 'y': 41 + }, + { + 'x': 1411761480000, + 'y': 18 + }, + { + 'x': 1411761510000, + 'y': 22 + }, + { + 'x': 1411761540000, + 'y': 17 + }, + { + 'x': 1411761570000, + 'y': 17 + }, + { + 'x': 1411761600000, + 'y': 21 + }, + { + 'x': 1411761630000, + 'y': 16 + }, + { + 'x': 1411761660000, + 'y': 17 + }, + { + 'x': 1411761690000, + 'y': 15 + }, + { + 'x': 1411761720000, + 'y': 19 + }, + { + 'x': 1411761750000, + 'y': 11 + }, + { + 'x': 1411761780000, + 'y': 13 + }, + { + 'x': 1411761810000, + 'y': 24 + }, + { + 'x': 1411761840000, + 'y': 20 + }, + { + 'x': 1411761870000, + 'y': 20 + }, + { + 'x': 1411761900000, + 'y': 21 + }, + { + 'x': 1411761930000, + 'y': 17 + }, + { + 'x': 1411761960000, + 'y': 20 + }, + { + 'x': 1411761990000, + 'y': 13 + }, + { + 'x': 1411762020000, + 'y': 14 + }, + { + 'x': 1411762050000, + 'y': 25 + }, + { + 'x': 1411762080000, + 'y': 17 + }, + { + 'x': 1411762110000, + 'y': 14 + }, + { + 'x': 1411762140000, + 'y': 22 + }, + { + 'x': 1411762170000, + 'y': 14 + }, + { + 'x': 1411762200000, + 'y': 19 + }, + { + 'x': 1411762230000, + 'y': 22 + }, + { + 'x': 1411762260000, + 'y': 17 + }, + { + 'x': 1411762290000, + 'y': 8 + }, + { + 'x': 1411762320000, + 'y': 15 + }, + { + 'x': 1411762350000, + 'y': 4 + } + ] + } + ], + 'hits': 533, + 'xAxisFormatter': function (thing) { + return moment(thing); + }, + 'tooltipFormatter': function (d) { + return d; + } + }; +}); diff --git a/src/fixtures/vislib/mock_data/date_histogram/_dual_axis_series_neg.js b/src/fixtures/vislib/mock_data/date_histogram/_dual_axis_series_neg.js new file mode 100644 index 0000000000000..e7ed8a43c2f5e --- /dev/null +++ b/src/fixtures/vislib/mock_data/date_histogram/_dual_axis_series_neg.js @@ -0,0 +1,282 @@ +define(function (require) { + let moment = require('moment'); + + return { + 'label': '', + 'xAxisLabel': '@timestamp per 30 sec', + 'ordered': { + 'date': true, + 'min': 1411761457636, + 'max': 1411762357636, + 'interval': 30000 + }, + 'yAxisLabel': 'Count of documents', + 'series': [ + { + 'onSecondaryYAxis': true, + 'values': [ + { + 'x': 1411761450000, + 'y': -4100 + }, + { + 'x': 1411761480000, + 'y': -1800 + }, + { + 'x': 1411761510000, + 'y': -2200 + }, + { + 'x': 1411761540000, + 'y': -1700 + }, + { + 'x': 1411761570000, + 'y': -1700 + }, + { + 'x': 1411761600000, + 'y': -2100 + }, + { + 'x': 1411761630000, + 'y': -1600 + }, + { + 'x': 1411761660000, + 'y': -1700 + }, + { + 'x': 1411761690000, + 'y': -1500 + }, + { + 'x': 1411761720000, + 'y': -1900 + }, + { + 'x': 1411761750000, + 'y': -1100 + }, + { + 'x': 1411761780000, + 'y': -1300 + }, + { + 'x': 1411761810000, + 'y': -2400 + }, + { + 'x': 1411761840000, + 'y': -2000 + }, + { + 'x': 1411761870000, + 'y': -2000 + }, + { + 'x': 1411761900000, + 'y': -2100 + }, + { + 'x': 1411761930000, + 'y': -1700 + }, + { + 'x': 1411761960000, + 'y': -2000 + }, + { + 'x': 1411761990000, + 'y': -1300 + }, + { + 'x': 1411762020000, + 'y': -1400 + }, + { + 'x': 1411762050000, + 'y': -2500 + }, + { + 'x': 1411762080000, + 'y': -1700 + }, + { + 'x': 1411762110000, + 'y': -1400 + }, + { + 'x': 1411762140000, + 'y': -2200 + }, + { + 'x': 1411762170000, + 'y': -1400 + }, + { + 'x': 1411762200000, + 'y': -1900 + }, + { + 'x': 1411762230000, + 'y': -2200 + }, + { + 'x': 1411762260000, + 'y': -1700 + }, + { + 'x': 1411762290000, + 'y': -800 + }, + { + 'x': 1411762320000, + 'y': -1500 + }, + { + 'x': 1411762350000, + 'y': -400 + } + ] + }, + { + 'onSecondaryYAxis': false, + 'values': [ + { + 'x': 1411761450000, + 'y': -41 + }, + { + 'x': 1411761480000, + 'y': -18 + }, + { + 'x': 1411761510000, + 'y': -22 + }, + { + 'x': 1411761540000, + 'y': -17 + }, + { + 'x': 1411761570000, + 'y': -17 + }, + { + 'x': 1411761600000, + 'y': -21 + }, + { + 'x': 1411761630000, + 'y': -16 + }, + { + 'x': 1411761660000, + 'y': -17 + }, + { + 'x': 1411761690000, + 'y': -15 + }, + { + 'x': 1411761720000, + 'y': -19 + }, + { + 'x': 1411761750000, + 'y': -11 + }, + { + 'x': 1411761780000, + 'y': -13 + }, + { + 'x': 1411761810000, + 'y': -24 + }, + { + 'x': 1411761840000, + 'y': -20 + }, + { + 'x': 1411761870000, + 'y': -20 + }, + { + 'x': 1411761900000, + 'y': -21 + }, + { + 'x': 1411761930000, + 'y': -17 + }, + { + 'x': 1411761960000, + 'y': -20 + }, + { + 'x': 1411761990000, + 'y': -13 + }, + { + 'x': 1411762020000, + 'y': -14 + }, + { + 'x': 1411762050000, + 'y': -25 + }, + { + 'x': 1411762080000, + 'y': -17 + }, + { + 'x': 1411762110000, + 'y': -14 + }, + { + 'x': 1411762140000, + 'y': -22 + }, + { + 'x': 1411762170000, + 'y': -14 + }, + { + 'x': 1411762200000, + 'y': -19 + }, + { + 'x': 1411762230000, + 'y': -22 + }, + { + 'x': 1411762260000, + 'y': -17 + }, + { + 'x': 1411762290000, + 'y': -8 + }, + { + 'x': 1411762320000, + 'y': -15 + }, + { + 'x': 1411762350000, + 'y': -4 + } + ] + } + ], + 'hits': 533, + 'xAxisFormatter': function (thing) { + return moment(thing); + }, + 'tooltipFormatter': function (d) { + return d; + } + }; +}); diff --git a/src/plugins/kbn_vislib_vis_types/public/controls/point_series_options.html b/src/plugins/kbn_vislib_vis_types/public/controls/point_series_options.html index b5587c530eb4f..65c88370d38a1 100644 --- a/src/plugins/kbn_vislib_vis_types/public/controls/point_series_options.html +++ b/src/plugins/kbn_vislib_vis_types/public/controls/point_series_options.html @@ -35,6 +35,32 @@ ng-model="vis.params.yAxis.min" ng-required="vis.params.setYExtents"> +
+ +
+ Min must not exceed max +
+ +
Min must exceed 0 when a log scale is selected diff --git a/src/plugins/kibana/public/visualize/editor/__tests__/agg.js b/src/plugins/kibana/public/visualize/editor/__tests__/agg.js index df8d592098b7f..434da707e9dd6 100644 --- a/src/plugins/kibana/public/visualize/editor/__tests__/agg.js +++ b/src/plugins/kibana/public/visualize/editor/__tests__/agg.js @@ -43,7 +43,7 @@ describe('Vis-Editor-Agg plugin directive', function () { } beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function ($rootScope, $compile) { + beforeEach(function () { $parentScope.agg = { id: 1, params: {}, @@ -57,7 +57,12 @@ describe('Vis-Editor-Agg plugin directive', function () { id: '2', schema: makeConfig('radius') }]; - + $parentScope.stats = { count: 1 }; + $parentScope.vis = { + type: { name: 'histogram' } + }; + }); + beforeEach(ngMock.inject(function ($rootScope, $compile) { // share the scope _.defaults($parentScope, $rootScope, Object.getPrototypeOf($rootScope)); @@ -76,6 +81,11 @@ describe('Vis-Editor-Agg plugin directive', function () { $scope = $elem.isolateScope(); })); + // cleanup after test case + afterEach(function () { + $parentScope.$destroy(); + }); + it('should only add the close button if there is more than the minimum', function () { expect($parentScope.canRemove($parentScope.agg)).to.be(false); $parentScope.group.push({ @@ -84,4 +94,37 @@ describe('Vis-Editor-Agg plugin directive', function () { }); expect($parentScope.canRemove($parentScope.agg)).to.be(true); }); + + it('can be secondary axis only for line graph with more than 2 y axis', function () { + $parentScope.stats.count = 2; + $parentScope.vis.type.name = 'line'; + + expect($parentScope.canBeSecondaryYAxis()).to.be(true); + }); + + it('can not be secondary axis non metric schema', function () { + $parentScope.agg.schema.name = 'non-metric'; + + expect($parentScope.canBeSecondaryYAxis()).to.be(false); + }); + + it('can not be secondary axis non y-axis title', function () { + $parentScope.agg.schema.title = 'non-y-axis'; + + expect($parentScope.canBeSecondaryYAxis()).to.be(false); + }); + + it('can not be secondary axis for histogram graph with 2 y axis', function () { + $parentScope.stats.count = 2; + $parentScope.vis.type.name = 'histogram'; + + expect($parentScope.canBeSecondaryYAxis()).to.be(false); + }); + + it('can not be secondary axis for line graph with only 1 y axis', function () { + $parentScope.stats.count = 1; + $parentScope.vis.type.name = 'line'; + + expect($parentScope.canBeSecondaryYAxis()).to.be(false); + }); }); diff --git a/src/plugins/kibana/public/visualize/editor/agg.html b/src/plugins/kibana/public/visualize/editor/agg.html index 965c54cc8fcb0..6037c5cc31e88 100644 --- a/src/plugins/kibana/public/visualize/editor/agg.html +++ b/src/plugins/kibana/public/visualize/editor/agg.html @@ -87,6 +87,14 @@ class="vis-editor-agg-editor"> +
+ +
+ = minYAxisCount; + }; + /** * Describe the aggregation, for display in the collapsed agg header * @return {[type]} [description] @@ -59,6 +84,15 @@ uiModules $scope.remove = function (agg) { const aggs = $scope.vis.aggs; + const yAxisCount = $scope.stats.count; + const minYAxisCount = 2; + let schema = $scope.agg.schema; + let isYAxisMetric = schema.name === 'metric' && schema.title === 'Y-Axis'; + let doesNotHaveMinimumYAxisAfterRemoval = isYAxisMetric && $scope.stats.count <= minYAxisCount; + if (doesNotHaveMinimumYAxisAfterRemoval || agg.onSecondaryYAxis) { + $scope.vis.params.hasSecondaryYAxis = false; + $scope.dual_y = ''; + } const index = aggs.indexOf(agg); if (index === -1) return notify.log('already removed'); diff --git a/src/plugins/kibana/public/visualize/editor/agg_group.html b/src/plugins/kibana/public/visualize/editor/agg_group.html index 98b12162d7c59..45696eca2cf3f 100644 --- a/src/plugins/kibana/public/visualize/editor/agg_group.html +++ b/src/plugins/kibana/public/visualize/editor/agg_group.html @@ -7,7 +7,7 @@
- +
diff --git a/src/ui/public/agg_response/point_series/__tests__/_add_to_siri.js b/src/ui/public/agg_response/point_series/__tests__/_add_to_siri.js index 4e9c563f78dbf..d2826933fa2b6 100644 --- a/src/ui/public/agg_response/point_series/__tests__/_add_to_siri.js +++ b/src/ui/public/agg_response/point_series/__tests__/_add_to_siri.js @@ -35,6 +35,7 @@ describe('addToSiri', function () { expect(series.has(id)).to.be(true); expect(series.get(id)).to.be.an('object'); expect(series.get(id).label).to.be(id); + expect(series.get(id).onSecondaryYAxis).to.be(false); expect(series.get(id).values).to.have.length(2); expect(series.get(id).values[0]).to.be(point); expect(series.get(id).values[1]).to.be(point2); @@ -45,11 +46,12 @@ describe('addToSiri', function () { let id = 'id'; let label = 'label'; let point = {}; - addToSiri(series, point, id, label); + addToSiri(series, point, id, label, true); expect(series.has(id)).to.be(true); expect(series.get(id)).to.be.an('object'); expect(series.get(id).label).to.be(label); + expect(series.get(id).onSecondaryYAxis).to.be(true); expect(series.get(id).values).to.have.length(1); expect(series.get(id).values[0]).to.be(point); }); diff --git a/src/ui/public/agg_response/point_series/__tests__/_get_series.js b/src/ui/public/agg_response/point_series/__tests__/_get_series.js index b09fd319624fd..a321f3f7977a5 100644 --- a/src/ui/public/agg_response/point_series/__tests__/_get_series.js +++ b/src/ui/public/agg_response/point_series/__tests__/_get_series.js @@ -35,7 +35,7 @@ describe('getSeries', function () { } }; - let series = getSeries(rows, chart); + let series = getSeries(rows, chart, {}); expect(series) .to.be.an('array') @@ -77,18 +77,26 @@ describe('getSeries', function () { ] } }; + let aggs = [ + { id: 1, onSecondaryYAxis: true }, + { id: 2, onSecondaryYAxis: false } + ]; - let series = getSeries(rows, chart); + let series = getSeries(rows, chart, aggs); expect(series) .to.be.an('array') .and.to.have.length(2); + expect(series[0].onSecondaryYAxis).to.be(true); + expect(series[1].onSecondaryYAxis).to.be(false); + series.forEach(function (siri, i) { expect(siri) .to.be.an('object') .and.have.property('label', '' + i) - .and.have.property('values'); + .and.have.property('values') + .and.have.property('onSecondaryYAxis'); expect(siri.values) .to.be.an('array') @@ -122,7 +130,7 @@ describe('getSeries', function () { } }; - let series = getSeries(rows, chart); + let series = getSeries(rows, chart, {}); expect(series) .to.be.an('array') @@ -167,7 +175,12 @@ describe('getSeries', function () { } }; - let series = getSeries(rows, chart); + let aggs = [ + { id: 1, onSecondaryYAxis: true }, + { id: 2, onSecondaryYAxis: false } + ]; + + let series = getSeries(rows, chart, aggs); expect(series) .to.be.an('array') @@ -217,7 +230,12 @@ describe('getSeries', function () { } }; - let series = getSeries(rows, chart); + let aggs = [ + { id: 1, onSecondaryYAxis: true }, + { id: 2, onSecondaryYAxis: false } + ]; + + let series = getSeries(rows, chart, aggs); expect(series[0]).to.have.property('label', '0: 0'); expect(series[1]).to.have.property('label', '0: 1'); expect(series[2]).to.have.property('label', '1: 0'); @@ -230,7 +248,7 @@ describe('getSeries', function () { y.i = i; }); - let series2 = getSeries(rows, chart); + let series2 = getSeries(rows, chart, aggs); expect(series2[0]).to.have.property('label', '0: 1'); expect(series2[1]).to.have.property('label', '0: 0'); expect(series2[2]).to.have.property('label', '1: 1'); diff --git a/src/ui/public/agg_response/point_series/__tests__/_init_y_axis.js b/src/ui/public/agg_response/point_series/__tests__/_init_y_axis.js index 97f1c98e12487..8aa2c23be5dca 100644 --- a/src/ui/public/agg_response/point_series/__tests__/_init_y_axis.js +++ b/src/ui/public/agg_response/point_series/__tests__/_init_y_axis.js @@ -11,29 +11,32 @@ describe('initYAxis', function () { initYAxis = Private(AggResponsePointSeriesInitYAxisProvider); })); - function agg() { + function agg(onSecondaryYAxis) { return { fieldFormatter: _.constant({}), write: _.constant({ params: {} }), - type: {} + type: {}, + onSecondaryYAxis: onSecondaryYAxis }; } - let baseChart = { - aspects: { - y: [ - { agg: agg(), col: { title: 'y1' } }, - { agg: agg(), col: { title: 'y2' } }, - ], - x: { - agg: agg(), - col: { title: 'x' } + let baseChart = function (forSecondaryYAxis) { + return { + aspects: { + y: [ + { agg: agg(false), col: { title: 'y1' } }, + { agg: agg(forSecondaryYAxis), col: { title: 'y2' } }, + ], + x: { + agg: agg(false), + col: { title: 'x' } + } } - } + }; }; describe('with a single y aspect', function () { - let singleYBaseChart = _.cloneDeep(baseChart); + let singleYBaseChart = baseChart(false); singleYBaseChart.aspects.y = singleYBaseChart.aspects.y[0]; it('sets the yAxisFormatter the the field formats convert fn', function () { @@ -51,7 +54,7 @@ describe('initYAxis', function () { describe('with mutliple y aspects', function () { it('sets the yAxisFormatter the the field formats convert fn for the first y aspect', function () { - let chart = _.cloneDeep(baseChart); + let chart = baseChart(false); initYAxis(chart); expect(chart).to.have.property('yAxisFormatter'); @@ -61,9 +64,19 @@ describe('initYAxis', function () { }); it('does not set the yAxisLabel, it does not make sense to put multiple labels on the same axis', function () { - let chart = _.cloneDeep(baseChart); + let chart = baseChart(false); initYAxis(chart); expect(chart).to.have.property('yAxisLabel', ''); }); + + it('sets the yAxislabel for secondary axis and use the right formatter', function () { + let chart = baseChart(true); + initYAxis(chart); + + expect(chart.secondYAxisLabel).to.be(chart.aspects.y[1].col.title); + expect(chart.secondYAxisFormatter) + .to.be(chart.aspects.y[1].agg.fieldFormatter()) + .and.not.be(chart.aspects.y[0].agg.fieldFormatter()); + }); }); }); diff --git a/src/ui/public/agg_response/point_series/_add_to_siri.js b/src/ui/public/agg_response/point_series/_add_to_siri.js index d5ad8a84530d9..beb2903e9f44b 100644 --- a/src/ui/public/agg_response/point_series/_add_to_siri.js +++ b/src/ui/public/agg_response/point_series/_add_to_siri.js @@ -1,15 +1,17 @@ export default function PointSeriesAddToSiri() { - return function addToSiri(series, point, id, label) { + return function addToSiri(series, point, id, label, onSecondaryYAxis) { id = id == null ? '' : id + ''; if (series.has(id)) { series.get(id).values.push(point); + series.get(id).onSecondaryYAxis = onSecondaryYAxis || false; return; } series.set(id, { label: label == null ? id : label, - values: [point] + values: [point], + onSecondaryYAxis: onSecondaryYAxis }); }; }; diff --git a/src/ui/public/agg_response/point_series/_get_series.js b/src/ui/public/agg_response/point_series/_get_series.js index 817ec2712d96f..c3dddf914b4b5 100644 --- a/src/ui/public/agg_response/point_series/_get_series.js +++ b/src/ui/public/agg_response/point_series/_get_series.js @@ -5,7 +5,7 @@ export default function PointSeriesGetSeries(Private) { let getPoint = Private(AggResponsePointSeriesGetPointProvider); let addToSiri = Private(AggResponsePointSeriesAddToSiriProvider); - return function getSeries(rows, chart) { + return function getSeries(rows, chart, aggs) { let aspects = chart.aspects; let multiY = _.isArray(aspects.y); let yScale = chart.yScale; @@ -26,8 +26,10 @@ export default function PointSeriesGetSeries(Private) { let prefix = point.series ? point.series + ': ' : ''; let seriesId = prefix + y.agg.id; let seriesLabel = prefix + y.col.title; + let aggId = y.agg.key ? y.agg.parentId : y.agg.id; + let onSecondaryYAxis = _.findWhere(aggs, {'id': aggId}).onSecondaryYAxis; - addToSiri(series, point, seriesId, seriesLabel); + addToSiri(series, point, seriesId, seriesLabel, onSecondaryYAxis); }); }, new Map()) diff --git a/src/ui/public/agg_response/point_series/_init_y_axis.js b/src/ui/public/agg_response/point_series/_init_y_axis.js index edd3060fc85c2..9f0d9c24250ce 100644 --- a/src/ui/public/agg_response/point_series/_init_y_axis.js +++ b/src/ui/public/agg_response/point_series/_init_y_axis.js @@ -9,6 +9,11 @@ export default function PointSeriesInitYAxis() { // TODO: vis option should allow choosing this format chart.yAxisFormatter = y[0].agg.fieldFormatter(); chart.yAxisLabel = ''; // use the legend + var secondaryYAxis = _.first(_(y).filter(function (yAxis) { return yAxis.agg.onSecondaryYAxis; }).value()); + if (secondaryYAxis) { + chart.secondYAxisFormatter = secondaryYAxis.agg.fieldFormatter(); + chart.secondYAxisLabel = secondaryYAxis.col.title; + } } else { chart.yAxisFormatter = y.agg.fieldFormatter(); chart.yAxisLabel = y.col.title; diff --git a/src/ui/public/agg_response/point_series/point_series.js b/src/ui/public/agg_response/point_series/point_series.js index 0a2ccd90729c4..27f11304eb3db 100644 --- a/src/ui/public/agg_response/point_series/point_series.js +++ b/src/ui/public/agg_response/point_series/point_series.js @@ -27,8 +27,9 @@ export default function PointSeriesProvider(Private) { if (datedX) { setupOrderedDateXAxis(vis, chart); } + let requiredVis = vis.getEditableVis() ? vis.getEditableVis() : vis; - chart.series = getSeries(table.rows, chart); + chart.series = getSeries(table.rows, chart, requiredVis.aggs); delete chart.aspects; return chart; diff --git a/src/ui/public/vis/__tests__/_agg_config.js b/src/ui/public/vis/__tests__/_agg_config.js index 6cd93b738b6b6..1f14c4173bd76 100644 --- a/src/ui/public/vis/__tests__/_agg_config.js +++ b/src/ui/public/vis/__tests__/_agg_config.js @@ -190,13 +190,14 @@ describe('AggConfig', function () { }); describe('#toJSON', function () { - it('includes the aggs id, params, type and schema', function () { + it('includes the aggs id, params, type, onSecondaryYAxis and schema', function () { let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { type: 'date_histogram', - schema: 'segment' + schema: 'segment', + onSecondaryYAxis: true } ] }); @@ -212,6 +213,7 @@ describe('AggConfig', function () { expect(state.params).to.be.an('object'); expect(state).to.have.property('type', 'date_histogram'); expect(state).to.have.property('schema', 'segment'); + expect(state).to.have.property('onSecondaryYAxis', true); }); }); diff --git a/src/ui/public/vis/agg_config.js b/src/ui/public/vis/agg_config.js index c1250db719b98..bcc579f637899 100644 --- a/src/ui/public/vis/agg_config.js +++ b/src/ui/public/vis/agg_config.js @@ -14,6 +14,7 @@ export default function AggConfigFactory(Private, fieldTypeFilter) { // setters self.type = opts.type; self.schema = opts.schema; + self.onSecondaryYAxis = opts.onSecondaryYAxis; // resolve the params self.fillDefaults(opts.params); @@ -86,6 +87,14 @@ export default function AggConfigFactory(Private, fieldTypeFilter) { } this.__schema = schema; + }, + onSecondaryYAxis: { + get: function () { + return this.__onSecondaryYAxis || false; + }, + set: function (onSecondaryYAxis) { + this.__onSecondaryYAxis = onSecondaryYAxis; + } } } }); @@ -236,6 +245,7 @@ export default function AggConfigFactory(Private, fieldTypeFilter) { enabled: self.enabled, type: self.type && self.type.name, schema: self.schema && self.schema.name, + onSecondaryYAxis: self.onSecondaryYAxis || false, params: outParams }; }; diff --git a/src/ui/public/vislib/__tests__/lib/axis_title.js b/src/ui/public/vislib/__tests__/lib/axis_title.js index bdb7f08b0701d..36cb000387718 100644 --- a/src/ui/public/vislib/__tests__/lib/axis_title.js +++ b/src/ui/public/vislib/__tests__/lib/axis_title.js @@ -11,12 +11,14 @@ import PersistedStatePersistedStateProvider from 'ui/persisted_state/persisted_s describe('Vislib AxisTitle Class Test Suite', function () { let AxisTitle; let Data; + let SingleYAxisStrategy; let PersistedState; - let axisTitle; let el; let dataObj; let xTitle; let yTitle; + let axisTitle; + let secondaryYTitle; let data = { hits: 621, label: '', @@ -27,6 +29,51 @@ describe('Vislib AxisTitle Class Test Suite', function () { min: 1408734082458 }, series: [ + { + onSecondaryYAxis: true, + values: [ + { + x: 1408734060000, + y: 80 + }, + { + x: 1408734090000, + y: 230 + }, + { + x: 1408734120000, + y: 300 + }, + { + x: 1408734150000, + y: 280 + }, + { + x: 1408734180000, + y: 360 + }, + { + x: 1408734210000, + y: 300 + }, + { + x: 1408734240000, + y: 260 + }, + { + x: 1408734270000, + y: 220 + }, + { + x: 1408734300000, + y: 290 + }, + { + x: 1408734330000, + y: 240 + } + ] + }, { values: [ { @@ -73,7 +120,8 @@ describe('Vislib AxisTitle Class Test Suite', function () { } ], xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count' + yAxisLabel: 'Count', + secondYAxisLabel: 'Average age' }; beforeEach(ngMock.module('kibana')); @@ -95,25 +143,30 @@ describe('Vislib AxisTitle Class Test Suite', function () { .style('height', '20px') .style('width', '20px'); + el.append('div') + .attr('class', 'secondary-y-axis-title') + .style('height', '20px') + .style('width', '20px'); dataObj = new Data(data, {}, new PersistedState()); xTitle = dataObj.get('xAxisLabel'); yTitle = dataObj.get('yAxisLabel'); - axisTitle = new AxisTitle($('.vis-wrapper')[0], xTitle, yTitle); + secondaryYTitle = dataObj.get('secondYAxisLabel'); })); afterEach(function () { el.remove(); }); - describe('render Method', function () { + describe('render Method for single y axis', function () { beforeEach(function () { + axisTitle = new AxisTitle($('.vis-wrapper')[0], xTitle, yTitle); axisTitle.render(); }); it('should append an svg to div', function () { - expect(el.select('.x-axis-title').selectAll('svg').length).to.be(1); - expect(el.select('.y-axis-title').selectAll('svg').length).to.be(1); + expect(el.select('.x-axis-title').selectAll('svg')[0].length).to.be(1); + expect(el.select('.y-axis-title').selectAll('svg')[0].length).to.be(1); }); it('should append a g element to the svg', function () { @@ -124,6 +177,26 @@ describe('Vislib AxisTitle Class Test Suite', function () { it('should append text', function () { expect(!!el.select('.x-axis-title').selectAll('svg').selectAll('text')).to.be(true); expect(!!el.select('.y-axis-title').selectAll('svg').selectAll('text')).to.be(true); + expect(el.select('.secondary-y-axis-title').selectAll('svg').selectAll('text')[0]).to.be(undefined); + }); + }); + + describe('render Method for secondary y axis', function () { + beforeEach(function () { + axisTitle = new AxisTitle($('.vis-wrapper')[0], xTitle, yTitle, secondaryYTitle); + axisTitle.render(); + }); + + it('should append an svg to div', function () { + expect(el.select('.x-axis-title').selectAll('svg')[0].length).to.be(1); + expect(el.select('.y-axis-title').selectAll('svg')[0].length).to.be(1); + expect(el.select('.secondary-y-axis-title').selectAll('svg')[0].length).to.be(1); + }); + + it('should append text', function () { + expect(el.select('.x-axis-title').selectAll('svg').selectAll('text')[0].length).to.be(1); + expect(el.select('.y-axis-title').selectAll('svg').selectAll('text')[0].length).to.be(1); + expect(el.select('.secondary-y-axis-title').selectAll('svg').selectAll('text')[0].length).to.be(1); }); }); diff --git a/src/ui/public/vislib/__tests__/lib/data.js b/src/ui/public/vislib/__tests__/lib/data.js index 20224d660e6d3..2c3eed565007d 100644 --- a/src/ui/public/vislib/__tests__/lib/data.js +++ b/src/ui/public/vislib/__tests__/lib/data.js @@ -4,9 +4,12 @@ import ngMock from 'ng_mock'; import expect from 'expect.js'; import dataSeries from 'fixtures/vislib/mock_data/date_histogram/_series'; +import dualAxisDataSeries from 'fixtures/vislib/mock_data/date_histogram/_dual_axis_series'; import dataSeriesNeg from 'fixtures/vislib/mock_data/date_histogram/_series_neg'; +import dualAxisDataSeriesNeg from 'fixtures/vislib/mock_data/date_histogram/_dual_axis_series_neg'; import dataStacked from 'fixtures/vislib/mock_data/stacked/_stacked'; import VislibLibDataProvider from 'ui/vislib/lib/data'; +import VislibLibDualYAxisStrategy from 'ui/vislib/lib/dual_y_axis_strategy'; import PersistedStatePersistedStateProvider from 'ui/persisted_state/persisted_state'; let seriesData = { @@ -19,6 +22,22 @@ let seriesData = { ] }; +let seriesDataWithDualAxis = { + 'label': '', + 'series': [ + { + 'label': '100', + 'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}], + 'onSecondaryYAxis': false + }, + { + 'label': '1001', + 'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}], + 'onSecondaryYAxis': true + } + ] +}; + let rowsData = { 'rows': [ { @@ -104,9 +123,12 @@ let colsData = { describe('Vislib Data Class Test Suite', function () { let Data; let persistedState; + let DualYAxisStrategy; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { + // single axis strategy is used by Data by default + DualYAxisStrategy = Private(VislibLibDualYAxisStrategy); Data = Private(VislibLibDataProvider); persistedState = new (Private(PersistedStatePersistedStateProvider))(); })); @@ -121,6 +143,39 @@ describe('Vislib Data Class Test Suite', function () { expect(_.isObject(rowIn)).to.be(true); }); + it('should decorate the values with false if there is no secondary Axis', function () { + let seriesDataWithoutLabelInSeries = { + 'label': '', + 'series': [ + { + 'label': '', + 'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}] + }, + { + 'values': [{x:10, y:11}, {x:11, y:12}, {x:12, y:13}] + } + ], + 'yAxisLabel': 'customLabel' + }; + let modifiedData = new Data(seriesDataWithoutLabelInSeries, {}, persistedState); + _.map(modifiedData.data.series[0].values, function (value) { + expect(value.belongsToSecondaryYAxis).to.be(false); + }); + _.map(modifiedData.data.series[1].values, function (value) { + expect(value.belongsToSecondaryYAxis).to.be(false); + }); + }); + + it('should decorate the values if it belongs to secondary Axis', function () { + let modifiedData = new Data(seriesDataWithDualAxis, {}, persistedState, new DualYAxisStrategy()); + _.map(modifiedData.data.series[0].values, function (value) { + expect(value.belongsToSecondaryYAxis).to.be(false); + }); + _.map(modifiedData.data.series[1].values, function (value) { + expect(value.belongsToSecondaryYAxis).to.be(true); + }); + }); + it('should update label in series data', function () { let seriesDataWithoutLabelInSeries = { 'label': '', @@ -223,7 +278,7 @@ describe('Vislib Data Class Test Suite', function () { }); }); - describe('Data.flatten', function () { + describe('Data.flatten for single y axis', function () { let serIn; let rowIn; let colIn; @@ -235,13 +290,15 @@ describe('Vislib Data Class Test Suite', function () { serIn = new Data(seriesData, {}, persistedState); rowIn = new Data(rowsData, {}, persistedState); colIn = new Data(colsData, {}, persistedState); - serOut = serIn.flatten(); - rowOut = rowIn.flatten(); - colOut = colIn.flatten(); + serOut = serIn._flatten(); + rowOut = rowIn._flatten(); + colOut = colIn._flatten(); }); it('should return an array of value objects from every series', function () { expect(serOut.every(_.isObject)).to.be(true); + expect(rowOut.every(_.isObject)).to.be(true); + expect(colOut.every(_.isObject)).to.be(true); }); it('should return all points from every series', testLength(seriesData)); @@ -257,11 +314,53 @@ describe('Vislib Data Class Test Suite', function () { }, 0); }, 0); - expect(data.flatten()).to.have.length(len); + expect(data._flatten()).to.have.length(len); }; } }); + describe('Data.flatten for dual y axis', function () { + let serIn; + let rowIn; + let colIn; + let serOutPrimary; + let serOutSecondary; + let rowOutPrimary; + let rowOutSecondary; + let colOutPrimary; + let colOutSecondary; + + beforeEach(function () { + serIn = new Data(seriesData, {}, persistedState, new DualYAxisStrategy()); + rowIn = new Data(rowsData, {}, persistedState, new DualYAxisStrategy()); + colIn = new Data(colsData, {}, persistedState, new DualYAxisStrategy()); + serOutPrimary = serIn._flatten(true); + serOutSecondary = serIn._flatten(false); + rowOutPrimary = rowIn._flatten(true); + rowOutSecondary = rowIn._flatten(false); + colOutPrimary = colIn._flatten(true); + colOutSecondary = colIn._flatten(false); + }); + + it('should return an array of value objects from every series', function () { + expect(serOutPrimary.every(_.isObject)).to.be(true); + expect(serOutSecondary.every(_.isObject)).to.be(true); + expect(rowOutPrimary.every(_.isObject)).to.be(true); + expect(rowOutSecondary.every(_.isObject)).to.be(true); + expect(colOutPrimary.every(_.isObject)).to.be(true); + expect(colOutSecondary.every(_.isObject)).to.be(true); + }); + + it('should return all points for specific graph in the series', function () { + let data = new Data(seriesDataWithDualAxis, {}, persistedState, new DualYAxisStrategy()); + let primaryChartLength = data.chartData()[0].series[0].values.length; + let secondaryChartLength = data.chartData()[0].series[1].values.length; + + expect(data._flatten(true)).to.have.length(primaryChartLength); + expect(data._flatten(false)).to.have.length(secondaryChartLength); + }); + }); + describe('getYMin method', function () { let visData; let visDataNeg; @@ -299,6 +398,31 @@ describe('Vislib Data Class Test Suite', function () { }); }); + describe('getSecondYMin method', function () { + let visData; + let visDataNeg; + let visDataStacked; + let minValue = 4; + let secondMinValue = 400; + let minValueNeg = -41; + let secondMinValueNeg = -4100; + + beforeEach(function () { + visData = new Data(dualAxisDataSeries, {}, persistedState, new DualYAxisStrategy()); + visDataNeg = new Data(dualAxisDataSeriesNeg, {}, persistedState, new DualYAxisStrategy()); + }); + + // The first value in the time series is less than the min date in the + // date range. It also has the largest y value. This value should be excluded + // when calculating the Y max value since it falls outside of the range. + it('should return the Y domain min values', function () { + expect(visData.getYMin()).to.be(minValue); + expect(visData.getSecondYMin()).to.be(secondMinValue); + expect(visDataNeg.getYMin()).to.be(minValueNeg); + expect(visDataNeg.getSecondYMin()).to.be(secondMinValueNeg); + }); + }); + describe('getYMax method', function () { let visData; let visDataNeg; @@ -336,6 +460,32 @@ describe('Vislib Data Class Test Suite', function () { }); }); + describe('getSecondYMax method', function () { + let visData; + let visDataNeg; + let visDataStacked; + let maxValue = 41; + let secondMaxValue = 4100; + let maxValueNeg = -4; + let secondMaxValueNeg = -400; + let maxValueStacked = 115; + + beforeEach(function () { + visData = new Data(dualAxisDataSeries, {}, persistedState, new DualYAxisStrategy()); + visDataNeg = new Data(dualAxisDataSeriesNeg, {}, persistedState, new DualYAxisStrategy()); + }); + + // The first value in the time series is less than the min date in the + // date range. It also has the largest y value. This value should be excluded + // when calculating the Y max value since it falls outside of the range. + it('should return the Y domain min values', function () { + expect(visData.getYMax()).to.be(maxValue); + expect(visData.getSecondYMax()).to.be(secondMaxValue); + expect(visDataNeg.getYMax()).to.be(maxValueNeg); + expect(visDataNeg.getSecondYMax()).to.be(secondMaxValueNeg); + }); + }); + describe('geohashGrid methods', function () { let data; let geohashGridData = { diff --git a/src/ui/public/vislib/__tests__/lib/layout/layout.js b/src/ui/public/vislib/__tests__/lib/layout/layout.js index a69e53aa89235..666af27d0b694 100644 --- a/src/ui/public/vislib/__tests__/lib/layout/layout.js +++ b/src/ui/public/vislib/__tests__/lib/layout/layout.js @@ -53,12 +53,15 @@ dateHistogramArray.forEach(function (data, i) { describe('createLayout Method', function () { it('should append all the divs', function () { expect($(vis.el).find('.vis-wrapper').length).to.be(1); - expect($(vis.el).find('.y-axis-col-wrapper').length).to.be(1); + expect($(vis.el).find('.y-axis-col-wrapper').length).to.be(2); expect($(vis.el).find('.vis-col-wrapper').length).to.be(1); expect($(vis.el).find('.y-axis-col').length).to.be(1); + expect($(vis.el).find('.secondary-y-axis-col').length).to.be(1); expect($(vis.el).find('.y-axis-title').length).to.be(1); + expect($(vis.el).find('.secondary-y-axis-title').length).to.be(1); expect($(vis.el).find('.y-axis-div-wrapper').length).to.be(1); - expect($(vis.el).find('.y-axis-spacer-block').length).to.be(1); + expect($(vis.el).find('.secondary-y-axis-div-wrapper').length).to.be(1); + expect($(vis.el).find('.y-axis-spacer-block').length).to.be(2); expect($(vis.el).find('.chart-wrapper').length).to.be(numberOfCharts); expect($(vis.el).find('.x-axis-wrapper').length).to.be(1); expect($(vis.el).find('.x-axis-div-wrapper').length).to.be(1); diff --git a/src/ui/public/vislib/__tests__/lib/layout/splits/column_chart/splits.js b/src/ui/public/vislib/__tests__/lib/layout/splits/column_chart/splits.js index ce07621a41d93..a7a27f43f564e 100644 --- a/src/ui/public/vislib/__tests__/lib/layout/splits/column_chart/splits.js +++ b/src/ui/public/vislib/__tests__/lib/layout/splits/column_chart/splits.js @@ -13,7 +13,7 @@ describe('Vislib Split Function Test Suite', function () { let chartSplit; let chartTitleSplit; let xAxisSplit; - let yAxisSplit; + let YAxisSplit; let el; let data = { rows: [ @@ -141,7 +141,7 @@ describe('Vislib Split Function Test Suite', function () { chartSplit = Private(VislibLibLayoutSplitsColumnChartChartSplitProvider); chartTitleSplit = Private(VislibLibLayoutSplitsColumnChartChartTitleSplitProvider); xAxisSplit = Private(VislibLibLayoutSplitsColumnChartXAxisSplitProvider); - yAxisSplit = Private(VislibLibLayoutSplitsColumnChartYAxisSplitProvider); + YAxisSplit = Private(VislibLibLayoutSplitsColumnChartYAxisSplitProvider); el = d3.select('body').append('div') .attr('class', 'visualization') @@ -245,7 +245,7 @@ describe('Vislib Split Function Test Suite', function () { .attr('class', 'rows') .datum({ rows: [{}, {}] }); - d3.select('.rows').call(yAxisSplit); + d3.select('.rows').call(new YAxisSplit('y-axis-div', false).build()); divs = d3.selectAll('.y-axis-div')[0]; })); diff --git a/src/ui/public/vislib/__tests__/lib/y_axis.js b/src/ui/public/vislib/__tests__/lib/y_axis.js index d89c1febb3003..39043cc5d8c72 100644 --- a/src/ui/public/vislib/__tests__/lib/y_axis.js +++ b/src/ui/public/vislib/__tests__/lib/y_axis.js @@ -81,6 +81,8 @@ function createData(seriesData) { el: node, yMin: dataObj.getYMin(), yMax: dataObj.getYMax(), + yAxisDiv: 'y-axis-div', + orientation: 'right', _attr: { margin: { top: 0, right: 0, bottom: 0, left: 0 }, defaultYMin: true, @@ -117,15 +119,19 @@ describe('Vislib yAxis Class Test Suite', function () { }); it('should append an svg to div', function () { - expect(el.selectAll('svg').length).to.be(1); + expect(el.selectAll('svg')[0].length).to.be(1); }); it('should append a g element to the svg', function () { - expect(el.selectAll('svg').select('g').length).to.be(1); + expect(el.selectAll('svg').select('g')[0].length).to.be(1); }); it('should append ticks with text', function () { - expect(!!el.selectAll('svg').selectAll('.tick text')).to.be(true); + expect(el.selectAll('svg').selectAll('.tick text').length).to.be(1); + }); + + it('should translate with a constant x component when on right orientation', function () { + expect(d3.transform(el.selectAll('svg').select('g').attr('transform')).translate[0]).to.be(4); }); }); diff --git a/src/ui/public/vislib/lib/axis_title.js b/src/ui/public/vislib/lib/axis_title.js index 297f55a9be81b..46ba2c67e85e4 100644 --- a/src/ui/public/vislib/lib/axis_title.js +++ b/src/ui/public/vislib/lib/axis_title.js @@ -16,7 +16,7 @@ export default function AxisTitleFactory(Private) { * @param yTitle {String} Y-axis title */ _.class(AxisTitle).inherits(ErrorHandler); - function AxisTitle(el, xTitle, yTitle) { + function AxisTitle(el, xTitle, yTitle, secondaryYTitle) { if (!(this instanceof AxisTitle)) { return new AxisTitle(el, xTitle, yTitle); } @@ -24,6 +24,7 @@ export default function AxisTitleFactory(Private) { this.el = el; this.xTitle = xTitle; this.yTitle = yTitle; + this.secondaryYTitle = secondaryYTitle; } /** @@ -35,6 +36,9 @@ export default function AxisTitleFactory(Private) { AxisTitle.prototype.render = function () { d3.select(this.el).select('.x-axis-title').call(this.draw(this.xTitle)); d3.select(this.el).select('.y-axis-title').call(this.draw(this.yTitle)); + if (this.secondaryYTitle) { + d3.select(this.el).select('.secondary-y-axis-title').call(this.draw(this.secondaryYTitle)); + } }; /** diff --git a/src/ui/public/vislib/lib/data.js b/src/ui/public/vislib/lib/data.js index 5adceeb12ec45..08558f0af7c50 100644 --- a/src/ui/public/vislib/lib/data.js +++ b/src/ui/public/vislib/lib/data.js @@ -5,12 +5,14 @@ import VislibComponentsZeroInjectionInjectZerosProvider from 'ui/vislib/componen import VislibComponentsZeroInjectionOrderedXKeysProvider from 'ui/vislib/components/zero_injection/ordered_x_keys'; import VislibComponentsLabelsLabelsProvider from 'ui/vislib/components/labels/labels'; import VislibComponentsColorColorProvider from 'ui/vislib/components/color/color'; +import VislibLibAxisStrategyProvider from 'ui/vislib/lib/single_y_axis_strategy'; export default function DataFactory(Private) { let injectZeros = Private(VislibComponentsZeroInjectionInjectZerosProvider); let orderKeys = Private(VislibComponentsZeroInjectionOrderedXKeysProvider); let getLabels = Private(VislibComponentsLabelsLabelsProvider); let color = Private(VislibComponentsColorColorProvider); + let SingleAxisStrategy = Private(VislibLibAxisStrategyProvider); /** * Provides an API for pulling values off the data @@ -20,13 +22,15 @@ export default function DataFactory(Private) { * @constructor * @param data {Object} Elasticsearch query results * @param attr {Object|*} Visualization options + * @param yAxisStrategy {Object} Optional; Strategy for single & dual y-axis */ - function Data(data, attr, uiState) { + function Data(data, attr, uiState, yAxisStrategy) { if (!(this instanceof Data)) { return new Data(data, attr, uiState); } this.uiState = uiState; + this.yAxisStrategy = yAxisStrategy || new SingleAxisStrategy(); let self = this; let offset; @@ -41,7 +45,8 @@ export default function DataFactory(Private) { offset = attr.mode; } - this.data = data; + // updating each series point if it belongs to secondary axis + this.data = this.yAxisStrategy.decorate(data); this.type = this.getDataType(); this.labels = this._getLabels(this.data); @@ -95,6 +100,13 @@ export default function DataFactory(Private) { return this.pieNames(); }; + /** + * Exposing flatten functionality of the strategies for testing purposes + */ + Data.prototype._flatten = function (isPrimary) { + return this.yAxisStrategy._flatten(this.chartData(), isPrimary); + }; + /** * Returns true for positive numbers */ @@ -326,23 +338,6 @@ export default function DataFactory(Private) { }); }; - /** - * Return an array of all value objects - * Pluck the data.series array from each data object - * Create an array of all the value objects from the series array - * - * @method flatten - * @returns {Array} Value objects - */ - Data.prototype.flatten = function () { - return _(this.chartData()) - .pluck('series') - .flattenDeep() - .pluck('values') - .flattenDeep() - .value(); - }; - /** * Determines whether histogram charts should be stacked * TODO: need to make this more generic @@ -351,15 +346,7 @@ export default function DataFactory(Private) { * @returns {boolean} */ Data.prototype.shouldBeStacked = function () { - let isHistogram = (this._attr.type === 'histogram'); - let isArea = (this._attr.type === 'area'); - let isOverlapping = (this._attr.mode === 'overlap'); - let grouped = (this._attr.mode === 'grouped'); - - let stackedHisto = isHistogram && !grouped; - let stackedArea = isArea && !isOverlapping; - - return stackedHisto || stackedArea; + return this.yAxisStrategy.shouldBeStacked(this._attr); }; /** @@ -387,33 +374,20 @@ export default function DataFactory(Private) { * @returns {Number} Min y axis value */ Data.prototype.getYMin = function (getValue) { - let self = this; - let arr = []; - - if (this._attr.mode === 'percentage' || this._attr.mode === 'wiggle' || - this._attr.mode === 'silhouette') { - return 0; - } - - let flat = this.flatten(); - // if there is only one data point and its less than zero, - // return 0 as the yMax value. - if (!flat.length || flat.length === 1 && flat[0].y > 0) { - return 0; - } - - let min = Infinity; - - // for each object in the dataArray, - // push the calculated y value to the initialized array (arr) - _.each(this.chartData(), function (chart) { - let calculatedMin = self._getYExtent(chart, 'min', getValue); - if (!_.isUndefined(calculatedMin)) { - min = Math.min(min, calculatedMin); - } - }); + return this.yAxisStrategy.getYMin(getValue, this.chartData(), this._attr); + }; - return min; + /** + * Calculates the lowest Y value across charts for secondary axis. + * + * @method getSecondYMin + * @param {function} [getValue] - optional getter that will receive a + * point and should return the value that should + * be considered + * @returns {Number} Min y axis value + */ + Data.prototype.getSecondYMin = function (getValue) { + return this.yAxisStrategy.getSecondYMin(getValue, this.chartData(), this._attr); }; /** @@ -427,32 +401,19 @@ export default function DataFactory(Private) { * @returns {Number} Max y axis value */ Data.prototype.getYMax = function (getValue) { - let self = this; - let arr = []; - - if (self._attr.mode === 'percentage') { - return 1; - } - - let flat = this.flatten(); - // if there is only one data point and its less than zero, - // return 0 as the yMax value. - if (!flat.length || flat.length === 1 && flat[0].y < 0) { - return 0; - } - - let max = -Infinity; - - // for each object in the dataArray, - // push the calculated y value to the initialized array (arr) - _.each(this.chartData(), function (chart) { - let calculatedMax = self._getYExtent(chart, 'max', getValue); - if (!_.isUndefined(calculatedMax)) { - max = Math.max(max, calculatedMax); - } - }); + return this.yAxisStrategy.getYMax(getValue, this.chartData(), this._attr); + }; - return max; + /** + * Return the highest Y value for the secondary Y Axis + * + * @method getSecondYMax + * @param {function} [getValue] - optional getter that will receive a + * point and should return the value that should + * be considered + */ + Data.prototype.getSecondYMax = function (getValue) { + return this.yAxisStrategy.getSecondYMax(getValue, this.chartData(), this._attr); }; /** diff --git a/src/ui/public/vislib/lib/dual_y_axis_strategy.js b/src/ui/public/vislib/lib/dual_y_axis_strategy.js new file mode 100644 index 0000000000000..f67160cfd3db5 --- /dev/null +++ b/src/ui/public/vislib/lib/dual_y_axis_strategy.js @@ -0,0 +1,221 @@ +import d3 from 'd3'; +import _ from 'lodash'; +export default function DualYAxisStrategyFactory(Private) { + let DualYAxisStrategy = function () { + }; + + /** + * Return an array of all value objects + * Pluck the data.series array from all data object which belongs to primary axis + * Create an array of all the value objects from the series array + * + * @method _primaryAxisFlatten + * @params chartData {Array} of actual y data value objects + * @returns {Array} Value objects + */ + DualYAxisStrategy.prototype._primaryAxisFlatten = function (chartData) { + return _(chartData) + .pluck('series') + .flatten() + .reject(function (series) { + return series.onSecondaryYAxis; + }) + .pluck('values') + .flatten() + .value(); + }; + + /** + * Return an array of all value objects + * Pluck the data.series array from all data object which belongs to secondary axis + * Create an array of all the value objects from the series array + * + * @method _secondaryAxisFlatten + * @params chartData {Array} of actual y data value objects + * @returns {Array} Value objects + */ + DualYAxisStrategy.prototype._secondaryAxisFlatten = function (chartData) { + return _(chartData) + .pluck('series') + .flatten() + .filter(function (series) { + return series.onSecondaryYAxis; + }) + .pluck('values') + .flatten() + .value(); + }; + + DualYAxisStrategy.prototype._flatten = function (chartData, isPrimary) { + return isPrimary ? + this._primaryAxisFlatten(chartData) : + this._secondaryAxisFlatten(chartData); + }; + + /** + * Returns data object after stamping all the y values in series + * which belong to secondary axis + * + * @method decorate + * @params data {Object} The object of class Data + * @returns data object + */ + DualYAxisStrategy.prototype.decorate = function (data) { + if (data.rows) { + _.map(data.rows, this._updateSeries, this); + } else if (data.columns) { + _.map(data.columns, this._updateSeries, this); + } else { + this._updateSeries(data); + } + return data; + }; + + DualYAxisStrategy.prototype._updateSeries = function (data) { + if (data.series) { + _.map(data.series, function (series) { + let onSecondaryYAxis = series.onSecondaryYAxis; + _.map(series.values, function (value) { + value.belongsToSecondaryYAxis = onSecondaryYAxis; + }); + }); + } + }; + + /** + * Returns the Y axis value for a `series` array based on + * a specified callback function (calculation) (Max/Min). + */ + DualYAxisStrategy.prototype._getYExtent = function (extent, points) { + return d3[extent](points); + }; + + /** + * Calculates the highest Y value across all charts for primary Axis + * + * @method getYMax + * @param {function} [getValue] + * @param chartData {Array} of actual y data value objects + * @param attr {Object} mode of the graph + * @returns {Number} Max y axis value + */ + DualYAxisStrategy.prototype.getYMax = function (getValue, chartData, attr) { + if (attr.mode === 'percentage') { + return 1; + } + + return this._calculateYMax(chartData, true); + }; + + /** + * Calculates the highest Y value for the chart on secondary Axis + * + * @method getSecondYMax + * @param {function} [getValue] + * @param chartData {Array} of actual y data value objects + * @param attr {Object} mode of the graph + * @returns {Number} Max y axis value + */ + DualYAxisStrategy.prototype.getSecondYMax = function (getValue, chartData, attr) { + if (attr.mode === 'percentage') { + return 1; + } + + return this._calculateYMax(chartData, false); + }; + + /** + * Caluates the max Y value across the charts + */ + DualYAxisStrategy.prototype._calculateYMax = function (chartData, isPrimary) { + var self = this; + var arr = []; + var flatData = this._flatten(chartData, isPrimary); + // if there is only one data point and its less than zero, + // return 0 as the yMax value. + if (!flatData.length || flatData.length === 1 && flatData[0].y < 0) { + return 0; + } + + var max = -Infinity; + var points = _(flatData).pluck('y').value(); + + // for each object in the dataArray, + // push the calculated y value to the initialized array (arr) + _.each(chartData, function (chart) { + var calculatedMax = self._getYExtent('max', points); + if (!_.isUndefined(calculatedMax)) { + max = Math.max(max, calculatedMax); + } + }); + + return max; + }; + + /** + * Calculates the lowest Y value across all charts for primary Axis + * + * @method getYMin + * @param {function} [getValue] + * @param chartData {Array} of actual y data value objects + * @param attr {Object} mode of the graph + * @returns {Number} Min y axis value + */ + DualYAxisStrategy.prototype.getYMin = function (getValue, chartData, attr) { + var self = this; + var arr = []; + + if (attr.mode === 'percentage' || attr.mode === 'wiggle' || + attr.mode === 'silhouette') { + return 0; + } + + return this._calculateYMin(chartData, true); + }; + + /** + * Calculates the lowest Y value for the chart on secondary Axis + * + * @method getSecondYMin + * @param {function} [getValue] + * @param chartData {Array} of actual y data value objects + * @param attr {Object} mode of the graph + * @returns {Number} Min y axis value + */ + DualYAxisStrategy.prototype.getSecondYMin = function (getValue, chartData, attr) { + if (attr.mode === 'percentage' || attr.mode === 'wiggle' || + attr.mode === 'silhouette') { + return 0; + } + + return this._calculateYMin(chartData, false); + }; + /** + * Caluates the min Y value across the charts + */ + DualYAxisStrategy.prototype._calculateYMin = function (chartData, isPrimary) { + var self = this; + var arr = []; + var flatData = this._flatten(chartData, isPrimary); + // if there is only one data point and its less than zero, + // return 0 as the yMax value. + if (!flatData.length || flatData.length === 1 && flatData[0].y > 0) { + return 0; + } + + var min = Infinity; + var points = _(flatData).pluck('y').value(); + + // for each object in the dataArray, + // push the calculated y value to the initialized array (arr) + _.each(chartData, function (chart) { + var calculatedMin = self._getYExtent('min', points); + if (!_.isUndefined(calculatedMin)) { + min = Math.min(min, calculatedMin); + } + }); + return min; + }; + + return DualYAxisStrategy; +}; diff --git a/src/ui/public/vislib/lib/handler/handler.js b/src/ui/public/vislib/lib/handler/handler.js index e496c1b39271f..21c2aabae8ee0 100644 --- a/src/ui/public/vislib/lib/handler/handler.js +++ b/src/ui/public/vislib/lib/handler/handler.js @@ -38,6 +38,7 @@ export default function HandlerBaseClass(Private) { this.chartTitle = opts.chartTitle; this.axisTitle = opts.axisTitle; this.alerts = opts.alerts; + this.secondaryYAxis = opts.secondaryYAxis; this.layout = new Layout(vis.el, vis.data, vis._attr.type, opts); this.binder = new Binder(); @@ -48,6 +49,7 @@ export default function HandlerBaseClass(Private) { this.alerts, this.xAxis, this.yAxis, + this.secondaryYAxis ], Boolean); // memoize so that the same function is returned every time, diff --git a/src/ui/public/vislib/lib/handler/types/point_series.js b/src/ui/public/vislib/lib/handler/types/point_series.js index 3e1f1d3aa070b..7d110b910830d 100644 --- a/src/ui/public/vislib/lib/handler/types/point_series.js +++ b/src/ui/public/vislib/lib/handler/types/point_series.js @@ -6,6 +6,8 @@ import VislibLibYAxisProvider from 'ui/vislib/lib/y_axis'; import VislibLibAxisTitleProvider from 'ui/vislib/lib/axis_title'; import VislibLibChartTitleProvider from 'ui/vislib/lib/chart_title'; import VislibLibAlertsProvider from 'ui/vislib/lib/alerts'; +import VislibLibSingleAxisStrategy from 'ui/vislib/lib/single_y_axis_strategy'; +import VislibLibDualAxisStrategy from 'ui/vislib/lib/dual_y_axis_strategy'; export default function ColumnHandler(Private) { let injectZeros = Private(VislibComponentsZeroInjectionInjectZerosProvider); @@ -16,6 +18,8 @@ export default function ColumnHandler(Private) { let AxisTitle = Private(VislibLibAxisTitleProvider); let ChartTitle = Private(VislibLibChartTitleProvider); let Alerts = Private(VislibLibAlertsProvider); + let SingleYAxisStrategy = Private(VislibLibSingleAxisStrategy); + let DualYAxisStrategy = Private(VislibLibDualAxisStrategy); /* * Create handlers for Area, Column, and Line charts which @@ -27,16 +31,35 @@ export default function ColumnHandler(Private) { return function (vis) { let isUserDefinedYAxis = vis._attr.setYExtents; let data; + let yAxisStrategy = vis.get('hasSecondaryYAxis') ? new DualYAxisStrategy() : new SingleYAxisStrategy(); + let secondaryYAxis; + let axisTitle; if (opts.zeroFill) { - data = new Data(injectZeros(vis.data), vis._attr, vis.uiState); + data = new Data(injectZeros(vis.data), vis._attr, vis.uiState, yAxisStrategy); } else { - data = new Data(vis.data, vis._attr, vis.uiState); + data = new Data(vis.data, vis._attr, vis.uiState, yAxisStrategy); + } + + if (vis.get('hasSecondaryYAxis')) { + secondaryYAxis = new YAxis({ + el : vis.el, + yMin : isUserDefinedYAxis ? vis._attr.secondaryYAxis.min : data.getSecondYMin(), + yMax : isUserDefinedYAxis ? vis._attr.secondaryYAxis.max : data.getSecondYMax(), + yAxisFormatter: data.get('secondYAxisFormatter'), + _attr: vis._attr, + orientation: 'right', + yAxisDiv: 'secondary-y-axis-div' + }); + axisTitle = new AxisTitle(vis.el, data.get('xAxisLabel'), data.get('yAxisLabel'), data.get('secondYAxisLabel')); + } else { + secondaryYAxis = new YAxis({}); + axisTitle = new AxisTitle(vis.el, data.get('xAxisLabel'), data.get('yAxisLabel')); } return new Handler(vis, { data: data, - axisTitle: new AxisTitle(vis.el, data.get('xAxisLabel'), data.get('yAxisLabel')), + axisTitle: axisTitle, chartTitle: new ChartTitle(vis.el), xAxis: new XAxis({ el : vis.el, @@ -52,8 +75,11 @@ export default function ColumnHandler(Private) { yMin : isUserDefinedYAxis ? vis._attr.yAxis.min : data.getYMin(), yMax : isUserDefinedYAxis ? vis._attr.yAxis.max : data.getYMax(), yAxisFormatter: data.get('yAxisFormatter'), - _attr: vis._attr - }) + _attr: vis._attr, + orientation: 'left', + yAxisDiv: 'y-axis-div' + }), + secondaryYAxis: secondaryYAxis }); }; diff --git a/src/ui/public/vislib/lib/layout/splits/column_chart/y_axis_split.js b/src/ui/public/vislib/lib/layout/splits/column_chart/y_axis_split.js index 797e681075aab..5564307281826 100644 --- a/src/ui/public/vislib/lib/layout/splits/column_chart/y_axis_split.js +++ b/src/ui/public/vislib/lib/layout/splits/column_chart/y_axis_split.js @@ -9,27 +9,38 @@ define(function () { */ // render and get bounding box width - return function (selection, parent, opts) { - let yAxis = opts && opts.yAxis; - selection.each(function () { - let div = d3.select(this); + let YAxisSplit = function (divClass, isSecondary) { + this.yAxisDivClass = divClass; + this.isSecondary = isSecondary; + }; + + YAxisSplit.prototype.build = function () { + let self = this; + return function (selection, parent, opts) { + let yAxis = self.isSecondary ? + opts && opts.secondaryYAxis : + opts && opts.yAxis; + + selection.each(function () { + let div = d3.select(this); - div.call(setWidth, yAxis); + div.call(self.setWidth, yAxis); - div.selectAll('.y-axis-div') - .append('div') - .data(function (d) { - return d.rows ? d.rows : [d]; - }) - .enter() + div.selectAll('.' + self.yAxisDivClass) .append('div') - .attr('class', 'y-axis-div'); - }); + .data(function (d) { + return d.rows ? d.rows : [d]; + }) + .enter() + .append('div') + .attr('class', self.yAxisDivClass); + }); + }; }; - function setWidth(el, yAxis) { - if (!yAxis) return; + YAxisSplit.prototype.setWidth = function (el, yAxis) { + if (!(yAxis && yAxis.el)) return; let padding = 5; let height = parseInt(el.node().clientHeight, 10); @@ -43,6 +54,8 @@ define(function () { svg.remove(); el.style('width', (width + padding) + 'px'); - } + }; + + return YAxisSplit; }; }); diff --git a/src/ui/public/vislib/lib/layout/types/column_layout.js b/src/ui/public/vislib/lib/layout/types/column_layout.js index 04062740841f2..6446cf496c30b 100644 --- a/src/ui/public/vislib/lib/layout/types/column_layout.js +++ b/src/ui/public/vislib/lib/layout/types/column_layout.js @@ -6,7 +6,7 @@ import VislibLibLayoutSplitsColumnChartChartTitleSplitProvider from 'ui/vislib/l export default function ColumnLayoutFactory(Private) { let chartSplit = Private(VislibLibLayoutSplitsColumnChartChartSplitProvider); - let yAxisSplit = Private(VislibLibLayoutSplitsColumnChartYAxisSplitProvider); + let YAxisSplit = Private(VislibLibLayoutSplitsColumnChartYAxisSplitProvider); let xAxisSplit = Private(VislibLibLayoutSplitsColumnChartXAxisSplitProvider); let chartTitleSplit = Private(VislibLibLayoutSplitsColumnChartChartTitleSplitProvider); @@ -59,7 +59,7 @@ export default function ColumnLayoutFactory(Private) { { type: 'div', class: 'y-axis-div-wrapper', - splits: yAxisSplit + splits: new YAxisSplit('y-axis-div', false).build() } ] }, @@ -103,6 +103,36 @@ export default function ColumnLayoutFactory(Private) { ] } ] + }, + { + type: 'div', + class: 'y-axis-col-wrapper', + children: [ + { + type: 'div', + class: 'secondary-y-axis-col', + children: [ + { + type: 'div', + class: 'secondary-y-axis-chart-title', + splits: chartTitleSplit + }, + { + type: 'div', + class: 'secondary-y-axis-div-wrapper', + splits: new YAxisSplit('secondary-y-axis-div', true).build() + }, + { + type: 'div', + class: 'secondary-y-axis-title' + } + ] + }, + { + type: 'div', + class: 'y-axis-spacer-block' + } + ] } ] } diff --git a/src/ui/public/vislib/lib/single_y_axis_strategy.js b/src/ui/public/vislib/lib/single_y_axis_strategy.js new file mode 100644 index 0000000000000..ed091b810ecbd --- /dev/null +++ b/src/ui/public/vislib/lib/single_y_axis_strategy.js @@ -0,0 +1,192 @@ +import d3 from 'd3'; +import _ from 'lodash'; +export default function SingleYAxisStrategyFactory(Private) { + let SingleYAxisStrategy = function () { + }; + + /** + * Return an array of all value objects + * Pluck the data.series array from each data object + * Create an array of all the value objects from the series array + * + * @method flatten + * @returns {Array} Value objects + */ + SingleYAxisStrategy.prototype._flatten = function (chartData) { + return _(chartData) + .pluck('series') + .flatten() + .pluck('values') + .flatten() + .value(); + }; + + SingleYAxisStrategy.prototype.decorate = function (data) { + if (data.series) { + _.map(data.series, function (series) { + _.map(series.values, function (value) { + value.belongsToSecondaryYAxis = false; + }); + }); + } + return data; + }; + + /** + * Returns the max Y axis value for a `series` array based on + * a specified callback function (calculation). + * @param chart {Object} - data for each chart + * @param extent {String} - max/min + * @param {function} [getValue] - Optional getter that will be used to read + * values from points when calculating the extent. + * default is either this._getYStack or this.getY + * based on this.shouldBeStacked(). + * @param attr {Object} - properties for the chart + */ + SingleYAxisStrategy.prototype._getYExtent = function (chart, extent, getValue, attr) { + if (this.shouldBeStacked(attr)) { + this.stackData(_.pluck(chart.series, 'values'), attr); + getValue = getValue || this._getYStack; + } else { + getValue = getValue || this._getY; + } + + var points = chart.series + .reduce(function (points, series) { + return points.concat(series.values); + }, []) + .map(getValue); + + return d3[extent](points); + }; + + /** + * Calculates the y stack value for each data object + */ + SingleYAxisStrategy.prototype._getYStack = function (d) { + return d.y0 + d.y; + }; + + /** + * Determines whether histogram charts should be stacked + * TODO: need to make this more generic + * + * @method shouldBeStacked + * @returns {boolean} + */ + SingleYAxisStrategy.prototype.shouldBeStacked = function (attr) { + let isHistogram = (attr.type === 'histogram'); + let isArea = (attr.type === 'area'); + let isOverlapping = (attr.mode === 'overlap'); + let grouped = (attr.mode === 'grouped'); + + let stackedHisto = isHistogram && !grouped; + let stackedArea = isArea && !isOverlapping; + + return stackedHisto || stackedArea; + }; + + /** + * Calculates the Y max value + */ + SingleYAxisStrategy.prototype._getY = function (d) { + return d.y; + }; + + /** + * Calculates the stacked values for each data object + * + * @method stackData + * @param series {Array} Array of data objects + * @returns {*} Array of data objects with x, y, y0 keys + */ + SingleYAxisStrategy.prototype.stackData = function (series, attr) { + // Should not stack values on line chart + if (attr.type === 'line') return series; + return attr.stack(series); + }; + + /** + * Calculates the highest Y value across all charts, taking + * stacking into consideration. + * + * @method getYMax + * @param {function} [getValue] - optional getter that will receive a + * point and should return the value that should + * be considered + * @param chartData {Array} of actual y data value objects + * @param attr {Object} mode of the graph + * @returns {Number} Max y axis value + */ + SingleYAxisStrategy.prototype.getYMax = function (getValue, chartData, attr) { + let self = this; + let arr = []; + + if (attr.mode === 'percentage') { + return 1; + } + + var flat = this._flatten(chartData); + // if there is only one data point and its less than zero, + // return 0 as the yMax value. + if (!flat.length || flat.length === 1 && flat[0].y < 0) { + return 0; + } + + let max = -Infinity; + + // for each object in the dataArray, + // push the calculated y value to the initialized array (arr) + _.each(chartData, function (chart) { + let calculatedMax = self._getYExtent(chart, 'max', getValue, attr); + if (!_.isUndefined(calculatedMax)) { + max = Math.max(max, calculatedMax); + } + }); + + return max; + }; + + /** + * Calculates the lowest Y value across all charts, taking + * stacking into consideration. + * + * @method getYMin + * @param {function} [getValue] - optional getter that will receive a + * point and should return the value that should + * be considered + * @param chartData {Array} of actual y data value objects + * @param attr {Object} mode of the graph + * @returns {Number} Min y axis value + */ + SingleYAxisStrategy.prototype.getYMin = function (getValue, chartData, attr) { + let self = this; + let arr = []; + + if (attr.mode === 'percentage' || attr.mode === 'wiggle' || + attr.mode === 'silhouette') { + return 0; + } + + let flat = this._flatten(chartData); + // if there is only one data point and its less than zero, + // return 0 as the yMax value. + if (!flat.length || flat.length === 1 && flat[0].y > 0) { + return 0; + } + + let min = Infinity; + + // for each object in the dataArray, + // push the calculated y value to the initialized array (arr) + _.each(chartData, function (chart) { + let calculatedMin = self._getYExtent(chart, 'min', getValue, attr); + if (!_.isUndefined(calculatedMin)) { + min = Math.min(min, calculatedMin); + } + }); + + return min; + }; + return SingleYAxisStrategy; +}; diff --git a/src/ui/public/vislib/lib/x_axis.js b/src/ui/public/vislib/lib/x_axis.js index 8e581969bfab0..9db7ba9ca280c 100644 --- a/src/ui/public/vislib/lib/x_axis.js +++ b/src/ui/public/vislib/lib/x_axis.js @@ -509,13 +509,13 @@ export default function XAxisFactory(Private) { let visEl = d3.select(this); if (visEl.select('.inner-spacer-block').node() === null) { - visEl.select('.y-axis-spacer-block') + visEl.selectAll('.y-axis-spacer-block') .append('div') .attr('class', 'inner-spacer-block'); } let xAxisHt = visEl.select('.x-axis-wrapper').style('height'); - visEl.select('.inner-spacer-block').style('height', xAxisHt); + visEl.selectAll('.inner-spacer-block').style('height', xAxisHt); }); }; diff --git a/src/ui/public/vislib/lib/y_axis.js b/src/ui/public/vislib/lib/y_axis.js index a81b9b7e7fe20..2e3f5767739b2 100644 --- a/src/ui/public/vislib/lib/y_axis.js +++ b/src/ui/public/vislib/lib/y_axis.js @@ -21,6 +21,8 @@ export default function YAxisFactory(Private) { this.domain = [args.yMin, args.yMax]; this.yAxisFormatter = args.yAxisFormatter; this._attr = args._attr || {}; + this.orientation = args.orientation; + this.yAxisDiv = args.yAxisDiv; } /** @@ -30,7 +32,7 @@ export default function YAxisFactory(Private) { * @return {D3.UpdateSelection} Renders y axis to visualization */ YAxis.prototype.render = function () { - d3.select(this.el).selectAll('.y-axis-div').call(this.draw()); + d3.select(this.el).selectAll('.' + this.yAxisDiv).call(this.draw()); }; YAxis.prototype._isPercentage = function () { @@ -157,7 +159,7 @@ export default function YAxisFactory(Private) { .scale(yScale) .tickFormat(this.tickFormat(this.domain)) .ticks(this.tickScale(height)) - .orient('left'); + .orient(this.orientation); return this.yAxis; }; @@ -210,21 +212,29 @@ export default function YAxisFactory(Private) { // The yAxis should not appear if mode is set to 'wiggle' or 'silhouette' if (!isWiggleOrSilhouette) { // Append svg and y axis + let xTranslation = width - 2; + if (self.orientation === 'right') { + xTranslation = 4; + } let svg = div.append('svg') .attr('width', width) .attr('height', height); svg.append('g') .attr('class', 'y axis') - .attr('transform', 'translate(' + (width - 2) + ',' + margin.top + ')') + .attr('transform', 'translate(' + xTranslation + ',' + margin.top + ')') .call(yAxis); let container = svg.select('g.y.axis').node(); if (container) { let cWidth = Math.max(width, container.getBBox().width); + xTranslation = cWidth - 2; + if (self.orientation === 'right') { + xTranslation = 4; + } svg.attr('width', cWidth); svg.select('g') - .attr('transform', 'translate(' + (cWidth - 2) + ',' + margin.top + ')'); + .attr('transform', 'translate(' + xTranslation + ',' + margin.top + ')'); } } }); diff --git a/src/ui/public/vislib/styles/_layout.less b/src/ui/public/vislib/styles/_layout.less index e704d18745ccf..25d460cd0e9e2 100644 --- a/src/ui/public/vislib/styles/_layout.less +++ b/src/ui/public/vislib/styles/_layout.less @@ -22,7 +22,7 @@ min-width: 0; } -.y-axis-col { +.y-axis-col, .secondary-y-axis-col { display: flex; flex-direction: row; flex: 1 0 36px; @@ -34,7 +34,7 @@ min-height: 45px; } -.y-axis-div-wrapper { +.y-axis-div-wrapper, .secondary-y-axis-div-wrapper { display: flex; flex-direction: column; min-height: 20px; @@ -47,12 +47,19 @@ min-height: 14px; } -.y-axis-title { +.secondary-y-axis-div { + flex: 1 1 25px; + min-width: 14px; + min-height: 14px; + margin-left: -10px; +} + +.y-axis-title, .secondary-y-axis-title { min-height: 14px; min-width: 14px; } -.y-axis-chart-title { +.y-axis-chart-title, .secondary-y-axis-chart-title { display: flex; flex-direction: column; min-height: 14px; diff --git a/src/ui/public/vislib/visualizations/line_chart.js b/src/ui/public/vislib/visualizations/line_chart.js index 34443feb0afdb..13fbbcda26bd8 100644 --- a/src/ui/public/vislib/visualizations/line_chart.js +++ b/src/ui/public/vislib/visualizations/line_chart.js @@ -72,6 +72,7 @@ export default function LineChartFactory(Private) { let color = this.handler.data.getColorFunc(); let xScale = this.handler.xAxis.xScale; let yScale = this.handler.yAxis.yScale; + let secondaryYScale = this.handler.secondaryYAxis.yScale; let ordered = this.handler.data.get('ordered'); let tooltip = this.tooltip; let isTooltip = this._attr.addTooltip; @@ -118,7 +119,11 @@ export default function LineChartFactory(Private) { } function cy(d) { - return yScale(d.y); + if (d.belongsToSecondaryYAxis) { + return secondaryYScale(d.y); + } else { + return yScale(d.y); + } } function cColor(d) { @@ -188,6 +193,7 @@ export default function LineChartFactory(Private) { let self = this; let xScale = this.handler.xAxis.xScale; let yScale = this.handler.yAxis.yScale; + let secondaryYScale = this.handler.secondaryYAxis.yScale; let xAxisFormatter = this.handler.data.get('xAxisFormatter'); let color = this.handler.data.getColorFunc(); let ordered = this.handler.data.get('ordered'); @@ -202,7 +208,11 @@ export default function LineChartFactory(Private) { return xScale(d.x) + xScale.rangeBand() / 2; }) .y(function y(d) { - return yScale(d.y); + if (d.belongsToSecondaryYAxis) { + return secondaryYScale(d.y); + } else { + return yScale(d.y); + } }); let lines; @@ -296,7 +306,8 @@ export default function LineChartFactory(Private) { _input: e, label: label, x: self._attr.xValue.call(d.values, e, i), - y: self._attr.yValue.call(d.values, e, i) + y: self._attr.yValue.call(d.values, e, i), + belongsToSecondaryYAxis: e.belongsToSecondaryYAxis }; }); });