Commit e3dd70bc by Torkel Ödegaard Committed by GitHub

Merge pull request #15937 from alexanderzobnin/heatmap-fixes

Heatmap fixes and improvements
parents ab2caf9e c248c1f4
...@@ -5,12 +5,15 @@ ...@@ -5,12 +5,15 @@
### Minor ### Minor
* **Cloudwatch**: Add AWS RDS MaximumUsedTransactionIDs metric [#15077](https://github.com/grafana/grafana/pull/15077), thx [@activeshadow](https://github.com/activeshadow) * **Cloudwatch**: Add AWS RDS MaximumUsedTransactionIDs metric [#15077](https://github.com/grafana/grafana/pull/15077), thx [@activeshadow](https://github.com/activeshadow)
* **Heatmap**: `Middle` bucket bound option [#15683](https://github.com/grafana/grafana/issues/15683)
* **Heatmap**: `Reverse order` option for changing order of buckets [#15683](https://github.com/grafana/grafana/issues/15683)
### Bug Fixes ### Bug Fixes
* **Api**: Invalid org invite code [#10506](https://github.com/grafana/grafana/issues/10506) * **Api**: Invalid org invite code [#10506](https://github.com/grafana/grafana/issues/10506)
* **Datasource**: Handles nil jsondata field gracefully [#14239](https://github.com/grafana/grafana/issues/14239) * **Datasource**: Handles nil jsondata field gracefully [#14239](https://github.com/grafana/grafana/issues/14239)
* **Gauge**: Interpolate scoped variables in repeated gauges [#15739](https://github.com/grafana/grafana/issues/15739) * **Gauge**: Interpolate scoped variables in repeated gauges [#15739](https://github.com/grafana/grafana/issues/15739)
* **Datasource**: Empty user/password was not updated when updating datasources [#15608](https://github.com/grafana/grafana/pull/15608), thx [@Maddin-619](https://github.com/Maddin-619) * **Datasource**: Empty user/password was not updated when updating datasources [#15608](https://github.com/grafana/grafana/pull/15608), thx [@Maddin-619](https://github.com/Maddin-619)
* **Heatmap**: legend shows wrong colors for small values [#14019](https://github.com/grafana/grafana/issues/14019)
# 6.0.1 (2019-03-06) # 6.0.1 (2019-03-06)
......
...@@ -32,6 +32,7 @@ export class AxesEditorCtrl { ...@@ -32,6 +32,7 @@ export class AxesEditorCtrl {
Auto: 'auto', Auto: 'auto',
Upper: 'upper', Upper: 'upper',
Lower: 'lower', Lower: 'lower',
Middle: 'middle',
}; };
} }
......
...@@ -11,6 +11,8 @@ const LEGEND_HEIGHT_PX = 6; ...@@ -11,6 +11,8 @@ const LEGEND_HEIGHT_PX = 6;
const LEGEND_WIDTH_PX = 100; const LEGEND_WIDTH_PX = 100;
const LEGEND_TICK_SIZE = 0; const LEGEND_TICK_SIZE = 0;
const LEGEND_VALUE_MARGIN = 0; const LEGEND_VALUE_MARGIN = 0;
const LEGEND_PADDING_LEFT = 10;
const LEGEND_SEGMENT_WIDTH = 10;
/** /**
* Color legend for heatmap editor. * Color legend for heatmap editor.
...@@ -95,27 +97,27 @@ function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minVal ...@@ -95,27 +97,27 @@ function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minVal
const legendWidth = Math.floor(legendElem.outerWidth()) - 30; const legendWidth = Math.floor(legendElem.outerWidth()) - 30;
const legendHeight = legendElem.attr('height'); const legendHeight = legendElem.attr('height');
let rangeStep = 1; const rangeStep = ((rangeTo - rangeFrom) / legendWidth) * LEGEND_SEGMENT_WIDTH;
if (rangeTo - rangeFrom > legendWidth) {
rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth);
}
const widthFactor = legendWidth / (rangeTo - rangeFrom); const widthFactor = legendWidth / (rangeTo - rangeFrom);
const valuesRange = d3.range(rangeFrom, rangeTo, rangeStep); const valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
const colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue); const colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue);
legend legend
.append('g')
.attr('class', 'legend-color-bar')
.attr('transform', 'translate(' + LEGEND_PADDING_LEFT + ',0)')
.selectAll('.heatmap-color-legend-rect') .selectAll('.heatmap-color-legend-rect')
.data(valuesRange) .data(valuesRange)
.enter() .enter()
.append('rect') .append('rect')
.attr('x', d => d * widthFactor) .attr('x', d => Math.round(d * widthFactor))
.attr('y', 0) .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('height', legendHeight)
.attr('stroke-width', 0) .attr('stroke-width', 0)
.attr('fill', d => colorScale(d)); .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) { function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue) {
...@@ -126,31 +128,31 @@ function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue ...@@ -126,31 +128,31 @@ function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue
const legendWidth = Math.floor(legendElem.outerWidth()) - 30; const legendWidth = Math.floor(legendElem.outerWidth()) - 30;
const legendHeight = legendElem.attr('height'); const legendHeight = legendElem.attr('height');
let rangeStep = 1; const rangeStep = ((rangeTo - rangeFrom) / legendWidth) * LEGEND_SEGMENT_WIDTH;
if (rangeTo - rangeFrom > legendWidth) {
rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth);
}
const widthFactor = legendWidth / (rangeTo - rangeFrom); const widthFactor = legendWidth / (rangeTo - rangeFrom);
const valuesRange = d3.range(rangeFrom, rangeTo, rangeStep); const valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
const opacityScale = getOpacityScale(options, maxValue, minValue); const opacityScale = getOpacityScale(options, maxValue, minValue);
legend legend
.append('g')
.attr('class', 'legend-color-bar')
.attr('transform', 'translate(' + LEGEND_PADDING_LEFT + ',0)')
.selectAll('.heatmap-opacity-legend-rect') .selectAll('.heatmap-opacity-legend-rect')
.data(valuesRange) .data(valuesRange)
.enter() .enter()
.append('rect') .append('rect')
.attr('x', d => d * widthFactor) .attr('x', d => Math.round(d * widthFactor))
.attr('y', 0) .attr('y', 0)
.attr('width', rangeStep * widthFactor) .attr('width', Math.round(rangeStep * widthFactor))
.attr('height', legendHeight) .attr('height', legendHeight)
.attr('stroke-width', 0) .attr('stroke-width', 0)
.attr('fill', options.cardColor) .attr('fill', options.cardColor)
.style('opacity', d => opacityScale(d)); .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 legendElem = $(elem).find('svg');
const legend = d3.select(legendElem.get(0)); const legend = d3.select(legendElem.get(0));
...@@ -171,7 +173,7 @@ function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minVal ...@@ -171,7 +173,7 @@ function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minVal
const colorRect = legendElem.find(':first-child'); const colorRect = legendElem.find(':first-child');
const posY = getSvgElemHeight(legendElem) + LEGEND_VALUE_MARGIN; const posY = getSvgElemHeight(legendElem) + LEGEND_VALUE_MARGIN;
const posX = getSvgElemX(colorRect); const posX = getSvgElemX(colorRect) + LEGEND_PADDING_LEFT;
d3.select(legendElem.get(0)) d3.select(legendElem.get(0))
.append('g') .append('g')
......
...@@ -34,6 +34,7 @@ const panelDefaults = { ...@@ -34,6 +34,7 @@ const panelDefaults = {
}, },
dataFormat: 'timeseries', dataFormat: 'timeseries',
yBucketBound: 'auto', yBucketBound: 'auto',
reverseYBuckets: false,
xAxis: { xAxis: {
show: true, show: true,
}, },
...@@ -109,7 +110,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl { ...@@ -109,7 +110,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
selectionActivated: boolean; selectionActivated: boolean;
unitFormats: any; unitFormats: any;
data: any; data: any;
series: any; series: any[];
timeSrv: any; timeSrv: any;
dataWarning: any; dataWarning: any;
decimals: number; decimals: number;
...@@ -147,7 +148,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl { ...@@ -147,7 +148,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
} }
onRender() { onRender() {
if (!this.range) { if (!this.range || !this.series) {
return; return;
} }
...@@ -226,13 +227,20 @@ export class HeatmapCtrl extends MetricsPanelCtrl { ...@@ -226,13 +227,20 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
this.series.sort(sortSeriesByLabel); 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 // 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); bucketsData = histogramToHeatmap(this.series);
tsBuckets = _.map(this.series, 'label'); tsBuckets = _.map(this.series, 'label');
const yBucketBound = this.panel.yBucketBound; 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. // Prometheus labels are upper inclusive bounds, so add empty bottom bucket label.
tsBuckets = [''].concat(tsBuckets); tsBuckets = [''].concat(tsBuckets);
} else { } else {
......
...@@ -114,7 +114,9 @@ export class HeatmapTooltip { ...@@ -114,7 +114,9 @@ export class HeatmapTooltip {
}; };
boundBottom = tickFormatter(yBucketIndex); 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 { } else {
// Display 0 if bucket is a special 'zero' bucket // Display 0 if bucket is a special 'zero' bucket
const bottom = yData.y ? yData.bounds.bottom : 0; const bottom = yData.y ? yData.bounds.bottom : 0;
...@@ -122,8 +124,9 @@ export class HeatmapTooltip { ...@@ -122,8 +124,9 @@ export class HeatmapTooltip {
boundTop = bucketBoundFormatter(yData.bounds.top); boundTop = bucketBoundFormatter(yData.bounds.top);
} }
valuesNumber = countValueFormatter(yData.count); valuesNumber = countValueFormatter(yData.count);
const boundStr = boundTop && boundBottom ? `${boundBottom} - ${boundTop}` : boundBottom || boundTop;
tooltipHtml += `<div> tooltipHtml += `<div>
bucket: <b>${boundBottom} - ${boundTop}</b> <br> bucket: <b>${boundStr}</b> <br>
count: <b>${valuesNumber}</b> <br> count: <b>${valuesNumber}</b> <br>
</div>`; </div>`;
} else { } else {
......
...@@ -40,6 +40,11 @@ ...@@ -40,6 +40,11 @@
</select> </select>
</div> </div>
</div> </div>
<gf-form-switch ng-if="ctrl.panel.dataFormat == 'tsbuckets'"
class="gf-form" label-class="width-8"
label="Reverse order"
checked="ctrl.panel.reverseYBuckets" on-change="ctrl.refresh()">
</gf-form-switch>
</div> </div>
<div class="section gf-form-group" ng-if="ctrl.panel.dataFormat == 'timeseries'"> <div class="section gf-form-group" ng-if="ctrl.panel.dataFormat == 'timeseries'">
......
...@@ -379,6 +379,12 @@ export class HeatmapRenderer { ...@@ -379,6 +379,12 @@ export class HeatmapRenderer {
const posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING; const posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING;
this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')'); this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
if (this.panel.yBucketBound === 'middle' && tickValues && tickValues.length) {
// Shift Y axis labels to the middle of bucket
const tickShift = 0 - this.chartHeight / (tickValues.length - 1) / 2;
this.heatmap.selectAll('.axis-y text').attr('transform', 'translate(' + 0 + ',' + tickShift + ')');
}
// Remove vertical line in the right of axis labels (called domain in d3) // Remove vertical line in the right of axis labels (called domain in d3)
this.heatmap this.heatmap
.select('.axis-y') .select('.axis-y')
...@@ -615,8 +621,8 @@ export class HeatmapRenderer { ...@@ -615,8 +621,8 @@ export class HeatmapRenderer {
w = this.cardWidth; w = this.cardWidth;
} }
// Card width should be MIN_CARD_SIZE at least // Card width should be MIN_CARD_SIZE at least, but cut cards shouldn't be displayed
w = Math.max(w, MIN_CARD_SIZE); w = w > 0 ? Math.max(w, MIN_CARD_SIZE) : 0;
return w; return w;
} }
......
...@@ -66,7 +66,6 @@ $font-size-heatmap-tick: 11px; ...@@ -66,7 +66,6 @@ $font-size-heatmap-tick: 11px;
height: 18px; height: 18px;
float: left; float: left;
white-space: nowrap; white-space: nowrap;
padding-left: 10px;
} }
.heatmap-legend-values { .heatmap-legend-values {
......
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