Commit 813e3ffc by Ryan McKinley Committed by Torkel Ödegaard

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
parent cf39a264
...@@ -10,7 +10,7 @@ export class AxesEditorCtrl { ...@@ -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 { ...@@ -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 */
......
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 { ...@@ -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;
}
// auto detect xaxis mode if (!dataList || !dataList.length) {
let firstItem; return list;
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();
}
} }
switch (this.panel.xaxis.mode) { for (const series of dataList) {
case 'series': const { fields } = series;
case 'time': { const cache = new FieldCache(fields);
return options.dataList.map((item, index) => { const time = cache.getFirstFieldOfType(FieldType.time);
return this.timeSeriesHandler(item, index, options);
}); if (!time) {
continue;
} }
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 { ...@@ -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 { ...@@ -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': {
......
...@@ -11,7 +11,8 @@ import { DataProcessor } from './data_processor'; ...@@ -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 { ...@@ -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 { ...@@ -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,
}); });
......
// 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],
},
]
`;
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 = []; alias: 'First (time_series)',
datapoints: [[1, 1001], [2, 1002], [3, 1003]],
processor.getSeriesList({ unit: 'watt',
dataList: [ },
{ {
type: 'docs', name: 'table_data',
datapoints: [{ hostname: 'server1', avg: 10 }], columns: [
}, { 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
it('Should automatically set xaxis mode to field', () => { [1003, 0.3, 3.3, 'c'], // c
expect(panel.xaxis.mode).toBe('field'); ],
}); },
});
describe('getDataFieldNames(', () => {
const dataList = [
{ {
type: 'docs', name: 'series',
datapoints: [ fields: [
{ { name: 'v1' }, // first
hostname: 'server1', { name: 'v2' }, // second
valueField: 11, { name: 'string' }, // skip
nested: { { name: 'time' }, // Time is last column
prop1: 'server2',
value2: 23,
},
},
], ],
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();
}); });
}); });
}); });
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment