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 {
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 */
......
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': {
......
......@@ -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,
});
......
// 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 { 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();
});
});
});
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