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); }