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 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
};
});
});