diff --git a/public/app/core/utils/ticks.ts b/public/app/core/utils/ticks.ts
new file mode 100644
index 00000000000..7e7abbcd8f0
--- /dev/null
+++ b/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;
+}
diff --git a/public/app/plugins/panel/graph/axes_editor.html b/public/app/plugins/panel/graph/axes_editor.html
index e446bb96490..0d30165817b 100644
--- a/public/app/plugins/panel/graph/axes_editor.html
+++ b/public/app/plugins/panel/graph/axes_editor.html
@@ -66,6 +66,12 @@
+
+
+
+
+
+
diff --git a/public/app/plugins/panel/graph/axes_editor.ts b/public/app/plugins/panel/graph/axes_editor.ts
index b73ddf3868e..adb3d1d6706 100644
--- a/public/app/plugins/panel/graph/axes_editor.ts
+++ b/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',
};
diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts
index dcd202c5bee..87b48f78550 100644
--- a/public/app/plugins/panel/graph/data_processor.ts
+++ b/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;
+ }
}
}
diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts
index 06af66ddbe7..5a684a42fd3 100755
--- a/public/app/plugins/panel/graph/graph.ts
+++ b/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) {
diff --git a/public/app/plugins/panel/graph/histogram.ts b/public/app/plugins/panel/graph/histogram.ts
new file mode 100644
index 00000000000..e6ad7ebb0f6
--- /dev/null
+++ b/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;
+}
diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts
index 01f7a485aa6..e98d1c25ad7 100644
--- a/public/app/plugins/panel/graph/module.ts
+++ b/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,
diff --git a/public/app/plugins/panel/graph/specs/histogram_specs.ts b/public/app/plugins/panel/graph/specs/histogram_specs.ts
new file mode 100644
index 00000000000..71b3def8d1d
--- /dev/null
+++ b/public/app/plugins/panel/graph/specs/histogram_specs.ts
@@ -0,0 +1,65 @@
+///
+
+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);
+ });
+ });
+});
diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts
index 12316e6be79..a0f6786baf1 100644
--- a/public/app/plugins/panel/heatmap/rendering.ts
+++ b/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);
}