mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
cf39a264ca
commit
813e3ffc15
@ -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 */
|
||||
|
@ -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;
|
||||
|
||||
if (!dataList || !dataList.length) {
|
||||
return list;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
for (const series of dataList) {
|
||||
const { fields } = series;
|
||||
const cache = new FieldCache(fields);
|
||||
const time = cache.getFirstFieldOfType(FieldType.time);
|
||||
|
||||
switch (this.panel.xaxis.mode) {
|
||||
case 'series':
|
||||
case 'time': {
|
||||
return options.dataList.map((item, index) => {
|
||||
return this.timeSeriesHandler(item, index, options);
|
||||
});
|
||||
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': {
|
||||
|
@ -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,
|
||||
});
|
||||
|
||||
|
@ -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],
|
||||
},
|
||||
]
|
||||
`;
|
@ -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 }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('Should automatically set xaxis mode to field', () => {
|
||||
expect(panel.xaxis.mode).toBe('field');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDataFieldNames(', () => {
|
||||
const dataList = [
|
||||
describe('getTimeSeries from LegacyResponseData', () => {
|
||||
// Try each type of data
|
||||
const dataList = getProcessedSeriesData([
|
||||
{
|
||||
type: 'docs',
|
||||
datapoints: [
|
||||
{
|
||||
hostname: 'server1',
|
||||
valueField: 11,
|
||||
nested: {
|
||||
prop1: 'server2',
|
||||
value2: 23,
|
||||
},
|
||||
},
|
||||
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
|
||||
],
|
||||
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', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user