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
488 lines (433 loc) • 14 kB
JavaScript
import d3 from 'd3';
import _ from 'lodash';
import VislibComponentsZeroInjectionInjectZerosProvider from '../components/zero_injection/inject_zeros';
import VislibComponentsZeroInjectionOrderedXKeysProvider from '../components/zero_injection/ordered_x_keys';
import VislibComponentsLabelsLabelsProvider from '../components/labels/labels';
import VislibComponentsColorColorProvider from 'ui/vis/components/color/color';
export default function DataFactory(Private) {
const injectZeros = Private(VislibComponentsZeroInjectionInjectZerosProvider);
const orderKeys = Private(VislibComponentsZeroInjectionOrderedXKeysProvider);
const getLabels = Private(VislibComponentsLabelsLabelsProvider);
const color = Private(VislibComponentsColorColorProvider);
/**
* 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
*/
class Data {
constructor(data, uiState) {
this.uiState = uiState;
this.data = this.copyDataObj(data);
this.type = this.getDataType();
this.labels = this._getLabels(this.data);
this.color = this.labels ? color(this.labels, uiState.get('vis.colors')) : undefined;
this._normalizeOrdered();
}
copyDataObj(data) {
const copyChart = data => {
const newData = {};
Object.keys(data).forEach(key => {
if (key !== 'series') {
newData[key] = data[key];
} else {
newData[key] = data[key].map(seri => {
return {
label: seri.label,
values: seri.values.map(val => {
const newVal = _.clone(val);
newVal.aggConfig = val.aggConfig;
newVal.aggConfigResult = val.aggConfigResult;
newVal.extraMetrics = val.extraMetrics;
newVal.series = val.series || seri.label;
return newVal;
})
};
});
}
});
return newData;
};
if (!data.series) {
const newData = {};
Object.keys(data).forEach(key => {
if (!['rows', 'columns'].includes(key)) {
newData[key] = data[key];
}
else {
newData[key] = data[key].map(chart => {
return copyChart(chart);
});
}
});
return newData;
}
return copyChart(data);
}
_getLabels(data) {
return this.type === 'series' ? getLabels(data) : this.pieNames();
}
getDataType() {
const data = this.getVisData();
let 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
*/
chartData() {
if (!this.data.series) {
const arr = this.data.rows ? this.data.rows : this.data.columns;
return _.toArray(arr);
}
return [this.data];
}
shouldBeStacked(seriesConfig) {
if (!seriesConfig) return false;
const isHistogram = (seriesConfig.type === 'histogram');
const isArea = (seriesConfig.type === 'area');
const stacked = (seriesConfig.mode === 'stacked');
return (isHistogram || isArea) && stacked;
}
getStackedSeries(chartConfig, axis, series, first = false) {
const matchingSeries = [];
chartConfig.series.forEach((seriArgs, i) => {
const matchingAxis = seriArgs.valueAxis === axis.axisConfig.get('id') || (!seriArgs.valueAxis && first);
if (matchingAxis && (this.shouldBeStacked(seriArgs) || axis.axisConfig.get('scale.stacked'))) {
matchingSeries.push(series[i]);
}
});
return matchingSeries;
}
stackChartData(handler, data, chartConfig) {
const stackedData = {};
handler.valueAxes.forEach((axis, i) => {
const id = axis.axisConfig.get('id');
stackedData[id] = this.getStackedSeries(chartConfig, axis, data, i === 0);
stackedData[id] = this.injectZeros(stackedData[id], handler.visConfig.get('orderBucketsBySum', false));
axis.stack(_.map(stackedData[id], 'values'));
});
return stackedData;
}
stackData(handler) {
const data = this.data;
if (data.rows || data.columns) {
const charts = data.rows ? data.rows : data.columns;
charts.forEach((chart, i) => {
this.stackChartData(handler, chart.series, handler.visConfig.get(`charts[${i}]`));
});
} else {
this.stackChartData(handler, data.series, handler.visConfig.get('charts[0]'));
}
}
/**
* Returns an array of chart data objects
*
* @method getVisData
* @returns {*} Array of chart data objects
*/
getVisData() {
let 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}
*/
getGeoExtents() {
const 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
*/
pieData() {
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
*/
get(thing, def) {
const source = (this.data.rows || this.data.columns || [this.data])[0];
return _.get(source, thing, def);
}
/**
* Returns true if null values are present
* @returns {*}
*/
hasNullValues() {
const 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
*/
flatten() {
return _(this.chartData())
.pluck('series')
.flattenDeep()
.pluck('values')
.flattenDeep()
.value();
}
/**
* 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
*/
validateUserDefinedYMin(val) {
if (!_.isNumber(val)) {
throw new Error('validateUserDefinedYMin expects a number');
}
return val;
}
/**
* 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)
*/
returnNames(array, index, columns) {
const names = [];
const self = this;
_.forEach(array, function (obj, i) {
names.push({
label: obj.name,
values: obj,
index: index
});
if (obj.children) {
const 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)
*/
getNames(data, columns) {
const slices = data.slices;
if (slices.children) {
const 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 {*}
*/
_removeZeroSlices(slices) {
const 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)
*/
pieNames(data) {
const self = this;
const names = [];
_.forEach(data, function (obj) {
const 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
*/
injectZeros(data, orderBucketsBySum = false) {
return injectZeros(data, this.data, orderBucketsBySum);
}
/**
* Returns an array of all x axis values from the data
*
* @method xValues
* @returns {Array} Array of x axis values
*/
xValues(orderBucketsBySum = false) {
return orderKeys(this.data, orderBucketsBySum);
}
/**
* 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)
*/
getLabels() {
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
*/
getColorFunc() {
if (this.type === 'slices') {
return this.getPieColorFunc();
}
const defaultColors = this.uiState.get('vis.defaultColors');
const overwriteColors = this.uiState.get('vis.colors');
const colors = defaultColors ? _.defaults({}, overwriteColors, defaultColors) : overwriteColors;
return color(this.getLabels(), colors);
}
/**
* Returns a function that does color lookup on names for pie charts
*
* @method getPieColorFunc
* @returns {Function} Performs lookup on string and returns hex color
*/
getPieColorFunc() {
return color(this.pieNames(this.getVisData()).map(function (d) {
return d.label;
}), this.uiState.get('vis.colors'));
}
/**
* ensure that the datas ordered property has a min and max
* if the data represents an ordered date range.
*
* @return {undefined}
*/
_normalizeOrdered() {
const data = this.getVisData();
const self = this;
data.forEach(function (d) {
if (!d.ordered || !d.ordered.date) return;
const missingMin = d.ordered.min == null;
const missingMax = d.ordered.max == null;
if (missingMax || missingMin) {
const extent = d3.extent(self.xValues());
if (missingMin) d.ordered.min = extent[0];
if (missingMax) d.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
*/
mapDataExtents(series) {
const values = _.map(series.rows, function (row) {
return row[row.length - 1];
});
return [_.min(values), _.max(values)];
}
/**
* Get the maximum number of series, considering each chart
* individually.
*
* @return {number} - the largest number of series from all charts
*/
maxNumberOfSeries() {
return this.chartData().reduce(function (max, chart) {
return Math.max(max, chart.series.length);
}, 0);
}
}
return Data;
}