mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* 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
This commit is contained in:
parent
e6cc5df9d9
commit
7e14797b10
27
public/app/core/utils/ticks.ts
Normal file
27
public/app/core/utils/ticks.ts
Normal file
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
public/app/plugins/panel/graph/histogram.ts
Normal file
48
public/app/plugins/panel/graph/histogram.ts
Normal file
@ -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;
|
||||
}
|
@ -59,6 +59,7 @@ class GraphCtrl extends MetricsPanelCtrl {
|
||||
mode: 'time',
|
||||
name: null,
|
||||
values: [],
|
||||
buckets: null
|
||||
},
|
||||
// show/hide lines
|
||||
lines : true,
|
||||
|
65
public/app/plugins/panel/graph/specs/histogram_specs.ts
Normal file
65
public/app/plugins/panel/graph/specs/histogram_specs.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user