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

488 lines (433 loc) 14 kB
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; }