Browse Source

graph: initial histogram support #600 (#8053)

* graph: initial histogram support #600

* graph histogram mode: add Bars number option

* graph histogram mode: fix X axis ticks calculation

* graph histogram mode: change bar style (align and width)

* refactor(graph): move histogram functions into separate module

* graph histogram mode: rename series to "count"

* graph histogram mode: fix errors if no data

* refactor(graph and heatmap): move shared code into app/core

* graph: add tests for histogram mode
Alexander Zobnin 8 years ago
parent
commit
7e14797b10

+ 27 - 0
public/app/core/utils/ticks.ts

@@ -0,0 +1,27 @@
+/**
+ * Calculate tick step.
+ * Implementation from d3-array (ticks.js)
+ * https://github.com/d3/d3-array/blob/master/src/ticks.js
+ * @param start Start value
+ * @param stop End value
+ * @param count Ticks count
+ */
+export function tickStep(start: number, stop: number, count: number): number {
+  let e10 = Math.sqrt(50),
+    e5 = Math.sqrt(10),
+    e2 = Math.sqrt(2);
+
+  let step0 = Math.abs(stop - start) / Math.max(0, count),
+    step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10)),
+    error = step0 / step1;
+
+  if (error >= e10) {
+    step1 *= 10;
+  } else if (error >= e5) {
+    step1 *= 5;
+  } else if (error >= e2) {
+    step1 *= 2;
+  }
+
+  return stop < start ? -step1 : step1;
+}

+ 6 - 0
public/app/plugins/panel/graph/axes_editor.html

@@ -66,6 +66,12 @@
       <metric-segment-model property="ctrl.panel.xaxis.values[0]" options="ctrl.xAxisStatOptions" on-change="ctrl.xAxisOptionChanged()" custom="false" css-class="width-10" select-mode="true"></metric-segment-model>
 		</div>
 
+		<!-- Histogram mode -->
+		<div class="gf-form" ng-if="ctrl.panel.xaxis.mode === 'histogram'">
+			<label class="gf-form-label width-5">Bars</label>
+			<input type="number" class="gf-form-input max-width-8" ng-model="ctrl.panel.xaxis.buckets" placeholder="auto" ng-change="ctrl.render()" ng-model-onblur>
+		</div>
+
 	</div>
 
 </div>

+ 1 - 0
public/app/plugins/panel/graph/axes_editor.ts

@@ -30,6 +30,7 @@ export class AxesEditorCtrl {
     this.xAxisModes = {
       'Time': 'time',
       'Series': 'series',
+      'Histogram': 'histogram'
       // 'Data field': 'field',
     };
 

+ 13 - 0
public/app/plugins/panel/graph/data_processor.ts

@@ -29,6 +29,7 @@ export class DataProcessor {
 
     switch (this.panel.xaxis.mode) {
       case 'series':
+      case 'histogram':
       case 'time': {
         return options.dataList.map((item, index) => {
           return this.timeSeriesHandler(item, index, options);
@@ -48,6 +49,9 @@ export class DataProcessor {
         if (this.panel.xaxis.mode === 'series') {
           return 'series';
         }
+        if (this.panel.xaxis.mode === 'histogram') {
+          return 'histogram';
+        }
         return 'time';
       }
     }
@@ -74,6 +78,15 @@ export class DataProcessor {
         this.panel.xaxis.values = ['total'];
         break;
       }
+      case 'histogram': {
+        this.panel.bars = true;
+        this.panel.lines = false;
+        this.panel.points = false;
+        this.panel.stack = false;
+        this.panel.legend.show = false;
+        this.panel.tooltip.shared = false;
+        break;
+      }
     }
   }
 

+ 58 - 1
public/app/plugins/panel/graph/graph.ts

@@ -12,10 +12,12 @@ import './jquery.flot.events';
 import $ from 'jquery';
 import _ from 'lodash';
 import moment from 'moment';
-import kbn from   'app/core/utils/kbn';
+import kbn from 'app/core/utils/kbn';
+import {tickStep} from 'app/core/utils/ticks';
 import {appEvents, coreModule} from 'app/core/core';
 import GraphTooltip from './graph_tooltip';
 import {ThresholdManager} from './threshold_manager';
+import {convertValuesToHistogram, getSeriesValues} from './histogram';
 
 coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
   return {
@@ -290,6 +292,29 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
             addXSeriesAxis(options);
             break;
           }
+          case 'histogram': {
+            let bucketSize: number;
+            let values = getSeriesValues(data);
+
+            if (data.length && values.length) {
+              let histMin = _.min(_.map(data, s => s.stats.min));
+              let histMax = _.max(_.map(data, s => s.stats.max));
+              let ticks = panel.xaxis.buckets || panelWidth / 50;
+              bucketSize = tickStep(histMin, histMax, ticks);
+              let histogram = convertValuesToHistogram(values, bucketSize);
+
+              data[0].data = histogram;
+              data[0].alias = data[0].label = data[0].id = "count";
+              data = [data[0]];
+
+              options.series.bars.barWidth = bucketSize * 0.8;
+            } else {
+              bucketSize = 0;
+            }
+
+            addXHistogramAxis(options, bucketSize);
+            break;
+          }
           case 'table': {
             options.series.bars.barWidth = 0.7;
             options.series.bars.align = 'center';
@@ -384,6 +409,38 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
         };
       }
 
+      function addXHistogramAxis(options, bucketSize) {
+        let ticks, min, max;
+
+        if (data.length) {
+          ticks = _.map(data[0].data, point => point[0]);
+
+          // Expand ticks for pretty view
+          min = Math.max(0, _.min(ticks) - bucketSize);
+          max = _.max(ticks) + bucketSize;
+
+          ticks = [];
+          for (let i = min; i <= max; i += bucketSize) {
+            ticks.push(i);
+          }
+        } else {
+          // Set defaults if no data
+          ticks = panelWidth / 100;
+          min = 0;
+          max = 1;
+        }
+
+        options.xaxis = {
+          timezone: dashboard.getTimezone(),
+          show: panel.xaxis.show,
+          mode: null,
+          min: min,
+          max: max,
+          label: "Histogram",
+          ticks: ticks
+        };
+      }
+
       function addXTableAxis(options) {
         var ticks = _.map(data, function(series, seriesIndex) {
           return _.map(series.datapoints, function(point, pointIndex) {

+ 48 - 0
public/app/plugins/panel/graph/histogram.ts

@@ -0,0 +1,48 @@
+import _ from 'lodash';
+
+/**
+ * Convert series into array of series values.
+ * @param data Array of series
+ */
+export function getSeriesValues(data: any): number[] {
+  let values = [];
+
+  // Count histogam stats
+  for (let i = 0; i < data.length; i++) {
+    let series = data[i];
+    for (let j = 0; j < series.data.length; j++) {
+      if (series.data[j][1] !== null) {
+        values.push(series.data[j][1]);
+      }
+    }
+  }
+
+  return values;
+}
+
+/**
+ * Convert array of values into timeseries-like histogram:
+ * [[val_1, count_1], [val_2, count_2], ..., [val_n, count_n]]
+ * @param values
+ * @param bucketSize
+ */
+export function convertValuesToHistogram(values: number[], bucketSize: number): any[] {
+  let histogram = {};
+
+  for (let i = 0; i < values.length; i++) {
+    let bound = getBucketBound(values[i], bucketSize);
+    if (histogram[bound]) {
+      histogram[bound] = histogram[bound] + 1;
+    } else {
+      histogram[bound] = 1;
+    }
+  }
+
+  return _.map(histogram, (count, bound) => {
+    return [Number(bound), count];
+  });
+}
+
+function getBucketBound(value: number, bucketSize: number): number {
+  return Math.floor(value / bucketSize) * bucketSize;
+}

+ 1 - 0
public/app/plugins/panel/graph/module.ts

@@ -59,6 +59,7 @@ class GraphCtrl extends MetricsPanelCtrl {
       mode: 'time',
       name: null,
       values: [],
+      buckets: null
     },
     // show/hide lines
     lines         : true,

+ 65 - 0
public/app/plugins/panel/graph/specs/histogram_specs.ts

@@ -0,0 +1,65 @@
+///<reference path="../../../../headers/common.d.ts" />
+
+import { describe, beforeEach, it, expect } from '../../../../../test/lib/common';
+
+import { convertValuesToHistogram, getSeriesValues } from '../histogram';
+
+describe('Graph Histogam Converter', function () {
+
+  describe('Values to histogram converter', () => {
+    let values;
+    let bucketSize = 10;
+
+    beforeEach(() => {
+      values = [1, 2, 10, 11, 17, 20, 29];
+    });
+
+    it('Should convert to series-like array', () => {
+      bucketSize = 10;
+      let expected = [
+        [0, 2], [10, 3], [20, 2]
+      ];
+
+      let histogram = convertValuesToHistogram(values, bucketSize);
+      expect(histogram).to.eql(expected);
+    });
+
+    it('Should not add empty buckets', () => {
+      bucketSize = 5;
+      let expected = [
+        [0, 2], [10, 2], [15, 1], [20, 1], [25, 1]
+      ];
+
+      let histogram = convertValuesToHistogram(values, bucketSize);
+      expect(histogram).to.eql(expected);
+    });
+  });
+
+  describe('Series to values converter', () => {
+    let data;
+
+    beforeEach(() => {
+      data = [
+        {
+          data: [[0, 1], [0, 2], [0, 10], [0, 11], [0, 17], [0, 20], [0, 29]]
+        }
+      ];
+    });
+
+    it('Should convert to values array', () => {
+      let expected = [1, 2, 10, 11, 17, 20, 29];
+
+      let values = getSeriesValues(data);
+      expect(values).to.eql(expected);
+    });
+
+    it('Should skip null values', () => {
+      data[0].data.push([0, null]);
+
+      let expected = [1, 2, 10, 11, 17, 20, 29];
+
+      let values = getSeriesValues(data);
+      expect(values).to.eql(expected);
+    });
+  });
+});

+ 1 - 23
public/app/plugins/panel/heatmap/rendering.ts

@@ -5,6 +5,7 @@ import $ from 'jquery';
 import moment from 'moment';
 import kbn from 'app/core/utils/kbn';
 import {appEvents, contextSrv} from 'app/core/core';
+import {tickStep} from 'app/core/utils/ticks';
 import d3 from 'd3';
 import {HeatmapTooltip} from './heatmap_tooltip';
 import {convertToCards, mergeZeroBuckets, removeZeroBuckets} from './heatmap_data_converter';
@@ -836,29 +837,6 @@ function grafanaTimeFormat(ticks, min, max) {
   return "%H:%M";
 }
 
-// Calculate tick step.
-// Implementation from d3-array (ticks.js)
-// https://github.com/d3/d3-array/blob/master/src/ticks.js
-function tickStep(start, stop, count) {
-  var e10 = Math.sqrt(50),
-      e5 = Math.sqrt(10),
-      e2 = Math.sqrt(2);
-
-  var step0 = Math.abs(stop - start) / Math.max(0, count),
-      step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10)),
-      error = step0 / step1;
-
-  if (error >= e10) {
-    step1 *= 10;
-  } else if (error >= e5) {
-    step1 *= 5;
-  } else if (error >= e2) {
-    step1 *= 2;
-  }
-
-  return stop < start ? -step1 : step1;
-}
-
 function logp(value, base) {
   return Math.log(value) / Math.log(base);
 }