diff --git a/src/kibana/components/agg_response/point_series/_add_to_siri.js b/src/kibana/components/agg_response/point_series/_add_to_siri.js index e9697e1921630..2838f3e8e9ac0 100644 --- a/src/kibana/components/agg_response/point_series/_add_to_siri.js +++ b/src/kibana/components/agg_response/point_series/_add_to_siri.js @@ -1,17 +1,19 @@ define(function (require) { return function PointSeriesAddToSiri() { - return function addToSiri(series, point, id, label) { + return function addToSiri(series, point, id, label, onSecondaryYAxis) { id = id == null ? '' : id + ''; if (series[id]) { series[id].values.push(point); + series[id].onSecondaryYAxis = onSecondaryYAxis; return; } series[id] = { label: label == null ? id : label, - values: [point] + values: [point], + onSecondaryYAxis: onSecondaryYAxis }; }; }; -}); \ No newline at end of file +}); diff --git a/src/kibana/components/agg_response/point_series/_get_series.js b/src/kibana/components/agg_response/point_series/_get_series.js index 84c59e50d63b1..bb42f5f6c894a 100644 --- a/src/kibana/components/agg_response/point_series/_get_series.js +++ b/src/kibana/components/agg_response/point_series/_get_series.js @@ -4,7 +4,7 @@ define(function (require) { var getPoint = Private(require('components/agg_response/point_series/_get_point')); var addToSiri = Private(require('components/agg_response/point_series/_add_to_siri')); - return function getSeries(rows, chart) { + return function getSeries(rows, chart, aggs) { var aspects = chart.aspects; var multiY = _.isArray(aspects.y); var yScale = chart.yScale; @@ -26,8 +26,10 @@ define(function (require) { var prefix = point.series ? point.series + ': ' : ''; var seriesId = prefix + y.agg.id; var seriesLabel = prefix + y.col.title; + var aggId = y.agg.key ? y.agg.parentId : y.agg.id; + var onSecondaryYAxis = _.findWhere(aggs, {'id': aggId}).onSecondaryYAxis; - addToSiri(series, point, seriesId, seriesLabel); + addToSiri(series, point, seriesId, seriesLabel, onSecondaryYAxis); }); }, {}) diff --git a/src/kibana/components/agg_response/point_series/_init_y_axis.js b/src/kibana/components/agg_response/point_series/_init_y_axis.js index b47ec2ef884c2..376c595489e47 100644 --- a/src/kibana/components/agg_response/point_series/_init_y_axis.js +++ b/src/kibana/components/agg_response/point_series/_init_y_axis.js @@ -10,6 +10,11 @@ define(function (require) { // 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/kibana/components/agg_response/point_series/point_series.js b/src/kibana/components/agg_response/point_series/point_series.js index eece0f4b22fab..260808f3dcf14 100644 --- a/src/kibana/components/agg_response/point_series/point_series.js +++ b/src/kibana/components/agg_response/point_series/point_series.js @@ -22,8 +22,9 @@ define(function (require) { if (datedX) { setupOrderedDateXAxis(vis, chart); } + var 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/kibana/components/vis/_agg_config.js b/src/kibana/components/vis/_agg_config.js index fca276c617ef4..89fc16cb48253 100644 --- a/src/kibana/components/vis/_agg_config.js +++ b/src/kibana/components/vis/_agg_config.js @@ -13,6 +13,7 @@ define(function (require) { // setters self.type = opts.type; self.schema = opts.schema; + self.onSecondaryYAxis = opts.onSecondaryYAxis; // resolve the params self.fillDefaults(opts.params); @@ -86,6 +87,14 @@ define(function (require) { this.__schema = schema; } + }, + onSecondaryYAxis: { + get: function () { + return this.__onSecondaryYAxis || false; + }, + set: function (onSecondaryYAxis) { + this.__onSecondaryYAxis = onSecondaryYAxis; + } } }); @@ -234,6 +243,7 @@ define(function (require) { id: self.id, type: self.type && self.type.name, schema: self.schema && self.schema.name, + onSecondaryYAxis: self.onSecondaryYAxis || false, params: outParams }; }; diff --git a/src/kibana/components/vislib/lib/_dual_y_axis_strategy.js b/src/kibana/components/vislib/lib/_dual_y_axis_strategy.js new file mode 100644 index 0000000000000..b0e062a027011 --- /dev/null +++ b/src/kibana/components/vislib/lib/_dual_y_axis_strategy.js @@ -0,0 +1,223 @@ +define(function (require) { + return function DualYAxisStrategyService(d3) { + var _ = require('lodash'); + var 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(); + }; + + DualYAxisStrategy.prototype._flatten = function (chartData, isPrimary) { + return isPrimary ? + this._primaryAxisFlatten(chartData) : + this._secondaryAxisFlatten(chartData); + }; + + /** + * 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(); + }; + + /** + * 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) { + var 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/kibana/components/vislib/lib/_single_y_axis_strategy.js b/src/kibana/components/vislib/lib/_single_y_axis_strategy.js new file mode 100644 index 0000000000000..8360bcaba5418 --- /dev/null +++ b/src/kibana/components/vislib/lib/_single_y_axis_strategy.js @@ -0,0 +1,193 @@ +define(function (require) { + return function SingleYAxisStrategyService(d3) { + var _ = require('lodash'); + var 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) { + var isHistogram = (attr.type === 'histogram'); + var isArea = (attr.type === 'area'); + var isOverlapping = (attr.mode === 'overlap'); + var grouped = (attr.mode === 'grouped'); + + var stackedHisto = isHistogram && !grouped; + var 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) { + var self = this; + var 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; + } + + var max = -Infinity; + + // for each object in the dataArray, + // push the calculated y value to the initialized array (arr) + _.each(chartData, function (chart) { + var 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) { + var self = this; + var arr = []; + + if (attr.mode === 'percentage' || attr.mode === 'wiggle' || + attr.mode === 'silhouette') { + return 0; + } + + 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; + } + + var min = Infinity; + + // for each object in the dataArray, + // push the calculated y value to the initialized array (arr) + _.each(chartData, function (chart) { + var calculatedMin = self._getYExtent(chart, 'min', getValue, attr); + if (!_.isUndefined(calculatedMin)) { + min = Math.min(min, calculatedMin); + } + }); + + return min; + }; + return SingleYAxisStrategy; + }; +}); diff --git a/src/kibana/components/vislib/lib/axis_title.js b/src/kibana/components/vislib/lib/axis_title.js index 328e3eaba082c..95233e8336a15 100644 --- a/src/kibana/components/vislib/lib/axis_title.js +++ b/src/kibana/components/vislib/lib/axis_title.js @@ -14,7 +14,7 @@ define(function (require) { * @param xTitle {String} X-axis title * @param yTitle {String} Y-axis title */ - function AxisTitle(el, xTitle, yTitle) { + function AxisTitle(el, xTitle, yTitle, secondaryYTitle) { if (!(this instanceof AxisTitle)) { return new AxisTitle(el, xTitle, yTitle); } @@ -22,6 +22,7 @@ define(function (require) { this.el = el; this.xTitle = xTitle; this.yTitle = yTitle; + this.secondaryYTitle = secondaryYTitle; } _(AxisTitle.prototype).extend(ErrorHandler.prototype); @@ -35,6 +36,9 @@ define(function (require) { 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/kibana/components/vislib/lib/data.js b/src/kibana/components/vislib/lib/data.js index 084f48a6ec9f0..e58f83286ad13 100644 --- a/src/kibana/components/vislib/lib/data.js +++ b/src/kibana/components/vislib/lib/data.js @@ -7,7 +7,6 @@ define(function (require) { var getLabels = Private(require('components/vislib/components/labels/labels')); var color = Private(require('components/vislib/components/color/color')); var errors = require('errors'); - /** * Provides an API for pulling values off the data * and calculating values using the data @@ -16,14 +15,16 @@ define(function (require) { * @constructor * @param data {Object} Elasticsearch query results * @param attr {Object|*} Visualization options + * @param yAxisStrategy {Object} Strategy for single & dual y-axis */ - function Data(data, attr) { + function Data(data, attr, yAxisStrategy) { if (!(this instanceof Data)) { return new Data(data, attr); } var self = this; var offset; + this.yAxisStrategy = yAxisStrategy; if (attr.mode === 'stacked') { offset = 'zero'; @@ -35,7 +36,8 @@ define(function (require) { 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; @@ -73,6 +75,13 @@ define(function (require) { } } + /** + * Exposing flatten functionality of the strategies for it to be tested + */ + Data.prototype._flatten = function (isPrimary) { + return this.yAxisStrategy._flatten(this.chartData(), isPrimary); + }; + /** * Returns true for positive numbers */ @@ -271,42 +280,6 @@ define(function (require) { return _.get(source, thing, def); }; - /** - * 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') - .flatten() - .pluck('values') - .flatten() - .value(); - }; - - /** - * Determines whether histogram charts should be stacked - * TODO: need to make this more generic - * - * @method shouldBeStacked - * @returns {boolean} - */ - Data.prototype.shouldBeStacked = function () { - var isHistogram = (this._attr.type === 'histogram'); - var isArea = (this._attr.type === 'area'); - var isOverlapping = (this._attr.mode === 'overlap'); - var grouped = (this._attr.mode === 'grouped'); - - var stackedHisto = isHistogram && !grouped; - var stackedArea = isArea && !isOverlapping; - - return stackedHisto || stackedArea; - }; - /** * Validates that the Y axis min value defined by user input * is a number. @@ -322,134 +295,51 @@ define(function (require) { }; /** - * Calculates the lowest Y value across all charts, taking - * stacking into consideration. - * - * @method getYMin + * Return the highest Y value for the primary Y Axis + * @method getYMax * @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.getYMin = function (getValue) { - var self = this; - var arr = []; - - if (this._attr.mode === 'percentage' || this._attr.mode === 'wiggle' || - this._attr.mode === 'silhouette') { - return 0; - } - - var 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; - } - - var min = Infinity; - - // for each object in the dataArray, - // push the calculated y value to the initialized array (arr) - _.each(this.chartData(), function (chart) { - var calculatedMin = self._getYExtent(chart, 'min', getValue); - if (!_.isUndefined(calculatedMin)) { - min = Math.min(min, calculatedMin); - } - }); - - return min; + Data.prototype.getYMax = function (getValue) { + return this.yAxisStrategy.getYMax(getValue, this.chartData(), this._attr); }; /** - * Calculates the highest Y value across all charts, taking - * stacking into consideration. - * - * @method getYMax + * 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 - * @returns {Number} Max y axis value */ - Data.prototype.getYMax = function (getValue) { - var self = this; - var arr = []; - - if (self._attr.mode === 'percentage') { - return 1; - } - - var 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; - } - - var max = -Infinity; - - // for each object in the dataArray, - // push the calculated y value to the initialized array (arr) - _.each(this.chartData(), function (chart) { - var calculatedMax = self._getYExtent(chart, 'max', getValue); - if (!_.isUndefined(calculatedMax)) { - max = Math.max(max, calculatedMax); - } - }); - - return max; + Data.prototype.getSecondYMax = function (getValue) { + return this.yAxisStrategy.getSecondYMax(getValue, this.chartData(), this._attr); }; - /** - * Calculates the stacked values for each data object + * Calculates the lowest Y value across charts, taking + * stacking into consideration for primary axis. * - * @method stackData - * @param series {Array} Array of data objects - * @returns {*} Array of data objects with x, y, y0 keys - */ - Data.prototype.stackData = function (series) { - // Should not stack values on line chart - if (this._attr.type === 'line') return series; - return this._attr.stack(series); - }; - - /** - * Returns the max Y axis value for a `series` array based on - * a specified callback function (calculation). - * @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(). - */ - Data.prototype._getYExtent = function (chart, extent, getValue) { - if (this.shouldBeStacked()) { - this.stackData(_.pluck(chart.series, 'values')); - 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 + * @method getYMin + * @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._getYStack = function (d) { - return d.y0 + d.y; + Data.prototype.getYMin = function (getValue) { + return this.yAxisStrategy.getYMin(getValue, this.chartData(), this._attr); }; /** - * Calculates the Y max value + * 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._getY = function (d) { - return d.y; + Data.prototype.getSecondYMin = function (getValue) { + return this.yAxisStrategy.getSecondYMin(getValue, this.chartData(), this._attr); }; /** @@ -624,6 +514,10 @@ define(function (require) { } }; + Data.prototype.shouldBeStacked = function () { + return this.yAxisStrategy.shouldBeStacked(this._attr); + }; + /** * Calculates min and max values for all map data * series.rows is an array of arrays diff --git a/src/kibana/components/vislib/lib/handler/handler.js b/src/kibana/components/vislib/lib/handler/handler.js index bbc96c66df781..6f332df91a57d 100644 --- a/src/kibana/components/vislib/lib/handler/handler.js +++ b/src/kibana/components/vislib/lib/handler/handler.js @@ -3,6 +3,7 @@ define(function (require) { var _ = require('lodash'); var errors = require('errors'); + var SingleYAxisStrategy = Private(require('components/vislib/lib/_single_y_axis_strategy')); var Data = Private(require('components/vislib/lib/data')); var Layout = Private(require('components/vislib/lib/layout/layout')); @@ -20,7 +21,7 @@ define(function (require) { return new Handler(vis, opts); } - this.data = opts.data || new Data(vis.data, vis._attr); + this.data = opts.data || new Data(vis.data, vis._attr, new SingleYAxisStrategy()); this.vis = vis; this.el = vis.el; this.ChartClass = vis.ChartClass; @@ -35,6 +36,7 @@ define(function (require) { this.chartTitle = opts.chartTitle; this.axisTitle = opts.axisTitle; this.alerts = opts.alerts; + this.secondaryYAxis = opts.secondaryYAxis; if (this._attr.addLegend) { this.legend = opts.legend; @@ -50,6 +52,7 @@ define(function (require) { this.alerts, this.xAxis, this.yAxis, + this.secondaryYAxis ], Boolean); // memoize so that the same function is returned every time, diff --git a/src/kibana/components/vislib/lib/handler/types/pie.js b/src/kibana/components/vislib/lib/handler/types/pie.js index 4bcb4a3c7d923..ca777a6bc0ea4 100644 --- a/src/kibana/components/vislib/lib/handler/types/pie.js +++ b/src/kibana/components/vislib/lib/handler/types/pie.js @@ -4,13 +4,13 @@ define(function (require) { var Data = Private(require('components/vislib/lib/data')); var Legend = Private(require('components/vislib/lib/legend')); var ChartTitle = Private(require('components/vislib/lib/chart_title')); - + var SingleYAxisStrategy = Private(require('components/vislib/lib/_single_y_axis_strategy')); /* * Handler for Pie visualizations. */ return function (vis) { - var data = new Data(vis.data, vis._attr); + var data = new Data(vis.data, vis._attr, new SingleYAxisStrategy()); return new Handler(vis, { legend: new Legend(vis, vis.el, data.pieNames(), data.getPieColorFunc(), vis._attr), diff --git a/src/kibana/components/vislib/lib/handler/types/point_series.js b/src/kibana/components/vislib/lib/handler/types/point_series.js index 70b7a774bbcb8..9c67722a10286 100644 --- a/src/kibana/components/vislib/lib/handler/types/point_series.js +++ b/src/kibana/components/vislib/lib/handler/types/point_series.js @@ -9,6 +9,8 @@ define(function (require) { var AxisTitle = Private(require('components/vislib/lib/axis_title')); var ChartTitle = Private(require('components/vislib/lib/chart_title')); var Alerts = Private(require('components/vislib/lib/alerts')); + var SingleYAxisStrategy = Private(require('components/vislib/lib/_single_y_axis_strategy')); + var DualYAxisStrategy = Private(require('components/vislib/lib/_dual_y_axis_strategy')); /* * Create handlers for Area, Column, and Line charts which @@ -20,17 +22,35 @@ define(function (require) { return function (vis) { var isUserDefinedYAxis = vis._attr.setYExtents; var data; - + var yAxisStrategy = vis.get('hasSecondaryYAxis') ? new DualYAxisStrategy() : new SingleYAxisStrategy(); + var secondaryYAxis; + var axisTitle; if (opts.zeroFill) { - data = new Data(injectZeros(vis.data), vis._attr); + data = new Data(injectZeros(vis.data), vis._attr, yAxisStrategy); + } else { + data = new Data(vis.data, vis._attr, 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 { - data = new Data(vis.data, vis._attr); + secondaryYAxis = new YAxis({}); + axisTitle = new AxisTitle(vis.el, data.get('xAxisLabel'), data.get('yAxisLabel')); } - return new Handler(vis, { + var handlerOpts = { data: data, legend: new Legend(vis, vis.el, data.labels, data.color, vis._attr), - axisTitle: new AxisTitle(vis.el, data.get('xAxisLabel'), data.get('yAxisLabel')), + axisTitle: axisTitle, chartTitle: new ChartTitle(vis.el), xAxis: new XAxis({ el : vis.el, @@ -46,10 +66,13 @@ define(function (require) { 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 + }; + return new Handler(vis, handlerOpts); }; } diff --git a/src/kibana/components/vislib/lib/handler/types/tile_map.js b/src/kibana/components/vislib/lib/handler/types/tile_map.js index 141e29031a1a4..616f2de2dc5aa 100644 --- a/src/kibana/components/vislib/lib/handler/types/tile_map.js +++ b/src/kibana/components/vislib/lib/handler/types/tile_map.js @@ -2,11 +2,12 @@ define(function (require) { return function MapHandler(d3, Private) { var _ = require('lodash'); + var SingleYAxisStrategy = Private(require('components/vislib/lib/_single_y_axis_strategy')); var Handler = Private(require('components/vislib/lib/handler/handler')); var Data = Private(require('components/vislib/lib/data')); return function (vis) { - var data = new Data(vis.data, vis._attr); + var data = new Data(vis.data, vis._attr, new SingleYAxisStrategy()); var MapHandler = new Handler(vis, { data: data diff --git a/src/kibana/components/vislib/lib/layout/layout_types.js b/src/kibana/components/vislib/lib/layout/layout_types.js index 291498344d146..c99190597d897 100644 --- a/src/kibana/components/vislib/lib/layout/layout_types.js +++ b/src/kibana/components/vislib/lib/layout/layout_types.js @@ -17,4 +17,4 @@ define(function (require) { tile_map: Private(require('components/vislib/lib/layout/types/map_layout')) }; }; -}); \ No newline at end of file +}); diff --git a/src/kibana/components/vislib/lib/layout/splits/column_chart/y_axis_split.js b/src/kibana/components/vislib/lib/layout/splits/column_chart/y_axis_split.js index bbcfd30eea490..1e576691a5066 100644 --- a/src/kibana/components/vislib/lib/layout/splits/column_chart/y_axis_split.js +++ b/src/kibana/components/vislib/lib/layout/splits/column_chart/y_axis_split.js @@ -5,29 +5,38 @@ define(function () { * For example, if the data has rows, it returns the same number of * `.y-axis-div` elements as row objects. */ - // render and get bounding box width - return function (selection, parent, opts) { - var yAxis = opts && opts.yAxis; + var YAxisSplit = function (divClass, isSecondary) { + this.yAxisDivClass = divClass; + this.isSecondary = isSecondary; + }; + + YAxisSplit.prototype.build = function () { + var self = this; + return function (selection, parent, opts) { + var yAxis = self.isSecondary ? + opts && opts.secondaryYAxis : + opts && opts.yAxis; - selection.each(function () { - var div = d3.select(this); + selection.each(function () { + var 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; var padding = 5; var height = parseInt(el.node().clientHeight, 10); @@ -41,6 +50,8 @@ define(function () { svg.remove(); el.style('width', (width + padding) + 'px'); - } + }; + + return YAxisSplit; }; -}); \ No newline at end of file +}); diff --git a/src/kibana/components/vislib/lib/layout/types/column_layout.js b/src/kibana/components/vislib/lib/layout/types/column_layout.js index bb2b4ae7abd9a..9694a05ec8f81 100644 --- a/src/kibana/components/vislib/lib/layout/types/column_layout.js +++ b/src/kibana/components/vislib/lib/layout/types/column_layout.js @@ -1,7 +1,7 @@ define(function (require) { return function ColumnLayoutFactory(d3, Private) { var chartSplit = Private(require('components/vislib/lib/layout/splits/column_chart/chart_split')); - var yAxisSplit = Private(require('components/vislib/lib/layout/splits/column_chart/y_axis_split')); + var YAxisSplit = Private(require('components/vislib/lib/layout/splits/column_chart/y_axis_split')); var xAxisSplit = Private(require('components/vislib/lib/layout/splits/column_chart/x_axis_split')); var chartTitleSplit = Private(require('components/vislib/lib/layout/splits/column_chart/chart_title_split')); @@ -54,7 +54,7 @@ define(function (require) { { type: 'div', class: 'y-axis-div-wrapper', - splits: yAxisSplit + splits: new YAxisSplit('y-axis-div', false).build() } ] }, @@ -99,6 +99,36 @@ define(function (require) { } ] }, + { + 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' + } + ] + }, { type: 'div', class: 'legend-col-wrapper' diff --git a/src/kibana/components/vislib/lib/x_axis.js b/src/kibana/components/vislib/lib/x_axis.js index 81cda58908534..bf70cd2fb546c 100644 --- a/src/kibana/components/vislib/lib/x_axis.js +++ b/src/kibana/components/vislib/lib/x_axis.js @@ -509,13 +509,13 @@ define(function (require) { var 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'); } var 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/kibana/components/vislib/lib/y_axis.js b/src/kibana/components/vislib/lib/y_axis.js index f179264aa721f..c8f63bf815ed2 100644 --- a/src/kibana/components/vislib/lib/y_axis.js +++ b/src/kibana/components/vislib/lib/y_axis.js @@ -19,6 +19,8 @@ define(function (require) { this.domain = [args.yMin, args.yMax]; this.yAxisFormatter = args.yAxisFormatter; this._attr = args._attr || {}; + this.orientation = args.orientation; + this.yAxisDiv = args.yAxisDiv; } _(YAxis.prototype).extend(ErrorHandler.prototype); @@ -30,7 +32,7 @@ define(function (require) { * @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 @@ define(function (require) { .scale(yScale) .tickFormat(this.tickFormat(this.domain)) .ticks(this.tickScale(height)) - .orient('left'); + .orient(this.orientation); return this.yAxis; }; @@ -210,21 +212,29 @@ define(function (require) { // The yAxis should not appear if mode is set to 'wiggle' or 'silhouette' if (!isWiggleOrSilhouette) { // Append svg and y axis + var xTranslation = width - 2; + if (self.orientation === 'right') { + xTranslation = 4; + } var 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); var container = svg.select('g.y.axis').node(); if (container) { var 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/kibana/components/vislib/styles/_layout.less b/src/kibana/components/vislib/styles/_layout.less index 7e428891964e9..f686ac5a99748 100644 --- a/src/kibana/components/vislib/styles/_layout.less +++ b/src/kibana/components/vislib/styles/_layout.less @@ -23,7 +23,7 @@ min-width: 0; } -.y-axis-col { +.y-axis-col, .secondary-y-axis-col { .display(flex); .flex-direction(row); .flex(1 0 36px); @@ -35,7 +35,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; @@ -48,12 +48,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/kibana/components/vislib/visualizations/line_chart.js b/src/kibana/components/vislib/visualizations/line_chart.js index d58cea98dcd99..fad28bdc9b8ef 100644 --- a/src/kibana/components/vislib/visualizations/line_chart.js +++ b/src/kibana/components/vislib/visualizations/line_chart.js @@ -71,6 +71,7 @@ define(function (require) { var color = this.handler.data.getColorFunc(); var xScale = this.handler.xAxis.xScale; var yScale = this.handler.yAxis.yScale; + var secondaryYScale = this.handler.secondaryYAxis.yScale; var ordered = this.handler.data.get('ordered'); var tooltip = this.tooltip; var isTooltip = this._attr.addTooltip; @@ -109,7 +110,11 @@ define(function (require) { } function cy(d) { - return yScale(d.y); + if (d.belongsToSecondaryYAxis) { + return secondaryYScale(d.y); + } else { + return yScale(d.y); + } } function cColor(d) { @@ -179,6 +184,7 @@ define(function (require) { var self = this; var xScale = this.handler.xAxis.xScale; var yScale = this.handler.yAxis.yScale; + var secondaryYScale = this.handler.secondaryYAxis.yScale; var xAxisFormatter = this.handler.data.get('xAxisFormatter'); var color = this.handler.data.getColorFunc(); var ordered = this.handler.data.get('ordered'); @@ -192,7 +198,11 @@ define(function (require) { 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); + } }); var lines; @@ -286,7 +296,8 @@ define(function (require) { _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 }; }); }); diff --git a/src/kibana/components/vislib/visualizations/vis_types.js b/src/kibana/components/vislib/visualizations/vis_types.js index 9a43e52561abb..20db45498970d 100644 --- a/src/kibana/components/vislib/visualizations/vis_types.js +++ b/src/kibana/components/vislib/visualizations/vis_types.js @@ -18,4 +18,4 @@ define(function (require) { }; }; -}); \ No newline at end of file +}); diff --git a/src/kibana/plugins/vis_types/controls/point_series_options.html b/src/kibana/plugins/vis_types/controls/point_series_options.html index e20c168f983bb..af55ad608b878 100644 --- a/src/kibana/plugins/vis_types/controls/point_series_options.html +++ b/src/kibana/plugins/vis_types/controls/point_series_options.html @@ -35,6 +35,32 @@ ng-model="vis.params.yAxis.min" ng-required="vis.params.setYExtents"> +