Commit 8634c9d4 by Alexander Zobnin Committed by Daniel Lee

Histogram fix (#8727)

* histogram: don't cut negative values, issue #8628

* histogram: add percent/count option

* histogram: add tests for values normalizing

* histogram: improved ticks rendering

* histogram: fix default value in axes editor
parent 3ac306a7
...@@ -68,6 +68,12 @@ ...@@ -68,6 +68,12 @@
<!-- Histogram mode --> <!-- Histogram mode -->
<div class="gf-form" ng-if="ctrl.panel.xaxis.mode === 'histogram'"> <div class="gf-form" ng-if="ctrl.panel.xaxis.mode === 'histogram'">
<label class="gf-form-label width-6">Value</label>
<div class="gf-form-select-wrapper max-width-15">
<select class="gf-form-input" ng-model="ctrl.panel.xaxis.histogramValue" ng-options="v as k for (k, v) in ctrl.histogramValues" ng-change="ctrl.xAxisOptionChanged()"> </select>
</div>
</div>
<div class="gf-form" ng-if="ctrl.panel.xaxis.mode === 'histogram'">
<label class="gf-form-label width-6">Buckets</label> <label class="gf-form-label width-6">Buckets</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 bs-tooltip="'Number of buckets'" data-placement="right"> <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 bs-tooltip="'Number of buckets'" data-placement="right">
</div> </div>
......
...@@ -10,6 +10,7 @@ export class AxesEditorCtrl { ...@@ -10,6 +10,7 @@ export class AxesEditorCtrl {
xAxisModes: any; xAxisModes: any;
xAxisStatOptions: any; xAxisStatOptions: any;
xNameSegment: any; xNameSegment: any;
histogramValues: any;
/** @ngInject **/ /** @ngInject **/
constructor(private $scope, private $q) { constructor(private $scope, private $q) {
...@@ -34,6 +35,11 @@ export class AxesEditorCtrl { ...@@ -34,6 +35,11 @@ export class AxesEditorCtrl {
// 'Data field': 'field', // 'Data field': 'field',
}; };
this.histogramValues = {
'Percent': 'percent',
'Count': 'count'
};
this.xAxisStatOptions = [ this.xAxisStatOptions = [
{text: 'Avg', value: 'avg'}, {text: 'Avg', value: 'avg'},
{text: 'Min', value: 'min'}, {text: 'Min', value: 'min'},
......
...@@ -312,10 +312,13 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) { ...@@ -312,10 +312,13 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) {
let histMax = _.max(_.map(data, s => s.stats.max)); let histMax = _.max(_.map(data, s => s.stats.max));
let ticks = panel.xaxis.buckets || panelWidth / 50; let ticks = panel.xaxis.buckets || panelWidth / 50;
bucketSize = tickStep(histMin, histMax, ticks); bucketSize = tickStep(histMin, histMax, ticks);
let histogram = convertValuesToHistogram(values, bucketSize);
let normalize = panel.xaxis.histogramValue === 'percent';
let histogram = convertValuesToHistogram(values, bucketSize, normalize);
let seriesLabel = panel.xaxis.histogramValue || "count";
data[0].data = histogram; data[0].data = histogram;
data[0].alias = data[0].label = data[0].id = "count"; data[0].alias = data[0].label = data[0].id = seriesLabel;
data = [data[0]]; data = [data[0]];
options.series.bars.barWidth = bucketSize * 0.8; options.series.bars.barWidth = bucketSize * 0.8;
...@@ -422,21 +425,32 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) { ...@@ -422,21 +425,32 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) {
function addXHistogramAxis(options, bucketSize) { function addXHistogramAxis(options, bucketSize) {
let ticks, min, max; let ticks, min, max;
let defaultTicks = panelWidth / 50;
if (data.length && bucketSize) { if (data.length && bucketSize) {
ticks = _.map(data[0].data, point => point[0]); ticks = _.map(data[0].data, point => point[0]);
min = _.min(ticks);
max = _.max(ticks);
// Adjust tick step
let tickStep = bucketSize;
let ticks_num = Math.floor((max - min) / tickStep);
while (ticks_num > defaultTicks) {
tickStep = tickStep * 2;
ticks_num = Math.ceil((max - min) / tickStep);
}
// Expand ticks for pretty view // Expand ticks for pretty view
min = Math.max(0, _.min(ticks) - bucketSize); min = Math.floor(min / tickStep) * tickStep;
max = _.max(ticks) + bucketSize; max = Math.ceil(max / tickStep) * tickStep;
ticks = []; ticks = [];
for (let i = min; i <= max; i += bucketSize) { for (let i = min; i <= max; i += tickStep) {
ticks.push(i); ticks.push(i);
} }
} else { } else {
// Set defaults if no data // Set defaults if no data
ticks = panelWidth / 100; ticks = defaultTicks / 2;
min = 0; min = 0;
max = 1; max = 1;
} }
...@@ -450,6 +464,9 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) { ...@@ -450,6 +464,9 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) {
label: "Histogram", label: "Histogram",
ticks: ticks ticks: ticks
}; };
// Use 'short' format for histogram values
configureAxisMode(options.xaxis, 'short');
} }
function addXTableAxis(options) { function addXTableAxis(options) {
......
...@@ -26,7 +26,7 @@ export function getSeriesValues(data: any): number[] { ...@@ -26,7 +26,7 @@ export function getSeriesValues(data: any): number[] {
* @param values * @param values
* @param bucketSize * @param bucketSize
*/ */
export function convertValuesToHistogram(values: number[], bucketSize: number): any[] { export function convertValuesToHistogram(values: number[], bucketSize: number, normalize = false): any[] {
let histogram = {}; let histogram = {};
for (let i = 0; i < values.length; i++) { for (let i = 0; i < values.length; i++) {
...@@ -38,9 +38,16 @@ export function convertValuesToHistogram(values: number[], bucketSize: number): ...@@ -38,9 +38,16 @@ export function convertValuesToHistogram(values: number[], bucketSize: number):
} }
} }
return _.map(histogram, (count, bound) => { let histogam_series = _.map(histogram, (count, bound) => {
if (normalize && values.length) {
return [Number(bound), count / values.length];
}
return [Number(bound), count]; return [Number(bound), count];
}); });
// Sort by Y axis values
return _.sortBy(histogam_series, point => point[0]);
} }
function getBucketBound(value: number, bucketSize: number): number { function getBucketBound(value: number, bucketSize: number): number {
......
...@@ -58,7 +58,8 @@ class GraphCtrl extends MetricsPanelCtrl { ...@@ -58,7 +58,8 @@ class GraphCtrl extends MetricsPanelCtrl {
mode: 'time', mode: 'time',
name: null, name: null,
values: [], values: [],
buckets: null buckets: null,
histogramValue: 'percent'
}, },
// show/hide lines // show/hide lines
lines : true, lines : true,
......
///<reference path="../../../../headers/common.d.ts" /> ///<reference path="../../../../headers/common.d.ts" />
import _ from 'lodash';
import { describe, beforeEach, it, expect } from '../../../../../test/lib/common'; import { describe, beforeEach, it, expect } from '../../../../../test/lib/common';
import { convertValuesToHistogram, getSeriesValues } from '../histogram'; import { convertValuesToHistogram, getSeriesValues } from '../histogram';
describe('Graph Histogam Converter', function () { describe('Graph Histogam Converter', function () {
...@@ -11,13 +10,13 @@ describe('Graph Histogam Converter', function () { ...@@ -11,13 +10,13 @@ describe('Graph Histogam Converter', function () {
let bucketSize = 10; let bucketSize = 10;
beforeEach(() => { beforeEach(() => {
values = [1, 2, 10, 11, 17, 20, 29]; values = [1, 2, 10, 11, 17, 20, 29, 30, 31, 33];
}); });
it('Should convert to series-like array', () => { it('Should convert to series-like array', () => {
bucketSize = 10; bucketSize = 10;
let expected = [ let expected = [
[0, 2], [10, 3], [20, 2] [0, 2], [10, 3], [20, 2], [30, 3]
]; ];
let histogram = convertValuesToHistogram(values, bucketSize); let histogram = convertValuesToHistogram(values, bucketSize);
...@@ -27,12 +26,35 @@ describe('Graph Histogam Converter', function () { ...@@ -27,12 +26,35 @@ describe('Graph Histogam Converter', function () {
it('Should not add empty buckets', () => { it('Should not add empty buckets', () => {
bucketSize = 5; bucketSize = 5;
let expected = [ let expected = [
[0, 2], [10, 2], [15, 1], [20, 1], [25, 1] [0, 2], [10, 2], [15, 1], [20, 1], [25, 1], [30, 3]
]; ];
let histogram = convertValuesToHistogram(values, bucketSize); let histogram = convertValuesToHistogram(values, bucketSize);
expect(histogram).to.eql(expected); expect(histogram).to.eql(expected);
}); });
it('Should normalize values', () => {
bucketSize = 5;
let normalize = true;
let expected = [
[0, 0.2], [10, 0.2], [15, 0.1], [20, 0.1], [25, 0.1], [30, 0.3]
];
let histogram = convertValuesToHistogram(values, bucketSize, normalize);
expect(histogram).to.eql(expected);
});
it('Sum of normalized values should be 1', () => {
bucketSize = 5;
let normalize = true;
let expected = [
[0, 0.2], [10, 0.2], [15, 0.1], [20, 0.1], [25, 0.1], [30, 0.3]
];
let histogram = convertValuesToHistogram(values, bucketSize, normalize);
let sum = _.reduce(histogram, (sum, point) => sum + point[1], 0);
expect(sum).to.eql(1);
});
}); });
describe('Series to values converter', () => { describe('Series to values converter', () => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment