diff --git a/src/ui/public/vislib/lib/axis.js b/src/ui/public/vislib/lib/axis.js index 84b2a10b40f14..f00b211de4fdf 100644 --- a/src/ui/public/vislib/lib/axis.js +++ b/src/ui/public/vislib/lib/axis.js @@ -1,695 +1,227 @@ import d3 from 'd3'; -import $ from 'jquery'; import _ from 'lodash'; -import moment from 'moment'; -import VislibLibErrorHandlerProvider from 'ui/vislib/lib/_error_handler'; -import errors from 'ui/errors'; +import $ from 'jquery'; +import ErrorHandlerProvider from 'ui/vislib/lib/_error_handler'; +import AxisTitleProvider from 'ui/vislib/lib/axis_title'; +import AxisLabelsProvider from 'ui/vislib/lib/axis_labels'; +import AxisScaleProvider from 'ui/vislib/lib/axis_scale'; export default function AxisFactory(Private) { - - const ErrorHandler = Private(VislibLibErrorHandlerProvider); - + const ErrorHandler = Private(ErrorHandlerProvider); + const AxisTitle = Private(AxisTitleProvider); + const AxisLabels = Private(AxisLabelsProvider); + const AxisScale = Private(AxisScaleProvider); + const defaults = { + show: true, + type: 'value', + elSelector: '.axis-wrapper-{pos} .axis-div', + position: 'left', + axisFormatter: null, // TODO: create default axis formatter + scale: 'linear', + expandLastBucket: true, //TODO: rename ... bucket has nothing to do with vis + inverted: false, + style: { + color: '#ddd', + lineWidth: '1px', + opacity: 1, + tickColor: '#ddd', + tickWidth: '1px', + tickLength: '6px' + } + }; + + const categoryDefaults = { + type: 'category', + position: 'bottom', + labels: { + rotate: 0, + rotateAnchor: 'end', + filter: true + } + }; /** - * Adds an x axis to the visualization + * Appends y axis to the visualization * * @class Axis * @constructor - * @param args {{el: (HTMLElement), xValues: (Array), ordered: (Object|*), - * xAxisFormatter: (Function), _attr: (Object|*)}} + * @param args {{el: (HTMLElement), yMax: (Number), _attr: (Object|*)}} */ class Axis extends ErrorHandler { constructor(args) { super(); - this.el = args.el; - this.xValues = args.xValues; - this.ordered = args.ordered; - 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; + if (args.type === 'category') { + _.extend(this, defaults, categoryDefaults, args); + } else { + _.extend(this, defaults, args); + } + + this._attr = args.vis._attr; + this.elSelector = this.elSelector.replace('{pos}', this.position); + this.scale = new AxisScale(this, {scale: this.scale}); + this.axisTitle = new AxisTitle(this, this.axisTitle); + this.axisLabels = new AxisLabels(this, this.labels); } - /** - * Renders the x axis - * - * @method render - * @returns {D3.UpdateSelection} Appends x axis to visualization - */ render() { - d3.select(this.el).selectAll(this.elSelector).call(this.draw()); - }; - - /** - * Returns d3 x axis scale function. - * If time, return time scale, else return d3 ordinal scale for nominal data - * - * @method getScale - * @returns {*} D3 scale function - */ - getScale() { - const ordered = this.ordered; - - if (ordered && ordered.date) { - return d3.time.scale.utc(); - } - return d3.scale.ordinal(); - }; - - /** - * Add domain to the x axis scale. - * if time, return a time domain, and calculate the min date, max date, and time interval - * else, return a nominal (d3.scale.ordinal) domain, i.e. array of x axis values - * - * @method getDomain - * @param scale {Function} D3 scale - * @returns {*} D3 scale function - */ - getDomain(scale) { - const ordered = this.ordered; - - if (ordered && ordered.date) { - return this.getTimeDomain(scale, this.xValues); - } - return this.getOrdinalDomain(scale, this.xValues); - }; - - /** - * Returns D3 time domain - * - * @method getTimeDomain - * @param scale {Function} D3 scale function - * @param data {Array} - * @returns {*} D3 scale function - */ - getTimeDomain(scale, data) { - return scale.domain([this.minExtent(data), this.maxExtent(data)]); - }; - - minExtent(data) { - return this._calculateExtent(data || this.xValues, 'min'); - }; - - maxExtent(data) { - return this._calculateExtent(data || this.xValues, 'max'); - }; - - /** - * - * @param data - * @param extent - */ - _calculateExtent(data, extent) { - const ordered = this.ordered; - const opts = [ordered[extent]]; - - let point = d3[extent](data); - if (this.expandLastBucket && extent === 'max') { - point = this.addInterval(point); - } - opts.push(point); - - return d3[extent](opts.reduce(function (opts, v) { - if (!_.isNumber(v)) v = +v; - if (!isNaN(v)) opts.push(v); - return opts; - }, [])); - }; - - /** - * Add the interval to a point on the x axis, - * this properly adds dates if needed. - * - * @param {number} x - a value on the x-axis - * @returns {number} - x + the ordered interval - */ - addInterval(x) { - return this.modByInterval(x, +1); - }; - - /** - * Subtract the interval to a point on the x axis, - * this properly subtracts dates if needed. - * - * @param {number} x - a value on the x-axis - * @returns {number} - x - the ordered interval - */ - subtractInterval(x) { - return this.modByInterval(x, -1); - }; - - /** - * Modify the x value by n intervals, properly - * handling dates if needed. - * - * @param {number} x - a value on the x-axis - * @param {number} n - the number of intervals - * @returns {number} - x + n intervals - */ - modByInterval(x, n) { - const ordered = this.ordered; - if (!ordered) return x; - const interval = ordered.interval; - if (!interval) return x; - - if (!ordered.date) { - return x += (ordered.interval * n); - } + d3.select(this.vis.el).selectAll(this.elSelector).call(this.draw()); + } - const y = moment(x); - const method = n > 0 ? 'add' : 'subtract'; + isHorizontal() { + return (this.position === 'top' || this.position === 'bottom'); + } - _.times(Math.abs(n), function () { - y[method](interval); - }); + getAxis(length) { + const scale = this.scale.getScale(length); - return y.valueOf(); - }; - - /** - * Return a nominal(d3 ordinal) domain - * - * @method getOrdinalDomain - * @param scale {Function} D3 scale function - * @param xValues {Array} Array of x axis values - * @returns {*} D3 scale function - */ - getOrdinalDomain(scale, xValues) { - return scale.domain(xValues); - }; - - /** - * Return the range for the x axis scale - * if time, return a normal range, else if nominal, return rangeBands with a default (0.1) spacer specified - * - * @method getRange - * @param scale {Function} D3 scale function - * @param width {Number} HTML Element width - * @returns {*} D3 scale function - */ - getRange(domain, width) { - const ordered = this.ordered; - - if (ordered && ordered.date) { - return domain.range([0, width]); - } - return domain.rangeBands([0, width], 0.1); - }; - - /** - * Return the x axis scale - * - * @method getXScale - * @param width {Number} HTML Element width - * @returns {*} D3 x scale function - */ - getXScale(width) { - const domain = this.getDomain(this.getScale()); - - return this.getRange(domain, width); - }; - - /** - * Creates d3 xAxis function - * - * @method getXAxis - * @param width {Number} HTML Element width - */ - getXAxis(width) { - this.xScale = this.getXScale(width); - - if (!this.xScale || _.isNaN(this.xScale)) { - throw new Error('xScale is ' + this.xScale); - } + return d3.svg.axis() + .scale(scale) + .tickFormat(this.tickFormat(this.domain)) + .ticks(this.tickScale(length)) + .orient(this.position); + } - this.xAxis = d3.svg.axis() - .scale(this.xScale) - .ticks(10) - .tickFormat(this.axisFormatter) - .orient('bottom'); - }; - - /** - * Renders the x axis - * - * @method draw - * @returns {Function} Renders the x axis to a D3 selection - */ - draw() { - const self = this; - this._attr.isRotated = false; - const margin = this._attr.margin; - const mode = this._attr.mode; - const isWiggleOrSilhouette = (mode === 'wiggle' || mode === 'silhouette'); + getScale() { + return this.scale.scale; + } - return function (selection) { - const n = selection[0].length; - const parentWidth = $(self.el) - .find('.x-axis-div-wrapper') - .width(); + addInterval(interval) { + return this.scale.addInterval(interval); + } - selection.each(function () { + substractInterval(interval) { + return this.scale.substractInterval(interval); + } - const div = d3.select(this); - const width = parentWidth / n; - const height = $(this.parentElement).height(); + tickScale(length) { + const yTickScale = d3.scale.linear() + .clamp(true) + .domain([20, 40, 1000]) + .range([0, 3, 11]); - /* - const width = $(el).parent().width(); - const height = $(el).height(); - */ - const adjustedHeight = height - margin.top - margin.bottom; + return Math.ceil(yTickScale(length)); + } + tickFormat() { + if (this.axisFormatter) return this.axisFormatter; + if (this.isPercentage()) return d3.format('%'); + return d3.format('n'); + } - self.validateWidthandHeight(width, height); + getLength(el, n) { + if (this.isHorizontal()) { + return $(el).parent().width() / n - this._attr.margin.left - this._attr.margin.right - 50; + } + return $(el).parent().height() / n - this._attr.margin.top - this._attr.margin.bottom; + } - const svg = div.append('svg') - .attr('width', width) - .attr('height', height); + updateXaxisHeight() { + const self = this; + const selection = d3.select(this.vis.el).selectAll('.vis-wrapper'); - 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.each(function () { + const visEl = d3.select(this); - if (self.type === 'category') { - selection.call(self.filterOrRotate()); + if (visEl.select('.inner-spacer-block').node() === null) { + visEl.selectAll('.y-axis-spacer-block') + .append('div') + .attr('class', 'inner-spacer-block'); } - }; - }; - - /** - * Returns a function that evaluates scale type and - * applies filter to tick labels on time scales - * rotates and truncates tick labels on nominal/ordinal scales - * - * @method filterOrRotate - * @returns {Function} Filters or rotates x axis tick labels - */ - filterOrRotate() { - const self = this; - const ordered = self.ordered; - return function (selection) { - selection.each(function () { - const axis = d3.select(this); - if (ordered && ordered.date) { - axis.call(self.filterAxisLabels()); - } else { - axis.call(self.rotateAxisLabels()); - } - }); - - self.updateXaxisHeight(); - - selection.call(self.fitTitles()); + const height = visEl.select(`.axis-wrapper-${self.position}`).style('height'); + visEl.selectAll(`.y-axis-spacer-block-${self.position} .inner-spacer-block`).style('height', height); + }); + } - }; - }; - - /** - * Rotate the axis tick labels within selection - * - * @returns {Function} Rotates x axis tick labels of a D3 selection - */ - rotateAxisLabels() { + adjustSize() { const self = this; - const barWidth = self.xScale.rangeBand(); - const maxRotatedLength = 120; const xAxisPadding = 15; - const lengths = []; - self._attr.isRotated = false; return function (selection) { const text = selection.selectAll('.tick text'); + const lengths = []; text.each(function textWidths() { - lengths.push(d3.select(this).node().getBBox().width); + lengths.push((() => { + if (self.isHorizontal()) { + return d3.select(this.parentNode).node().getBBox().height; + } else { + return d3.select(this.parentNode).node().getBBox().width; + } + })()); }); - const length = _.max(lengths); - self._attr.xAxisLabelHt = length + xAxisPadding; - - // if longer than bar width, rotate - if (length > barWidth) { - self._attr.isRotated = true; - } - - // if longer than maxRotatedLength, truncate - if (length > maxRotatedLength) { - self._attr.xAxisLabelHt = maxRotatedLength; - } - - if (self._attr.isRotated) { - text - .text(function truncate() { - return self.truncateLabel(this, self._attr.xAxisLabelHt); - }) - .style('text-anchor', 'end') - .attr('dx', '-.8em') - .attr('dy', '-.60em') - .attr('transform', function rotate() { - return 'rotate(-90)'; - }) - .append('title') - .text(text => text); - - selection.select('svg') - .attr('height', self._attr.xAxisLabelHt); + const length = lengths.length > 0 ? _.max(lengths) : 0; + + if (self.isHorizontal()) { + selection.attr('height', length); + self.updateXaxisHeight(); + if (self.position === 'top') { + selection.select('g') + .attr('transform', `translate(0, ${length - parseInt(self.style.lineWidth)})`); + selection.select('path') + .attr('transform', 'translate(1,0)'); + } + } else { + selection.attr('width', length + xAxisPadding); + if (self.position === 'left') { + const translateWidth = length + xAxisPadding - 2 - parseInt(self.style.lineWidth); + selection.select('g') + .attr('transform', `translate(${translateWidth},${self._attr.margin.top})`); + } } }; - }; - - _isPercentage() { - return (this._attr.mode === 'percentage'); - }; - - _isUserDefined() { - return (this._attr.setYExtents); - }; - - _isYExtents() { - return (this._attr.defaultYExtents); - }; + } - _validateUserExtents(domain) { + draw() { 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; - }; + return function (selection) { + const n = selection[0].length; + if (self.axisTitle) { + self.axisTitle.render(selection); + } + selection.each(function () { + const el = this; + const div = d3.select(el); + const width = $(el).parent().width(); + const height = $(el).height(); + const length = self.getLength(el, n); - 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]); + // Validate whether width and height are not 0 or `NaN` + self.validateWidthandHeight(width, height); - return Math.ceil(yTickScale(height)); - }; - - /** - * Returns a string that is truncated to fit size - * - * @method truncateLabel - * @param text {HTMLElement} - * @param size {Number} - * @returns {*|jQuery} - */ - truncateLabel(text, size) { - const node = d3.select(text).node(); - let str = $(node).text(); - const width = node.getBBox().width; - const chars = str.length; - const pxPerChar = width / chars; - let endChar = 0; - const ellipsesPad = 4; - - if (width > size) { - endChar = Math.floor((size / pxPerChar) - ellipsesPad); - while (str[endChar - 1] === ' ' || str[endChar - 1] === '-' || str[endChar - 1] === ',') { - endChar = endChar - 1; - } - str = str.substr(0, endChar) + '...'; - } - return str; - }; - - /** - * Filter out text labels by width and position on axis - * trims labels that would overlap each other - * or extend past left or right edges - * if prev label pos (or 0) + half of label width is < label pos - * and label pos + half width is not > width of axis - * - * @method filterAxisLabels - * @returns {Function} - */ - filterAxisLabels() { - const self = this; - let startX = 0; - let maxW; - let par; - let myX; - let myWidth; - let halfWidth; - const padding = 1.1; + const axis = self.getAxis(length); - return function (selection) { - selection.selectAll('.tick text') - .text(function (d) { - par = d3.select(this.parentNode).node(); - myX = self.xScale(d); - myWidth = par.getBBox().width * padding; - halfWidth = myWidth / 2; - maxW = $(self.el).find('.x-axis-div').width(); - - if ((startX + halfWidth) < myX && maxW > (myX + halfWidth)) { - startX = myX + halfWidth; - return self.axisFormatter(d); - } else { - d3.select(this.parentNode).remove(); - } - }); - }; - }; - - /** - * Returns a function that adjusts axis titles and - * chart title transforms to fit axis label divs. - * Sets transform of x-axis-title to fit .x-axis-title div width - * if x-axis-chart-titles, set transform of x-axis-chart-titles - * to fit .chart-title div width - * - * @method fitTitles - * @returns {Function} - */ - fitTitles() { - const visEls = $('.vis-wrapper'); - let xAxisChartTitle; - let yAxisChartTitle; - let text; - let titles; - - return function () { - - visEls.each(function () { - const visEl = d3.select(this); - const $visEl = $(this); - const xAxisTitle = $visEl.find('.x-axis-title'); - const yAxisTitle = $visEl.find('.y-axis-title'); - let titleWidth = xAxisTitle.width(); - let titleHeight = yAxisTitle.height(); - - text = visEl.select('.x-axis-title') - .select('svg') - .attr('width', titleWidth) - .select('text') - .attr('transform', 'translate(' + (titleWidth / 2) + ',11)'); - - text = visEl.select('.y-axis-title') - .select('svg') - .attr('height', titleHeight) - .select('text') - .attr('transform', 'translate(11,' + (titleHeight / 2) + ')rotate(-90)'); - - if ($visEl.find('.x-axis-chart-title').length) { - xAxisChartTitle = $visEl.find('.x-axis-chart-title'); - titleWidth = xAxisChartTitle.find('.chart-title').width(); - - titles = visEl.select('.x-axis-chart-title').selectAll('.chart-title'); - titles.each(function () { - text = d3.select(this) - .select('svg') - .attr('width', titleWidth) - .select('text') - .attr('transform', 'translate(' + (titleWidth / 2) + ',11)'); - }); - } + if (self.show) { + const svg = div.append('svg') + .attr('width', width) + .attr('height', height); - if ($visEl.find('.y-axis-chart-title').length) { - yAxisChartTitle = $visEl.find('.y-axis-chart-title'); - titleHeight = yAxisChartTitle.find('.chart-title').height(); - - titles = visEl.select('.y-axis-chart-title').selectAll('.chart-title'); - titles.each(function () { - text = d3.select(this) - .select('svg') - .attr('height', titleHeight) - .select('text') - .attr('transform', 'translate(11,' + (titleHeight / 2) + ')rotate(-90)'); - }); + svg.append('g') + .attr('class', `axis ${self.id}`) + .call(axis); + + const container = svg.select('g.axis').node(); + if (container) { + svg.select('path') + .style('stroke', self.style.color) + .style('stroke-width', self.style.lineWidth) + .style('stroke-opacity', self.style.opacity); + svg.selectAll('line') + .style('stroke', self.style.tickColor) + .style('stroke-width', self.style.tickWidth) + .style('stroke-opacity', self.style.opacity); + // TODO: update to be depenent on position ... + //.attr('x1', -parseInt(self.style.lineWidth) / 2) + //.attr('x2', -parseInt(self.style.lineWidth) / 2 - parseInt(self.style.tickLength)); + + if (self.axisLabels) self.axisLabels.render(svg); + svg.call(self.adjustSize()); + } } - }); - }; - }; - - /** - * Appends div to make .y-axis-spacer-block - * match height of .x-axis-wrapper - * - * @method updateXaxisHeight - */ - updateXaxisHeight() { - const selection = d3.select(this.el).selectAll('.vis-wrapper'); - - selection.each(function () { - const visEl = d3.select(this); - - if (visEl.select('.inner-spacer-block').node() === null) { - visEl.select('.y-axis-spacer-block') - .append('div') - .attr('class', 'inner-spacer-block'); - } - const xAxisHt = visEl.select('.x-axis-wrapper').style('height'); - - visEl.select('.inner-spacer-block').style('height', xAxisHt); - }); - - }; + } } return Axis; diff --git a/src/ui/public/vislib/lib/axis_labels.js b/src/ui/public/vislib/lib/axis_labels.js new file mode 100644 index 0000000000000..73e72dd59e2bd --- /dev/null +++ b/src/ui/public/vislib/lib/axis_labels.js @@ -0,0 +1,164 @@ +import d3 from 'd3'; +import $ from 'jquery'; +import _ from 'lodash'; +import ErrorHandlerProvider from 'ui/vislib/lib/_error_handler'; +export default function AxisLabelsFactory(Private) { + + const ErrorHandler = Private(ErrorHandlerProvider); + const defaults = { + show: true, + rotate: 0, + rotateAnchor: 'center', + filter: false, + color: '#ddd', + font: '"Open Sans", "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif', // TODO + fontSize: '8pt', + truncate: 100 + }; + + /** + * Appends axis title(s) to the visualization + * + * @class AxisLabels + * @constructor + * @param el {HTMLElement} DOM element + * @param xTitle {String} X-axis title + * @param yTitle {String} Y-axis title + */ + + class AxisLabels extends ErrorHandler { + constructor(axis, attr) { + super(); + _.extend(this, defaults, attr); + this.axis = axis; + + // horizontal axis with ordinal scale should have labels rotated (so we can fit more) + if (this.axis.isHorizontal() && this.axis.scale.isOrdinal()) { + this.filter = attr && attr.filter ? attr.filter : false; + this.rotate = attr && attr.rotate ? attr.rotate : 70; + } + } + + render(selection) { + selection.call(this.draw()); + }; + + rotateAxisLabels() { + const self = this; + return function (selection) { + const text = selection.selectAll('.tick text'); + + if (self.rotate) { + text + .style('text-anchor', function () { + return self.rotateAnchor === 'center' ? 'center' : 'end'; + }) + .attr('dy', function () { + if (self.axis.isHorizontal()) { + if (self.axis.position === 'top') return '-0.9em'; + else return '0.3em'; + } + return '0'; + }) + .attr('dx', function () { + return self.axis.isHorizontal() ? '-0.9em' : '0'; + }) + .attr('transform', function rotate(d, j) { + if (self.rotateAnchor === 'center') { + const coord = text[0][j].getBBox(); + const transX = ((coord.x) + (coord.width / 2)); + const transY = ((coord.y) + (coord.height / 2)); + return `rotate(${self.rotate}, ${transX}, ${transY})`; + } else { + const rotateDeg = self.axis.position === 'top' ? self.rotate : -self.rotate; + return `rotate(${rotateDeg})`; + } + }); + } + }; + }; + + truncateLabel(text, size) { + const node = d3.select(text).node(); + let str = $(node).text(); + const width = node.getBBox().width; + const chars = str.length; + const pxPerChar = width / chars; + let endChar = 0; + const ellipsesPad = 4; + + if (width > size) { + endChar = Math.floor((size / pxPerChar) - ellipsesPad); + while (str[endChar - 1] === ' ' || str[endChar - 1] === '-' || str[endChar - 1] === ',') { + endChar = endChar - 1; + } + str = str.substr(0, endChar) + '...'; + } + return str; + }; + + truncateLabels() { + const self = this; + return function (selection) { + selection.selectAll('.tick text') + .text(function () { + // TODO: add title to trancuated labels + return self.truncateLabel(this, self.truncate); + }); + }; + }; + + filterAxisLabels() { + const self = this; + let startX = 0; + let maxW; + let par; + let myX; + let myWidth; + let halfWidth; + let padding = 1.1; + + return function (selection) { + if (!self.filter) return; + + selection.selectAll('.tick text') + .text(function (d) { + par = d3.select(this.parentNode).node(); + myX = self.axis.scale.scale(d); + myWidth = par.getBBox().width * padding; + halfWidth = myWidth / 2; + maxW = $(self.axis.vis.el).find(self.axis.elSelector).width(); + + if ((startX + halfWidth) < myX && maxW > (myX + halfWidth)) { + startX = myX + halfWidth; + return self.axis.axisFormatter(d); + } else { + d3.select(this.parentNode).remove(); + } + }); + }; + }; + + draw() { + const self = this; + + return function (selection) { + selection.each(function () { + selection.selectAll('text') + .attr('style', function () { + const currentStyle = d3.select(this).attr('style'); + return `${currentStyle} font-size: ${self.fontSize};`; + }); + //.attr('x', -3 - parseInt(self.style.lineWidth) / 2 - parseInt(self.style.tickLength)); + if (!self.show) selection.selectAll('test').attr('style', 'display: none;'); + + selection.call(self.truncateLabels()); + selection.call(self.rotateAxisLabels()); + selection.call(self.filterAxisLabels()); + }); + }; + }; + } + + return AxisLabels; +}; diff --git a/src/ui/public/vislib/lib/axis_scale.js b/src/ui/public/vislib/lib/axis_scale.js new file mode 100644 index 0000000000000..adddca6bfca84 --- /dev/null +++ b/src/ui/public/vislib/lib/axis_scale.js @@ -0,0 +1,250 @@ +import d3 from 'd3'; +import $ from 'jquery'; +import _ from 'lodash'; +import moment from 'moment'; +import errors from 'ui/errors'; + +import ErrorHandlerProvider from 'ui/vislib/lib/_error_handler'; +export default function AxisScaleFactory(Private) { + + const ErrorHandler = Private(ErrorHandlerProvider); + + /** + * Appends axis title(s) to the visualization + * + * @class AxisScale + * @constructor + * @param el {HTMLElement} DOM element + * @param xTitle {String} X-axis title + * @param yTitle {String} Y-axis title + */ + class AxisScale extends ErrorHandler { + constructor(axis, attr) { + super(); + this.attr = attr; + this.axis = axis; + }; + + isPercentage() { + return (this.axis._attr.mode === 'percentage'); + }; + + isUserDefined() { + return (this.axis._attr.setYExtents); + }; + + isYExtents() { + return (this.axis._attr.defaultYExtents); + }; + + isOrdinal() { + return !!this.axis.values && (!this.isTimeDomain()); + }; + + isTimeDomain() { + return this.axis.ordered && this.axis.ordered.date; + }; + + isLogScale() { + return this.attr.scale === 'log'; + }; + + getScaleType() { + return this.attr.scale; + }; + + validateUserExtents(domain) { + return domain.map((val) => { + val = parseInt(val, 10); + + if (isNaN(val)) throw new Error(val + ' is not a valid number'); + if (this.axis.isPercentage() && this.axis._attr.setYExtents) return val / 100; + return val; + }); + }; + + /** + * Returns D3 time domain + * + * @method getTimeDomain + * @param scale {Function} D3 scale function + * @param data {Array} + * @returns {*} D3 scale function + */ + getTimeDomain(data) { + return [this.minExtent(data), this.maxExtent(data)]; + }; + + minExtent(data) { + return this.calculateExtent(data || this.axis.values, 'min'); + }; + + maxExtent(data) { + return this.calculateExtent(data || this.axis.values, 'max'); + }; + + /** + * + * @param data + * @param extent + */ + calculateExtent(data, extent) { + const ordered = this.axis.ordered; + const opts = [ordered[extent]]; + + let point = d3[extent](data); + if (this.axis.expandLastBucket && extent === 'max') { + point = this.addInterval(point); + } + opts.push(point); + + return d3[extent](opts.reduce(function (opts, v) { + if (!_.isNumber(v)) v = +v; + if (!isNaN(v)) opts.push(v); + return opts; + }, [])); + }; + + /** + * Add the interval to a point on the x axis, + * this properly adds dates if needed. + * + * @param {number} x - a value on the x-axis + * @returns {number} - x + the ordered interval + */ + addInterval(x) { + return this.modByInterval(x, +1); + }; + + /** + * Subtract the interval to a point on the x axis, + * this properly subtracts dates if needed. + * + * @param {number} x - a value on the x-axis + * @returns {number} - x - the ordered interval + */ + subtractInterval(x) { + return this.modByInterval(x, -1); + }; + + /** + * Modify the x value by n intervals, properly + * handling dates if needed. + * + * @param {number} x - a value on the x-axis + * @param {number} n - the number of intervals + * @returns {number} - x + n intervals + */ + modByInterval(x, n) { + const ordered = this.axis.ordered; + if (!ordered) return x; + const interval = ordered.interval; + if (!interval) return x; + + if (!ordered.date) { + return x += (ordered.interval * n); + } + + const y = moment(x); + const method = n > 0 ? 'add' : 'subtract'; + + _.times(Math.abs(n), function () { + y[method](interval); + }); + + return y.valueOf(); + }; + + getExtents() { + if (this.isTimeDomain()) return this.getTimeDomain(this.axis.values); + if (this.isOrdinal()) return this.axis.values; + + const min = this.axis.min || this.axis.data.getYMin(); + const max = this.axis.max || this.axis.data.getYMax(); + const domain = [min, max]; + if (this.isUserDefined()) return this.validateUserExtents(domain); + if (this.isYExtents()) return domain; + if (this.isLogScale()) return this.logDomain(min, max); + return [Math.min(0, min), Math.max(0, max)]; + }; + + getRange(length) { + if (this.axis.isHorizontal()) { + return !this.axis.inverted ? [0, length] : [length, 0]; + } else { + return this.axis.inverted ? [0, length] : [length, 0]; + } + }; + + throwCustomError(message) { + throw new Error(message); + }; + + throwLogScaleValuesError() { + throw new errors.InvalidLogScaleValues(); + }; + + /** + * 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 scale + * @param yMin + * @param yMax + * @returns {*[]} + */ + logDomain(min, max) { + if (min < 0 || max < 0) return this.throwLogScaleValuesError(); + return [1, max]; + }; + + /** + * 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 (this.isTimeDomain()) return d3.time.scale.utc(); // allow time scale + if (this.isOrdinal()) return d3.scale.ordinal(); + if (typeof d3.scale[fnName] !== 'function') return this.throwCustomError('Axis.getScaleType: ' + fnName + ' is not a function'); + + return d3.scale[fnName](); + }; + + /** + * Creates the d3 y scale function + * + * @method getscale + * @param length {Number} DOM Element height + * @returns {D3.Scale.QuantitiveScale|*} D3 scale function + */ + getScale(length) { + const scale = this.getScaleType(this.attr.scale); + const domain = this.getExtents(); + const range = this.getRange(length); + this.scale = scale.domain(domain); + if (this.isOrdinal()) { + this.scale.rangeBands(range, 0.1); + } else { + this.scale.range(range); + } + + if (!this.isUserDefined() && !this.isOrdinal() && !this.isTimeDomain()) this.scale.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.axis._attr.type === 'histogram' && this.scale.clamp) this.scale.clamp(true); + + this.validateScale(this.scale); + + return this.scale; + }; + + validateScale(scale) { + if (!scale || _.isNaN(scale)) throw new Error('scale is ' + scale); + }; + } + return AxisScale; +}; diff --git a/src/ui/public/vislib/lib/axis_title.js b/src/ui/public/vislib/lib/axis_title.js index 7fa2f54230d23..49a2ce2868b06 100644 --- a/src/ui/public/vislib/lib/axis_title.js +++ b/src/ui/public/vislib/lib/axis_title.js @@ -1,9 +1,14 @@ import d3 from 'd3'; import $ from 'jquery'; -import VislibLibErrorHandlerProvider from 'ui/vislib/lib/_error_handler'; +import _ from 'lodash'; +import ErrorHandlerProvider from 'ui/vislib/lib/_error_handler'; export default function AxisTitleFactory(Private) { - const ErrorHandler = Private(VislibLibErrorHandlerProvider); + const ErrorHandler = Private(ErrorHandlerProvider); + const defaults = { + title: '', + elSelector: '.axis-wrapper-{pos} .axis-title' + }; /** * Appends axis title(s) to the visualization @@ -15,11 +20,11 @@ export default function AxisTitleFactory(Private) { * @param yTitle {String} Y-axis title */ class AxisTitle extends ErrorHandler { - constructor(el, xTitle, yTitle) { + constructor(axis, attr) { super(); - this.el = el; - this.xTitle = xTitle; - this.yTitle = yTitle; + _.extend(this, defaults, attr); + this.axis = axis; + this.elSelector = this.elSelector.replace('{pos}', axis.position); } /** @@ -28,9 +33,8 @@ export default function AxisTitleFactory(Private) { * @method render * @returns {HTMLElement} DOM Element with axis titles */ - render() { - 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)); + render(selection) { + d3.select(this.axis.vis.el).selectAll(this.elSelector).call(this.draw()); }; /** @@ -40,7 +44,7 @@ export default function AxisTitleFactory(Private) { * @param title {String} Axis title * @returns {Function} Appends axis title to a D3 selection */ - draw(title) { + draw() { const self = this; return function (selection) { @@ -52,22 +56,31 @@ export default function AxisTitleFactory(Private) { self.validateWidthandHeight(width, height); - div.append('svg') + const svg = div.append('svg') .attr('width', width) - .attr('height', height) - .append('text') + .attr('height', height); + + + const bbox = svg.append('text') .attr('transform', function () { - if (div.attr('class') === 'x-axis-title') { + if (self.axis.isHorizontal()) { return 'translate(' + width / 2 + ',11)'; } - return 'translate(11,' + height / 2 + ')rotate(270)'; + return 'translate(11,' + height / 2 + ') rotate(270)'; }) .attr('text-anchor', 'middle') - .text(title); + .text(self.title) + .node() + .getBBox(); + + if (self.axis.isHorizontal()) { + svg.attr('height', bbox.height); + } else { + svg.attr('width', bbox.width); + } }); }; }; } - return AxisTitle; }; diff --git a/src/ui/public/vislib/lib/dispatch.js b/src/ui/public/vislib/lib/dispatch.js index 98f03a3ffdd35..f0888c6e9e46e 100644 --- a/src/ui/public/vislib/lib/dispatch.js +++ b/src/ui/public/vislib/lib/dispatch.js @@ -160,12 +160,13 @@ export default function DispatchClass(Private) { * @returns {Boolean} */ allowBrushing() { - const xAxis = this.handler.xAxis; + const xAxis = this.handler.categoryAxes[0]; + const xScale = xAxis.getScale(); // Don't allow brushing for time based charts from non-time-based indices const hasTimeField = this.handler.vis._attr.hasTimeField; - return Boolean(hasTimeField && xAxis.ordered && xAxis.xScale && _.isFunction(xAxis.xScale.invert)); - }; + return Boolean(hasTimeField && xAxis.ordered && xScale && _.isFunction(xScale.invert)); + }; /** * Determine if brushing is currently enabled @@ -185,7 +186,7 @@ export default function DispatchClass(Private) { addBrushEvent(svg) { if (!this.isBrushable()) return; - const xScale = this.handler.xAxis.xScale; + const xScale = this.handler.categoryAxes[0].getScale(); const brush = this.createBrush(xScale, svg); function brushEnd() { diff --git a/src/ui/public/vislib/lib/handler/handler.js b/src/ui/public/vislib/lib/handler/handler.js index 18aa28ab5686b..61658f1391894 100644 --- a/src/ui/public/vislib/lib/handler/handler.js +++ b/src/ui/public/vislib/lib/handler/handler.js @@ -31,8 +31,8 @@ export default function HandlerBaseClass(Private) { 'margin': {top: 10, right: 3, bottom: 5, left: 3} }); - this.xAxis = opts.xAxis; - this.yAxis = opts.yAxis; + this.categoryAxes = opts.categoryAxes; + this.valueAxes = opts.valueAxes; this.chartTitle = opts.chartTitle; this.axisTitle = opts.axisTitle; this.alerts = opts.alerts; @@ -43,10 +43,9 @@ export default function HandlerBaseClass(Private) { this.layout, this.axisTitle, this.chartTitle, - this.alerts, - this.xAxis, - this.yAxis, - ], Boolean); + this.alerts + ].concat(_.values(this.categoryAxes)) + .concat(_.values(this.valueAxes)), Boolean); // memoize so that the same function is returned every time, // allowing us to remove/re-add the same function 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 40aa0078c28f6..d6a554160528c 100644 --- a/src/ui/public/vislib/lib/handler/types/point_series.js +++ b/src/ui/public/vislib/lib/handler/types/point_series.js @@ -1,17 +1,17 @@ +import d3 from 'd3'; +import _ from 'lodash'; import VislibComponentsZeroInjectionInjectZerosProvider from 'ui/vislib/components/zero_injection/inject_zeros'; import VislibLibHandlerHandlerProvider from 'ui/vislib/lib/handler/handler'; import VislibLibDataProvider from 'ui/vislib/lib/data'; -import VislibLibAxisProvider from 'ui/vislib/lib/axis'; -import VislibLibAxisTitleProvider from 'ui/vislib/lib/axis_title'; import VislibLibChartTitleProvider from 'ui/vislib/lib/chart_title'; import VislibLibAlertsProvider from 'ui/vislib/lib/alerts'; +import VislibAxis from 'ui/vislib/lib/axis'; export default function ColumnHandler(Private) { const injectZeros = Private(VislibComponentsZeroInjectionInjectZerosProvider); const Handler = Private(VislibLibHandlerHandlerProvider); const Data = Private(VislibLibDataProvider); - const Axis = Private(VislibLibAxisProvider); - const AxisTitle = Private(VislibLibAxisTitleProvider); + const Axis = Private(VislibAxis); const ChartTitle = Private(VislibLibChartTitleProvider); const Alerts = Private(VislibLibAlertsProvider); @@ -35,27 +35,39 @@ export default function ColumnHandler(Private) { return new Handler(vis, { data: data, - 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'), - xAxisFormatter : data.get('xAxisFormatter'), - expandLastBucket : opts.expandLastBucket, - _attr : vis._attr - }), + categoryAxes: [ + new Axis({ + id: 'CategoryAxis-1', + type: 'category', + vis: vis, + data: data, + values: data.xValues(), + ordered: data.get('ordered'), + axisFormatter: data.get('xAxisFormatter'), + expandLastBucket: opts.expandLastBucket, + axisTitle: { + title: data.get('xAxisLabel') + } + }) + ], alerts: new Alerts(vis, data, opts.alerts), - yAxis: new Axis({ - type : 'value', - el : vis.el, - yMin : isUserDefinedYAxis ? vis._attr.yAxis.min : data.getYMin(), - yMax : isUserDefinedYAxis ? vis._attr.yAxis.max : data.getYMax(), - yAxisFormatter: data.get('yAxisFormatter'), - _attr: vis._attr - }) + valueAxes: [ + new Axis({ + id: 'ValueAxis-1', + type: 'value', + vis: vis, + data: data, + min : isUserDefinedYAxis ? vis._attr.yAxis.min : 0, + max : isUserDefinedYAxis ? vis._attr.yAxis.max : 0, + axisFormatter: data.get('yAxisFormatter'), + axisTitle: { + title: data.get('yAxisLabel') + } + }) + ] }); + }; } diff --git a/src/ui/public/vislib/lib/layout/splits/column_chart/x_axis_split.js b/src/ui/public/vislib/lib/layout/splits/column_chart/x_axis_split.js index 2d99d4bbd25bc..f014d8925675d 100644 --- a/src/ui/public/vislib/lib/layout/splits/column_chart/x_axis_split.js +++ b/src/ui/public/vislib/lib/layout/splits/column_chart/x_axis_split.js @@ -19,7 +19,7 @@ define(function () { }) .enter() .append('div') - .attr('class', 'x-axis-div'); + .attr('class', 'x-axis-div axis-div'); }); }; }; diff --git a/src/ui/public/vislib/lib/layout/splits/column_chart/y_axis_split.js b/src/ui/public/vislib/lib/layout/splits/column_chart/y_axis_split.js index 93d8188073a18..f60f782d7d1ce 100644 --- a/src/ui/public/vislib/lib/layout/splits/column_chart/y_axis_split.js +++ b/src/ui/public/vislib/lib/layout/splits/column_chart/y_axis_split.js @@ -1,4 +1,6 @@ import d3 from 'd3'; +import _ from 'lodash'; + define(function () { return function YAxisSplitFactory() { @@ -10,12 +12,12 @@ define(function () { // render and get bounding box width return function (selection, parent, opts) { - const yAxis = opts && opts.yAxis; + let yAxis = opts && opts.valueAxes[0]; selection.each(function () { const div = d3.select(this); - div.call(setWidth, yAxis); + //div.call(setWidth, yAxis); div.selectAll('.y-axis-div') .append('div') @@ -24,7 +26,7 @@ define(function () { }) .enter() .append('div') - .attr('class', 'y-axis-div'); + .attr('class', 'y-axis-div axis-div'); }); }; @@ -38,8 +40,10 @@ define(function () { const svg = d3.select('body') .append('svg') .attr('style', 'position:absolute; top:-10000; left:-10000'); - const width = svg.append('g') - .call(yAxis.getYAxis(height)).node().getBBox().width + padding; + let width = svg.append('g') + .call(() => { + yAxis.getAxis(height); + }).node().getBBox().width + padding; svg.remove(); el.style('width', (width + padding) + 'px'); diff --git a/src/ui/public/vislib/lib/layout/types/column_layout.js b/src/ui/public/vislib/lib/layout/types/column_layout.js index b4bd968d2ae9c..709048067c4e1 100644 --- a/src/ui/public/vislib/lib/layout/types/column_layout.js +++ b/src/ui/public/vislib/lib/layout/types/column_layout.js @@ -44,11 +44,15 @@ export default function ColumnLayoutFactory(Private) { children: [ { type: 'div', - class: 'y-axis-col', + class: 'y-axis-spacer-block y-axis-spacer-block-top' + }, + { + type: 'div', + class: 'y-axis-col axis-wrapper-left', children: [ { type: 'div', - class: 'y-axis-title' + class: 'y-axis-title axis-title' }, { type: 'div', @@ -64,7 +68,7 @@ export default function ColumnLayoutFactory(Private) { }, { type: 'div', - class: 'y-axis-spacer-block' + class: 'y-axis-spacer-block y-axis-spacer-block-bottom' } ] }, @@ -72,6 +76,26 @@ export default function ColumnLayoutFactory(Private) { type: 'div', class: 'vis-col-wrapper', children: [ + { + type: 'div', + class: 'x-axis-wrapper axis-wrapper-top', + children: [ + { + type: 'div', + class: 'x-axis-div-wrapper', + splits: xAxisSplit + }, + /*{ + type: 'div', + class: 'x-axis-chart-title', + splits: chartTitleSplit + },*/ + { + type: 'div', + class: 'x-axis-title axis-title' + } + ] + }, { type: 'div', class: 'chart-wrapper', @@ -83,7 +107,7 @@ export default function ColumnLayoutFactory(Private) { }, { type: 'div', - class: 'x-axis-wrapper', + class: 'x-axis-wrapper axis-wrapper-bottom', children: [ { type: 'div', @@ -97,11 +121,45 @@ export default function ColumnLayoutFactory(Private) { }, { type: 'div', - class: 'x-axis-title' + class: 'x-axis-title axis-title' } ] } ] + }, + { + type: 'div', + class: 'y-axis-col-wrapper', + children: [ + { + type: 'div', + class: 'y-axis-spacer-block y-axis-spacer-block-top' + }, + { + type: 'div', + class: 'y-axis-col axis-wrapper-right', + children: [ + { + type: 'div', + class: 'y-axis-div-wrapper', + splits: yAxisSplit + }, + /*{ + type: 'div', + class: 'y-axis-chart-title', + splits: chartTitleSplit + },*/ + { + type: 'div', + class: 'y-axis-title axis-title' + } + ] + }, + { + type: 'div', + class: 'y-axis-spacer-block y-axis-spacer-block-bottom' + } + ] } ] } diff --git a/src/ui/public/vislib/lib/y_axis.js b/src/ui/public/vislib/lib/y_axis.js deleted file mode 100644 index f5d7d55ff0e77..0000000000000 --- a/src/ui/public/vislib/lib/y_axis.js +++ /dev/null @@ -1,236 +0,0 @@ -import d3 from 'd3'; -import _ from 'lodash'; -import $ from 'jquery'; -import errors from 'ui/errors'; -import VislibLibErrorHandlerProvider from 'ui/vislib/lib/_error_handler'; -export default function YAxisFactory(Private) { - - const ErrorHandler = Private(VislibLibErrorHandlerProvider); - - /** - * Appends y axis to the visualization - * - * @class YAxis - * @constructor - * @param args {{el: (HTMLElement), yMax: (Number), _attr: (Object|*)}} - */ - class YAxis extends ErrorHandler { - constructor(args) { - super(); - this.el = args.el; - this.scale = null; - this.domain = [args.yMin, args.yMax]; - this.yAxisFormatter = args.yAxisFormatter; - this._attr = args._attr || {}; - } - - /** - * Renders the y axis - * - * @method render - * @return {D3.UpdateSelection} Renders y axis to visualization - */ - render() { - d3.select(this.el).selectAll('.y-axis-div').call(this.draw()); - }; - - _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.yAxisFormatter) return this.yAxisFormatter; - 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)); - }; - - /** - * Renders the y axis to the visualization - * - * @method draw - * @returns {Function} Renders y axis to visualization - */ - draw() { - const self = this; - const margin = this._attr.margin; - const mode = this._attr.mode; - const isWiggleOrSilhouette = (mode === 'wiggle' || mode === 'silhouette'); - - return function (selection) { - selection.each(function () { - const el = this; - - const div = d3.select(el); - const width = $(el).parent().width(); - const height = $(el).height(); - const adjustedHeight = height - margin.top - margin.bottom; - - // Validate whether width and height are not 0 or `NaN` - self.validateWidthandHeight(width, adjustedHeight); - - const yAxis = self.getYAxis(adjustedHeight); - - // The yAxis should not appear if mode is set to 'wiggle' or 'silhouette' - if (!isWiggleOrSilhouette) { - // Append svg and y axis - const svg = div.append('svg') - .attr('width', width) - .attr('height', height); - - 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 + ')'); - } - } - }); - }; - }; - } - - return YAxis; -}; diff --git a/src/ui/public/vislib/styles/_layout.less b/src/ui/public/vislib/styles/_layout.less index e704d18745ccf..3217be106fc64 100644 --- a/src/ui/public/vislib/styles/_layout.less +++ b/src/ui/public/vislib/styles/_layout.less @@ -31,7 +31,7 @@ } .y-axis-spacer-block { - min-height: 45px; + min-height: 2px; } .y-axis-div-wrapper { @@ -43,13 +43,13 @@ .y-axis-div { flex: 1 1 25px; - min-width: 14px; + min-width: 1px; min-height: 14px; } .y-axis-title { min-height: 14px; - min-width: 14px; + min-width: 1px; } .y-axis-chart-title { @@ -57,7 +57,6 @@ flex-direction: column; min-height: 14px; min-width: 0; - width: 14px; } .y-axis-title text, .x-axis-title text { @@ -138,7 +137,7 @@ .x-axis-wrapper { display: flex; flex-direction: column; - min-height: 45px; + min-height: 1px; min-width: 0; overflow: visible; } @@ -146,20 +145,20 @@ .x-axis-div-wrapper { display: flex; flex-direction: row; - min-height: 20px; + min-height: 1px; min-width: 0; } .x-axis-chart-title { display: flex; flex-direction: row; - min-height: 15px; + min-height: 1px; max-height: 15px; min-width: 20px; } .x-axis-title { - min-height: 15px; + min-height: 1px; max-height: 15px; min-width: 20px; overflow: hidden; @@ -167,6 +166,6 @@ .x-axis-div { margin-top: -5px; - min-height: 20px; + min-height: 1px; min-width: 20px; } diff --git a/src/ui/public/vislib/visualizations/_point_series_chart.js b/src/ui/public/vislib/visualizations/_point_series_chart.js index 7cad5cda2dadc..38ca13a967909 100644 --- a/src/ui/public/vislib/visualizations/_point_series_chart.js +++ b/src/ui/public/vislib/visualizations/_point_series_chart.js @@ -88,8 +88,8 @@ export default function PointSeriesChartProvider(Private) { */ createEndZones(svg) { const self = this; - const xAxis = this.handler.xAxis; - const xScale = xAxis.xScale; + const xAxis = this.handler.categoryAxes[0]; + const xScale = xAxis.getScale(); const ordered = xAxis.ordered; const missingMinMax = !ordered || _.isUndefined(ordered.min) || _.isUndefined(ordered.max); @@ -112,7 +112,7 @@ export default function PointSeriesChartProvider(Private) { w: Math.max(xScale(ordered.min), 0) }; - const rightLastVal = xAxis.expandLastBucket ? ordered.max : Math.min(ordered.max, _.last(xAxis.xValues)); + const rightLastVal = xAxis.expandLastBucket ? ordered.max : Math.min(ordered.max, _.last(xAxis.values)); const rightStart = rightLastVal + oneUnit; const rightEndzone = { x: xScale(rightStart), diff --git a/src/ui/public/vislib/visualizations/area_chart.js b/src/ui/public/vislib/visualizations/area_chart.js index c7599800d4c25..ea2c7b905a393 100644 --- a/src/ui/public/vislib/visualizations/area_chart.js +++ b/src/ui/public/vislib/visualizations/area_chart.js @@ -76,8 +76,8 @@ export default function AreaChartFactory(Private) { const isTimeSeries = (ordered && ordered.date); const isOverlapping = this.isOverlapping; const color = this.handler.data.getColorFunc(); - const xScale = this.handler.xAxis.xScale; - const yScale = this.handler.yAxis.yScale; + const xScale = this.handler.categoryAxes[0].getScale(); + const yScale = this.handler.valueAxes[0].getScale(); const interpolate = (this._attr.smoothLines) ? 'cardinal' : this._attr.interpolate; const area = d3.svg.area() .x(function (d) { @@ -165,8 +165,8 @@ export default function AreaChartFactory(Private) { */ addCircles(svg, data) { const color = this.handler.data.getColorFunc(); - const xScale = this.handler.xAxis.xScale; - const yScale = this.handler.yAxis.yScale; + const xScale = this.handler.categoryAxes[0].getScale(); + const yScale = this.handler.valueAxes[0].getScale(); const ordered = this.handler.data.get('ordered'); const circleRadius = 12; const circleStrokeWidth = 0; @@ -211,7 +211,7 @@ export default function AreaChartFactory(Private) { } return xScale(d.x) + xScale.rangeBand() / 2; }) - .attr('cy', function cy(d) { + .attr('cy', function cy(d, i, j) { if (isOverlapping) { return yScale(d.y); } @@ -284,13 +284,13 @@ export default function AreaChartFactory(Private) { draw() { // Attributes const self = this; - const xScale = this.handler.xAxis.xScale; + const xScale = this.handler.categoryAxes[0].getScale(); const $elem = $(this.chartEl); const margin = this._attr.margin; const elWidth = this._attr.width = $elem.width(); const elHeight = this._attr.height = $elem.height(); - const yMin = this.handler.yAxis.yMin; - const yScale = this.handler.yAxis.yScale; + const yMin = this.handler.valueAxes[0].yMin; + const yScale = this.handler.valueAxes[0].getScale(); const minWidth = 20; const minHeight = 20; const addTimeMarker = this._attr.addTimeMarker; diff --git a/src/ui/public/vislib/visualizations/column_chart.js b/src/ui/public/vislib/visualizations/column_chart.js index 172fef57fd57b..6ac6366b5e5ee 100644 --- a/src/ui/public/vislib/visualizations/column_chart.js +++ b/src/ui/public/vislib/visualizations/column_chart.js @@ -109,10 +109,10 @@ export default function ColumnChartFactory(Private) { */ addStackedBars(bars) { const data = this.chartData; - const xScale = this.handler.xAxis.xScale; - const yScale = this.handler.yAxis.yScale; + const xScale = this.handler.categoryAxes[0].getScale(); + const yScale = this.handler.valueAxes[0].getScale(); const height = yScale.range()[0]; - const yMin = this.handler.yAxis.yScale.domain()[0]; + const yMin = yScale.domain()[0]; let barWidth; if (data.ordered && data.ordered.date) { @@ -170,8 +170,8 @@ export default function ColumnChartFactory(Private) { * @returns {D3.UpdateSelection} */ addGroupedBars(bars) { - const xScale = this.handler.xAxis.xScale; - const yScale = this.handler.yAxis.yScale; + const xScale = this.handler.categoryAxes[0].getScale(); + const yScale = this.handler.valueAxes[0].getScale(); const data = this.chartData; const n = data.series.length; const height = yScale.range()[0]; @@ -256,8 +256,8 @@ export default function ColumnChartFactory(Private) { const margin = this._attr.margin; const elWidth = this._attr.width = $elem.width(); const elHeight = this._attr.height = $elem.height(); - const yScale = this.handler.yAxis.yScale; - const xScale = this.handler.xAxis.xScale; + const yScale = this.handler.valueAxes[0].getScale(); + const xScale = this.handler.categoryAxes[0].getScale(); const minWidth = 20; const minHeight = 20; const addTimeMarker = this._attr.addTimeMarker; diff --git a/src/ui/public/vislib/visualizations/line_chart.js b/src/ui/public/vislib/visualizations/line_chart.js index 867a503056a37..b62f521b8340e 100644 --- a/src/ui/public/vislib/visualizations/line_chart.js +++ b/src/ui/public/vislib/visualizations/line_chart.js @@ -70,8 +70,8 @@ export default function LineChartFactory(Private) { const self = this; const showCircles = this._attr.showCircles; const color = this.handler.data.getColorFunc(); - const xScale = this.handler.xAxis.xScale; - const yScale = this.handler.yAxis.yScale; + const xScale = this.handler.categoryAxes[0].getScale(); + const yScale = this.handler.valueAxes[0].getScale(); const ordered = this.handler.data.get('ordered'); const tooltip = this.tooltip; const isTooltip = this._attr.addTooltip; @@ -186,8 +186,8 @@ export default function LineChartFactory(Private) { * @returns {D3.UpdateSelection} SVG with paths added */ addLines(svg, data) { - const xScale = this.handler.xAxis.xScale; - const yScale = this.handler.yAxis.yScale; + const xScale = this.handler.categoryAxes[0].getScale(); + const yScale = this.handler.valueAxes[0].getScale(); const xAxisFormatter = this.handler.data.get('xAxisFormatter'); const color = this.handler.data.getColorFunc(); const ordered = this.handler.data.get('ordered'); @@ -268,9 +268,9 @@ export default function LineChartFactory(Private) { const margin = this._attr.margin; const elWidth = this._attr.width = $elem.width(); const elHeight = this._attr.height = $elem.height(); - const scaleType = this.handler.yAxis.getScaleType(); - const yScale = this.handler.yAxis.yScale; - const xScale = this.handler.xAxis.xScale; + const scaleType = this.handler.valueAxes[0].scale.getScaleType(); + const yScale = this.handler.valueAxes[0].getScale(); + const xScale = this.handler.categoryAxes[0].getScale(); const minWidth = 20; const minHeight = 20; const startLineX = 0;