diff --git a/public/app/plugins/panel/graph/axes_editor.ts b/public/app/plugins/panel/graph/axes_editor.ts index 04ef62f16fb..2cb7aa4eed0 100644 --- a/public/app/plugins/panel/graph/axes_editor.ts +++ b/public/app/plugins/panel/graph/axes_editor.ts @@ -10,7 +10,7 @@ export class AxesEditorCtrl { xNameSegment: any; /** @ngInject */ - constructor(private $scope, private $q) { + constructor(private $scope) { this.panelCtrl = $scope.ctrl; this.panel = this.panelCtrl.panel; this.$scope.ctrl = this; @@ -65,15 +65,6 @@ export class AxesEditorCtrl { xAxisValueChanged() { this.panelCtrl.onDataReceived(this.panelCtrl.dataList); } - - getDataFieldNames(onlyNumbers) { - const props = this.panelCtrl.processor.getDataFieldNames(this.panelCtrl.dataList, onlyNumbers); - const items = props.map(prop => { - return { text: prop, value: prop }; - }); - - return this.$q.when(items); - } } /** @ngInject */ diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index 4b57934824c..37fe9f3e219 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -1,11 +1,10 @@ import _ from 'lodash'; -import { colors, getColorFromHexRgbOrName } from '@grafana/ui'; +import { TimeRange, colors, getColorFromHexRgbOrName, FieldCache, FieldType, Field, SeriesData } from '@grafana/ui'; import TimeSeries from 'app/core/time_series2'; import config from 'app/core/config'; -import { LegacyResponseData, TimeRange } from '@grafana/ui'; type Options = { - dataList: LegacyResponseData[]; + dataList: SeriesData[]; range?: TimeRange; }; @@ -13,68 +12,81 @@ export class DataProcessor { constructor(private panel) {} getSeriesList(options: Options): TimeSeries[] { - if (!options.dataList || options.dataList.length === 0) { - return []; - } + const list: TimeSeries[] = []; + const { dataList, range } = options; - // auto detect xaxis mode - let firstItem; - if (options.dataList && options.dataList.length > 0) { - firstItem = options.dataList[0]; - const autoDetectMode = this.getAutoDetectXAxisMode(firstItem); - if (this.panel.xaxis.mode !== autoDetectMode) { - this.panel.xaxis.mode = autoDetectMode; - this.setPanelDefaultsForNewXAxisMode(); - } + if (!dataList || !dataList.length) { + return list; } - switch (this.panel.xaxis.mode) { - case 'series': - case 'time': { - return options.dataList.map((item, index) => { - return this.timeSeriesHandler(item, index, options); - }); + for (const series of dataList) { + const { fields } = series; + const cache = new FieldCache(fields); + const time = cache.getFirstFieldOfType(FieldType.time); + + if (!time) { + continue; } - case 'histogram': { - let histogramDataList; - if (this.panel.stack) { - histogramDataList = options.dataList; - } else { - histogramDataList = [ - { - target: 'count', - datapoints: _.concat([], _.flatten(_.map(options.dataList, 'datapoints'))), - }, - ]; + + const seriesName = series.name ? series.name : series.refId; + + for (let i = 0; i < fields.length; i++) { + if (fields[i].type !== FieldType.number) { + continue; } - return histogramDataList.map((item, index) => { - return this.timeSeriesHandler(item, index, options); - }); - } - case 'field': { - return this.customHandler(firstItem); + + const field = fields[i]; + let name = field.title; + + if (!field.title) { + name = field.name; + } + + if (seriesName && dataList.length > 0 && name !== seriesName) { + name = seriesName + ' ' + name; + } + + const datapoints = []; + for (const row of series.rows) { + datapoints.push([row[i], row[time.index]]); + } + + list.push(this.toTimeSeries(field, name, datapoints, list.length, range)); } } - return []; + // Merge all the rows if we want to show a histogram + if (this.panel.xaxis.mode === 'histogram' && !this.panel.stack && list.length > 1) { + const first = list[0]; + first.alias = first.aliasEscaped = 'Count'; + for (let i = 1; i < list.length; i++) { + first.datapoints = first.datapoints.concat(list[i].datapoints); + } + return [first]; + } + return list; } - getAutoDetectXAxisMode(firstItem) { - switch (firstItem.type) { - case 'docs': - return 'field'; - case 'table': - return 'field'; - default: { - if (this.panel.xaxis.mode === 'series') { - return 'series'; - } - if (this.panel.xaxis.mode === 'histogram') { - return 'histogram'; - } - return 'time'; + private toTimeSeries(field: Field, alias: string, datapoints: any[][], index: number, range?: TimeRange) { + const colorIndex = index % colors.length; + const color = this.panel.aliasColors[alias] || colors[colorIndex]; + + const series = new TimeSeries({ + datapoints: datapoints || [], + alias: alias, + color: getColorFromHexRgbOrName(color, config.theme.type), + unit: field.unit, + }); + + if (datapoints && datapoints.length > 0 && range) { + const last = datapoints[datapoints.length - 1][1]; + const from = range.from; + + if (last - from.valueOf() < -10000) { + series.isOutsideRange = true; } } + return series; } setPanelDefaultsForNewXAxisMode() { @@ -110,43 +122,6 @@ export class DataProcessor { } } - timeSeriesHandler(seriesData: LegacyResponseData, index: number, options: Options) { - const datapoints = seriesData.datapoints || []; - const alias = seriesData.target; - - const colorIndex = index % colors.length; - - const color = this.panel.aliasColors[alias] || colors[colorIndex]; - - const series = new TimeSeries({ - datapoints: datapoints, - alias: alias, - color: getColorFromHexRgbOrName(color, config.theme.type), - unit: seriesData.unit, - }); - - if (datapoints && datapoints.length > 0) { - const last = datapoints[datapoints.length - 1][1]; - const from = options.range.from; - - if (last - from.valueOf() < -10000) { - series.isOutsideRange = true; - } - } - - return series; - } - - customHandler(dataItem) { - const nameField = this.panel.xaxis.name; - if (!nameField) { - throw { - message: 'No field name specified to use for x-axis, check your axes settings', - }; - } - return []; - } - validateXAxisSeriesValue() { switch (this.panel.xaxis.mode) { case 'series': { @@ -165,40 +140,6 @@ export class DataProcessor { } } - getDataFieldNames(dataList, onlyNumbers) { - if (dataList.length === 0) { - return []; - } - - const fields = []; - const firstItem = dataList[0]; - const fieldParts = []; - - function getPropertiesRecursive(obj) { - _.forEach(obj, (value, key) => { - if (_.isObject(value)) { - fieldParts.push(key); - getPropertiesRecursive(value); - } else { - if (!onlyNumbers || _.isNumber(value)) { - const field = fieldParts.concat(key).join('.'); - fields.push(field); - } - } - }); - fieldParts.pop(); - } - - if (firstItem.type === 'docs') { - if (firstItem.datapoints.length === 0) { - return []; - } - getPropertiesRecursive(firstItem.datapoints[0]); - } - - return fields; - } - getXAxisValueOptions(options) { switch (this.panel.xaxis.mode) { case 'series': { diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 2412224eca6..f34ed05039d 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -11,7 +11,8 @@ import { DataProcessor } from './data_processor'; import { axesEditorComponent } from './axes_editor'; import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; -import { getColorFromHexRgbOrName, LegacyResponseData } from '@grafana/ui'; +import { getColorFromHexRgbOrName, LegacyResponseData, SeriesData } from '@grafana/ui'; +import { getProcessedSeriesData } from 'app/features/dashboard/state/PanelQueryState'; class GraphCtrl extends MetricsPanelCtrl { static template = template; @@ -19,7 +20,7 @@ class GraphCtrl extends MetricsPanelCtrl { renderError: boolean; hiddenSeries: any = {}; seriesList: TimeSeries[] = []; - dataList: LegacyResponseData[] = []; + dataList: SeriesData[] = []; annotations: any = []; alertState: any; @@ -188,9 +189,9 @@ class GraphCtrl extends MetricsPanelCtrl { } onDataReceived(dataList: LegacyResponseData[]) { - this.dataList = dataList; + this.dataList = getProcessedSeriesData(dataList); this.seriesList = this.processor.getSeriesList({ - dataList: dataList, + dataList: this.dataList, range: this.range, }); diff --git a/public/app/plugins/panel/graph/specs/__snapshots__/data_processor.test.ts.snap b/public/app/plugins/panel/graph/specs/__snapshots__/data_processor.test.ts.snap new file mode 100644 index 00000000000..99c3366fff3 --- /dev/null +++ b/public/app/plugins/panel/graph/specs/__snapshots__/data_processor.test.ts.snap @@ -0,0 +1,233 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Graph DataProcessor getTimeSeries from LegacyResponseData Should return a new series for each field 1`] = ` +Array [ + TimeSeries { + "alias": "Value", + "aliasEscaped": "Value", + "bars": Object { + "fillColor": "#7EB26D", + }, + "color": "#7EB26D", + "datapoints": Array [ + Array [ + 1, + 1001, + ], + Array [ + 2, + 1002, + ], + Array [ + 3, + 1003, + ], + ], + "hasMsResolution": false, + "id": "Value", + "label": "Value", + "legend": true, + "stats": Object {}, + "unit": "watt", + "valueFormater": [Function], + }, + TimeSeries { + "alias": "table_data v1", + "aliasEscaped": "table_data v1", + "bars": Object { + "fillColor": "#EAB839", + }, + "color": "#EAB839", + "datapoints": Array [ + Array [ + 0.1, + 1001, + ], + Array [ + 0.2, + 1002, + ], + Array [ + 0.3, + 1003, + ], + ], + "hasMsResolution": false, + "id": "table_data v1", + "label": "table_data v1", + "legend": true, + "stats": Object {}, + "unit": "ohm", + "valueFormater": [Function], + }, + TimeSeries { + "alias": "table_data v2", + "aliasEscaped": "table_data v2", + "bars": Object { + "fillColor": "#6ED0E0", + }, + "color": "#6ED0E0", + "datapoints": Array [ + Array [ + 1.1, + 1001, + ], + Array [ + 2.2, + 1002, + ], + Array [ + 3.3, + 1003, + ], + ], + "hasMsResolution": false, + "id": "table_data v2", + "label": "table_data v2", + "legend": true, + "stats": Object {}, + "unit": undefined, + "valueFormater": [Function], + }, + TimeSeries { + "alias": "series v1", + "aliasEscaped": "series v1", + "bars": Object { + "fillColor": "#EF843C", + }, + "color": "#EF843C", + "datapoints": Array [ + Array [ + 0.1, + 1001, + ], + Array [ + 0.2, + 1002, + ], + Array [ + 0.3, + 1003, + ], + ], + "hasMsResolution": false, + "id": "series v1", + "label": "series v1", + "legend": true, + "stats": Object {}, + "unit": undefined, + "valueFormater": [Function], + }, + TimeSeries { + "alias": "series v2", + "aliasEscaped": "series v2", + "bars": Object { + "fillColor": "#E24D42", + }, + "color": "#E24D42", + "datapoints": Array [ + Array [ + 1.1, + 1001, + ], + Array [ + 2.2, + 1002, + ], + Array [ + 3.3, + 1003, + ], + ], + "hasMsResolution": false, + "id": "series v2", + "label": "series v2", + "legend": true, + "stats": Object {}, + "unit": undefined, + "valueFormater": [Function], + }, +] +`; + +exports[`Graph DataProcessor getTimeSeries from LegacyResponseData Should return single histogram 1`] = ` +Array [ + TimeSeries { + "alias": "Count", + "aliasEscaped": "Count", + "bars": Object { + "fillColor": "#7EB26D", + }, + "color": "#7EB26D", + "datapoints": Array [ + Array [ + 1, + 1001, + ], + Array [ + 2, + 1002, + ], + Array [ + 3, + 1003, + ], + Array [ + 0.1, + 1001, + ], + Array [ + 0.2, + 1002, + ], + Array [ + 0.3, + 1003, + ], + Array [ + 1.1, + 1001, + ], + Array [ + 2.2, + 1002, + ], + Array [ + 3.3, + 1003, + ], + Array [ + 0.1, + 1001, + ], + Array [ + 0.2, + 1002, + ], + Array [ + 0.3, + 1003, + ], + Array [ + 1.1, + 1001, + ], + Array [ + 2.2, + 1002, + ], + Array [ + 3.3, + 1003, + ], + ], + "hasMsResolution": false, + "id": "Value", + "label": "Value", + "legend": true, + "stats": Object {}, + "unit": "watt", + "valueFormater": [Function], + }, +] +`; diff --git a/public/app/plugins/panel/graph/specs/data_processor.test.ts b/public/app/plugins/panel/graph/specs/data_processor.test.ts index b919bb0f3b1..d8b791bd386 100644 --- a/public/app/plugins/panel/graph/specs/data_processor.test.ts +++ b/public/app/plugins/panel/graph/specs/data_processor.test.ts @@ -1,62 +1,60 @@ import { DataProcessor } from '../data_processor'; +import { getProcessedSeriesData } from 'app/features/dashboard/state/PanelQueryState'; describe('Graph DataProcessor', () => { const panel: any = { - xaxis: {}, + xaxis: { mode: 'series' }, + aliasColors: {}, }; const processor = new DataProcessor(panel); - describe('Given default xaxis options and query that returns docs', () => { - beforeEach(() => { - panel.xaxis.mode = 'time'; - panel.xaxis.name = 'hostname'; - panel.xaxis.values = []; - - processor.getSeriesList({ - dataList: [ - { - type: 'docs', - datapoints: [{ hostname: 'server1', avg: 10 }], - }, + describe('getTimeSeries from LegacyResponseData', () => { + // Try each type of data + const dataList = getProcessedSeriesData([ + { + alias: 'First (time_series)', + datapoints: [[1, 1001], [2, 1002], [3, 1003]], + unit: 'watt', + }, + { + name: 'table_data', + columns: [ + { text: 'time' }, + { text: 'v1', unit: 'ohm' }, + { text: 'v2' }, // no unit + { text: 'string' }, // skipped ], - }); - }); - - it('Should automatically set xaxis mode to field', () => { - expect(panel.xaxis.mode).toBe('field'); - }); - }); - - describe('getDataFieldNames(', () => { - const dataList = [ + rows: [ + [1001, 0.1, 1.1, 'a'], // a + [1002, 0.2, 2.2, 'b'], // b + [1003, 0.3, 3.3, 'c'], // c + ], + }, { - type: 'docs', - datapoints: [ - { - hostname: 'server1', - valueField: 11, - nested: { - prop1: 'server2', - value2: 23, - }, - }, + name: 'series', + fields: [ + { name: 'v1' }, // first + { name: 'v2' }, // second + { name: 'string' }, // skip + { name: 'time' }, // Time is last column ], + rows: [[0.1, 1.1, 'a', 1001], [0.2, 2.2, 'b', 1002], [0.3, 3.3, 'c', 1003]], }, - ]; + ]); - it('Should return all field names', () => { - const fields = processor.getDataFieldNames(dataList, false); - expect(fields).toContain('hostname'); - expect(fields).toContain('valueField'); - expect(fields).toContain('nested.prop1'); - expect(fields).toContain('nested.value2'); + it('Should return a new series for each field', () => { + panel.xaxis.mode = 'series'; + const series = processor.getSeriesList({ dataList }); + expect(series.length).toEqual(5); + expect(series).toMatchSnapshot(); }); - it('Should return all number fields', () => { - const fields = processor.getDataFieldNames(dataList, true); - expect(fields).toContain('valueField'); - expect(fields).toContain('nested.value2'); + it('Should return single histogram', () => { + panel.xaxis.mode = 'histogram'; + const series = processor.getSeriesList({ dataList }); + expect(series.length).toEqual(1); + expect(series).toMatchSnapshot(); }); }); });