UNPKG

@spalger/kibana

Version:

Kibana is an open source (Apache Licensed), browser based analytics and search dashboard for Elasticsearch. Kibana is a snap to setup and start using. Kibana strives to be easy to get started with, while also being flexible and powerful, just like Elastic

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; }; });