mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'feat-8539' of https://github.com/alexanderzobnin/grafana
This commit is contained in:
commit
1507c02ebb
300
public/app/plugins/panel/heatmap/color_legend.ts
Normal file
300
public/app/plugins/panel/heatmap/color_legend.ts
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
///<reference path="../../../headers/common.d.ts" />
|
||||||
|
import angular from 'angular';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import $ from 'jquery';
|
||||||
|
import d3 from 'd3';
|
||||||
|
import {contextSrv} from 'app/core/core';
|
||||||
|
import {tickStep} from 'app/core/utils/ticks';
|
||||||
|
|
||||||
|
let module = angular.module('grafana.directives');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color legend for heatmap editor.
|
||||||
|
*/
|
||||||
|
module.directive('colorLegend', function() {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
template: '<div class="heatmap-color-legend"><svg width="16.8rem" height="24px"></svg></div>',
|
||||||
|
link: function(scope, elem, attrs) {
|
||||||
|
let ctrl = scope.ctrl;
|
||||||
|
let panel = scope.ctrl.panel;
|
||||||
|
|
||||||
|
render();
|
||||||
|
|
||||||
|
ctrl.events.on('render', function() {
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
let legendElem = $(elem).find('svg');
|
||||||
|
let legendWidth = Math.floor(legendElem.outerWidth());
|
||||||
|
|
||||||
|
if (panel.color.mode === 'spectrum') {
|
||||||
|
let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
|
||||||
|
let colorScale = getColorScale(colorScheme, legendWidth);
|
||||||
|
drawSimpleColorLegend(elem, colorScale);
|
||||||
|
} else if (panel.color.mode === 'opacity') {
|
||||||
|
let colorOptions = panel.color;
|
||||||
|
drawSimpleOpacityLegend(elem, colorOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Heatmap legend with scale values.
|
||||||
|
*/
|
||||||
|
module.directive('heatmapLegend', function() {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
template: '<div class="heatmap-color-legend"><svg width="100px" height="14px"></svg></div>',
|
||||||
|
link: function(scope, elem, attrs) {
|
||||||
|
let ctrl = scope.ctrl;
|
||||||
|
let panel = scope.ctrl.panel;
|
||||||
|
|
||||||
|
render();
|
||||||
|
ctrl.events.on('render', function() {
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
clearLegend(elem);
|
||||||
|
if (!_.isEmpty(ctrl.data) && !_.isEmpty(ctrl.data.cards)) {
|
||||||
|
let rangeFrom = 0;
|
||||||
|
let rangeTo = ctrl.data.cardStats.max;
|
||||||
|
let maxValue = panel.color.max || rangeTo;
|
||||||
|
let minValue = panel.color.min || 0;
|
||||||
|
|
||||||
|
if (panel.color.mode === 'spectrum') {
|
||||||
|
let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
|
||||||
|
drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue);
|
||||||
|
} else if (panel.color.mode === 'opacity') {
|
||||||
|
let colorOptions = panel.color;
|
||||||
|
drawOpacityLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue) {
|
||||||
|
let legendElem = $(elem).find('svg');
|
||||||
|
let legend = d3.select(legendElem.get(0));
|
||||||
|
clearLegend(elem);
|
||||||
|
|
||||||
|
let legendWidth = Math.floor(legendElem.outerWidth()) - 30;
|
||||||
|
let legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
|
let rangeStep = 1;
|
||||||
|
if (rangeTo - rangeFrom > legendWidth) {
|
||||||
|
rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth);
|
||||||
|
}
|
||||||
|
let widthFactor = legendWidth / (rangeTo - rangeFrom);
|
||||||
|
let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
||||||
|
|
||||||
|
let colorScale = getColorScale(colorScheme, maxValue, minValue);
|
||||||
|
legend.selectAll(".heatmap-color-legend-rect")
|
||||||
|
.data(valuesRange)
|
||||||
|
.enter().append("rect")
|
||||||
|
.attr("x", d => d * widthFactor)
|
||||||
|
.attr("y", 0)
|
||||||
|
.attr("width", 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue) {
|
||||||
|
let legendElem = $(elem).find('svg');
|
||||||
|
let legend = d3.select(legendElem.get(0));
|
||||||
|
clearLegend(elem);
|
||||||
|
|
||||||
|
let legendWidth = Math.floor(legendElem.outerWidth()) - 30;
|
||||||
|
let legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
|
let rangeStep = 10;
|
||||||
|
let widthFactor = legendWidth / (rangeTo - rangeFrom);
|
||||||
|
let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
||||||
|
|
||||||
|
let opacityScale = getOpacityScale(options, maxValue, minValue);
|
||||||
|
legend.selectAll(".heatmap-opacity-legend-rect")
|
||||||
|
.data(valuesRange)
|
||||||
|
.enter().append("rect")
|
||||||
|
.attr("x", d => d * widthFactor)
|
||||||
|
.attr("y", 0)
|
||||||
|
.attr("width", 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth) {
|
||||||
|
let legendElem = $(elem).find('svg');
|
||||||
|
let legend = d3.select(legendElem.get(0));
|
||||||
|
|
||||||
|
if (legendWidth <= 0 || legendElem.get(0).childNodes.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let legendValueDomain = _.sortBy(colorScale.domain());
|
||||||
|
let legendValueScale = d3.scaleLinear()
|
||||||
|
.domain([0, rangeTo])
|
||||||
|
.range([0, legendWidth]);
|
||||||
|
|
||||||
|
let ticks = buildLegendTicks(0, rangeTo, maxValue, minValue);
|
||||||
|
let xAxis = d3.axisBottom(legendValueScale)
|
||||||
|
.tickValues(ticks)
|
||||||
|
.tickSize(2);
|
||||||
|
|
||||||
|
let colorRect = legendElem.find(":first-child");
|
||||||
|
let posY = colorRect.height() + 2;
|
||||||
|
let posX = getSvgElemX(colorRect);
|
||||||
|
d3.select(legendElem.get(0)).append("g")
|
||||||
|
.attr("class", "axis")
|
||||||
|
.attr("transform", "translate(" + posX + "," + posY + ")")
|
||||||
|
.call(xAxis);
|
||||||
|
|
||||||
|
legend.select(".axis").select(".domain").remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSimpleColorLegend(elem, colorScale) {
|
||||||
|
let legendElem = $(elem).find('svg');
|
||||||
|
clearLegend(elem);
|
||||||
|
|
||||||
|
let legendWidth = Math.floor(legendElem.outerWidth());
|
||||||
|
let legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
|
if (legendWidth) {
|
||||||
|
let valuesNumber = Math.floor(legendWidth / 2);
|
||||||
|
let rangeStep = Math.floor(legendWidth / valuesNumber);
|
||||||
|
let valuesRange = d3.range(0, legendWidth, rangeStep);
|
||||||
|
|
||||||
|
let legend = d3.select(legendElem.get(0));
|
||||||
|
var legendRects = legend.selectAll(".heatmap-color-legend-rect").data(valuesRange);
|
||||||
|
|
||||||
|
legendRects.enter().append("rect")
|
||||||
|
.attr("x", d => d)
|
||||||
|
.attr("y", 0)
|
||||||
|
.attr("width", rangeStep + 1) // Overlap rectangles to prevent gaps
|
||||||
|
.attr("height", legendHeight)
|
||||||
|
.attr("stroke-width", 0)
|
||||||
|
.attr("fill", d => colorScale(d));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSimpleOpacityLegend(elem, options) {
|
||||||
|
let legendElem = $(elem).find('svg');
|
||||||
|
clearLegend(elem);
|
||||||
|
|
||||||
|
let legend = d3.select(legendElem.get(0));
|
||||||
|
let legendWidth = Math.floor(legendElem.outerWidth());
|
||||||
|
let legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
|
if (legendWidth) {
|
||||||
|
let legendOpacityScale;
|
||||||
|
if (options.colorScale === 'linear') {
|
||||||
|
legendOpacityScale = d3.scaleLinear()
|
||||||
|
.domain([0, legendWidth])
|
||||||
|
.range([0, 1]);
|
||||||
|
} else if (options.colorScale === 'sqrt') {
|
||||||
|
legendOpacityScale = d3.scalePow().exponent(options.exponent)
|
||||||
|
.domain([0, legendWidth])
|
||||||
|
.range([0, 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rangeStep = 10;
|
||||||
|
let valuesRange = d3.range(0, legendWidth, rangeStep);
|
||||||
|
var legendRects = legend.selectAll(".heatmap-opacity-legend-rect").data(valuesRange);
|
||||||
|
|
||||||
|
legendRects.enter().append("rect")
|
||||||
|
.attr("x", d => d)
|
||||||
|
.attr("y", 0)
|
||||||
|
.attr("width", rangeStep)
|
||||||
|
.attr("height", legendHeight)
|
||||||
|
.attr("stroke-width", 0)
|
||||||
|
.attr("fill", options.cardColor)
|
||||||
|
.style("opacity", d => legendOpacityScale(d));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLegend(elem) {
|
||||||
|
let legendElem = $(elem).find('svg');
|
||||||
|
legendElem.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColorScale(colorScheme, maxValue, minValue = 0) {
|
||||||
|
let colorInterpolator = d3[colorScheme.value];
|
||||||
|
let colorScaleInverted = colorScheme.invert === 'always' ||
|
||||||
|
(colorScheme.invert === 'dark' && !contextSrv.user.lightTheme);
|
||||||
|
|
||||||
|
let start = colorScaleInverted ? maxValue : minValue;
|
||||||
|
let end = colorScaleInverted ? minValue : maxValue;
|
||||||
|
|
||||||
|
return d3.scaleSequential(colorInterpolator).domain([start, end]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOpacityScale(options, maxValue, minValue = 0) {
|
||||||
|
let legendOpacityScale;
|
||||||
|
if (options.colorScale === 'linear') {
|
||||||
|
legendOpacityScale = d3.scaleLinear()
|
||||||
|
.domain([minValue, maxValue])
|
||||||
|
.range([0, 1]);
|
||||||
|
} else if (options.colorScale === 'sqrt') {
|
||||||
|
legendOpacityScale = d3.scalePow().exponent(options.exponent)
|
||||||
|
.domain([minValue, maxValue])
|
||||||
|
.range([0, 1]);
|
||||||
|
}
|
||||||
|
return legendOpacityScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSvgElemX(elem) {
|
||||||
|
let svgElem = elem.get(0);
|
||||||
|
if (svgElem && svgElem.x && svgElem.x.baseVal) {
|
||||||
|
return elem.get(0).x.baseVal.value;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) {
|
||||||
|
let range = rangeTo - rangeFrom;
|
||||||
|
let tickStepSize = tickStep(rangeFrom, rangeTo, 3);
|
||||||
|
let ticksNum = Math.round(range / tickStepSize);
|
||||||
|
let ticks = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < ticksNum; i++) {
|
||||||
|
let current = tickStepSize * i;
|
||||||
|
// Add user-defined min and max if it had been set
|
||||||
|
if (isValueCloseTo(minValue, current, tickStepSize)) {
|
||||||
|
ticks.push(minValue);
|
||||||
|
continue;
|
||||||
|
} else if (minValue < current) {
|
||||||
|
ticks.push(minValue);
|
||||||
|
}
|
||||||
|
if (isValueCloseTo(maxValue, current, tickStepSize)) {
|
||||||
|
ticks.push(maxValue);
|
||||||
|
continue;
|
||||||
|
} else if (maxValue < current) {
|
||||||
|
ticks.push(maxValue);
|
||||||
|
}
|
||||||
|
ticks.push(tickStepSize * i);
|
||||||
|
}
|
||||||
|
if (!isValueCloseTo(maxValue, rangeTo, tickStepSize)) {
|
||||||
|
ticks.push(maxValue);
|
||||||
|
}
|
||||||
|
ticks.push(rangeTo);
|
||||||
|
ticks = _.sortBy(_.uniq(ticks));
|
||||||
|
return ticks;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValueCloseTo(val, valueTo, step) {
|
||||||
|
let diff = Math.abs(val - valueTo);
|
||||||
|
return diff < step * 0.3;
|
||||||
|
}
|
@ -1,4 +1,10 @@
|
|||||||
///<reference path="../../../headers/common.d.ts" />
|
///<reference path="../../../headers/common.d.ts" />
|
||||||
|
import _ from 'lodash';
|
||||||
|
import $ from 'jquery';
|
||||||
|
import d3 from 'd3';
|
||||||
|
import {contextSrv} from 'app/core/core';
|
||||||
|
|
||||||
|
const COLOR_LEGEND_SELECTOR = '.heatmap-color-legend';
|
||||||
|
|
||||||
export class HeatmapDisplayEditorCtrl {
|
export class HeatmapDisplayEditorCtrl {
|
||||||
panel: any;
|
panel: any;
|
||||||
|
@ -7,7 +7,7 @@ import TimeSeries from 'app/core/time_series';
|
|||||||
import {axesEditor} from './axes_editor';
|
import {axesEditor} from './axes_editor';
|
||||||
import {heatmapDisplayEditor} from './display_editor';
|
import {heatmapDisplayEditor} from './display_editor';
|
||||||
import rendering from './rendering';
|
import rendering from './rendering';
|
||||||
import { convertToHeatMap, elasticHistogramToHeatmap, calculateBucketSize, getMinLog} from './heatmap_data_converter';
|
import {convertToHeatMap, convertToCards, elasticHistogramToHeatmap, calculateBucketSize, getMinLog} from './heatmap_data_converter';
|
||||||
|
|
||||||
let X_BUCKET_NUMBER_DEFAULT = 30;
|
let X_BUCKET_NUMBER_DEFAULT = 30;
|
||||||
let Y_BUCKET_NUMBER_DEFAULT = 10;
|
let Y_BUCKET_NUMBER_DEFAULT = 10;
|
||||||
@ -26,6 +26,9 @@ let panelDefaults = {
|
|||||||
exponent: 0.5,
|
exponent: 0.5,
|
||||||
colorScheme: 'interpolateOranges',
|
colorScheme: 'interpolateOranges',
|
||||||
},
|
},
|
||||||
|
legend: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
dataFormat: 'timeseries',
|
dataFormat: 'timeseries',
|
||||||
xAxis: {
|
xAxis: {
|
||||||
show: true,
|
show: true,
|
||||||
@ -188,11 +191,15 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
yBucketSize = 1;
|
yBucketSize = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let {cards, cardStats} = convertToCards(bucketsData);
|
||||||
|
|
||||||
this.data = {
|
this.data = {
|
||||||
buckets: bucketsData,
|
buckets: bucketsData,
|
||||||
heatmapStats: heatmapStats,
|
heatmapStats: heatmapStats,
|
||||||
xBucketSize: xBucketSize,
|
xBucketSize: xBucketSize,
|
||||||
yBucketSize: yBucketSize
|
yBucketSize: yBucketSize,
|
||||||
|
cards: cards,
|
||||||
|
cardStats: cardStats
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +51,7 @@ function elasticHistogramToHeatmap(seriesList) {
|
|||||||
* @return {Array} Array of "card" objects
|
* @return {Array} Array of "card" objects
|
||||||
*/
|
*/
|
||||||
function convertToCards(buckets) {
|
function convertToCards(buckets) {
|
||||||
|
let min = 0, max = 0;
|
||||||
let cards = [];
|
let cards = [];
|
||||||
_.forEach(buckets, xBucket => {
|
_.forEach(buckets, xBucket => {
|
||||||
_.forEach(xBucket.buckets, yBucket=> {
|
_.forEach(xBucket.buckets, yBucket=> {
|
||||||
@ -62,10 +63,19 @@ function convertToCards(buckets) {
|
|||||||
count: yBucket.count,
|
count: yBucket.count,
|
||||||
};
|
};
|
||||||
cards.push(card);
|
cards.push(card);
|
||||||
|
|
||||||
|
if (cards.length === 1) {
|
||||||
|
min = yBucket.count;
|
||||||
|
max = yBucket.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
min = yBucket.count < min ? yBucket.count : min;
|
||||||
|
max = yBucket.count > max ? yBucket.count : max;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return cards;
|
let cardStats = {min, max};
|
||||||
|
return {cards, cardStats};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -7,5 +7,8 @@
|
|||||||
|
|
||||||
<div class="heatmap-panel" ng-dblclick="ctrl.zoomOut()"></div>
|
<div class="heatmap-panel" ng-dblclick="ctrl.zoomOut()"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="heatmap-legend-wrapper" ng-if="ctrl.panel.legend.show">
|
||||||
|
<heatmap-legend></heatmap-legend>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
///<reference path="../../../headers/common.d.ts" />
|
///<reference path="../../../headers/common.d.ts" />
|
||||||
|
import './color_legend';
|
||||||
import {HeatmapCtrl} from './heatmap_ctrl';
|
import {HeatmapCtrl} from './heatmap_ctrl';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -25,9 +25,6 @@
|
|||||||
<label class="gf-form-label width-9">Exponent</label>
|
<label class="gf-form-label width-9">Exponent</label>
|
||||||
<input type="number" class="gf-form-input width-8" placeholder="auto" data-placement="right" bs-tooltip="''" ng-model="ctrl.panel.color.exponent" ng-change="ctrl.refresh()" ng-model-onblur>
|
<input type="number" class="gf-form-input width-8" placeholder="auto" data-placement="right" bs-tooltip="''" ng-model="ctrl.panel.color.exponent" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
|
||||||
<svg id="heatmap-opacity-legend" width="19em" height="2em"></svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-show="ctrl.panel.color.mode === 'spectrum'">
|
<div ng-show="ctrl.panel.color.mode === 'spectrum'">
|
||||||
@ -37,10 +34,31 @@
|
|||||||
<select class="input-small gf-form-input" ng-model="ctrl.panel.color.colorScheme" ng-options="s.value as s.name for s in ctrl.colorSchemes" ng-change="ctrl.render()"></select>
|
<select class="input-small gf-form-input" ng-model="ctrl.panel.color.colorScheme" ng-options="s.value as s.name for s in ctrl.colorSchemes" ng-change="ctrl.render()"></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
|
||||||
<svg id="heatmap-color-legend" width="19em" height="2em"></svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form">
|
||||||
|
<color-legend></color-legend>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section gf-form-group">
|
||||||
|
<h5 class="section-heading">Color scale</h5>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-8">Min</label>
|
||||||
|
<input type="number" ng-model="ctrl.panel.color.min" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-8">Max</label>
|
||||||
|
<input type="number" ng-model="ctrl.panel.color.max" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section gf-form-group">
|
||||||
|
<h5 class="section-heading">Legend</h5>
|
||||||
|
<gf-form-switch class="gf-form" label-class="width-8"
|
||||||
|
label="Show legend"
|
||||||
|
checked="ctrl.panel.legend.show" on-change="ctrl.render()">
|
||||||
|
</gf-form-switch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section gf-form-group">
|
<div class="section gf-form-group">
|
||||||
|
@ -8,7 +8,7 @@ import {appEvents, contextSrv} from 'app/core/core';
|
|||||||
import {tickStep, getScaledDecimals, getFlotTickSize} from 'app/core/utils/ticks';
|
import {tickStep, getScaledDecimals, getFlotTickSize} from 'app/core/utils/ticks';
|
||||||
import d3 from 'd3';
|
import d3 from 'd3';
|
||||||
import {HeatmapTooltip} from './heatmap_tooltip';
|
import {HeatmapTooltip} from './heatmap_tooltip';
|
||||||
import {convertToCards, mergeZeroBuckets} from './heatmap_data_converter';
|
import {mergeZeroBuckets} from './heatmap_data_converter';
|
||||||
|
|
||||||
let MIN_CARD_SIZE = 1,
|
let MIN_CARD_SIZE = 1,
|
||||||
CARD_PADDING = 1,
|
CARD_PADDING = 1,
|
||||||
@ -384,10 +384,12 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
data.buckets = mergeZeroBuckets(data.buckets, _.min(tick_values));
|
data.buckets = mergeZeroBuckets(data.buckets, _.min(tick_values));
|
||||||
}
|
}
|
||||||
|
|
||||||
let cardsData = convertToCards(data.buckets);
|
let cardsData = data.cards;
|
||||||
let maxValue = d3.max(cardsData, card => card.count);
|
let maxValueAuto = data.cardStats.max;
|
||||||
|
let maxValue = panel.color.max || maxValueAuto;
|
||||||
|
let minValue = panel.color.min || 0;
|
||||||
|
|
||||||
colorScale = getColorScale(maxValue);
|
colorScale = getColorScale(maxValue, minValue);
|
||||||
setOpacityScale(maxValue);
|
setOpacityScale(maxValue);
|
||||||
setCardSize();
|
setCardSize();
|
||||||
|
|
||||||
@ -434,14 +436,14 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
.style("stroke-width", 0);
|
.style("stroke-width", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getColorScale(maxValue) {
|
function getColorScale(maxValue, minValue = 0) {
|
||||||
let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
|
let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
|
||||||
let colorInterpolator = d3[colorScheme.value];
|
let colorInterpolator = d3[colorScheme.value];
|
||||||
let colorScaleInverted = colorScheme.invert === 'always' ||
|
let colorScaleInverted = colorScheme.invert === 'always' ||
|
||||||
(colorScheme.invert === 'dark' && !contextSrv.user.lightTheme);
|
(colorScheme.invert === 'dark' && !contextSrv.user.lightTheme);
|
||||||
|
|
||||||
let start = colorScaleInverted ? maxValue : 0;
|
let start = colorScaleInverted ? maxValue : minValue;
|
||||||
let end = colorScaleInverted ? 0 : maxValue;
|
let end = colorScaleInverted ? minValue : maxValue;
|
||||||
|
|
||||||
return d3.scaleSequential(colorInterpolator).domain([start, end]);
|
return d3.scaleSequential(colorInterpolator).domain([start, end]);
|
||||||
}
|
}
|
||||||
@ -704,78 +706,11 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawColorLegend() {
|
|
||||||
d3.select("#heatmap-color-legend").selectAll("rect").remove();
|
|
||||||
|
|
||||||
let legend = d3.select("#heatmap-color-legend");
|
|
||||||
let legendWidth = Math.floor($(d3.select("#heatmap-color-legend").node()).outerWidth());
|
|
||||||
let legendHeight = d3.select("#heatmap-color-legend").attr("height");
|
|
||||||
|
|
||||||
let legendColorScale = getColorScale(legendWidth);
|
|
||||||
|
|
||||||
let rangeStep = 2;
|
|
||||||
let valuesRange = d3.range(0, legendWidth, rangeStep);
|
|
||||||
var legendRects = legend.selectAll(".heatmap-color-legend-rect").data(valuesRange);
|
|
||||||
|
|
||||||
legendRects.enter().append("rect")
|
|
||||||
.attr("x", d => d)
|
|
||||||
.attr("y", 0)
|
|
||||||
.attr("width", rangeStep + 1) // Overlap rectangles to prevent gaps
|
|
||||||
.attr("height", legendHeight)
|
|
||||||
.attr("stroke-width", 0)
|
|
||||||
.attr("fill", d => {
|
|
||||||
return legendColorScale(d);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawOpacityLegend() {
|
|
||||||
d3.select("#heatmap-opacity-legend").selectAll("rect").remove();
|
|
||||||
|
|
||||||
let legend = d3.select("#heatmap-opacity-legend");
|
|
||||||
let legendWidth = Math.floor($(d3.select("#heatmap-opacity-legend").node()).outerWidth());
|
|
||||||
let legendHeight = d3.select("#heatmap-opacity-legend").attr("height");
|
|
||||||
|
|
||||||
let legendOpacityScale;
|
|
||||||
if (panel.color.colorScale === 'linear') {
|
|
||||||
legendOpacityScale = d3.scaleLinear()
|
|
||||||
.domain([0, legendWidth])
|
|
||||||
.range([0, 1]);
|
|
||||||
} else if (panel.color.colorScale === 'sqrt') {
|
|
||||||
legendOpacityScale = d3.scalePow().exponent(panel.color.exponent)
|
|
||||||
.domain([0, legendWidth])
|
|
||||||
.range([0, 1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let rangeStep = 1;
|
|
||||||
let valuesRange = d3.range(0, legendWidth, rangeStep);
|
|
||||||
var legendRects = legend.selectAll(".heatmap-opacity-legend-rect").data(valuesRange);
|
|
||||||
|
|
||||||
legendRects.enter().append("rect")
|
|
||||||
.attr("x", d => d)
|
|
||||||
.attr("y", 0)
|
|
||||||
.attr("width", rangeStep)
|
|
||||||
.attr("height", legendHeight)
|
|
||||||
.attr("stroke-width", 0)
|
|
||||||
.attr("fill", panel.color.cardColor)
|
|
||||||
.style("opacity", d => {
|
|
||||||
return legendOpacityScale(d);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
data = ctrl.data;
|
data = ctrl.data;
|
||||||
panel = ctrl.panel;
|
panel = ctrl.panel;
|
||||||
timeRange = ctrl.range;
|
timeRange = ctrl.range;
|
||||||
|
|
||||||
// Draw only if color editor is opened
|
|
||||||
if (!d3.select("#heatmap-color-legend").empty()) {
|
|
||||||
drawColorLegend();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!d3.select("#heatmap-opacity-legend").empty()) {
|
|
||||||
drawOpacityLegend();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!setElementHeight() || !data) {
|
if (!setElementHeight() || !data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common';
|
import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common';
|
||||||
import TimeSeries from 'app/core/time_series2';
|
import TimeSeries from 'app/core/time_series2';
|
||||||
import { convertToHeatMap, elasticHistogramToHeatmap, calculateBucketSize, isHeatmapDataEqual } from '../heatmap_data_converter';
|
import {convertToHeatMap, convertToCards, elasticHistogramToHeatmap,
|
||||||
|
calculateBucketSize, isHeatmapDataEqual} from '../heatmap_data_converter';
|
||||||
|
|
||||||
describe('isHeatmapDataEqual', () => {
|
describe('isHeatmapDataEqual', () => {
|
||||||
let ctx: any = {};
|
let ctx: any = {};
|
||||||
@ -244,6 +245,47 @@ describe('ES Histogram converter', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('convertToCards', () => {
|
||||||
|
let buckets = {};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
buckets = {
|
||||||
|
'1422774000000': {
|
||||||
|
x: 1422774000000,
|
||||||
|
buckets: {
|
||||||
|
'1': { y: 1, values: [1], count: 1, bounds: {} },
|
||||||
|
'2': { y: 2, values: [2], count: 1, bounds: {} }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'1422774060000': {
|
||||||
|
x: 1422774060000,
|
||||||
|
buckets: {
|
||||||
|
'2': { y: 2, values: [2, 3], count: 2, bounds: {} }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should build proper cards data', () => {
|
||||||
|
let expectedCards = [
|
||||||
|
{x: 1422774000000, y: 1, count: 1, values: [1], yBounds: {}},
|
||||||
|
{x: 1422774000000, y: 2, count: 1, values: [2], yBounds: {}},
|
||||||
|
{x: 1422774060000, y: 2, count: 2, values: [2, 3], yBounds: {}}
|
||||||
|
];
|
||||||
|
let {cards, cardStats} = convertToCards(buckets);
|
||||||
|
expect(cards).to.eql(expectedCards);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should build proper cards stats', () => {
|
||||||
|
let expectedStats = {
|
||||||
|
min: 1,
|
||||||
|
max: 2
|
||||||
|
};
|
||||||
|
let {cards, cardStats} = convertToCards(buckets);
|
||||||
|
expect(cardStats).to.eql(expectedStats);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare two numbers with given precision. Suitable for compare float numbers after conversions with precision loss.
|
* Compare two numbers with given precision. Suitable for compare float numbers after conversions with precision loss.
|
||||||
* @param a
|
* @param a
|
||||||
|
@ -11,8 +11,7 @@ import TimeSeries from 'app/core/time_series2';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Emitter } from 'app/core/core';
|
import { Emitter } from 'app/core/core';
|
||||||
import rendering from '../rendering';
|
import rendering from '../rendering';
|
||||||
import { convertToHeatMap } from '../heatmap_data_converter';
|
import {convertToHeatMap, convertToCards} from '../heatmap_data_converter';
|
||||||
// import d3 from 'd3';
|
|
||||||
|
|
||||||
describe('grafanaHeatmap', function () {
|
describe('grafanaHeatmap', function () {
|
||||||
|
|
||||||
@ -115,8 +114,9 @@ describe('grafanaHeatmap', function () {
|
|||||||
let bucketsData = convertToHeatMap(ctx.series, ctx.data.yBucketSize, ctx.data.xBucketSize, logBase);
|
let bucketsData = convertToHeatMap(ctx.series, ctx.data.yBucketSize, ctx.data.xBucketSize, logBase);
|
||||||
ctx.data.buckets = bucketsData;
|
ctx.data.buckets = bucketsData;
|
||||||
|
|
||||||
// console.log("bucketsData", bucketsData);
|
let {cards, cardStats} = convertToCards(bucketsData);
|
||||||
// console.log("series", ctrl.panel.yAxis.logBase, ctx.series.length);
|
ctx.data.cards = cards;
|
||||||
|
ctx.data.cardStats = cardStats;
|
||||||
|
|
||||||
let elemHtml = `
|
let elemHtml = `
|
||||||
<div class="heatmap-wrapper">
|
<div class="heatmap-wrapper">
|
||||||
|
@ -46,3 +46,46 @@
|
|||||||
stroke-width: 1;
|
stroke-width: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.heatmap-selection {
|
||||||
|
stroke-width: 1;
|
||||||
|
fill: rgba(102, 102, 102, 0.4);
|
||||||
|
stroke: rgba(102, 102, 102, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heatmap-legend-wrapper {
|
||||||
|
@include clearfix();
|
||||||
|
margin: 0 $spacer;
|
||||||
|
padding-top: 10px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
height: 33px;
|
||||||
|
float: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heatmap-legend-values {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.axis .tick {
|
||||||
|
text {
|
||||||
|
fill: $text-color;
|
||||||
|
color: $text-color;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
line {
|
||||||
|
opacity: 0.4;
|
||||||
|
stroke: $text-color-weak;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain {
|
||||||
|
opacity: 0.4;
|
||||||
|
stroke: $text-color-weak;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user