diff --git a/src/ui/public/vislib/__tests__/lib/x_axis.js b/src/ui/public/vislib/__tests__/lib/x_axis.js index 9d4e839b01b0f..5a1b2af35a3bc 100644 --- a/src/ui/public/vislib/__tests__/lib/x_axis.js +++ b/src/ui/public/vislib/__tests__/lib/x_axis.js @@ -93,6 +93,7 @@ describe('Vislib xAxis Class Test Suite', function () { dataObj = new Data(data, {}, persistedState); xAxis = new Axis({ + type: 'category', el: $('.x-axis-div')[0], xValues: dataObj.xValues(), ordered: dataObj.get('ordered'), @@ -217,7 +218,7 @@ describe('Vislib xAxis Class Test Suite', function () { }); it('should create an xAxisFormatter function on the xAxis class', function () { - expect(_.isFunction(xAxis.xAxisFormatter)).to.be(true); + expect(_.isFunction(xAxis.axisFormatter)).to.be(true); }); }); diff --git a/src/ui/public/vislib/lib/axis.js b/src/ui/public/vislib/lib/axis.js index dc6e433ecb132..84b2a10b40f14 100644 --- a/src/ui/public/vislib/lib/axis.js +++ b/src/ui/public/vislib/lib/axis.js @@ -3,6 +3,8 @@ import $ from 'jquery'; import _ from 'lodash'; import moment from 'moment'; import VislibLibErrorHandlerProvider from 'ui/vislib/lib/_error_handler'; +import errors from 'ui/errors'; + export default function AxisFactory(Private) { const ErrorHandler = Private(VislibLibErrorHandlerProvider); @@ -21,9 +23,13 @@ export default function AxisFactory(Private) { this.el = args.el; this.xValues = args.xValues; this.ordered = args.ordered; - this.xAxisFormatter = args.xAxisFormatter; + this.axisFormatter = args.type === 'category' ? args.xAxisFormatter : args.yAxisFormatter; this.expandLastBucket = args.expandLastBucket == null ? true : args.expandLastBucket; this._attr = _.defaults(args._attr || {}); + this.scale = null; + this.domain = [args.yMin, args.yMax]; + this.elSelector = args.type === 'category' ? '.x-axis-div' : '.y-axis-div'; + this.type = args.type; } /** @@ -33,7 +39,7 @@ export default function AxisFactory(Private) { * @returns {D3.UpdateSelection} Appends x axis to visualization */ render() { - d3.select(this.el).selectAll('.x-axis-div').call(this.draw()); + d3.select(this.el).selectAll(this.elSelector).call(this.draw()); }; /** @@ -221,7 +227,7 @@ export default function AxisFactory(Private) { this.xAxis = d3.svg.axis() .scale(this.xScale) .ticks(10) - .tickFormat(this.xAxisFormatter) + .tickFormat(this.axisFormatter) .orient('bottom'); }; @@ -234,6 +240,9 @@ export default function AxisFactory(Private) { draw() { const self = this; this._attr.isRotated = false; + const margin = this._attr.margin; + const mode = this._attr.mode; + const isWiggleOrSilhouette = (mode === 'wiggle' || mode === 'silhouette'); return function (selection) { const n = selection[0].length; @@ -247,21 +256,48 @@ export default function AxisFactory(Private) { const width = parentWidth / n; const height = $(this.parentElement).height(); - self.validateWidthandHeight(width, height); + /* + const width = $(el).parent().width(); + const height = $(el).height(); + */ + const adjustedHeight = height - margin.top - margin.bottom; - self.getXAxis(width); + + self.validateWidthandHeight(width, height); const svg = div.append('svg') .attr('width', width) .attr('height', height); - svg.append('g') - .attr('class', 'x axis') - .attr('transform', 'translate(0,0)') - .call(self.xAxis); + if (self.type === 'category') { + self.getXAxis(width); + svg.append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0,0)') + .call(self.xAxis); + } else { + const yAxis = self.getYAxis(adjustedHeight); + if (!isWiggleOrSilhouette) { + svg.append('g') + .attr('class', 'y axis') + .attr('transform', 'translate(' + (width - 2) + ',' + margin.top + ')') + .call(yAxis); + + const container = svg.select('g.y.axis').node(); + if (container) { + const cWidth = Math.max(width, container.getBBox().width); + svg.attr('width', cWidth); + svg.select('g') + .attr('transform', 'translate(' + (cWidth - 2) + ',' + margin.top + ')'); + } + } + + } }); - selection.call(self.filterOrRotate()); + if (self.type === 'category') { + selection.call(self.filterOrRotate()); + } }; }; @@ -346,6 +382,153 @@ export default function AxisFactory(Private) { }; }; + _isPercentage() { + return (this._attr.mode === 'percentage'); + }; + + _isUserDefined() { + return (this._attr.setYExtents); + }; + + _isYExtents() { + return (this._attr.defaultYExtents); + }; + + _validateUserExtents(domain) { + const self = this; + + return domain.map(function (val) { + val = parseInt(val, 10); + + if (isNaN(val)) throw new Error(val + ' is not a valid number'); + if (self._isPercentage() && self._attr.setYExtents) return val / 100; + return val; + }); + }; + + _getExtents(domain) { + const min = domain[0]; + const max = domain[1]; + + if (this._isUserDefined()) return this._validateUserExtents(domain); + if (this._isYExtents()) return domain; + if (this._attr.scale === 'log') return this._logDomain(min, max); // Negative values cannot be displayed with a log scale. + if (!this._isYExtents() && !this._isUserDefined()) return [Math.min(0, min), Math.max(0, max)]; + return domain; + }; + + _throwCustomError(message) { + throw new Error(message); + }; + + _throwLogScaleValuesError() { + throw new errors.InvalidLogScaleValues(); + }; + + /** + * Returns the appropriate D3 scale + * + * @param fnName {String} D3 scale + * @returns {*} + */ + _getScaleType(fnName) { + if (fnName === 'square root') fnName = 'sqrt'; // Rename 'square root' to 'sqrt' + fnName = fnName || 'linear'; + + if (typeof d3.scale[fnName] !== 'function') return this._throwCustomError('YAxis.getScaleType: ' + fnName + ' is not a function'); + + return d3.scale[fnName](); + }; + + /** + * Return the domain for log scale, i.e. the extent of the log scale. + * Log scales must begin at 1 since the log(0) = -Infinity + * + * @param {Number} min + * @param {Number} max + * @returns {Array} + */ + _logDomain(min, max) { + if (min < 0 || max < 0) return this._throwLogScaleValuesError(); + return [1, max]; + }; + + /** + * Creates the d3 y scale function + * + * @method getYScale + * @param height {Number} DOM Element height + * @returns {D3.Scale.QuantitiveScale|*} D3 yScale function + */ + getYScale(height) { + const scale = this._getScaleType(this._attr.scale); + const domain = this._getExtents(this.domain); + + this.yScale = scale + .domain(domain) + .range([height, 0]); + + if (!this._isUserDefined()) this.yScale.nice(); // round extents when not user defined + // Prevents bars from going off the chart when the y extents are within the domain range + if (this._attr.type === 'histogram') this.yScale.clamp(true); + return this.yScale; + }; + + getScaleType() { + return this._attr.scale; + }; + + tickFormat() { + const isPercentage = this._attr.mode === 'percentage'; + if (isPercentage) return d3.format('%'); + if (this.axisFormatter) return this.axisFormatter; + return d3.format('n'); + }; + + _validateYScale(yScale) { + if (!yScale || _.isNaN(yScale)) throw new Error('yScale is ' + yScale); + }; + + /** + * Creates the d3 y axis function + * + * @method getYAxis + * @param height {Number} DOM Element height + * @returns {D3.Svg.Axis|*} D3 yAxis function + */ + getYAxis(height) { + const yScale = this.getYScale(height); + this._validateYScale(yScale); + + // Create the d3 yAxis function + this.yAxis = d3.svg.axis() + .scale(yScale) + .tickFormat(this.tickFormat(this.domain)) + .ticks(this.tickScale(height)) + .orient('left'); + + return this.yAxis; + }; + + /** + * Create a tick scale for the y axis that modifies the number of ticks + * based on the height of the wrapping DOM element + * Avoid using even numbers in the yTickScale.range + * Causes the top most tickValue in the chart to be missing + * + * @method tickScale + * @param height {Number} DOM element height + * @returns {number} Number of y axis ticks + */ + tickScale(height) { + const yTickScale = d3.scale.linear() + .clamp(true) + .domain([20, 40, 1000]) + .range([0, 3, 11]); + + return Math.ceil(yTickScale(height)); + }; + /** * Returns a string that is truncated to fit size * @@ -404,7 +587,7 @@ export default function AxisFactory(Private) { if ((startX + halfWidth) < myX && maxW > (myX + halfWidth)) { startX = myX + halfWidth; - return self.xAxisFormatter(d); + return self.axisFormatter(d); } else { d3.select(this.parentNode).remove(); } diff --git a/src/ui/public/vislib/lib/handler/types/point_series.js b/src/ui/public/vislib/lib/handler/types/point_series.js index 19fbfc2d55173..40aa0078c28f6 100644 --- a/src/ui/public/vislib/lib/handler/types/point_series.js +++ b/src/ui/public/vislib/lib/handler/types/point_series.js @@ -2,7 +2,6 @@ import VislibComponentsZeroInjectionInjectZerosProvider from 'ui/vislib/componen import VislibLibHandlerHandlerProvider from 'ui/vislib/lib/handler/handler'; import VislibLibDataProvider from 'ui/vislib/lib/data'; import VislibLibAxisProvider from 'ui/vislib/lib/axis'; -import VislibLibYAxisProvider from 'ui/vislib/lib/y_axis'; import VislibLibAxisTitleProvider from 'ui/vislib/lib/axis_title'; import VislibLibChartTitleProvider from 'ui/vislib/lib/chart_title'; import VislibLibAlertsProvider from 'ui/vislib/lib/alerts'; @@ -12,7 +11,6 @@ export default function ColumnHandler(Private) { const Handler = Private(VislibLibHandlerHandlerProvider); const Data = Private(VislibLibDataProvider); const Axis = Private(VislibLibAxisProvider); - const YAxis = Private(VislibLibYAxisProvider); const AxisTitle = Private(VislibLibAxisTitleProvider); const ChartTitle = Private(VislibLibChartTitleProvider); const Alerts = Private(VislibLibAlertsProvider); @@ -40,6 +38,7 @@ export default function ColumnHandler(Private) { axisTitle: new AxisTitle(vis.el, data.get('xAxisLabel'), data.get('yAxisLabel')), chartTitle: new ChartTitle(vis.el), xAxis: new Axis({ + type : 'category', el : vis.el, xValues : data.xValues(), ordered : data.get('ordered'), @@ -48,7 +47,8 @@ export default function ColumnHandler(Private) { _attr : vis._attr }), alerts: new Alerts(vis, data, opts.alerts), - yAxis: new YAxis({ + yAxis: new Axis({ + type : 'value', el : vis.el, yMin : isUserDefinedYAxis ? vis._attr.yAxis.min : data.getYMin(), yMax : isUserDefinedYAxis ? vis._attr.yAxis.max : data.getYMax(), @@ -56,7 +56,6 @@ export default function ColumnHandler(Private) { _attr: vis._attr }) }); - }; }