UNPKG

kibana-with-account

Version:

kibana-with-account is develop based kibana project

526 lines (454 loc) 15 kB
define(function (require) { return function XAxisFactory(Private) { var d3 = require('d3'); var $ = require('jquery'); var _ = require('lodash'); var moment = require('moment'); var ErrorHandler = Private(require('ui/vislib/lib/_error_handler')); /** * Adds an x axis to the visualization * * @class XAxis * @constructor * @param args {{el: (HTMLElement), xValues: (Array), ordered: (Object|*), * xAxisFormatter: (Function), _attr: (Object|*)}} */ _.class(XAxis).inherits(ErrorHandler); function XAxis(args) { if (!(this instanceof XAxis)) { return new XAxis(args); } this.el = args.el; this.xValues = args.xValues; this.ordered = args.ordered; this.xAxisFormatter = args.xAxisFormatter; this.expandLastBucket = args.expandLastBucket == null ? true : args.expandLastBucket; this._attr = _.defaults(args._attr || {}); } /** * Renders the x axis * * @method render * @returns {D3.UpdateSelection} Appends x axis to visualization */ XAxis.prototype.render = function () { d3.select(this.el).selectAll('.x-axis-div').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 */ XAxis.prototype.getScale = function () { var 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 */ XAxis.prototype.getDomain = function (scale) { var 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 */ XAxis.prototype.getTimeDomain = function (scale, data) { return scale.domain([this.minExtent(data), this.maxExtent(data)]); }; XAxis.prototype.minExtent = function (data) { return this._calculateExtent(data || this.xValues, 'min'); }; XAxis.prototype.maxExtent = function (data) { return this._calculateExtent(data || this.xValues, 'max'); }; /** * * @param data * @param extent */ XAxis.prototype._calculateExtent = function (data, extent) { var ordered = this.ordered; var opts = [ordered[extent]]; var 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 */ XAxis.prototype.addInterval = function (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 */ XAxis.prototype.subtractInterval = function (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 */ XAxis.prototype.modByInterval = function (x, n) { var ordered = this.ordered; if (!ordered) return x; var interval = ordered.interval; if (!interval) return x; if (!ordered.date) { return x += (ordered.interval * n); } var y = moment(x); var method = n > 0 ? 'add' : 'subtract'; _.times(Math.abs(n), function () { y[method](interval); }); 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 */ XAxis.prototype.getOrdinalDomain = function (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 */ XAxis.prototype.getRange = function (domain, width) { var 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 */ XAxis.prototype.getXScale = function (width) { var domain = this.getDomain(this.getScale()); return this.getRange(domain, width); }; /** * Creates d3 xAxis function * * @method getXAxis * @param width {Number} HTML Element width */ XAxis.prototype.getXAxis = function (width) { this.xScale = this.getXScale(width); if (!this.xScale || _.isNaN(this.xScale)) { throw new Error('xScale is ' + this.xScale); } this.xAxis = d3.svg.axis() .scale(this.xScale) .ticks(10) .tickFormat(this.xAxisFormatter) .orient('bottom'); }; /** * Renders the x axis * * @method draw * @returns {Function} Renders the x axis to a D3 selection */ XAxis.prototype.draw = function () { var self = this; var div; var width; var height; var svg; var parentWidth; var n; this._attr.isRotated = false; return function (selection) { n = selection[0].length; parentWidth = $(self.el) .find('.x-axis-div-wrapper') .width(); selection.each(function () { div = d3.select(this); width = parentWidth / n; height = $(this.parentElement).height(); self.validateWidthandHeight(width, height); self.getXAxis(width); 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); }); selection.call(self.filterOrRotate()); }; }; /** * 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 */ XAxis.prototype.filterOrRotate = function () { var self = this; var ordered = self.ordered; var axis; var labels; return function (selection) { selection.each(function () { axis = d3.select(this); labels = axis.selectAll('.tick text'); if (ordered && ordered.date) { axis.call(self.filterAxisLabels()); } else { axis.call(self.rotateAxisLabels()); } }); self.updateXaxisHeight(); selection.call(self.fitTitles()); }; }; /** * Rotate the axis tick labels within selection * * @returns {Function} Rotates x axis tick labels of a D3 selection */ XAxis.prototype.rotateAxisLabels = function () { var self = this; var text; var barWidth = self.xScale.rangeBand(); var maxRotatedLength = 180; var xAxisPadding = 15; var svg; var lengths = []; var length; self._attr.isRotated = false; return function (selection) { text = selection.selectAll('.tick text'); text.each(function textWidths() { lengths.push(d3.select(this).node().getBBox().width); }); 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)'; }); selection.select('svg') .attr('height', self._attr.xAxisLabelHt); } }; }; /** * Returns a string that is truncated to fit size * * @method truncateLabel * @param text {HTMLElement} * @param size {Number} * @returns {*|jQuery} */ XAxis.prototype.truncateLabel = function (text, size) { var node = d3.select(text).node(); var str = $(node).text(); var width = node.getBBox().width; var chars = str.length; var pxPerChar = width / chars; var endChar = 0; var 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} */ XAxis.prototype.filterAxisLabels = function () { var self = this; var startX = 0; var maxW; var par; var myX; var myWidth; var halfWidth; var padding = 1.1; 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.xAxisFormatter(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} */ XAxis.prototype.fitTitles = function () { var visEls = $('.vis-wrapper'); var xAxisChartTitle; var yAxisChartTitle; var text; var titles; return function () { visEls.each(function () { var visEl = d3.select(this); var $visEl = $(this); var xAxisTitle = $visEl.find('.x-axis-title'); var yAxisTitle = $visEl.find('.y-axis-title'); var titleWidth = xAxisTitle.width(); var 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 ($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)'); }); } }); }; }; /** * Appends div to make .y-axis-spacer-block * match height of .x-axis-wrapper * * @method updateXaxisHeight */ XAxis.prototype.updateXaxisHeight = function () { var selection = d3.select(this.el).selectAll('.vis-wrapper'); selection.each(function () { var 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'); } var xAxisHt = visEl.select('.x-axis-wrapper').style('height'); visEl.select('.inner-spacer-block').style('height', xAxisHt); }); }; return XAxis; }; });