Kaynağa Gözat

SingleStat: use DataFrame results rather than TimeSeries/TableData (#18580)

Ryan McKinley 6 yıl önce
ebeveyn
işleme
68f7413566

+ 231 - 0
devenv/dev-dashboards/panel-singlestat/singlestat_test.json

@@ -501,6 +501,237 @@
         }
       ],
       "valueName": "current"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorPrefix": false,
+      "colorValue": false,
+      "colors": ["#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a"],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "none",
+      "gauge": {
+        "maxValue": 150,
+        "minValue": 0,
+        "show": false,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 4,
+        "w": 8,
+        "x": 0,
+        "y": 14
+      },
+      "id": 8,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "options": {},
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": false
+      },
+      "tableColumn": "Info",
+      "targets": [
+        {
+          "alias": "",
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "random_walk_table",
+          "stringInput": ""
+        }
+      ],
+      "thresholds": "81,90",
+      "title": "TableData 'Info' string Column",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [],
+      "valueName": "current"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorPrefix": false,
+      "colorValue": false,
+      "colors": ["#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a"],
+      "datasource": "gdev-testdata",
+      "decimals": 2,
+      "description": "",
+      "format": "celsius",
+      "gauge": {
+        "maxValue": 150,
+        "minValue": 0,
+        "show": false,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 4,
+        "w": 8,
+        "x": 8,
+        "y": 14
+      },
+      "id": 9,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "options": {},
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": false
+      },
+      "tableColumn": "Min",
+      "targets": [
+        {
+          "alias": "",
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "random_walk_table",
+          "stringInput": ""
+        }
+      ],
+      "thresholds": "81,90",
+      "title": "TableData 'Value' as temp Column",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [],
+      "valueName": "current"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorPrefix": false,
+      "colorValue": false,
+      "colors": ["#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a"],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "dateTimeFromNow",
+      "gauge": {
+        "maxValue": 150,
+        "minValue": 0,
+        "show": false,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 4,
+        "w": 8,
+        "x": 16,
+        "y": 14
+      },
+      "id": 10,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "options": {},
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": false
+      },
+      "tableColumn": "time",
+      "targets": [
+        {
+          "alias": "",
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "random_walk",
+          "stringInput": ""
+        }
+      ],
+      "thresholds": "81,90",
+      "title": "last_time display (a few seconds ago)",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [],
+      "valueName": "last_time"
     }
   ],
   "refresh": false,

+ 1 - 1
packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts

@@ -192,7 +192,7 @@ export function migrateOldThresholds(thresholds?: any[]): Threshold[] | undefine
 /**
  * Convert the angular single stat mapping to new react style
  */
-function convertOldAngulrValueMapping(panel: any): ValueMapping[] {
+export function convertOldAngulrValueMapping(panel: any): ValueMapping[] {
   const mappings: ValueMapping[] = [];
 
   // Guess the right type based on options

+ 1 - 0
packages/grafana-ui/src/components/SingleStatShared/index.ts

@@ -5,4 +5,5 @@ export {
   SingleStatBaseOptions,
   sharedSingleStatPanelChangedHandler,
   sharedSingleStatMigrationHandler,
+  convertOldAngulrValueMapping,
 } from './SingleStatBaseOptions';

+ 8 - 0
packages/grafana-ui/src/utils/displayProcessor.ts

@@ -58,6 +58,14 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
         if (shouldFormat && !_.isBoolean(value)) {
           const { decimals, scaledDecimals } = getDecimalsForValue(value, field.decimals);
           text = formatFunc(numeric, decimals, scaledDecimals, options.isUtc);
+
+          // Check if the formatted text mapped to a different value
+          if (mappings && mappings.length > 0) {
+            const mappedValue = getMappedValue(mappings, text);
+            if (mappedValue) {
+              text = mappedValue.text;
+            }
+          }
         }
         if (thresholds && thresholds.length) {
           color = getColorFromThreshold(numeric, thresholds, theme);

+ 21 - 14
public/app/plugins/panel/singlestat/editor.html

@@ -40,16 +40,18 @@
     <h5 class="section-heading">Value</h5>
 
     <div class="gf-form-inline">
-      <div class="gf-form" ng-show="ctrl.dataType === 'timeseries'">
-        <label class="gf-form-label width-6">Stat</label>
+      <div class="gf-form" ng-if="ctrl.fieldNames.length > 1">
+        <label class="gf-form-label width-6">Field</label>
         <div class="gf-form-select-wrapper width-12">
-          <select class="gf-form-input" ng-model="ctrl.panel.valueName" ng-options="f.value as f.text for f in ctrl.valueNameOptions" ng-change="ctrl.refresh()"></select>
+          <select class="gf-form-input" ng-model="ctrl.panel.tableColumn" ng-options="f for f in ctrl.fieldNames" ng-change="ctrl.refresh()"></select>
         </div>
       </div>
-      <div class="gf-form" ng-show="ctrl.dataType === 'table'">
-        <label class="gf-form-label width-6">Column</label>
+    </div>
+    <div class="gf-form-inline">
+      <div class="gf-form">
+        <label class="gf-form-label width-6">Show</label>
         <div class="gf-form-select-wrapper width-12">
-          <select class="gf-form-input" ng-model="ctrl.panel.tableColumn" ng-options="f for f in ctrl.tableColumnOptions" ng-change="ctrl.refresh()"></select>
+          <select class="gf-form-input" ng-model="ctrl.panel.valueName" ng-options="f.value as f.text for f in ctrl.valueNameOptions" ng-change="ctrl.refresh()"></select>
         </div>
       </div>
       <div class="gf-form">
@@ -64,19 +66,24 @@
       <div class="gf-form">
         <label class="gf-form-label width-6">Prefix</label>
         <input type="text" class="gf-form-input width-12" ng-model="ctrl.panel.prefix" ng-change="ctrl.render()" ng-model-onblur>
+      </div>
+      <div class="gf-form">
         <label class="gf-form-label width-6">Font size</label>
         <div class="gf-form-select-wrapper">
           <select class="gf-form-input" ng-model="ctrl.panel.prefixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></select>
         </div>
       </div>
     </div>
-
-    <div class="gf-form">
-      <label class="gf-form-label width-6">Postfix</label>
-      <input type="text" class="gf-form-input width-12" ng-model="ctrl.panel.postfix" ng-change="ctrl.render()" ng-model-onblur>
-      <label class="gf-form-label width-6">Font size</label>
-      <div class="gf-form-select-wrapper">
-        <select class="input-small gf-form-input" ng-model="ctrl.panel.postfixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></select>
+    <div class="gf-form-inline">
+      <div class="gf-form">
+        <label class="gf-form-label width-6">Postfix</label>
+        <input type="text" class="gf-form-input width-12" ng-model="ctrl.panel.postfix" ng-change="ctrl.render()" ng-model-onblur>
+      </div>
+      <div class="gf-form">
+        <label class="gf-form-label width-6">Font size</label>
+        <div class="gf-form-select-wrapper">
+          <select class="input-small gf-form-input" ng-model="ctrl.panel.postfixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></select>
+        </div>
       </div>
     </div>
     <div class="gf-form">
@@ -122,7 +129,7 @@
 
   <div class="section gf-form-group">
     <h5 class="section-heading">Spark lines</h5>
-    <gf-form-switch class="gf-form" label-class="width-9" label="Show" checked="ctrl.panel.sparkline.show" on-change="ctrl.render()"></gf-form-switch>
+    <gf-form-switch class="gf-form" label-class="width-9" label="Show" checked="ctrl.panel.sparkline.show" on-change="ctrl.refresh()"></gf-form-switch>
     <div ng-if="ctrl.panel.sparkline.show">
       <gf-form-switch class="gf-form" label-class="width-9" label="Full height" checked="ctrl.panel.sparkline.full" on-change="ctrl.render()"></gf-form-switch>
       <div class="gf-form">

+ 6 - 6
public/app/plugins/panel/singlestat/mappings.html

@@ -6,7 +6,7 @@
         </span>
         <div class="gf-form-select-wrapper">
           <select class="gf-form-input" ng-model="ctrl.panel.mappingType"
-                                                        ng-options="f.value as f.name for f in ctrl.panel.mappingTypes" ng-change="ctrl.render()"></select>
+                                                        ng-options="f.value as f.name for f in ctrl.panel.mappingTypes" ng-change="ctrl.refresh()"></select>
         </div>
     </div>
   </div>
@@ -18,11 +18,11 @@
       <span class="gf-form-label">
         <i class="fa fa-remove pointer" ng-click="ctrl.removeValueMap(map)"></i>
       </span>
-      <input type="text" ng-model="map.value" placeholder="value" class="gf-form-input max-width-6" ng-blur="ctrl.render()">
+      <input type="text" ng-model="map.value" placeholder="value" class="gf-form-input max-width-6" ng-blur="ctrl.refresh()">
       <span class="gf-form-label">
         <i class="fa fa-arrow-right"></i>
       </span>
-      <input type="text" placeholder="text" ng-model="map.text" class="gf-form-input max-width-8" ng-blur="ctrl.render()">
+      <input type="text" placeholder="text" ng-model="map.text" class="gf-form-input max-width-8" ng-blur="ctrl.refresh()">
     </div>
 
     <div class="gf-form-button-row">
@@ -41,11 +41,11 @@
           <i class="fa fa-remove pointer" ng-click="ctrl.removeRangeMap(rangeMap)"></i>
         </span>
         <span class="gf-form-label">From</span>
-        <input type="text" ng-model="rangeMap.from" class="gf-form-input max-width-6" ng-blur="ctrl.render()">
+        <input type="text" ng-model="rangeMap.from" class="gf-form-input max-width-6" ng-blur="ctrl.refresh()">
         <span class="gf-form-label">To</span>
-        <input type="text" ng-model="rangeMap.to" class="gf-form-input max-width-6" ng-blur="ctrl.render()">
+        <input type="text" ng-model="rangeMap.to" class="gf-form-input max-width-6" ng-blur="ctrl.refresh()">
         <span class="gf-form-label">Text</span>
-        <input type="text" ng-model="rangeMap.text" class="gf-form-input max-width-8" ng-blur="ctrl.render()">
+        <input type="text" ng-model="rangeMap.text" class="gf-form-input max-width-8" ng-blur="ctrl.refresh()">
     </div>
 
     <div class="gf-form-button-row">

+ 203 - 208
public/app/plugins/panel/singlestat/module.ts

@@ -3,34 +3,56 @@ import $ from 'jquery';
 import 'vendor/flot/jquery.flot';
 import 'vendor/flot/jquery.flot.gauge';
 import 'app/features/panel/panellinks/link_srv';
-import { getDecimalsForValue } from '@grafana/ui';
+import {
+  LegacyResponseData,
+  getFlotPairs,
+  getDisplayProcessor,
+  convertOldAngulrValueMapping,
+  getColorFromHexRgbOrName,
+} from '@grafana/ui';
 
 import kbn from 'app/core/utils/kbn';
 import config from 'app/core/config';
-import TimeSeries from 'app/core/time_series2';
 import { MetricsPanelCtrl } from 'app/plugins/sdk';
-import { isTableData } from '@grafana/data';
-import { GrafanaThemeType, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui';
+import {
+  DataFrame,
+  FieldType,
+  reduceField,
+  ReducerID,
+  Field,
+  GraphSeriesValue,
+  DisplayValue,
+  fieldReducers,
+  KeyValue,
+} from '@grafana/data';
 import { auto } from 'angular';
 import { LinkSrv, LinkModel } from 'app/features/panel/panellinks/link_srv';
-import TableModel from 'app/core/table_model';
+import { PanelQueryRunnerFormat } from 'app/features/dashboard/state/PanelQueryRunner';
+import { getProcessedDataFrames } from 'app/features/dashboard/state/PanelQueryState';
 
 const BASE_FONT_SIZE = 38;
 
-interface DataFormat {
-  value: string | number;
-  valueFormatted: string;
-  valueRounded: number;
+export interface ShowData {
+  field: Field;
+  value: any;
+  sparkline: GraphSeriesValue[][];
+  display: DisplayValue;
+
+  scopedVars: any;
+
+  thresholds: any[];
+  colorMap: any;
 }
 
 class SingleStatCtrl extends MetricsPanelCtrl {
   static templateUrl = 'module.html';
 
-  dataType = 'timeseries';
-  series: any[];
-  data: any;
+  data: Partial<ShowData> = {};
+
   fontSizes: any[];
   unitFormats: any[];
+  fieldNames: string[] = [];
+
   invalidGaugeRange: boolean;
   panel: any;
   events: any;
@@ -47,7 +69,6 @@ class SingleStatCtrl extends MetricsPanelCtrl {
     { value: 'range', text: 'Range' },
     { value: 'last_time', text: 'Time of last point' },
   ];
-  tableColumnOptions: any;
 
   // Set and populate defaults
   panelDefaults: any = {
@@ -102,6 +123,8 @@ class SingleStatCtrl extends MetricsPanelCtrl {
     this.events.on('data-snapshot-load', this.onDataReceived.bind(this));
     this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
 
+    this.dataFormat = PanelQueryRunnerFormat.frames;
+
     this.onSparklineColorChange = this.onSparklineColorChange.bind(this);
     this.onSparklineFillChange = this.onSparklineFillChange.bind(this);
   }
@@ -128,104 +151,115 @@ class SingleStatCtrl extends MetricsPanelCtrl {
   }
 
   onDataError(err: any) {
-    this.onDataReceived([]);
+    this.handleDataFrames([]);
   }
 
-  onDataReceived(dataList: any[]) {
-    const data: any = {
-      scopedVars: _.extend({}, this.panel.scopedVars),
-    };
+  // This should only be called from the snapshot callback
+  onDataReceived(dataList: LegacyResponseData[]) {
+    this.handleDataFrames(getProcessedDataFrames(dataList));
+  }
+
+  // Directly support DataFrame skipping event callbacks
+  handleDataFrames(frames: DataFrame[]) {
+    const { panel } = this;
+    super.handleDataFrames(frames);
+    this.loading = false;
+
+    const distinct = getDistinctNames(frames);
+    let fieldInfo = distinct.byName[panel.tableColumn]; //
+    this.fieldNames = distinct.names;
+
+    if (!fieldInfo) {
+      fieldInfo = distinct.first;
+    }
 
-    if (dataList.length > 0 && isTableData(dataList[0])) {
-      this.dataType = 'table';
-      const tableData = dataList.map(this.tableHandler.bind(this));
-      this.setTableValues(tableData, data);
+    if (!fieldInfo) {
+      // When we don't have any field
+      this.data = {
+        value: 'No Data',
+        display: {
+          text: 'No Data',
+          numeric: NaN,
+        },
+      };
     } else {
-      this.dataType = 'timeseries';
-      this.series = dataList.map(this.seriesHandler.bind(this));
-      this.setValues(data);
+      this.data = this.processField(fieldInfo);
     }
 
-    this.data = data;
     this.render();
   }
 
-  seriesHandler(dataFrame: any) {
-    const series = new TimeSeries({
-      datapoints: dataFrame.datapoints || [],
-      alias: dataFrame.target,
-    });
+  processField(fieldInfo: FieldInfo) {
+    const { panel, dashboard } = this;
 
-    series.flotpairs = series.getFlotPairs(this.panel.nullPointMode);
-    return series;
-  }
+    const name = fieldInfo.field.config.title || fieldInfo.field.name;
+    let calc = panel.valueName;
+    let calcField = fieldInfo.field;
+    let val: any = undefined;
 
-  tableHandler(tableData: TableModel) {
-    const datapoints: any[] = [];
-    const columnNames: string[] = [];
+    if ('name' === calc) {
+      val = name;
+    } else {
+      if ('last_time' === calc) {
+        if (fieldInfo.frame.firstTimeField) {
+          calcField = fieldInfo.frame.firstTimeField;
+          calc = ReducerID.last;
+        }
+      }
 
-    tableData.columns.forEach((column, columnIndex) => {
-      columnNames[columnIndex] = column.text;
-    });
+      // Normalize functions (avg -> mean, etc)
+      const r = fieldReducers.getIfExists(calc);
+      if (r) {
+        calc = r.id;
+        // With strings, don't accidentally use a math function
+        if (calcField.type === FieldType.string) {
+          const avoid = [ReducerID.mean, ReducerID.sum];
+          if (avoid.includes(calc)) {
+            calc = panel.valueName = ReducerID.first;
+          }
+        }
+      } else {
+        calc = ReducerID.lastNotNull;
+      }
 
-    this.tableColumnOptions = columnNames;
-    if (!_.find(tableData.columns, ['text', this.panel.tableColumn])) {
-      this.setTableColumnToSensibleDefault(tableData);
+      // Calculate the value
+      val = reduceField({
+        field: calcField,
+        reducers: [calc],
+      })[calc];
     }
 
-    tableData.rows.forEach(row => {
-      const datapoint: any = {};
-
-      row.forEach((value: any, columnIndex: number) => {
-        const key = columnNames[columnIndex];
-        datapoint[key] = value;
-      });
-
-      datapoints.push(datapoint);
+    const processor = getDisplayProcessor({
+      field: {
+        ...fieldInfo.field.config,
+        unit: panel.format,
+        decimals: panel.decimals,
+        mappings: convertOldAngulrValueMapping(panel),
+      },
+      theme: config.theme,
+      isUtc: dashboard.isTimezoneUtc && dashboard.isTimezoneUtc(),
     });
 
-    return datapoints;
-  }
-
-  setTableColumnToSensibleDefault(tableData: TableModel) {
-    if (tableData.columns.length === 1) {
-      this.panel.tableColumn = tableData.columns[0].text;
-    } else {
-      this.panel.tableColumn = _.find(tableData.columns, col => {
-        return col.type !== 'time';
-      }).text;
-    }
-  }
-
-  setTableValues(tableData: any[], data: DataFormat) {
-    if (!tableData || tableData.length === 0) {
-      return;
-    }
-
-    if (tableData[0].length === 0 || tableData[0][0][this.panel.tableColumn] === undefined) {
-      return;
-    }
+    const data = {
+      field: fieldInfo.field,
+      value: val,
+      display: processor(val),
+      scopedVars: _.extend({}, panel.scopedVars),
+    };
 
-    const datapoint = tableData[0][0];
-    data.value = datapoint[this.panel.tableColumn];
+    data.scopedVars['__name'] = name;
+    panel.tableColumn = this.fieldNames.length > 1 ? name : '';
 
-    if (_.isString(data.value)) {
-      data.valueFormatted = _.escape(data.value);
-      data.value = 0;
-      data.valueRounded = 0;
-    } else {
-      const decimalInfo = getDecimalsForValue(data.value, this.panel.decimals);
-      const formatFunc = getValueFormat(this.panel.format);
-
-      data.valueFormatted = formatFunc(
-        datapoint[this.panel.tableColumn],
-        decimalInfo.decimals,
-        decimalInfo.scaledDecimals
-      );
-      data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals);
+    // Get the fields for a sparkline
+    if (panel.sparkline && panel.sparkline.show && fieldInfo.frame.firstTimeField) {
+      this.data.sparkline = getFlotPairs({
+        xField: fieldInfo.frame.firstTimeField,
+        yField: fieldInfo.field,
+        nullValueMode: panel.nullPointMode,
+      });
     }
 
-    this.setValueMapping(data);
+    return data;
   }
 
   canModifyText() {
@@ -267,106 +301,6 @@ class SingleStatCtrl extends MetricsPanelCtrl {
     this.render();
   }
 
-  setValues(data: any) {
-    data.flotpairs = [];
-
-    if (this.series.length > 1) {
-      const error: any = new Error();
-      error.message = 'Multiple Series Error';
-      error.data =
-        'Metric query returns ' +
-        this.series.length +
-        ' series. Single Stat Panel expects a single series.\n\nResponse:\n' +
-        JSON.stringify(this.series);
-      throw error;
-    }
-
-    if (this.series && this.series.length > 0) {
-      const lastPoint: any = _.last(this.series[0].datapoints);
-      const lastValue = _.isArray(lastPoint) ? lastPoint[0] : null;
-      const formatFunc = getValueFormat(this.panel.format);
-
-      if (this.panel.valueName === 'name') {
-        data.value = 0;
-        data.valueRounded = 0;
-        data.valueFormatted = this.series[0].alias;
-      } else if (_.isString(lastValue)) {
-        data.value = 0;
-        data.valueFormatted = _.escape(lastValue);
-        data.valueRounded = 0;
-      } else if (this.panel.valueName === 'last_time') {
-        data.value = lastPoint[1];
-        data.valueRounded = data.value;
-        data.valueFormatted = formatFunc(data.value, 0, 0, this.dashboard.isTimezoneUtc());
-      } else {
-        data.value = this.series[0].stats[this.panel.valueName];
-        data.flotpairs = this.series[0].flotpairs;
-
-        const decimalInfo = getDecimalsForValue(data.value, this.panel.decimals);
-
-        data.valueFormatted = formatFunc(
-          data.value,
-          decimalInfo.decimals,
-          decimalInfo.scaledDecimals,
-          this.dashboard.isTimezoneUtc()
-        );
-        data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals);
-      }
-
-      // Add $__name variable for using in prefix or postfix
-      data.scopedVars['__name'] = { value: this.series[0].label };
-    }
-    this.setValueMapping(data);
-  }
-
-  setValueMapping(data: DataFormat) {
-    // check value to text mappings if its enabled
-    if (this.panel.mappingType === 1) {
-      for (let i = 0; i < this.panel.valueMaps.length; i++) {
-        const map = this.panel.valueMaps[i];
-        // special null case
-        if (map.value === 'null') {
-          if (data.value === null || data.value === void 0) {
-            data.valueFormatted = map.text;
-            return;
-          }
-          continue;
-        }
-
-        // value/number to text mapping
-        const value = parseFloat(map.value);
-        if (value === data.valueRounded) {
-          data.valueFormatted = map.text;
-          return;
-        }
-      }
-    } else if (this.panel.mappingType === 2) {
-      for (let i = 0; i < this.panel.rangeMaps.length; i++) {
-        const map = this.panel.rangeMaps[i];
-        // special null case
-        if (map.from === 'null' && map.to === 'null') {
-          if (data.value === null || data.value === void 0) {
-            data.valueFormatted = map.text;
-            return;
-          }
-          continue;
-        }
-
-        // value/number to range mapping
-        const from = parseFloat(map.from);
-        const to = parseFloat(map.to);
-        if (to >= data.valueRounded && from <= data.valueRounded) {
-          data.valueFormatted = map.text;
-          return;
-        }
-      }
-    }
-
-    if (data.value === null || data.value === void 0) {
-      data.valueFormatted = 'no value';
-    }
-  }
-
   removeValueMap(map: any) {
     const index = _.indexOf(this.panel.valueMaps, map);
     this.panel.valueMaps.splice(index, 1);
@@ -394,12 +328,12 @@ class SingleStatCtrl extends MetricsPanelCtrl {
     const $sanitize = this.$sanitize;
     const panel = ctrl.panel;
     const templateSrv = this.templateSrv;
-    let data: any;
     let linkInfo: LinkModel | null = null;
     const $panelContainer = elem.find('.panel-container');
     elem = elem.find('.singlestat-panel');
 
     function applyColoringThresholds(valueString: string) {
+      const data = ctrl.data;
       const color = getColorForValue(data, data.value);
       if (color) {
         return '<span style="color:' + color + '">' + valueString + '</span>';
@@ -409,20 +343,21 @@ class SingleStatCtrl extends MetricsPanelCtrl {
     }
 
     function getSpan(className: string, fontSizePercent: string, applyColoring: any, value: string) {
-      value = $sanitize(templateSrv.replace(value, data.scopedVars));
+      value = $sanitize(templateSrv.replace(value, ctrl.data.scopedVars));
       value = applyColoring ? applyColoringThresholds(value) : value;
       const pixelSize = (parseInt(fontSizePercent, 10) / 100) * BASE_FONT_SIZE;
       return '<span class="' + className + '" style="font-size:' + pixelSize + 'px">' + value + '</span>';
     }
 
     function getBigValueHtml() {
+      const data: ShowData = ctrl.data;
       let body = '<div class="singlestat-panel-value-container">';
 
       if (panel.prefix) {
         body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, panel.colorPrefix, panel.prefix);
       }
 
-      body += getSpan('singlestat-panel-value', panel.valueFontSize, panel.colorValue, data.valueFormatted);
+      body += getSpan('singlestat-panel-value', panel.valueFontSize, panel.colorValue, data.display.text);
 
       if (panel.postfix) {
         body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, panel.colorPostfix, panel.postfix);
@@ -434,14 +369,16 @@ class SingleStatCtrl extends MetricsPanelCtrl {
     }
 
     function getValueText() {
+      const data: ShowData = ctrl.data;
       let result = panel.prefix ? templateSrv.replace(panel.prefix, data.scopedVars) : '';
-      result += data.valueFormatted;
+      result += data.display.text;
       result += panel.postfix ? templateSrv.replace(panel.postfix, data.scopedVars) : '';
 
       return result;
     }
 
     function addGauge() {
+      const data: ShowData = ctrl.data;
       const width = elem.width();
       const height = elem.height();
       // Allow to use a bit more space for wide gauges
@@ -513,7 +450,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
               width: thresholdMarkersWidth,
             },
             value: {
-              color: panel.colorValue ? getColorForValue(data, data.valueRounded) : null,
+              color: panel.colorValue ? getColorForValue(data, data.display.numeric) : null,
               formatter: () => {
                 return getValueText();
               },
@@ -537,6 +474,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
     }
 
     function addSparkline() {
+      const data: ShowData = ctrl.data;
       const width = elem.width();
       if (width < 30) {
         // element has not gotten it's width yet
@@ -544,6 +482,10 @@ class SingleStatCtrl extends MetricsPanelCtrl {
         setTimeout(addSparkline, 30);
         return;
       }
+      if (!data.sparkline || !data.sparkline.length) {
+        // no sparkline data
+        return;
+      }
 
       const height = ctrl.height;
       const plotCanvas = $('<div></div>');
@@ -592,7 +534,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
       elem.append(plotCanvas);
 
       const plotSeries = {
-        data: data.flotpairs,
+        data: data.sparkline,
         color: getColorFromHexRgbOrName(panel.sparkline.lineColor, config.theme.type),
       };
 
@@ -603,26 +545,24 @@ class SingleStatCtrl extends MetricsPanelCtrl {
       if (!ctrl.data) {
         return;
       }
-      data = ctrl.data;
+      const { data, panel } = ctrl;
 
       // get thresholds
-      data.thresholds = panel.thresholds.split(',').map((strVale: string) => {
-        return Number(strVale.trim());
-      });
+      data.thresholds = panel.thresholds
+        ? panel.thresholds.split(',').map((strVale: string) => {
+            return Number(strVale.trim());
+          })
+        : [];
 
       // Map panel colors to hex or rgb/a values
-      data.colorMap = panel.colors.map((color: string) =>
-        getColorFromHexRgbOrName(
-          color,
-          config.bootData.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark
-        )
-      );
+      if (panel.colors) {
+        data.colorMap = panel.colors.map((color: string) => getColorFromHexRgbOrName(color, config.theme.type));
+      }
 
       const body = panel.gauge.show ? '' : getBigValueHtml();
 
       if (panel.colorBackground) {
-        const color = getColorForValue(data, data.value);
-        console.log(color);
+        const color = getColorForValue(data, data.display.numeric);
         if (color) {
           $panelContainer.css('background-color', color);
           if (scope.fullscreen) {
@@ -729,4 +669,59 @@ function getColorForValue(data: any, value: number) {
   return _.first(data.colorMap);
 }
 
+//------------------------------------------------
+// Private utility functions
+// Somethign like this should be avaliable in a
+//  DataFrame[] abstraction helper
+//------------------------------------------------
+
+interface FrameInfo {
+  firstTimeField?: Field;
+  frame: DataFrame;
+}
+
+interface FieldInfo {
+  field: Field;
+  frame: FrameInfo;
+}
+
+interface DistinctFieldsInfo {
+  first?: FieldInfo;
+  byName: KeyValue<FieldInfo>;
+  names: string[];
+}
+
+function getDistinctNames(data: DataFrame[]): DistinctFieldsInfo {
+  const distinct: DistinctFieldsInfo = {
+    byName: {},
+    names: [],
+  };
+  for (const frame of data) {
+    const info: FrameInfo = { frame };
+    for (const field of frame.fields) {
+      if (field.type === FieldType.time) {
+        if (!info.firstTimeField) {
+          info.firstTimeField = field;
+        }
+      } else {
+        const f = { field, frame: info };
+        if (!distinct.first) {
+          distinct.first = f;
+        }
+        let t = field.config.title;
+        if (t && !distinct.byName[t]) {
+          distinct.byName[t] = f;
+          distinct.names.push(t);
+        }
+        t = field.name;
+        if (t && !distinct.byName[t]) {
+          distinct.byName[t] = f;
+          distinct.names.push(t);
+        }
+      }
+    }
+  }
+  return distinct;
+}
+
 export { SingleStatCtrl, SingleStatCtrl as PanelCtrl, getColorForValue };

+ 81 - 84
public/app/plugins/panel/singlestat/specs/singlestat.test.ts

@@ -1,9 +1,17 @@
-import { SingleStatCtrl } from '../module';
-import { dateTime } from '@grafana/data';
+import { SingleStatCtrl, ShowData } from '../module';
+import { dateTime, ReducerID } from '@grafana/data';
 import { LinkSrv } from 'app/features/panel/panellinks/link_srv';
+import { LegacyResponseData } from '@grafana/ui';
+
+interface TestContext {
+  ctrl: SingleStatCtrl;
+  input: LegacyResponseData[];
+  data: Partial<ShowData>;
+  setup: (setupFunc: any) => void;
+}
 
 describe('SingleStatCtrl', () => {
-  const ctx = {} as any;
+  const ctx: TestContext = {} as TestContext;
   const epoch = 1505826363746;
   Date.now = () => epoch;
 
@@ -37,7 +45,7 @@ describe('SingleStatCtrl', () => {
           // @ts-ignore
           ctx.ctrl = new SingleStatCtrl($scope, $injector, {} as LinkSrv, $sanitize);
           setupFunc();
-          ctx.ctrl.onDataReceived(ctx.data);
+          ctx.ctrl.onDataReceived(ctx.input);
           ctx.data = ctx.ctrl.data;
         });
       };
@@ -46,40 +54,38 @@ describe('SingleStatCtrl', () => {
     });
   }
 
-  singleStatScenario('with defaults', (ctx: any) => {
+  singleStatScenario('with defaults', (ctx: TestContext) => {
     ctx.setup(() => {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }];
+      ctx.input = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }];
     });
 
     it('Should use series avg as default main value', () => {
       expect(ctx.data.value).toBe(15);
-      expect(ctx.data.valueRounded).toBe(15);
     });
 
     it('should set formatted falue', () => {
-      expect(ctx.data.valueFormatted).toBe('15');
+      expect(ctx.data.display.text).toBe('15');
     });
   });
 
-  singleStatScenario('showing serie name instead of value', (ctx: any) => {
+  singleStatScenario('showing serie name instead of value', (ctx: TestContext) => {
     ctx.setup(() => {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }];
+      ctx.input = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }];
       ctx.ctrl.panel.valueName = 'name';
     });
 
     it('Should use series avg as default main value', () => {
-      expect(ctx.data.value).toBe(0);
-      expect(ctx.data.valueRounded).toBe(0);
+      expect(ctx.data.value).toBe('test.cpu1');
     });
 
     it('should set formatted value', () => {
-      expect(ctx.data.valueFormatted).toBe('test.cpu1');
+      expect(ctx.data.display.text).toBe('test.cpu1');
     });
   });
 
-  singleStatScenario('showing last iso time instead of value', (ctx: any) => {
+  singleStatScenario('showing last iso time instead of value', (ctx: TestContext) => {
     ctx.setup(() => {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
+      ctx.input = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
       ctx.ctrl.panel.valueName = 'last_time';
       ctx.ctrl.panel.format = 'dateTimeAsIso';
       ctx.ctrl.dashboard.isTimezoneUtc = () => false;
@@ -87,30 +93,29 @@ describe('SingleStatCtrl', () => {
 
     it('Should use time instead of value', () => {
       expect(ctx.data.value).toBe(1505634997920);
-      expect(ctx.data.valueRounded).toBe(1505634997920);
     });
 
     it('should set formatted value', () => {
-      expect(dateTime(ctx.data.valueFormatted).valueOf()).toBe(1505634997000);
+      expect(dateTime(ctx.data.display.text).valueOf()).toBe(1505634997000);
     });
   });
 
-  singleStatScenario('showing last iso time instead of value (in UTC)', (ctx: any) => {
+  singleStatScenario('showing last iso time instead of value (in UTC)', (ctx: TestContext) => {
     ctx.setup(() => {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 5000]] }];
+      ctx.input = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 5000]] }];
       ctx.ctrl.panel.valueName = 'last_time';
       ctx.ctrl.panel.format = 'dateTimeAsIso';
       ctx.ctrl.dashboard.isTimezoneUtc = () => true;
     });
 
     it('should set value', () => {
-      expect(ctx.data.valueFormatted).toBe('1970-01-01 00:00:05');
+      expect(ctx.data.display.text).toBe('1970-01-01 00:00:05');
     });
   });
 
-  singleStatScenario('showing last us time instead of value', (ctx: any) => {
+  singleStatScenario('showing last us time instead of value', (ctx: TestContext) => {
     ctx.setup(() => {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
+      ctx.input = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
       ctx.ctrl.panel.valueName = 'last_time';
       ctx.ctrl.panel.format = 'dateTimeAsUS';
       ctx.ctrl.dashboard.isTimezoneUtc = () => false;
@@ -118,79 +123,76 @@ describe('SingleStatCtrl', () => {
 
     it('Should use time instead of value', () => {
       expect(ctx.data.value).toBe(1505634997920);
-      expect(ctx.data.valueRounded).toBe(1505634997920);
     });
 
     it('should set formatted value', () => {
-      expect(ctx.data.valueFormatted).toBe(dateTime(1505634997920).format('MM/DD/YYYY h:mm:ss a'));
+      expect(ctx.data.display.text).toBe(dateTime(1505634997920).format('MM/DD/YYYY h:mm:ss a'));
     });
   });
 
-  singleStatScenario('showing last us time instead of value (in UTC)', (ctx: any) => {
+  singleStatScenario('showing last us time instead of value (in UTC)', (ctx: TestContext) => {
     ctx.setup(() => {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 5000]] }];
+      ctx.input = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 5000]] }];
       ctx.ctrl.panel.valueName = 'last_time';
       ctx.ctrl.panel.format = 'dateTimeAsUS';
       ctx.ctrl.dashboard.isTimezoneUtc = () => true;
     });
 
     it('should set formatted value', () => {
-      expect(ctx.data.valueFormatted).toBe('01/01/1970 12:00:05 am');
+      expect(ctx.data.display.text).toBe('01/01/1970 12:00:05 am');
     });
   });
 
-  singleStatScenario('showing last time from now instead of value', (ctx: any) => {
+  singleStatScenario('showing last time from now instead of value', (ctx: TestContext) => {
     ctx.setup(() => {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
+      ctx.input = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
       ctx.ctrl.panel.valueName = 'last_time';
       ctx.ctrl.panel.format = 'dateTimeFromNow';
     });
 
     it('Should use time instead of value', () => {
       expect(ctx.data.value).toBe(1505634997920);
-      expect(ctx.data.valueRounded).toBe(1505634997920);
     });
 
     it('should set formatted value', () => {
-      expect(ctx.data.valueFormatted).toBe('2 days ago');
+      expect(ctx.data.display.text).toBe('2 days ago');
     });
   });
 
-  singleStatScenario('showing last time from now instead of value (in UTC)', (ctx: any) => {
+  singleStatScenario('showing last time from now instead of value (in UTC)', (ctx: TestContext) => {
     ctx.setup(() => {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
+      ctx.input = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
       ctx.ctrl.panel.valueName = 'last_time';
       ctx.ctrl.panel.format = 'dateTimeFromNow';
     });
 
     it('should set formatted value', () => {
-      expect(ctx.data.valueFormatted).toBe('2 days ago');
+      expect(ctx.data.display.text).toBe('2 days ago');
     });
   });
 
   singleStatScenario(
     'MainValue should use same number for decimals as displayed when checking thresholds',
-    (ctx: any) => {
+    (ctx: TestContext) => {
       ctx.setup(() => {
-        ctx.data = [{ target: 'test.cpu1', datapoints: [[99.999, 1], [99.99999, 2]] }];
+        ctx.input = [{ target: 'test.cpu1', datapoints: [[99.999, 1], [99.99999, 2]] }];
         ctx.ctrl.panel.valueName = 'avg';
         ctx.ctrl.panel.format = 'none';
       });
 
       it('Should be rounded', () => {
         expect(ctx.data.value).toBe(99.999495);
-        expect(ctx.data.valueRounded).toBe(100);
       });
 
       it('should set formatted value', () => {
-        expect(ctx.data.valueFormatted).toBe('100');
+        expect(ctx.data.display.text).toBe('100');
       });
     }
   );
 
-  singleStatScenario('When value to text mapping is specified', (ctx: any) => {
+  singleStatScenario('When value to text mapping is specified', (ctx: TestContext) => {
     ctx.setup(() => {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[9.9, 1]] }];
+      ctx.input = [{ target: 'test.cpu1', datapoints: [[9.9, 1]] }];
       ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }];
     });
 
@@ -198,36 +200,32 @@ describe('SingleStatCtrl', () => {
       expect(ctx.data.value).toBe(9.9);
     });
 
-    it('round should be rounded up', () => {
-      expect(ctx.data.valueRounded).toBe(10);
-    });
-
     it('Should replace value with text', () => {
-      expect(ctx.data.valueFormatted).toBe('OK');
+      expect(ctx.data.display.text).toBe('OK');
     });
   });
 
-  singleStatScenario('When range to text mapping is specified for first range', (ctx: any) => {
+  singleStatScenario('When range to text mapping is specified for first range', (ctx: TestContext) => {
     ctx.setup(() => {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[41, 50]] }];
+      ctx.input = [{ target: 'test.cpu1', datapoints: [[41, 50]] }];
       ctx.ctrl.panel.mappingType = 2;
       ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }];
     });
 
     it('Should replace value with text OK', () => {
-      expect(ctx.data.valueFormatted).toBe('OK');
+      expect(ctx.data.display.text).toBe('OK');
     });
   });
 
-  singleStatScenario('When range to text mapping is specified for other ranges', (ctx: any) => {
+  singleStatScenario('When range to text mapping is specified for other ranges', (ctx: TestContext) => {
     ctx.setup(() => {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[65, 75]] }];
+      ctx.input = [{ target: 'test.cpu1', datapoints: [[65, 75]] }];
       ctx.ctrl.panel.mappingType = 2;
       ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }];
     });
 
     it('Should replace value with text NOT OK', () => {
-      expect(ctx.data.valueFormatted).toBe('NOT OK');
+      expect(ctx.data.display.text).toBe('NOT OK');
     });
   });
 
@@ -240,9 +238,9 @@ describe('SingleStatCtrl', () => {
       },
     ];
 
-    singleStatScenario('with default values', (ctx: any) => {
+    singleStatScenario('with default values', (ctx: TestContext) => {
       ctx.setup(() => {
-        ctx.data = tableData;
+        ctx.input = tableData;
         ctx.ctrl.panel = {
           emit: () => {},
         };
@@ -252,17 +250,16 @@ describe('SingleStatCtrl', () => {
 
       it('Should use first rows value as default main value', () => {
         expect(ctx.data.value).toBe(15);
-        expect(ctx.data.valueRounded).toBe(15);
       });
 
       it('should set formatted value', () => {
-        expect(ctx.data.valueFormatted).toBe('15');
+        expect(ctx.data.display.text).toBe('15');
       });
     });
 
-    singleStatScenario('When table data has multiple columns', (ctx: any) => {
+    singleStatScenario('When table data has multiple columns', (ctx: TestContext) => {
       ctx.setup(() => {
-        ctx.data = tableData;
+        ctx.input = tableData;
         ctx.ctrl.panel.tableColumn = '';
       });
 
@@ -273,29 +270,28 @@ describe('SingleStatCtrl', () => {
 
     singleStatScenario(
       'MainValue should use same number for decimals as displayed when checking thresholds',
-      (ctx: any) => {
+      (ctx: TestContext) => {
         ctx.setup(() => {
-          ctx.data = tableData;
-          ctx.data[0].rows[0] = [1492759673649, 'ignore1', 99.99999, 'ignore2'];
+          ctx.input = tableData;
+          ctx.input[0].rows[0] = [1492759673649, 'ignore1', 99.99999, 'ignore2'];
           ctx.ctrl.panel.mappingType = 0;
           ctx.ctrl.panel.tableColumn = 'mean';
         });
 
         it('Should be rounded', () => {
           expect(ctx.data.value).toBe(99.99999);
-          expect(ctx.data.valueRounded).toBe(100);
         });
 
         it('should set formatted falue', () => {
-          expect(ctx.data.valueFormatted).toBe('100');
+          expect(ctx.data.display.text).toBe('100');
         });
       }
     );
 
-    singleStatScenario('When value to text mapping is specified', (ctx: any) => {
+    singleStatScenario('When value to text mapping is specified', (ctx: TestContext) => {
       ctx.setup(() => {
-        ctx.data = tableData;
-        ctx.data[0].rows[0] = [1492759673649, 'ignore1', 9.9, 'ignore2'];
+        ctx.input = tableData;
+        ctx.input[0].rows[0] = [1492759673649, 'ignore1', 9.9, 'ignore2'];
         ctx.ctrl.panel.mappingType = 2;
         ctx.ctrl.panel.tableColumn = 'mean';
         ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }];
@@ -305,59 +301,60 @@ describe('SingleStatCtrl', () => {
         expect(ctx.data.value).toBe(9.9);
       });
 
-      it('round should be rounded up', () => {
-        expect(ctx.data.valueRounded).toBe(10);
-      });
+      // it('round should be rounded up', () => {
+      //   expect(ctx.data.valueRounded).toBe(10);
+      // });
 
       it('Should replace value with text', () => {
-        expect(ctx.data.valueFormatted).toBe('OK');
+        expect(ctx.data.display.text).toBe('OK');
       });
     });
 
-    singleStatScenario('When range to text mapping is specified for first range', (ctx: any) => {
+    singleStatScenario('When range to text mapping is specified for first range', (ctx: TestContext) => {
       ctx.setup(() => {
-        ctx.data = tableData;
-        ctx.data[0].rows[0] = [1492759673649, 'ignore1', 41, 'ignore2'];
+        ctx.input = tableData;
+        ctx.input[0].rows[0] = [1492759673649, 'ignore1', 41, 'ignore2'];
         ctx.ctrl.panel.tableColumn = 'mean';
         ctx.ctrl.panel.mappingType = 2;
         ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }];
       });
 
       it('Should replace value with text OK', () => {
-        expect(ctx.data.valueFormatted).toBe('OK');
+        expect(ctx.data.display.text).toBe('OK');
       });
     });
 
-    singleStatScenario('When range to text mapping is specified for other ranges', (ctx: any) => {
+    singleStatScenario('When range to text mapping is specified for other ranges', (ctx: TestContext) => {
       ctx.setup(() => {
-        ctx.data = tableData;
-        ctx.data[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2'];
+        ctx.input = tableData;
+        ctx.input[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2'];
         ctx.ctrl.panel.tableColumn = 'mean';
         ctx.ctrl.panel.mappingType = 2;
         ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }];
       });
 
       it('Should replace value with text NOT OK', () => {
-        expect(ctx.data.valueFormatted).toBe('NOT OK');
+        expect(ctx.data.display.text).toBe('NOT OK');
       });
     });
 
-    singleStatScenario('When value is string', (ctx: any) => {
+    singleStatScenario('When value is string', (ctx: TestContext) => {
       ctx.setup(() => {
-        ctx.data = tableData;
-        ctx.data[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2'];
+        ctx.input = tableData;
+        ctx.input[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2'];
         ctx.ctrl.panel.tableColumn = 'test1';
+        ctx.ctrl.panel.valueName = ReducerID.first;
       });
 
       it('Should replace value with text NOT OK', () => {
-        expect(ctx.data.valueFormatted).toBe('ignore1');
+        expect(ctx.data.display.text).toBe('ignore1');
       });
     });
 
-    singleStatScenario('When value is zero', (ctx: any) => {
+    singleStatScenario('When value is zero', (ctx: TestContext) => {
       ctx.setup(() => {
-        ctx.data = tableData;
-        ctx.data[0].rows[0] = [1492759673649, 'ignore1', 0, 'ignore2'];
+        ctx.input = tableData;
+        ctx.input[0].rows[0] = [1492759673649, 'ignore1', 0, 'ignore2'];
         ctx.ctrl.panel.tableColumn = 'mean';
       });