ソースを参照

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
Ryan McKinley 6 年 前
コミット
813e3ffc15

+ 1 - 10
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 */

+ 65 - 124
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': {

+ 5 - 4
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,
     });
 

+ 233 - 0
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],
+  },
+]
+`;

+ 42 - 44
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();
     });
   });
 });