@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
713 lines (618 loc) • 20.5 kB
JavaScript
define(function (require) {
return function DataFactory(Private) {
var d3 = require('d3');
var _ = require('lodash');
var injectZeros = Private(require('ui/vislib/components/zero_injection/inject_zeros'));
var orderKeys = Private(require('ui/vislib/components/zero_injection/ordered_x_keys'));
var getLabels = Private(require('ui/vislib/components/labels/labels'));
var color = Private(require('ui/vislib/components/color/color'));
var errors = require('ui/errors');
/**
* Provides an API for pulling values off the data
* and calculating values using the data
*
* @class Data
* @constructor
* @param data {Object} Elasticsearch query results
* @param attr {Object|*} Visualization options
*/
function Data(data, attr) {
if (!(this instanceof Data)) {
return new Data(data, attr);
}
var self = this;
var offset;
if (attr.mode === 'stacked') {
offset = 'zero';
} else if (attr.mode === 'percentage') {
offset = 'expand';
} else if (attr.mode === 'grouped') {
offset = 'group';
} else {
offset = attr.mode;
}
this.data = data;
this.type = this.getDataType();
this.labels = this._getLabels(this.data);
this.color = this.labels ? color(this.labels) : undefined;
this._normalizeOrdered();
this._attr = _.defaults(attr || {}, {
stack: d3.layout.stack()
.x(function (d) { return d.x; })
.y(function (d) {
if (offset === 'expand') {
return Math.abs(d.y);
}
return d.y;
})
.offset(offset || 'zero')
});
if (attr.mode === 'stacked' && attr.type === 'histogram') {
this._attr.stack.out(function (d, y0, y) {
return self._stackNegAndPosVals(d, y0, y);
});
}
}
Data.prototype._updateData = function () {
if (this.data.rows) {
_.map(this.data.rows, this._updateDataSeriesLabel, this);
} else if (this.data.columns) {
_.map(this.data.columns, this._updateDataSeriesLabel, this);
} else {
this._updateDataSeriesLabel(this.data);
}
};
Data.prototype._updateDataSeriesLabel = function (eachData) {
if (eachData.series) {
eachData.series[0].label = this.get('yAxisLabel');
}
};
Data.prototype._getLabels = function (data) {
if (this.type === 'series') {
var noLabel = getLabels(data).length === 1 && getLabels(data)[0] === '';
if (noLabel) {
this._updateData();
return [(this.get('yAxisLabel'))];
}
return getLabels(data);
}
return this.pieNames();
};
/**
* Returns true for positive numbers
*/
Data.prototype._isPositive = function (num) {
return num >= 0;
};
/**
* Returns true for negative numbers
*/
Data.prototype._isNegative = function (num) {
return num < 0;
};
/**
* Adds two input values
*/
Data.prototype._addVals = function (a, b) {
return a + b;
};
/**
* Returns the results of the addition of numbers in a filtered array.
*/
Data.prototype._sumYs = function (arr, callback) {
var filteredArray = arr.filter(callback);
return (filteredArray.length) ? filteredArray.reduce(this._addVals) : 0;
};
/**
* Calculates the d.y0 value for stacked data in D3.
*/
Data.prototype._calcYZero = function (y, arr) {
if (y >= 0) return this._sumYs(arr, this._isPositive);
return this._sumYs(arr, this._isNegative);
};
/**
*
*/
Data.prototype._getCounts = function (i, j) {
var data = this.chartData();
var 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;
};
/**
*
*/
Data.prototype._createCache = function () {
var 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.
*/
Data.prototype._stackNegAndPosVals = function (d, y0, y) {
var data = this.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);
++this._cache.index.stack;
// last stack, or last value, reset the stack count and y value array
var 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
var 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
var 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;
}
};
Data.prototype.getDataType = function () {
var data = this.getVisData();
var type;
data.forEach(function (obj) {
if (obj.series) {
type = 'series';
} else if (obj.slices) {
type = 'slices';
} else if (obj.geoJson) {
type = 'geoJson';
}
});
return type;
};
/**
* Returns an array of the actual x and y data value objects
* from data with series keys
*
* @method chartData
* @returns {*} Array of data objects
*/
Data.prototype.chartData = function () {
if (!this.data.series) {
var arr = this.data.rows ? this.data.rows : this.data.columns;
return _.toArray(arr);
}
return [this.data];
};
/**
* Returns an array of chart data objects
*
* @method getVisData
* @returns {*} Array of chart data objects
*/
Data.prototype.getVisData = function () {
var visData;
if (this.data.rows) {
visData = this.data.rows;
} else if (this.data.columns) {
visData = this.data.columns;
} else {
visData = [this.data];
}
return visData;
};
/**
* get min and max for all cols, rows of data
*
* @method getMaxMin
* @return {Object}
*/
Data.prototype.getGeoExtents = function () {
var visData = this.getVisData();
return _.reduce(_.pluck(visData, 'geoJson.properties'), function (minMax, props) {
return {
min: Math.min(props.min, minMax.min),
max: Math.max(props.max, minMax.max)
};
}, { min: Infinity, max: -Infinity });
};
/**
* Returns array of chart data objects for pie data objects
*
* @method pieData
* @returns {*} Array of chart data objects
*/
Data.prototype.pieData = function () {
if (!this.data.slices) {
return this.data.rows ? this.data.rows : this.data.columns;
}
return [this.data];
};
/**
* Get attributes off the data, e.g. `tooltipFormatter` or `xAxisFormatter`
* pulls the value off the first item in the array
* these values are typically the same between data objects of the same chart
* TODO: May need to verify this or refactor
*
* @method get
* @param thing {String} Data object key
* @returns {*} Data object value
*/
Data.prototype.get = function (thing, def) {
var source = (this.data.rows || this.data.columns || [this.data])[0];
return _.get(source, thing, def);
};
/**
* Returns true if null values are present
* @returns {*}
*/
Data.prototype.hasNullValues = function () {
var chartData = this.chartData();
return chartData.some(function (chart) {
return chart.series.some(function (obj) {
return obj.values.some(function (d) {
return d.y === null;
});
});
});
};
/**
* Return an array of all value objects
* Pluck the data.series array from each data object
* Create an array of all the value objects from the series array
*
* @method flatten
* @returns {Array} Value objects
*/
Data.prototype.flatten = function () {
return _(this.chartData())
.pluck('series')
.flattenDeep()
.pluck('values')
.flattenDeep()
.value();
};
/**
* Determines whether histogram charts should be stacked
* TODO: need to make this more generic
*
* @method shouldBeStacked
* @returns {boolean}
*/
Data.prototype.shouldBeStacked = function () {
var isHistogram = (this._attr.type === 'histogram');
var isArea = (this._attr.type === 'area');
var isOverlapping = (this._attr.mode === 'overlap');
var grouped = (this._attr.mode === 'grouped');
var stackedHisto = isHistogram && !grouped;
var stackedArea = isArea && !isOverlapping;
return stackedHisto || stackedArea;
};
/**
* Validates that the Y axis min value defined by user input
* is a number.
*
* @param val {Number} Y axis min value
* @returns {Number} Y axis min value
*/
Data.prototype.validateUserDefinedYMin = function (val) {
if (!_.isNumber(val)) {
throw new Error('validateUserDefinedYMin expects a number');
}
return val;
};
/**
* Calculates the lowest Y value across all charts, taking
* stacking into consideration.
*
* @method getYMin
* @param {function} [getValue] - optional getter that will receive a
* point and should return the value that should
* be considered
* @returns {Number} Min y axis value
*/
Data.prototype.getYMin = function (getValue) {
var self = this;
var arr = [];
if (this._attr.mode === 'percentage' || this._attr.mode === 'wiggle' ||
this._attr.mode === 'silhouette') {
return 0;
}
var flat = this.flatten();
// if there is only one data point and its less than zero,
// return 0 as the yMax value.
if (!flat.length || flat.length === 1 && flat[0].y > 0) {
return 0;
}
var min = Infinity;
// for each object in the dataArray,
// push the calculated y value to the initialized array (arr)
_.each(this.chartData(), function (chart) {
var calculatedMin = self._getYExtent(chart, 'min', getValue);
if (!_.isUndefined(calculatedMin)) {
min = Math.min(min, calculatedMin);
}
});
return min;
};
/**
* Calculates the highest Y value across all charts, taking
* stacking into consideration.
*
* @method getYMax
* @param {function} [getValue] - optional getter that will receive a
* point and should return the value that should
* be considered
* @returns {Number} Max y axis value
*/
Data.prototype.getYMax = function (getValue) {
var self = this;
var arr = [];
if (self._attr.mode === 'percentage') {
return 1;
}
var flat = this.flatten();
// if there is only one data point and its less than zero,
// return 0 as the yMax value.
if (!flat.length || flat.length === 1 && flat[0].y < 0) {
return 0;
}
var max = -Infinity;
// for each object in the dataArray,
// push the calculated y value to the initialized array (arr)
_.each(this.chartData(), function (chart) {
var calculatedMax = self._getYExtent(chart, 'max', getValue);
if (!_.isUndefined(calculatedMax)) {
max = Math.max(max, calculatedMax);
}
});
return max;
};
/**
* Calculates the stacked values for each data object
*
* @method stackData
* @param series {Array} Array of data objects
* @returns {*} Array of data objects with x, y, y0 keys
*/
Data.prototype.stackData = function (series) {
// Should not stack values on line chart
if (this._attr.type === 'line') return series;
return this._attr.stack(series);
};
/**
* Returns the max Y axis value for a `series` array based on
* a specified callback function (calculation).
* @param {function} [getValue] - Optional getter that will be used to read
* values from points when calculating the extent.
* default is either this._getYStack or this.getY
* based on this.shouldBeStacked().
*/
Data.prototype._getYExtent = function (chart, extent, getValue) {
if (this.shouldBeStacked()) {
this.stackData(_.pluck(chart.series, 'values'));
getValue = getValue || this._getYStack;
} else {
getValue = getValue || this._getY;
}
var points = chart.series
.reduce(function (points, series) {
return points.concat(series.values);
}, [])
.map(getValue);
return d3[extent](points);
};
/**
* Calculates the y stack value for each data object
*/
Data.prototype._getYStack = function (d) {
return d.y0 + d.y;
};
/**
* Calculates the Y max value
*/
Data.prototype._getY = function (d) {
return d.y;
};
/**
* Helper function for getNames
* Returns an array of objects with a name (key) value and an index value.
* The index value allows us to sort the names in the correct nested order.
*
* @method returnNames
* @param array {Array} Array of data objects
* @param index {Number} Number of times the object is nested
* @param columns {Object} Contains name formatter information
* @returns {Array} Array of labels (strings)
*/
Data.prototype.returnNames = function (array, index, columns) {
var names = [];
var self = this;
_.forEach(array, function (obj) {
names.push({ label: obj.name, values: obj, index: index });
if (obj.children) {
var plusIndex = index + 1;
_.forEach(self.returnNames(obj.children, plusIndex, columns), function (namedObj) {
names.push(namedObj);
});
}
});
return names;
};
/**
* Flattens hierarchical data into an array of objects with a name and index value.
* The indexed value determines the order of nesting in the data.
* Returns an array with names sorted by the index value.
*
* @method getNames
* @param data {Object} Chart data object
* @param columns {Object} Contains formatter information
* @returns {Array} Array of names (strings)
*/
Data.prototype.getNames = function (data, columns) {
var slices = data.slices;
if (slices.children) {
var namedObj = this.returnNames(slices.children, 0, columns);
return _(namedObj)
.sortBy(function (obj) {
return obj.index;
})
.unique(function (d) {
return d.label;
})
.value();
}
};
/**
* Removes zeros from pie chart data
* @param slices
* @returns {*}
*/
Data.prototype._removeZeroSlices = function (slices) {
var self = this;
if (!slices.children) return slices;
slices = _.clone(slices);
slices.children = slices.children.reduce(function (children, child) {
if (child.size !== 0) {
children.push(self._removeZeroSlices(child));
}
return children;
}, []);
return slices;
};
/**
* Returns an array of names ordered by appearance in the nested array
* of objects
*
* @method pieNames
* @returns {Array} Array of unique names (strings)
*/
Data.prototype.pieNames = function (data) {
var self = this;
var names = [];
_.forEach(data, function (obj) {
var columns = obj.raw ? obj.raw.columns : undefined;
obj.slices = self._removeZeroSlices(obj.slices);
_.forEach(self.getNames(obj, columns), function (name) {
names.push(name);
});
});
return _.uniq(names, 'label');
};
/**
* Inject zeros into the data
*
* @method injectZeros
* @returns {Object} Data object with zeros injected
*/
Data.prototype.injectZeros = function () {
return injectZeros(this.data);
};
/**
* Returns an array of all x axis values from the data
*
* @method xValues
* @returns {Array} Array of x axis values
*/
Data.prototype.xValues = function () {
return orderKeys(this.data);
};
/**
* Return an array of unique labels
* Curently, only used for vertical bar and line charts,
* or any data object with series values
*
* @method getLabels
* @returns {Array} Array of labels (strings)
*/
Data.prototype.getLabels = function () {
return getLabels(this.data);
};
/**
* Returns a function that does color lookup on labels
*
* @method getColorFunc
* @returns {Function} Performs lookup on string and returns hex color
*/
Data.prototype.getColorFunc = function () {
return color(this.getLabels());
};
/**
* Returns a function that does color lookup on names for pie charts
*
* @method getPieColorFunc
* @returns {Function} Performs lookup on string and returns hex color
*/
Data.prototype.getPieColorFunc = function () {
return color(this.pieNames(this.getVisData()).map(function (d) {
return d.label;
}));
};
/**
* ensure that the datas ordered property has a min and max
* if the data represents an ordered date range.
*
* @return {undefined}
*/
Data.prototype._normalizeOrdered = function () {
if (!this.data.ordered || !this.data.ordered.date) return;
var missingMin = this.data.ordered.min == null;
var missingMax = this.data.ordered.max == null;
if (missingMax || missingMin) {
var extent = d3.extent(this.xValues());
if (missingMin) this.data.ordered.min = extent[0];
if (missingMax) this.data.ordered.max = extent[1];
}
};
/**
* Calculates min and max values for all map data
* series.rows is an array of arrays
* each row is an array of values
* last value in row array is bucket count
*
* @method mapDataExtents
* @param series {Array} Array of data objects
* @returns {Array} min and max values
*/
Data.prototype.mapDataExtents = function (series) {
var values;
values = _.map(series.rows, function (row) {
return row[row.length - 1];
});
var extents = [_.min(values), _.max(values)];
return extents;
};
/**
* Get the maximum number of series, considering each chart
* individually.
*
* @return {number} - the largest number of series from all charts
*/
Data.prototype.maxNumberOfSeries = function () {
return this.chartData().reduce(function (max, chart) {
return Math.max(max, chart.series.length);
}, 0);
};
return Data;
};
});