Y-Axes
diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts
index 4033e4b3778..e3eed6fc382 100755
--- a/public/app/plugins/panel/graph/graph.ts
+++ b/public/app/plugins/panel/graph/graph.ts
@@ -337,9 +337,17 @@ class GraphElement {
let bucketSize: number;
if (this.data.length) {
- const histMin = _.min(_.map(this.data, s => s.stats.min));
- const histMax = _.max(_.map(this.data, s => s.stats.max));
+ let histMin = _.min(_.map(this.data, s => s.stats.min));
+ let histMax = _.max(_.map(this.data, s => s.stats.max));
const ticks = panel.xaxis.buckets || this.panelWidth / 50;
+ if (panel.xaxis.min != null) {
+ const isInvalidXaxisMin = tickStep(panel.xaxis.min, histMax, ticks) <= 0;
+ histMin = isInvalidXaxisMin ? histMin : panel.xaxis.min;
+ }
+ if (panel.xaxis.max != null) {
+ const isInvalidXaxisMax = tickStep(histMin, panel.xaxis.max, ticks) <= 0;
+ histMax = isInvalidXaxisMax ? histMax : panel.xaxis.max;
+ }
bucketSize = tickStep(histMin, histMax, ticks);
options.series.bars.barWidth = bucketSize * 0.8;
this.data = convertToHistogramData(this.data, bucketSize, this.ctrl.hiddenSeries, histMin, histMax);
diff --git a/public/app/plugins/panel/graph/histogram.ts b/public/app/plugins/panel/graph/histogram.ts
index b09226d15e7..b5c478198c0 100644
--- a/public/app/plugins/panel/graph/histogram.ts
+++ b/public/app/plugins/panel/graph/histogram.ts
@@ -43,6 +43,10 @@ export function convertValuesToHistogram(values: number[], bucketSize: number, m
}
for (let i = 0; i < values.length; i++) {
+ // filter out values outside the min and max boundaries
+ if (values[i] < min || values[i] > max) {
+ continue;
+ }
const bound = getBucketBound(values[i], bucketSize);
histogram[bound] = histogram[bound] + 1;
}
diff --git a/public/app/plugins/panel/graph/specs/graph.test.ts b/public/app/plugins/panel/graph/specs/graph.test.ts
index 58a35ea2a5f..ff81a8ae6eb 100644
--- a/public/app/plugins/panel/graph/specs/graph.test.ts
+++ b/public/app/plugins/panel/graph/specs/graph.test.ts
@@ -516,4 +516,408 @@ describe('grafanaGraph', () => {
expect(ctx.plotData[0].data[0][1]).toBe(2);
});
});
+
+ describe('when graph is histogram, and xaxis min is set', () => {
+ beforeEach(() => {
+ setupCtx(() => {
+ ctrl.panel.xaxis.mode = 'histogram';
+ ctrl.panel.xaxis.min = 150;
+ ctrl.panel.stack = false;
+ ctrl.hiddenSeries = {};
+ ctx.data[0] = new TimeSeries({
+ datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+ alias: 'series1',
+ });
+ });
+ });
+
+ it('should not contain values lower than min', () => {
+ const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+ expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(200);
+ expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+ });
+ });
+
+ describe('when graph is histogram, and xaxis min is zero', () => {
+ beforeEach(() => {
+ setupCtx(() => {
+ ctrl.panel.xaxis.mode = 'histogram';
+ ctrl.panel.xaxis.min = 0;
+ ctrl.panel.stack = false;
+ ctrl.hiddenSeries = {};
+ ctx.data[0] = new TimeSeries({
+ datapoints: [[-100, 1], [100, 2], [200, 3], [300, 4]],
+ alias: 'series1',
+ });
+ });
+ });
+
+ it('should not contain values lower than zero', () => {
+ const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+ expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+ expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+ });
+ });
+
+ describe('when graph is histogram, and xaxis min is null', () => {
+ beforeEach(() => {
+ setupCtx(() => {
+ ctrl.panel.xaxis.mode = 'histogram';
+ ctrl.panel.xaxis.min = null;
+ ctrl.panel.stack = false;
+ ctrl.hiddenSeries = {};
+ ctx.data[0] = new TimeSeries({
+ datapoints: [[-100, 1], [100, 2], [200, 3], [300, 4]],
+ alias: 'series1',
+ });
+ });
+ });
+
+ it('xaxis min should not affect the histogram', () => {
+ const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+ expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(-100);
+ expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+ });
+ });
+
+ describe('when graph is histogram, and xaxis min is undefined', () => {
+ beforeEach(() => {
+ setupCtx(() => {
+ ctrl.panel.xaxis.mode = 'histogram';
+ ctrl.panel.xaxis.min = undefined;
+ ctrl.panel.stack = false;
+ ctrl.hiddenSeries = {};
+ ctx.data[0] = new TimeSeries({
+ datapoints: [[-100, 1], [100, 2], [200, 3], [300, 4]],
+ alias: 'series1',
+ });
+ });
+ });
+
+ it('xaxis min should not affect the histogram', () => {
+ const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+ expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(-100);
+ expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+ });
+ });
+
+ describe('when graph is histogram, and xaxis max is set', () => {
+ beforeEach(() => {
+ setupCtx(() => {
+ ctrl.panel.xaxis.mode = 'histogram';
+ ctrl.panel.xaxis.max = 250;
+ ctrl.panel.stack = false;
+ ctrl.hiddenSeries = {};
+ ctx.data[0] = new TimeSeries({
+ datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+ alias: 'series1',
+ });
+ });
+ });
+
+ it('should not contain values greater than max', () => {
+ const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+ expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+ expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(200);
+ });
+ });
+
+ describe('when graph is histogram, and xaxis max is zero', () => {
+ beforeEach(() => {
+ setupCtx(() => {
+ ctrl.panel.xaxis.mode = 'histogram';
+ ctrl.panel.xaxis.max = 0;
+ ctrl.panel.stack = false;
+ ctrl.hiddenSeries = {};
+ ctx.data[0] = new TimeSeries({
+ datapoints: [[-100, 1], [100, 1], [100, 2], [200, 3], [300, 4]],
+ alias: 'series1',
+ });
+ });
+ });
+
+ it('should not contain values greater than zero', () => {
+ const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+ expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(-100);
+ expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(-100);
+ });
+ });
+
+ describe('when graph is histogram, and xaxis max is null', () => {
+ beforeEach(() => {
+ setupCtx(() => {
+ ctrl.panel.xaxis.mode = 'histogram';
+ ctrl.panel.xaxis.max = null;
+ ctrl.panel.stack = false;
+ ctrl.hiddenSeries = {};
+ ctx.data[0] = new TimeSeries({
+ datapoints: [[-100, 1], [100, 1], [100, 2], [200, 3], [300, 4]],
+ alias: 'series1',
+ });
+ });
+ });
+
+ it('xaxis max should not affect the histogram', () => {
+ const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+ expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(-100);
+ expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+ });
+ });
+
+ describe('when graph is histogram, and xaxis max is undefined', () => {
+ beforeEach(() => {
+ setupCtx(() => {
+ ctrl.panel.xaxis.mode = 'histogram';
+ ctrl.panel.xaxis.max = undefined;
+ ctrl.panel.stack = false;
+ ctrl.hiddenSeries = {};
+ ctx.data[0] = new TimeSeries({
+ datapoints: [[-100, 1], [100, 1], [100, 2], [200, 3], [300, 4]],
+ alias: 'series1',
+ });
+ });
+ });
+
+ it('xaxis max should not should node affect the histogram', () => {
+ const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+ expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(-100);
+ expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+ });
+ });
+
+ describe('when graph is histogram, and xaxis min and max are set', () => {
+ beforeEach(() => {
+ setupCtx(() => {
+ ctrl.panel.xaxis.mode = 'histogram';
+ ctrl.panel.xaxis.min = 150;
+ ctrl.panel.xaxis.max = 250;
+ ctrl.panel.stack = false;
+ ctrl.hiddenSeries = {};
+ ctx.data[0] = new TimeSeries({
+ datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+ alias: 'series1',
+ });
+ });
+ });
+
+ it('should not contain values lower than min and greater than max', () => {
+ const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+ expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(200);
+ expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(200);
+ });
+ });
+
+ describe('when graph is histogram, and xaxis min and max are zero', () => {
+ beforeEach(() => {
+ setupCtx(() => {
+ ctrl.panel.xaxis.mode = 'histogram';
+ ctrl.panel.xaxis.min = 0;
+ ctrl.panel.xaxis.max = 0;
+ ctrl.panel.stack = false;
+ ctrl.hiddenSeries = {};
+ ctx.data[0] = new TimeSeries({
+ datapoints: [[-100, 1], [100, 1], [100, 2], [200, 3], [300, 4]],
+ alias: 'series1',
+ });
+ });
+ });
+
+ it('xaxis max should be ignored otherwise the bucketSize is zero', () => {
+ const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+ expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+ expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+ });
+ });
+
+ describe('when graph is histogram, and xaxis min and max are null', () => {
+ beforeEach(() => {
+ setupCtx(() => {
+ ctrl.panel.xaxis.mode = 'histogram';
+ ctrl.panel.xaxis.min = null;
+ ctrl.panel.xaxis.max = null;
+ ctrl.panel.stack = false;
+ ctrl.hiddenSeries = {};
+ ctx.data[0] = new TimeSeries({
+ datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+ alias: 'series1',
+ });
+ });
+ });
+
+ it('xaxis min and max should not affect the histogram', () => {
+ const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+ expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+ expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+ });
+ });
+
+ describe('when graph is histogram, and xaxis min and max are undefined', () => {
+ beforeEach(() => {
+ setupCtx(() => {
+ ctrl.panel.xaxis.mode = 'histogram';
+ ctrl.panel.xaxis.min = undefined;
+ ctrl.panel.xaxis.max = undefined;
+ ctrl.panel.stack = false;
+ ctrl.hiddenSeries = {};
+ ctx.data[0] = new TimeSeries({
+ datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+ alias: 'series1',
+ });
+ });
+ });
+
+ it('xaxis min and max should not affect the histogram', () => {
+ const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+ expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+ expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+ });
+ });
+
+ describe('when graph is histogram, and xaxis min is greater than xaxis max', () => {
+ beforeEach(() => {
+ setupCtx(() => {
+ ctrl.panel.xaxis.mode = 'histogram';
+ ctrl.panel.xaxis.min = 150;
+ ctrl.panel.xaxis.max = 100;
+ ctrl.panel.stack = false;
+ ctrl.hiddenSeries = {};
+ ctx.data[0] = new TimeSeries({
+ datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+ alias: 'series1',
+ });
+ });
+ });
+
+ it('xaxis max should be ignored otherwise the bucketSize is negative', () => {
+ const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+ expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(200);
+ expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+ });
+ });
+
+ // aaa
+ describe('when graph is histogram, and xaxis min is greater than the maximum value', () => {
+ beforeEach(() => {
+ setupCtx(() => {
+ ctrl.panel.xaxis.mode = 'histogram';
+ ctrl.panel.xaxis.min = 301;
+ ctrl.panel.stack = false;
+ ctrl.hiddenSeries = {};
+ ctx.data[0] = new TimeSeries({
+ datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+ alias: 'series1',
+ });
+ });
+ });
+
+ it('xaxis min should be ignored otherwise the bucketSize is negative', () => {
+ const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+ expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+ expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+ });
+ });
+
+ describe('when graph is histogram, and xaxis min is equal to the maximum value', () => {
+ beforeEach(() => {
+ setupCtx(() => {
+ ctrl.panel.xaxis.mode = 'histogram';
+ ctrl.panel.xaxis.min = 300;
+ ctrl.panel.stack = false;
+ ctrl.hiddenSeries = {};
+ ctx.data[0] = new TimeSeries({
+ datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+ alias: 'series1',
+ });
+ });
+ });
+
+ it('xaxis min should be ignored otherwise the bucketSize is zero', () => {
+ const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+ expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+ expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+ });
+ });
+
+ describe('when graph is histogram, and xaxis min is lower than the minimum value', () => {
+ beforeEach(() => {
+ setupCtx(() => {
+ ctrl.panel.xaxis.mode = 'histogram';
+ ctrl.panel.xaxis.min = 99;
+ ctrl.panel.stack = false;
+ ctrl.hiddenSeries = {};
+ ctx.data[0] = new TimeSeries({
+ datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+ alias: 'series1',
+ });
+ });
+ });
+
+ it('xaxis min should not affect the histogram', () => {
+ const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+ expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+ expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+ });
+ });
+
+ describe('when graph is histogram, and xaxis max is equal to the minimum value', () => {
+ beforeEach(() => {
+ setupCtx(() => {
+ ctrl.panel.xaxis.mode = 'histogram';
+ ctrl.panel.xaxis.max = 100;
+ ctrl.panel.stack = false;
+ ctrl.hiddenSeries = {};
+ ctx.data[0] = new TimeSeries({
+ datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+ alias: 'series1',
+ });
+ });
+ });
+
+ it('should calculate correct histogram', () => {
+ const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+ expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+ expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+ });
+ });
+
+ describe('when graph is histogram, and xaxis max is a lower than the minimum value', () => {
+ beforeEach(() => {
+ setupCtx(() => {
+ ctrl.panel.xaxis.mode = 'histogram';
+ ctrl.panel.xaxis.max = 99;
+ ctrl.panel.stack = false;
+ ctrl.hiddenSeries = {};
+ ctx.data[0] = new TimeSeries({
+ datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+ alias: 'series1',
+ });
+ });
+ });
+
+ it('should calculate empty histogram', () => {
+ const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+ expect(nonZero.length).toBe(0);
+ });
+ });
+
+ describe('when graph is histogram, and xaxis max is greater than the maximum value', () => {
+ beforeEach(() => {
+ setupCtx(() => {
+ ctrl.panel.xaxis.mode = 'histogram';
+ ctrl.panel.xaxis.max = 301;
+ ctrl.panel.stack = false;
+ ctrl.hiddenSeries = {};
+ ctx.data[0] = new TimeSeries({
+ datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+ alias: 'series1',
+ });
+ });
+ });
+
+ it('should calculate correct histogram', () => {
+ const nonZero = ctx.plotData[0].data.filter(t => t[1] > 0);
+ expect(Math.min.apply(Math, nonZero.map(t => t[0]))).toBe(100);
+ expect(Math.max.apply(Math, nonZero.map(t => t[0]))).toBe(300);
+ });
+ });
});
diff --git a/public/app/plugins/panel/heatmap/axes_editor.ts b/public/app/plugins/panel/heatmap/axes_editor.ts
index 81df957e2ea..cec3df9f8c5 100644
--- a/public/app/plugins/panel/heatmap/axes_editor.ts
+++ b/public/app/plugins/panel/heatmap/axes_editor.ts
@@ -32,6 +32,7 @@ export class AxesEditorCtrl {
Auto: 'auto',
Upper: 'upper',
Lower: 'lower',
+ Middle: 'middle',
};
}
diff --git a/public/app/plugins/panel/heatmap/color_legend.ts b/public/app/plugins/panel/heatmap/color_legend.ts
index c36fad45cba..a1c80629c7e 100644
--- a/public/app/plugins/panel/heatmap/color_legend.ts
+++ b/public/app/plugins/panel/heatmap/color_legend.ts
@@ -11,6 +11,8 @@ const LEGEND_HEIGHT_PX = 6;
const LEGEND_WIDTH_PX = 100;
const LEGEND_TICK_SIZE = 0;
const LEGEND_VALUE_MARGIN = 0;
+const LEGEND_PADDING_LEFT = 10;
+const LEGEND_SEGMENT_WIDTH = 10;
/**
* Color legend for heatmap editor.
@@ -19,7 +21,7 @@ coreModule.directive('colorLegend', () => {
return {
restrict: 'E',
template: '
',
- link: (scope, elem, attrs) => {
+ link: (scope: any, elem, attrs) => {
const ctrl = scope.ctrl;
const panel = scope.ctrl.panel;
@@ -55,7 +57,7 @@ coreModule.directive('heatmapLegend', () => {
return {
restrict: 'E',
template: `
`,
- link: (scope, elem, attrs) => {
+ link: (scope: any, elem, attrs) => {
const ctrl = scope.ctrl;
const panel = scope.ctrl.panel;
@@ -67,10 +69,11 @@ coreModule.directive('heatmapLegend', () => {
function render() {
clearLegend(elem);
if (!_.isEmpty(ctrl.data) && !_.isEmpty(ctrl.data.cards)) {
- const rangeFrom = 0;
- const rangeTo = ctrl.data.cardStats.max;
- const maxValue = panel.color.max || rangeTo;
- const minValue = panel.color.min || 0;
+ const cardStats = ctrl.data.cardStats;
+ const rangeFrom = _.isNil(panel.color.min) ? Math.min(cardStats.min, 0) : panel.color.min;
+ const rangeTo = _.isNil(panel.color.max) ? cardStats.max : panel.color.max;
+ const maxValue = cardStats.max;
+ const minValue = cardStats.min;
if (panel.color.mode === 'spectrum') {
const colorScheme = _.find(ctrl.colorSchemes, {
@@ -95,27 +98,27 @@ function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minVal
const legendWidth = Math.floor(legendElem.outerWidth()) - 30;
const legendHeight = legendElem.attr('height');
- let rangeStep = 1;
- if (rangeTo - rangeFrom > legendWidth) {
- rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth);
- }
+ const rangeStep = ((rangeTo - rangeFrom) / legendWidth) * LEGEND_SEGMENT_WIDTH;
const widthFactor = legendWidth / (rangeTo - rangeFrom);
const valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
const colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue);
legend
+ .append('g')
+ .attr('class', 'legend-color-bar')
+ .attr('transform', 'translate(' + LEGEND_PADDING_LEFT + ',0)')
.selectAll('.heatmap-color-legend-rect')
.data(valuesRange)
.enter()
.append('rect')
- .attr('x', d => d * widthFactor)
+ .attr('x', d => Math.round((d - rangeFrom) * widthFactor))
.attr('y', 0)
- .attr('width', rangeStep * widthFactor + 1) // Overlap rectangles to prevent gaps
+ .attr('width', Math.round(rangeStep * widthFactor + 1)) // Overlap rectangles to prevent gaps
.attr('height', legendHeight)
.attr('stroke-width', 0)
.attr('fill', d => colorScale(d));
- drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth);
+ drawLegendValues(elem, rangeFrom, rangeTo, maxValue, minValue, legendWidth, valuesRange);
}
function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue) {
@@ -126,31 +129,31 @@ function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue
const legendWidth = Math.floor(legendElem.outerWidth()) - 30;
const legendHeight = legendElem.attr('height');
- let rangeStep = 1;
- if (rangeTo - rangeFrom > legendWidth) {
- rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth);
- }
+ const rangeStep = ((rangeTo - rangeFrom) / legendWidth) * LEGEND_SEGMENT_WIDTH;
const widthFactor = legendWidth / (rangeTo - rangeFrom);
const valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
const opacityScale = getOpacityScale(options, maxValue, minValue);
legend
+ .append('g')
+ .attr('class', 'legend-color-bar')
+ .attr('transform', 'translate(' + LEGEND_PADDING_LEFT + ',0)')
.selectAll('.heatmap-opacity-legend-rect')
.data(valuesRange)
.enter()
.append('rect')
- .attr('x', d => d * widthFactor)
+ .attr('x', d => Math.round((d - rangeFrom) * widthFactor))
.attr('y', 0)
- .attr('width', rangeStep * widthFactor)
+ .attr('width', Math.round(rangeStep * widthFactor))
.attr('height', legendHeight)
.attr('stroke-width', 0)
.attr('fill', options.cardColor)
.style('opacity', d => opacityScale(d));
- drawLegendValues(elem, opacityScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth);
+ drawLegendValues(elem, rangeFrom, rangeTo, maxValue, minValue, legendWidth, valuesRange);
}
-function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth) {
+function drawLegendValues(elem, rangeFrom, rangeTo, maxValue, minValue, legendWidth, valuesRange) {
const legendElem = $(elem).find('svg');
const legend = d3.select(legendElem.get(0));
@@ -160,10 +163,10 @@ function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minVal
const legendValueScale = d3
.scaleLinear()
- .domain([0, rangeTo])
+ .domain([rangeFrom, rangeTo])
.range([0, legendWidth]);
- const ticks = buildLegendTicks(0, rangeTo, maxValue, minValue);
+ const ticks = buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue);
const xAxis = d3
.axisBottom(legendValueScale)
.tickValues(ticks)
@@ -171,7 +174,7 @@ function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minVal
const colorRect = legendElem.find(':first-child');
const posY = getSvgElemHeight(legendElem) + LEGEND_VALUE_MARGIN;
- const posX = getSvgElemX(colorRect);
+ const posX = getSvgElemX(colorRect) + LEGEND_PADDING_LEFT;
d3.select(legendElem.get(0))
.append('g')
@@ -284,11 +287,12 @@ function getSvgElemHeight(elem) {
function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) {
const range = rangeTo - rangeFrom;
const tickStepSize = tickStep(rangeFrom, rangeTo, 3);
- const ticksNum = Math.round(range / tickStepSize);
+ const ticksNum = Math.ceil(range / tickStepSize);
+ const firstTick = getFirstCloseTick(rangeFrom, tickStepSize);
let ticks = [];
for (let i = 0; i < ticksNum; i++) {
- const current = tickStepSize * i;
+ const current = firstTick + tickStepSize * i;
// Add user-defined min and max if it had been set
if (isValueCloseTo(minValue, current, tickStepSize)) {
ticks.push(minValue);
@@ -302,7 +306,7 @@ function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) {
} else if (maxValue < current) {
ticks.push(maxValue);
}
- ticks.push(tickStepSize * i);
+ ticks.push(current);
}
if (!isValueCloseTo(maxValue, rangeTo, tickStepSize)) {
ticks.push(maxValue);
@@ -316,3 +320,10 @@ function isValueCloseTo(val, valueTo, step) {
const diff = Math.abs(val - valueTo);
return diff < step * 0.3;
}
+
+function getFirstCloseTick(minValue, step) {
+ if (minValue < 0) {
+ return Math.floor(minValue / step) * step;
+ }
+ return 0;
+}
diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts
index 71e059a5750..99741a0c6d7 100644
--- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts
+++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts
@@ -34,6 +34,7 @@ const panelDefaults = {
},
dataFormat: 'timeseries',
yBucketBound: 'auto',
+ reverseYBuckets: false,
xAxis: {
show: true,
},
@@ -55,6 +56,7 @@ const panelDefaults = {
showHistogram: false,
},
highlightCards: true,
+ hideZeroBuckets: false,
};
const colorModes = ['opacity', 'spectrum'];
@@ -97,7 +99,7 @@ const colorSchemes = [
{ name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'dark' },
];
-const dsSupportHistogramSort = ['prometheus', 'elasticsearch'];
+const dsSupportHistogramSort = ['elasticsearch'];
export class HeatmapCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html';
@@ -108,7 +110,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
selectionActivated: boolean;
unitFormats: any;
data: any;
- series: any;
+ series: any[];
timeSrv: any;
dataWarning: any;
decimals: number;
@@ -146,7 +148,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
}
onRender() {
- if (!this.range) {
+ if (!this.range || !this.series) {
return;
}
@@ -204,7 +206,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
yBucketSize = 1;
}
- const { cards, cardStats } = convertToCards(bucketsData);
+ const { cards, cardStats } = convertToCards(bucketsData, this.panel.hideZeroBuckets);
this.data = {
buckets: bucketsData,
@@ -225,13 +227,20 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
this.series.sort(sortSeriesByLabel);
}
+ if (this.panel.reverseYBuckets) {
+ this.series.reverse();
+ }
+
// Convert histogram to heatmap. Each histogram bucket represented by the series which name is
- // a top (or bottom, depends of datasource) bucket bound. Further, these values will be used as X axis labels.
+ // a top (or bottom, depends of datasource) bucket bound. Further, these values will be used as Y axis labels.
bucketsData = histogramToHeatmap(this.series);
tsBuckets = _.map(this.series, 'label');
const yBucketBound = this.panel.yBucketBound;
- if ((panelDatasource === 'prometheus' && yBucketBound !== 'lower') || yBucketBound === 'upper') {
+ if (
+ (panelDatasource === 'prometheus' && yBucketBound !== 'lower' && yBucketBound !== 'middle') ||
+ yBucketBound === 'upper'
+ ) {
// Prometheus labels are upper inclusive bounds, so add empty bottom bucket label.
tsBuckets = [''].concat(tsBuckets);
} else {
@@ -246,7 +255,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
// Always let yBucketSize=1 in 'tsbuckets' mode
yBucketSize = 1;
- const { cards, cardStats } = convertToCards(bucketsData);
+ const { cards, cardStats } = convertToCards(bucketsData, this.panel.hideZeroBuckets);
this.data = {
buckets: bucketsData,
diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts
index 99b61be40dc..07960f68ae3 100644
--- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts
+++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts
@@ -93,25 +93,43 @@ function parseHistogramLabel(label: string): number {
return value;
}
+interface HeatmapCard {
+ x: number;
+ y: number;
+ yBounds: {
+ top: number | null;
+ bottom: number | null;
+ };
+ values: number[];
+ count: number;
+}
+
+interface HeatmapCardStats {
+ min: number;
+ max: number;
+}
+
/**
* Convert buckets into linear array of "cards" - objects, represented heatmap elements.
* @param {Object} buckets
- * @return {Array} Array of "card" objects
+ * @return {Object} Array of "card" objects and stats
*/
-function convertToCards(buckets) {
+function convertToCards(buckets: any, hideZero = false): { cards: HeatmapCard[]; cardStats: HeatmapCardStats } {
let min = 0,
max = 0;
- const cards = [];
+ const cards: HeatmapCard[] = [];
_.forEach(buckets, xBucket => {
_.forEach(xBucket.buckets, yBucket => {
- const card = {
+ const card: HeatmapCard = {
x: xBucket.x,
y: yBucket.y,
yBounds: yBucket.bounds,
values: yBucket.values,
count: yBucket.count,
};
- cards.push(card);
+ if (!hideZero || card.count !== 0) {
+ cards.push(card);
+ }
if (cards.length === 1) {
min = yBucket.count;
diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts
index c447825eef8..90cbecd6657 100644
--- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts
+++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts
@@ -114,7 +114,9 @@ export class HeatmapTooltip {
};
boundBottom = tickFormatter(yBucketIndex);
- boundTop = yBucketIndex < data.tsBuckets.length - 1 ? tickFormatter(yBucketIndex + 1) : '';
+ if (this.panel.yBucketBound !== 'middle') {
+ boundTop = yBucketIndex < data.tsBuckets.length - 1 ? tickFormatter(yBucketIndex + 1) : '';
+ }
} else {
// Display 0 if bucket is a special 'zero' bucket
const bottom = yData.y ? yData.bounds.bottom : 0;
@@ -122,8 +124,9 @@ export class HeatmapTooltip {
boundTop = bucketBoundFormatter(yData.bounds.top);
}
valuesNumber = countValueFormatter(yData.count);
+ const boundStr = boundTop && boundBottom ? `${boundBottom} - ${boundTop}` : boundBottom || boundTop;
tooltipHtml += `
- bucket: ${boundBottom} - ${boundTop}
+ bucket: ${boundStr}
count: ${valuesNumber}
`;
} else {
diff --git a/public/app/plugins/panel/heatmap/partials/axes_editor.html b/public/app/plugins/panel/heatmap/partials/axes_editor.html
index 0b12ac52e24..0327ee87251 100644
--- a/public/app/plugins/panel/heatmap/partials/axes_editor.html
+++ b/public/app/plugins/panel/heatmap/partials/axes_editor.html
@@ -40,6 +40,11 @@
+