UNPKG

kibana-123

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

325 lines (277 loc) 9.94 kB
import d3 from 'd3'; import _ from 'lodash'; import $ from 'jquery'; import ErrorHandlerProvider from '../_error_handler'; import AxisTitleProvider from './axis_title'; import AxisLabelsProvider from './axis_labels'; import AxisScaleProvider from './axis_scale'; import AxisConfigProvider from './axis_config'; import errors from 'ui/errors'; export default function AxisFactory(Private) { const ErrorHandler = Private(ErrorHandlerProvider); const AxisTitle = Private(AxisTitleProvider); const AxisLabels = Private(AxisLabelsProvider); const AxisScale = Private(AxisScaleProvider); const AxisConfig = Private(AxisConfigProvider); class Axis extends ErrorHandler { constructor(visConfig, axisConfigArgs) { super(); this.visConfig = visConfig; this.axisConfig = new AxisConfig(this.visConfig, axisConfigArgs); if (this.axisConfig.get('type') === 'category') { this.values = this.axisConfig.values; this.ordered = this.axisConfig.ordered; } this.axisScale = new AxisScale(this.axisConfig, visConfig); this.axisTitle = new AxisTitle(this.axisConfig); this.axisLabels = new AxisLabels(this.axisConfig, this.axisScale); this.stack = d3.layout.stack() .x(d => { return d.x; }) .y(d => { if (this.axisConfig.get('scale.offset') === 'expand') { return Math.abs(d.y); } return d.y; }) .offset(this.axisConfig.get('scale.offset', 'zero')); const stackedMode = ['normal', 'grouped'].includes(this.axisConfig.get('scale.mode')); if (stackedMode) { this.stack.out((d, y0, y) => { return this._stackNegAndPosVals(d, y0, y); }); } } /** * Returns true for positive numbers */ _isPositive(num) { return num >= 0; }; /** * Returns true for negative numbers */ _isNegative(num) { return num < 0; }; /** * Adds two input values */ _addVals(a, b) { return a + b; }; /** * Returns the results of the addition of numbers in a filtered array. */ _sumYs(arr, callback) { const filteredArray = arr.filter(callback); return (filteredArray.length) ? filteredArray.reduce(this._addVals) : 0; }; /** * Calculates the d.y0 value for stacked data in D3. */ _calcYZero(y, arr) { if (y === 0 && this._lastY0) return this._sumYs(arr, this._lastY0 > 0 ? this._isPositive : this._isNegative); if (y >= 0) return this._sumYs(arr, this._isPositive); return this._sumYs(arr, this._isNegative); }; _getCounts(i, j) { const data = this.visConfig.data.chartData(); const dataLengths = {}; dataLengths.charts = data.length; dataLengths.stacks = dataLengths.charts ? data[i].series.length : 0; dataLengths.values = dataLengths.stacks ? data[i].series[j].values.length : 0; return dataLengths; }; _createCache() { const cache = { index: { chart: 0, stack: 0, value: 0 }, yValsArr: [] }; cache.count = this._getCounts(cache.index.chart, cache.index.stack); return cache; }; /** * Stacking function passed to the D3 Stack Layout `.out` API. * See: https://github.com/mbostock/d3/wiki/Stack-Layout * It is responsible for calculating the correct d.y0 value for * mixed datasets containing both positive and negative values. */ _stackNegAndPosVals(d, y0, y) { const data = this.visConfig.data.chartData(); // Storing counters and data characteristics needed to stack values properly if (!this._cache) { this._cache = this._createCache(); } d.y0 = this._calcYZero(y, this._cache.yValsArr); if (d.y0 > 0) this._lastY0 = 1; if (d.y0 < 0) this._lastY0 = -1; ++this._cache.index.stack; // last stack, or last value, reset the stack count and y value array const lastStack = (this._cache.index.stack >= this._cache.count.stacks); if (lastStack) { this._cache.index.stack = 0; ++this._cache.index.value; this._cache.yValsArr = []; // still building the stack collection, push v value to array } else if (y !== 0) { this._cache.yValsArr.push(y); } // last value, prepare for the next chart, if one exists const lastValue = (this._cache.index.value >= this._cache.count.values); if (lastValue) { this._cache.index.value = 0; ++this._cache.index.chart; // no more charts, reset the queue and finish if (this._cache.index.chart >= this._cache.count.charts) { this._cache = this._createCache(); return; } // get stack and value count for next chart const chartSeries = data[this._cache.index.chart].series; this._cache.count.stacks = chartSeries.length; this._cache.count.values = chartSeries.length ? chartSeries[this._cache.index.stack].values.length : 0; } }; render() { const elSelector = this.axisConfig.get('elSelector'); const rootEl = this.axisConfig.get('rootEl'); d3.select(rootEl).selectAll(elSelector).call(this.draw()); } destroy() { const elSelector = this.axisConfig.get('elSelector'); const rootEl = this.axisConfig.get('rootEl'); $(rootEl).find(elSelector).find('svg').remove(); } getAxis(length) { const scale = this.axisScale.getScale(length); const position = this.axisConfig.get('position'); const axisFormatter = this.axisConfig.get('labels.axisFormatter'); return d3.svg.axis() .scale(scale) .tickFormat(axisFormatter) .ticks(this.tickScale(length)) .orient(position); } getScale() { return this.axisScale.scale; } addInterval(interval) { return this.axisScale.addInterval(interval); } substractInterval(interval) { return this.axisScale.substractInterval(interval); } tickScale(length) { const yTickScale = d3.scale.linear() .clamp(true) .domain([20, 40, 1000]) .range([0, 3, 11]); return Math.ceil(yTickScale(length)); } getLength(el) { if (this.axisConfig.isHorizontal()) { return $(el).width(); } else { return $(el).height(); } } adjustSize() { const config = this.axisConfig; const style = config.get('style'); const margin = this.visConfig.get('style.margin'); const chartEl = this.visConfig.get('el'); const position = config.get('position'); const axisPadding = 5; return function (selection) { const text = selection.selectAll('.tick text'); const lengths = []; text.each(function textWidths() { lengths.push((() => { if (config.isHorizontal()) { return d3.select(this.parentNode).node().getBBox().height; } else { return d3.select(this.parentNode).node().getBBox().width; } })()); }); let length = lengths.length > 0 ? _.max(lengths) : 0; length += axisPadding; if (config.isHorizontal()) { selection.attr('height', Math.ceil(length)); if (position === 'top') { selection.select('g') .attr('transform', `translate(0, ${length - parseInt(style.lineWidth)})`); selection.select('path') .attr('transform', 'translate(1,0)'); } if (config.get('type') === 'value') { const spacerNodes = $(chartEl).find(`.y-axis-spacer-block-${position}`); const elHeight = $(chartEl).find(`.axis-wrapper-${position}`).height(); spacerNodes.height(elHeight); } } else { const axisWidth = Math.ceil(length); selection.attr('width', axisWidth); if (position === 'left') { selection.select('g') .attr('transform', `translate(${axisWidth},0)`); } } }; } validate() { if (this.axisConfig.isLogScale() && this.axisConfig.isPercentage()) { throw new errors.VislibError(`Can't mix percentage mode with log scale.`); } } draw() { const self = this; const config = this.axisConfig; const style = config.get('style'); return function (selection) { const n = selection[0].length; if (config.get('show') && self.axisTitle) { self.axisTitle.render(selection); } selection.each(function () { const el = this; const div = d3.select(el); const width = $(el).width(); const height = $(el).height(); const length = self.getLength(el, n); self.validate(); const axis = self.getAxis(length); if (config.get('show')) { const svg = div.append('svg') .attr('width', width) .attr('height', height); const axisClass = self.axisConfig.isHorizontal() ? 'x' : 'y'; svg.append('g') .attr('class', `${axisClass} axis ${config.get('id')}`) .call(axis); const container = svg.select('g.axis').node(); if (container) { svg.select('path') .style('stroke', style.color) .style('stroke-width', style.lineWidth) .style('stroke-opacity', style.opacity); svg.selectAll('line') .style('stroke', style.tickColor) .style('stroke-width', style.tickWidth) .style('stroke-opacity', style.opacity); } if (self.axisLabels) self.axisLabels.render(svg); svg.call(self.adjustSize()); } }); }; } } return Axis; };