Commit 7e14797b by Alexander Zobnin Committed by Torkel Ödegaard

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
parent e6cc5df9
/**
* 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;
}
......@@ -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>
......@@ -30,6 +30,7 @@ export class AxesEditorCtrl {
this.xAxisModes = {
'Time': 'time',
'Series': 'series',
'Histogram': 'histogram'
// 'Data field': 'field',
};
......
......@@ -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;
}
}
}
......
......@@ -13,9 +13,11 @@ import $ from 'jquery';
import _ from 'lodash';
import moment from 'moment';
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) {
......
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;
}
......@@ -59,6 +59,7 @@ class GraphCtrl extends MetricsPanelCtrl {
mode: 'time',
name: null,
values: [],
buckets: null
},
// show/hide lines
lines : true,
......
///<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);
});
});
});
......@@ -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);
}
......
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