GraphPanel: show results for all SeriesData (#16966)

* Graph panel should support SeriesData

* Graph panel should support SeriesData

* same path for all series

* merge master

* support docs

* add test for processor

* Graph: removed old unused data processing logic

* Graph: minor refactoring data processing

* fix histogram

* set Count as title
This commit is contained in:
Ryan McKinley 2019-05-10 22:41:32 -07:00 committed by Torkel Ödegaard
parent cf39a264ca
commit 813e3ffc15
5 changed files with 347 additions and 183 deletions

View File

@ -10,7 +10,7 @@ export class AxesEditorCtrl {
xNameSegment: any; xNameSegment: any;
/** @ngInject */ /** @ngInject */
constructor(private $scope, private $q) { constructor(private $scope) {
this.panelCtrl = $scope.ctrl; this.panelCtrl = $scope.ctrl;
this.panel = this.panelCtrl.panel; this.panel = this.panelCtrl.panel;
this.$scope.ctrl = this; this.$scope.ctrl = this;
@ -65,15 +65,6 @@ export class AxesEditorCtrl {
xAxisValueChanged() { xAxisValueChanged() {
this.panelCtrl.onDataReceived(this.panelCtrl.dataList); 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 */ /** @ngInject */

View File

@ -1,11 +1,10 @@
import _ from 'lodash'; 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 TimeSeries from 'app/core/time_series2';
import config from 'app/core/config'; import config from 'app/core/config';
import { LegacyResponseData, TimeRange } from '@grafana/ui';
type Options = { type Options = {
dataList: LegacyResponseData[]; dataList: SeriesData[];
range?: TimeRange; range?: TimeRange;
}; };
@ -13,68 +12,81 @@ export class DataProcessor {
constructor(private panel) {} constructor(private panel) {}
getSeriesList(options: Options): TimeSeries[] { getSeriesList(options: Options): TimeSeries[] {
if (!options.dataList || options.dataList.length === 0) { const list: TimeSeries[] = [];
return []; const { dataList, range } = options;
if (!dataList || !dataList.length) {
return list;
} }
// auto detect xaxis mode for (const series of dataList) {
let firstItem; const { fields } = series;
if (options.dataList && options.dataList.length > 0) { const cache = new FieldCache(fields);
firstItem = options.dataList[0]; const time = cache.getFirstFieldOfType(FieldType.time);
const autoDetectMode = this.getAutoDetectXAxisMode(firstItem);
if (this.panel.xaxis.mode !== autoDetectMode) {
this.panel.xaxis.mode = autoDetectMode;
this.setPanelDefaultsForNewXAxisMode();
}
}
switch (this.panel.xaxis.mode) { if (!time) {
case 'series': continue;
case 'time': {
return options.dataList.map((item, index) => {
return this.timeSeriesHandler(item, index, options);
});
} }
case 'histogram': {
let histogramDataList; const seriesName = series.name ? series.name : series.refId;
if (this.panel.stack) {
histogramDataList = options.dataList; for (let i = 0; i < fields.length; i++) {
} else { if (fields[i].type !== FieldType.number) {
histogramDataList = [ continue;
{
target: 'count',
datapoints: _.concat([], _.flatten(_.map(options.dataList, 'datapoints'))),
},
];
} }
return histogramDataList.map((item, index) => {
return this.timeSeriesHandler(item, index, options); const field = fields[i];
}); let name = field.title;
}
case 'field': { if (!field.title) {
return this.customHandler(firstItem); 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) { private toTimeSeries(field: Field, alias: string, datapoints: any[][], index: number, range?: TimeRange) {
switch (firstItem.type) { const colorIndex = index % colors.length;
case 'docs': const color = this.panel.aliasColors[alias] || colors[colorIndex];
return 'field';
case 'table': const series = new TimeSeries({
return 'field'; datapoints: datapoints || [],
default: { alias: alias,
if (this.panel.xaxis.mode === 'series') { color: getColorFromHexRgbOrName(color, config.theme.type),
return 'series'; unit: field.unit,
} });
if (this.panel.xaxis.mode === 'histogram') {
return 'histogram'; if (datapoints && datapoints.length > 0 && range) {
} const last = datapoints[datapoints.length - 1][1];
return 'time'; const from = range.from;
if (last - from.valueOf() < -10000) {
series.isOutsideRange = true;
} }
} }
return series;
} }
setPanelDefaultsForNewXAxisMode() { 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() { validateXAxisSeriesValue() {
switch (this.panel.xaxis.mode) { switch (this.panel.xaxis.mode) {
case 'series': { 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) { getXAxisValueOptions(options) {
switch (this.panel.xaxis.mode) { switch (this.panel.xaxis.mode) {
case 'series': { case 'series': {

View File

@ -11,7 +11,8 @@ import { DataProcessor } from './data_processor';
import { axesEditorComponent } from './axes_editor'; import { axesEditorComponent } from './axes_editor';
import config from 'app/core/config'; import config from 'app/core/config';
import TimeSeries from 'app/core/time_series2'; 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 { class GraphCtrl extends MetricsPanelCtrl {
static template = template; static template = template;
@ -19,7 +20,7 @@ class GraphCtrl extends MetricsPanelCtrl {
renderError: boolean; renderError: boolean;
hiddenSeries: any = {}; hiddenSeries: any = {};
seriesList: TimeSeries[] = []; seriesList: TimeSeries[] = [];
dataList: LegacyResponseData[] = []; dataList: SeriesData[] = [];
annotations: any = []; annotations: any = [];
alertState: any; alertState: any;
@ -188,9 +189,9 @@ class GraphCtrl extends MetricsPanelCtrl {
} }
onDataReceived(dataList: LegacyResponseData[]) { onDataReceived(dataList: LegacyResponseData[]) {
this.dataList = dataList; this.dataList = getProcessedSeriesData(dataList);
this.seriesList = this.processor.getSeriesList({ this.seriesList = this.processor.getSeriesList({
dataList: dataList, dataList: this.dataList,
range: this.range, range: this.range,
}); });

View File

@ -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],
},
]
`;

View File

@ -1,62 +1,60 @@
import { DataProcessor } from '../data_processor'; import { DataProcessor } from '../data_processor';
import { getProcessedSeriesData } from 'app/features/dashboard/state/PanelQueryState';
describe('Graph DataProcessor', () => { describe('Graph DataProcessor', () => {
const panel: any = { const panel: any = {
xaxis: {}, xaxis: { mode: 'series' },
aliasColors: {},
}; };
const processor = new DataProcessor(panel); const processor = new DataProcessor(panel);
describe('Given default xaxis options and query that returns docs', () => { describe('getTimeSeries from LegacyResponseData', () => {
beforeEach(() => { // Try each type of data
panel.xaxis.mode = 'time'; const dataList = getProcessedSeriesData([
panel.xaxis.name = 'hostname';
panel.xaxis.values = [];
processor.getSeriesList({
dataList: [
{
type: 'docs',
datapoints: [{ hostname: 'server1', avg: 10 }],
},
],
});
});
it('Should automatically set xaxis mode to field', () => {
expect(panel.xaxis.mode).toBe('field');
});
});
describe('getDataFieldNames(', () => {
const dataList = [
{ {
type: 'docs', alias: 'First (time_series)',
datapoints: [ datapoints: [[1, 1001], [2, 1002], [3, 1003]],
{ unit: 'watt',
hostname: 'server1', },
valueField: 11, {
nested: { name: 'table_data',
prop1: 'server2', columns: [
value2: 23, { text: 'time' },
}, { text: 'v1', unit: 'ohm' },
}, { text: 'v2' }, // no unit
{ text: 'string' }, // skipped
],
rows: [
[1001, 0.1, 1.1, 'a'], // a
[1002, 0.2, 2.2, 'b'], // b
[1003, 0.3, 3.3, 'c'], // c
], ],
}, },
]; {
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', () => { it('Should return a new series for each field', () => {
const fields = processor.getDataFieldNames(dataList, false); panel.xaxis.mode = 'series';
expect(fields).toContain('hostname'); const series = processor.getSeriesList({ dataList });
expect(fields).toContain('valueField'); expect(series.length).toEqual(5);
expect(fields).toContain('nested.prop1'); expect(series).toMatchSnapshot();
expect(fields).toContain('nested.value2');
}); });
it('Should return all number fields', () => { it('Should return single histogram', () => {
const fields = processor.getDataFieldNames(dataList, true); panel.xaxis.mode = 'histogram';
expect(fields).toContain('valueField'); const series = processor.getSeriesList({ dataList });
expect(fields).toContain('nested.value2'); expect(series.length).toEqual(1);
expect(series).toMatchSnapshot();
}); });
}); });
}); });