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"> +
+ +
+ Min must not exceed max +
+ +
Min must exceed 0 when a log scale is selected diff --git a/src/kibana/plugins/visualize/editor/agg.html b/src/kibana/plugins/visualize/editor/agg.html index 56b3906d6fdde..f606f6c77a052 100644 --- a/src/kibana/plugins/visualize/editor/agg.html +++ b/src/kibana/plugins/visualize/editor/agg.html @@ -75,6 +75,14 @@ class="vis-editor-agg-editor"> +
+ +
+ diff --git a/src/kibana/plugins/visualize/editor/agg.js b/src/kibana/plugins/visualize/editor/agg.js index 784204f11a4cb..dd599560bb48c 100644 --- a/src/kibana/plugins/visualize/editor/agg.js +++ b/src/kibana/plugins/visualize/editor/agg.js @@ -18,16 +18,20 @@ define(function (require) { restrict: 'A', template: require('text!plugins/visualize/editor/agg.html'), require: 'form', + scope: false, link: function ($scope, $el, attrs, kbnForm) { $scope.$bind('outputAgg', 'outputVis.aggs.byId[agg.id]', $scope); + $scope.$bind('dual_y', 'attrs.dual_y'); $scope.editorOpen = !!$scope.agg.brandNew; + if ($scope.agg.onSecondaryYAxis) { + $scope.dual_y = $scope.agg.id; + } $scope.$watch('editorOpen', function (open) { // make sure that all of the form inputs are "touched" // so that their errors propogate if (!open) kbnForm.$setTouched(); }); - $scope.$watchMulti([ '$index', 'group.length' @@ -35,6 +39,27 @@ define(function (require) { $scope.aggIsTooLow = calcAggIsTooLow(); }); + $scope.$watch('dual_y', function updateAggs(newValue, oldValue) { + if (newValue) { + var requiredAgg = _.findWhere($scope.vis.aggs, {'id': $scope.agg.id}); + if (newValue === $scope.agg.id) { + requiredAgg.onSecondaryYAxis = true; + $scope.vis.params.hasSecondaryYAxis = true; + } else { + requiredAgg.onSecondaryYAxis = false; + } + } + }, true); + + $scope.canBeSecondaryYAxis = function () { + var schema = $scope.agg.schema; + var isYAxisMetric = schema.name === 'metric' && schema.title === 'Y-Axis'; + var yAxisCount = $scope.stats.count; + var isLineGraph = $scope.vis.type.name === 'line'; + var minYAxisCount = 2; + return isLineGraph && isYAxisMetric && yAxisCount >= minYAxisCount; + }; + /** * Describe the aggregation, for display in the collapsed agg header * @return {[type]} [description] @@ -55,6 +80,15 @@ define(function (require) { $scope.remove = function (agg) { var aggs = $scope.vis.aggs; + var yAxisCount = $scope.stats.count; + var minYAxisCount = 2; + var schema = $scope.agg.schema; + var isYAxisMetric = schema.name === 'metric' && schema.title === 'Y-Axis'; + var doesNotHaveMinimumYAxisAfterRemoval = isYAxisMetric && $scope.stats.count <= minYAxisCount; + if (doesNotHaveMinimumYAxisAfterRemoval || agg.onSecondaryYAxis) { + $scope.vis.params.hasSecondaryYAxis = false; + $scope.dual_y = ''; + } var index = aggs.indexOf(agg); if (index === -1) return notify.log('already removed'); diff --git a/src/kibana/plugins/visualize/editor/agg_group.html b/src/kibana/plugins/visualize/editor/agg_group.html index a9cc68ce5d108..2a5fcef9440ea 100644 --- a/src/kibana/plugins/visualize/editor/agg_group.html +++ b/src/kibana/plugins/visualize/editor/agg_group.html @@ -14,7 +14,7 @@ - +
diff --git a/src/kibana/plugins/visualize/editor/agg_group.js b/src/kibana/plugins/visualize/editor/agg_group.js index 372e75ac592e0..94f51f942868a 100644 --- a/src/kibana/plugins/visualize/editor/agg_group.js +++ b/src/kibana/plugins/visualize/editor/agg_group.js @@ -49,4 +49,4 @@ define(function (require) { }; }); -}); \ No newline at end of file +}); diff --git a/test/unit/specs/components/agg_response/point_series/_add_to_siri.js b/test/unit/specs/components/agg_response/point_series/_add_to_siri.js index 986d4bc3590dd..95fdd17ffc9c9 100644 --- a/test/unit/specs/components/agg_response/point_series/_add_to_siri.js +++ b/test/unit/specs/components/agg_response/point_series/_add_to_siri.js @@ -44,13 +44,14 @@ define(function (require) { var id = 'id'; var label = 'label'; var point = {}; - addToSiri(series, point, id, label); + addToSiri(series, point, id, label, true); expect(series).to.have.own.property(id); expect(series[id]).to.be.an('object'); expect(series[id].label).to.be(label); + expect(series[id].onSecondaryYAxis).to.be(true); expect(series[id].values).to.have.length(1); expect(series[id].values[0]).to.be(point); }); }]; -}); \ No newline at end of file +}); diff --git a/test/unit/specs/components/agg_response/point_series/_get_series.js b/test/unit/specs/components/agg_response/point_series/_get_series.js index 7e875ef2a4d59..c352c639ff164 100644 --- a/test/unit/specs/components/agg_response/point_series/_get_series.js +++ b/test/unit/specs/components/agg_response/point_series/_get_series.js @@ -33,7 +33,7 @@ define(function (require) { } }; - var series = getSeries(rows, chart); + var series = getSeries(rows, chart, {}); expect(series) .to.be.an('array') @@ -76,17 +76,26 @@ define(function (require) { } }; - var series = getSeries(rows, chart); + var aggs = [ + { id: 1, onSecondaryYAxis: true }, + { id: 2, onSecondaryYAxis: false } + ]; + + var 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') @@ -120,7 +129,7 @@ define(function (require) { } }; - var series = getSeries(rows, chart); + var series = getSeries(rows, chart, {}); expect(series) .to.be.an('array') @@ -165,7 +174,13 @@ define(function (require) { } }; - var series = getSeries(rows, chart); + var aggs = [ + { id: 1, onSecondaryYAxis: true }, + { id: 2, onSecondaryYAxis: false } + ]; + + + var series = getSeries(rows, chart, aggs); expect(series) .to.be.an('array') @@ -215,7 +230,13 @@ define(function (require) { } }; - var series = getSeries(rows, chart); + var aggs = [ + { id: 1, onSecondaryYAxis: true }, + { id: 2, onSecondaryYAxis: false } + ]; + + + var 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'); @@ -228,7 +249,7 @@ define(function (require) { y.i = i; }); - var series2 = getSeries(rows, chart); + var 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/test/unit/specs/components/agg_response/point_series/_init_y_axis.js b/test/unit/specs/components/agg_response/point_series/_init_y_axis.js index 182956217b35c..69f222b8d7ac6 100644 --- a/test/unit/specs/components/agg_response/point_series/_init_y_axis.js +++ b/test/unit/specs/components/agg_response/point_series/_init_y_axis.js @@ -9,29 +9,32 @@ define(function (require) { initYAxis = Private(require('components/agg_response/point_series/_init_y_axis')); })); - function agg() { + function agg(onSecondaryYAxis) { return { fieldFormatter: _.constant({}), write: _.constant({ params: {} }), - type: {} + type: {}, + onSecondaryYAxis: onSecondaryYAxis }; } - var baseChart = { - aspects: { - y: [ - { agg: agg(), col: { title: 'y1' } }, - { agg: agg(), col: { title: 'y2' } }, - ], - x: { - agg: agg(), - col: { title: 'x' } + var 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 () { - var singleYBaseChart = _.cloneDeep(baseChart); + var singleYBaseChart = _.cloneDeep(baseChart(false)); singleYBaseChart.aspects.y = singleYBaseChart.aspects.y[0]; it('sets the yAxisFormatter the the field formats convert fn', function () { @@ -49,7 +52,7 @@ define(function (require) { describe('with mutliple y aspects', function () { it('sets the yAxisFormatter the the field formats convert fn for the first y aspect', function () { - var chart = _.cloneDeep(baseChart); + var chart = _.cloneDeep(baseChart(false)); initYAxis(chart); expect(chart).to.have.property('yAxisFormatter'); @@ -59,10 +62,20 @@ define(function (require) { }); it('does not set the yAxisLabel, it does not make sense to put multiple labels on the same axis', function () { - var chart = _.cloneDeep(baseChart); + var chart = _.cloneDeep(baseChart(false)); initYAxis(chart); expect(chart).to.have.property('yAxisLabel', ''); }); + + it('sets the yAxislabel for secondary axis and use the right formatter', function () { + var chart = _.cloneDeep(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()); + }); }); }]; -}); \ No newline at end of file +}); diff --git a/test/unit/specs/components/vis/_agg_config.js b/test/unit/specs/components/vis/_agg_config.js index 18fc477be4b1a..682ad7a78b635 100644 --- a/test/unit/specs/components/vis/_agg_config.js +++ b/test/unit/specs/components/vis/_agg_config.js @@ -184,13 +184,14 @@ define(function (require) { }); describe('#toJSON', function () { - it('includes the aggs id, params, type and schema', function () { + it('includes the aggs id, params, type, onSecondaryYAxis and schema', function () { var vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { type: 'date_histogram', - schema: 'segment' + schema: 'segment', + onSecondaryYAxis: true } ] }); @@ -206,6 +207,7 @@ define(function (require) { 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/test/unit/specs/plugins/visualize/editor/agg.js b/test/unit/specs/plugins/visualize/editor/agg.js index 726aaa486b314..0b4517a32032a 100644 --- a/test/unit/specs/plugins/visualize/editor/agg.js +++ b/test/unit/specs/plugins/visualize/editor/agg.js @@ -57,6 +57,10 @@ define(function (require) { id: '2', schema: makeConfig('radius') }]; + $parentScope.stats = { count: 1 }; + $parentScope.vis = { + type: { name: 'histogram' } + }; }); beforeEach(inject(function ($rootScope, $compile) { // share the scope @@ -85,5 +89,38 @@ define(function (require) { }); 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/test/unit/specs/vislib/fixture/mock_data/date_histogram/_dual_axis_series.js b/test/unit/specs/vislib/fixture/mock_data/date_histogram/_dual_axis_series.js new file mode 100644 index 0000000000000..5acf2837e7c3d --- /dev/null +++ b/test/unit/specs/vislib/fixture/mock_data/date_histogram/_dual_axis_series.js @@ -0,0 +1,282 @@ +define(function (require) { + var 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/test/unit/specs/vislib/fixture/mock_data/date_histogram/_dual_axis_series_neg.js b/test/unit/specs/vislib/fixture/mock_data/date_histogram/_dual_axis_series_neg.js new file mode 100644 index 0000000000000..f36295717d537 --- /dev/null +++ b/test/unit/specs/vislib/fixture/mock_data/date_histogram/_dual_axis_series_neg.js @@ -0,0 +1,282 @@ +define(function (require) { + var 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/test/unit/specs/vislib/lib/axis_title.js b/test/unit/specs/vislib/lib/axis_title.js index f0ac3d1be3065..02b36de389ada 100644 --- a/test/unit/specs/vislib/lib/axis_title.js +++ b/test/unit/specs/vislib/lib/axis_title.js @@ -8,11 +8,13 @@ define(function (require) { describe('Vislib AxisTitle Class Test Suite', function () { var AxisTitle; var Data; + var SingleYAxisStrategy; var axisTitle; var el; var dataObj; var xTitle; var yTitle; + var secondaryYTitle; var data = { hits: 621, label: '', @@ -23,6 +25,51 @@ define(function (require) { 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: [ { @@ -69,7 +116,8 @@ define(function (require) { } ], xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count' + yAxisLabel: 'Count', + secondYAxisLabel: 'Average age' }; beforeEach(function () { @@ -80,6 +128,7 @@ define(function (require) { inject(function (d3, Private) { AxisTitle = Private(require('components/vislib/lib/axis_title')); Data = Private(require('components/vislib/lib/data')); + SingleYAxisStrategy = Private(require('components/vislib/lib/_single_y_axis_strategy')); el = d3.select('body').append('div') .attr('class', 'vis-wrapper'); @@ -94,11 +143,15 @@ define(function (require) { .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, {}); + dataObj = new Data(data, {}, new SingleYAxisStrategy()); xTitle = dataObj.get('xAxisLabel'); yTitle = dataObj.get('yAxisLabel'); - axisTitle = new AxisTitle($('.vis-wrapper')[0], xTitle, yTitle); + secondaryYTitle = dataObj.get('secondYAxisLabel'); }); }); @@ -106,14 +159,16 @@ define(function (require) { 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); + expect(el.select('.secondary-y-axis-title').selectAll('svg')[0].length).to.be(0); }); it('should append a g element to the svg', function () { @@ -124,6 +179,26 @@ define(function (require) { 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/test/unit/specs/vislib/lib/chart_title.js b/test/unit/specs/vislib/lib/chart_title.js index 6698d5a75ab23..bd441149135de 100644 --- a/test/unit/specs/vislib/lib/chart_title.js +++ b/test/unit/specs/vislib/lib/chart_title.js @@ -8,6 +8,7 @@ define(function (require) { describe('Vislib ChartTitle Class Test Suite', function () { var ChartTitle; var Data; + var SingleYAxisStrategy; var chartTitle; var el; var dataObj; @@ -78,6 +79,7 @@ define(function (require) { inject(function (d3, Private) { ChartTitle = Private(require('components/vislib/lib/chart_title')); Data = Private(require('components/vislib/lib/data')); + SingleYAxisStrategy = Private(require('components/vislib/lib/_single_y_axis_strategy')); el = d3.select('body').append('div') .attr('class', 'vis-wrapper') @@ -87,7 +89,7 @@ define(function (require) { .attr('class', 'chart-title') .style('height', '20px'); - dataObj = new Data(data, {}); + dataObj = new Data(data, {}, new SingleYAxisStrategy()); chartTitle = new ChartTitle($('.vis-wrapper')[0], 'rows'); }); }); diff --git a/test/unit/specs/vislib/lib/data.js b/test/unit/specs/vislib/lib/data.js index 10e4e70377035..8d2f1e7470f36 100644 --- a/test/unit/specs/vislib/lib/data.js +++ b/test/unit/specs/vislib/lib/data.js @@ -3,8 +3,12 @@ define(function (require) { var _ = require('lodash'); var Data; + var SingleYAxisStrategy; + var DualYAxisStrategy; var dataSeries = require('vislib_fixtures/mock_data/date_histogram/_series'); + var dualAxisDataSeries = require('vislib_fixtures/mock_data/date_histogram/_dual_axis_series'); var dataSeriesNeg = require('vislib_fixtures/mock_data/date_histogram/_series_neg'); + var dualAxisDataSeriesNeg = require('vislib_fixtures/mock_data/date_histogram/_dual_axis_series_neg'); var dataStacked = require('vislib_fixtures/mock_data/stacked/_stacked'); var seriesData = { @@ -17,6 +21,22 @@ define(function (require) { ] }; + var 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 + } + ] + }; + var rowsData = { 'rows': [ { @@ -107,6 +127,8 @@ define(function (require) { module('DataFactory'); inject(function (Private) { + SingleYAxisStrategy = Private(require('components/vislib/lib/_single_y_axis_strategy')); + DualYAxisStrategy = Private(require('components/vislib/lib/_dual_y_axis_strategy')); Data = Private(require('components/vislib/lib/data')); }); }); @@ -117,9 +139,58 @@ define(function (require) { }); it('should return an object', function () { - var rowIn = new Data(rowsData, {}); + var rowIn = new Data(rowsData, {}, new SingleYAxisStrategy()); expect(_.isObject(rowIn)).to.be(true); }); + + it('should decorate the values with false if there is no secondary Axis', function () { + var 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' + }; + var modifiedData = new Data(seriesDataWithoutLabelInSeries, {}, new SingleYAxisStrategy()); + _.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 () { + var seriesDataWithoutLabelInSeries = { + 'label': '', + 'series': [ + { + 'label': '', + 'onSecondaryYAxis': true, + 'values': [{x: 0, y: 1}, {x: 1, y: 2}, {x: 2, y: 3}] + }, + { + 'onSecondaryYAxis': false, + 'values': [{x:10, y:11}, {x:11, y:12}, {x:12, y:13}] + } + ], + 'yAxisLabel': 'customLabel' + }; + var modifiedData = new Data(seriesDataWithoutLabelInSeries, {}, new DualYAxisStrategy()); + _.map(modifiedData.data.series[0].values, function (value) { + expect(value.belongsToSecondaryYAxis).to.be(true); + }); + _.map(modifiedData.data.series[1].values, function (value) { + expect(value.belongsToSecondaryYAxis).to.be(false); + }); + }); + }); describe('_removeZeroSlices', function () { @@ -135,7 +206,7 @@ define(function (require) { }; beforeEach(function () { - data = new Data(pieData, {}); + data = new Data(pieData, {}, new SingleYAxisStrategy()); data._removeZeroSlices(pieData.slices); }); @@ -145,7 +216,7 @@ define(function (require) { }); }); - describe('Data.flatten', function () { + describe('Data.flatten for single y axis', function () { var serIn; var rowIn; var colIn; @@ -154,16 +225,18 @@ define(function (require) { var colOut; beforeEach(function () { - serIn = new Data(seriesData, {}); - rowIn = new Data(rowsData, {}); - colIn = new Data(colsData, {}); - serOut = serIn.flatten(); - rowOut = rowIn.flatten(); - colOut = colIn.flatten(); + serIn = new Data(seriesData, {}, new SingleYAxisStrategy()); + rowIn = new Data(rowsData, {}, new SingleYAxisStrategy()); + colIn = new Data(colsData, {}, new SingleYAxisStrategy()); + 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)); @@ -172,18 +245,60 @@ define(function (require) { function testLength(inputData) { return function () { - var data = new Data(inputData, {}); + var data = new Data(inputData, {}, new SingleYAxisStrategy()); var len = _.reduce(data.chartData(), function (sum, chart) { return sum + chart.series.reduce(function (sum, series) { return sum + series.values.length; }, 0); }, 0); - expect(data.flatten()).to.have.length(len); + expect(data._flatten()).to.have.length(len); }; } }); + describe('Data.flatten for dual y axis', function () { + var serIn; + var rowIn; + var colIn; + var serOutPrimary; + var serOutSecondary; + var rowOutPrimary; + var rowOutSecondary; + var colOutPrimary; + var colOutSecondary; + + beforeEach(function () { + serIn = new Data(seriesData, {}, new DualYAxisStrategy()); + rowIn = new Data(rowsData, {}, new DualYAxisStrategy()); + colIn = new Data(colsData, {}, 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 () { + var data = new Data(seriesDataWithDualAxis, {}, new DualYAxisStrategy()); + var primaryChartLength = data.chartData()[0].series[0].values.length; + var 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 () { var visData; var visDataNeg; @@ -193,9 +308,9 @@ define(function (require) { var minValueStacked = 15; beforeEach(function () { - visData = new Data(dataSeries, {}); - visDataNeg = new Data(dataSeriesNeg, {}); - visDataStacked = new Data(dataStacked, { type: 'histogram' }); + visData = new Data(dataSeries, {}, new SingleYAxisStrategy()); + visDataNeg = new Data(dataSeriesNeg, {}, new SingleYAxisStrategy()); + visDataStacked = new Data(dataStacked, { type: 'histogram' }, new SingleYAxisStrategy()); }); // The first value in the time series is less than the min date in the @@ -221,6 +336,32 @@ define(function (require) { }); }); + describe('getSecondYMin method', function () { + var visData; + var visDataNeg; + var visDataStacked; + var minValue = 4; + var secondMinValue = 400; + var minValueNeg = -41; + var secondMinValueNeg = -4100; + + beforeEach(function () { + visData = new Data(dualAxisDataSeries, {}, new DualYAxisStrategy()); + visDataNeg = new Data(dualAxisDataSeriesNeg, {}, 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 () { var visData; var visDataNeg; @@ -230,9 +371,9 @@ define(function (require) { var maxValueStacked = 115; beforeEach(function () { - visData = new Data(dataSeries, {}); - visDataNeg = new Data(dataSeriesNeg, {}); - visDataStacked = new Data(dataStacked, { type: 'histogram' }); + visData = new Data(dataSeries, {}, new SingleYAxisStrategy()); + visDataNeg = new Data(dataSeriesNeg, {}, new SingleYAxisStrategy()); + visDataStacked = new Data(dataStacked, { type: 'histogram' }, new SingleYAxisStrategy()); }); // The first value in the time series is less than the min date in the @@ -258,5 +399,32 @@ define(function (require) { }); }); + describe('getSecondYMax method', function () { + var visData; + var visDataNeg; + var visDataStacked; + var maxValue = 41; + var secondMaxValue = 4100; + var maxValueNeg = -4; + var secondMaxValueNeg = -400; + var maxValueStacked = 115; + + beforeEach(function () { + visData = new Data(dualAxisDataSeries, {}, new DualYAxisStrategy()); + visDataNeg = new Data(dualAxisDataSeriesNeg, {}, 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); + }); + + }); + }); }); diff --git a/test/unit/specs/vislib/lib/layout/layout.js b/test/unit/specs/vislib/lib/layout/layout.js index cea1cf0ba2983..d6998220ce1b7 100644 --- a/test/unit/specs/vislib/lib/layout/layout.js +++ b/test/unit/specs/vislib/lib/layout/layout.js @@ -53,13 +53,16 @@ define(function (require) { 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('.legend-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/test/unit/specs/vislib/lib/layout/splits/column_chart/splits.js b/test/unit/specs/vislib/lib/layout/splits/column_chart/splits.js index c69461440c342..9cbdf5f3ffdd2 100644 --- a/test/unit/specs/vislib/lib/layout/splits/column_chart/splits.js +++ b/test/unit/specs/vislib/lib/layout/splits/column_chart/splits.js @@ -12,7 +12,7 @@ define(function (require) { var chartSplit; var chartTitleSplit; var xAxisSplit; - var yAxisSplit; + var YAxisSplit; var el; var data = { rows: [ @@ -147,7 +147,7 @@ define(function (require) { chartSplit = Private(require('components/vislib/lib/layout/splits/column_chart/chart_split')); chartTitleSplit = Private(require('components/vislib/lib/layout/splits/column_chart/chart_title_split')); xAxisSplit = Private(require('components/vislib/lib/layout/splits/column_chart/x_axis_split')); - yAxisSplit = Private(require('components/vislib/lib/layout/splits/column_chart/y_axis_split')); + YAxisSplit = Private(require('components/vislib/lib/layout/splits/column_chart/y_axis_split')); el = d3.select('body').append('div') .attr('class', 'visualization') @@ -257,7 +257,7 @@ define(function (require) { .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/test/unit/specs/vislib/lib/x_axis.js b/test/unit/specs/vislib/lib/x_axis.js index de5ab11167751..d4be831bb6b65 100644 --- a/test/unit/specs/vislib/lib/x_axis.js +++ b/test/unit/specs/vislib/lib/x_axis.js @@ -8,6 +8,7 @@ define(function (require) { describe('Vislib xAxis Class Test Suite', function () { var XAxis; var Data; + var SingleYAxisStrategy; var xAxis; var el; var fixture; @@ -81,6 +82,7 @@ define(function (require) { beforeEach(function () { inject(function (d3, Private) { Data = Private(require('components/vislib/lib/data')); + SingleYAxisStrategy = Private(require('components/vislib/lib/_single_y_axis_strategy')); XAxis = Private(require('components/vislib/lib/x_axis')); el = d3.select('body').append('div') @@ -90,7 +92,7 @@ define(function (require) { fixture = el.append('div') .attr('class', 'x-axis-div'); - dataObj = new Data(data, {}); + dataObj = new Data(data, {}, new SingleYAxisStrategy()); xAxis = new XAxis({ el: $('.x-axis-div')[0], xValues: dataObj.xValues(), diff --git a/test/unit/specs/vislib/lib/y_axis.js b/test/unit/specs/vislib/lib/y_axis.js index 0d448110778f4..5eb78409d21ed 100644 --- a/test/unit/specs/vislib/lib/y_axis.js +++ b/test/unit/specs/vislib/lib/y_axis.js @@ -5,6 +5,7 @@ define(function (require) { var YAxis; var Data; + var SingleYAxisStrategy; var el; var buildYAxis; var yAxis; @@ -69,13 +70,15 @@ define(function (require) { var dataObj = new Data(data, { defaultYMin: true - }); + }, new SingleYAxisStrategy()); buildYAxis = function (params) { return new YAxis(_.merge({}, params, { 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, @@ -95,6 +98,7 @@ define(function (require) { beforeEach(inject(function (Private, _d3_) { d3Provider = _d3_; + SingleYAxisStrategy = Private(require('components/vislib/lib/_single_y_axis_strategy')); Data = Private(require('components/vislib/lib/data')); YAxis = Private(require('components/vislib/lib/y_axis')); @@ -114,15 +118,19 @@ define(function (require) { }); 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/test/unit/specs/vislib/visualizations/chart.js b/test/unit/specs/vislib/visualizations/chart.js index ead142115df4c..5aaeed4b131e7 100644 --- a/test/unit/specs/vislib/visualizations/chart.js +++ b/test/unit/specs/vislib/visualizations/chart.js @@ -8,6 +8,7 @@ define(function (require) { var ColumnChart; var Chart; var Data; + var SingleYAxisStrategy; var Vis; var chartData = {}; var vis; @@ -88,6 +89,7 @@ define(function (require) { inject(function (d3, Private) { Vis = Private(require('components/vislib/vis')); Data = Private(require('components/vislib/lib/data')); + SingleYAxisStrategy = Private(require('components/vislib/lib/_single_y_axis_strategy')); ColumnChart = Private(require('components/vislib/visualizations/column_chart')); Chart = Private(require('components/vislib/visualizations/_chart')); @@ -102,7 +104,7 @@ define(function (require) { }; vis = new Vis(el[0][0], config); - vis.data = new Data(data, config); + vis.data = new Data(data, config, new SingleYAxisStrategy()); myChart = new ColumnChart(vis, el, chartData); });