mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
heatmap: initial legend
This commit is contained in:
@@ -4,12 +4,13 @@ import _ from 'lodash';
|
|||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import d3 from 'd3';
|
import d3 from 'd3';
|
||||||
import {contextSrv} from 'app/core/core';
|
import {contextSrv} from 'app/core/core';
|
||||||
|
import {tickStep} from 'app/core/utils/ticks';
|
||||||
|
|
||||||
let module = angular.module('grafana.directives');
|
let module = angular.module('grafana.directives');
|
||||||
module.directive('colorLegend', function() {
|
module.directive('colorLegend', function() {
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
template: '<div class="heatmap-color-legend"><svg width="19em" height="2em"></svg></div>',
|
template: '<div class="heatmap-color-legend"><svg width="16.8rem" height="24px"></svg></div>',
|
||||||
link: function(scope, elem, attrs) {
|
link: function(scope, elem, attrs) {
|
||||||
let ctrl = scope.ctrl;
|
let ctrl = scope.ctrl;
|
||||||
let panel = scope.ctrl.panel;
|
let panel = scope.ctrl.panel;
|
||||||
@@ -27,10 +28,10 @@ module.directive('colorLegend', function() {
|
|||||||
if (panel.color.mode === 'spectrum') {
|
if (panel.color.mode === 'spectrum') {
|
||||||
let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
|
let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
|
||||||
let colorScale = getColorScale(colorScheme, legendWidth);
|
let colorScale = getColorScale(colorScheme, legendWidth);
|
||||||
drawColorLegend(elem, colorScale);
|
drawSimpleColorLegend(elem, colorScale);
|
||||||
} else if (panel.color.mode === 'opacity') {
|
} else if (panel.color.mode === 'opacity') {
|
||||||
let colorOptions = panel.color;
|
let colorOptions = panel.color;
|
||||||
drawOpacityLegend(elem, colorOptions);
|
drawSimpleOpacityLegend(elem, colorOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,7 +41,7 @@ module.directive('colorLegend', function() {
|
|||||||
module.directive('heatmapLegend', function() {
|
module.directive('heatmapLegend', function() {
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
template: '<div class="heatmap-color-legend"><svg width="19em" height="2em"></svg></div>',
|
template: '<div class="heatmap-color-legend"><svg width="100px" height="14px"></svg></div>',
|
||||||
link: function(scope, elem, attrs) {
|
link: function(scope, elem, attrs) {
|
||||||
let ctrl = scope.ctrl;
|
let ctrl = scope.ctrl;
|
||||||
let panel = scope.ctrl.panel;
|
let panel = scope.ctrl.panel;
|
||||||
@@ -50,13 +51,19 @@ module.directive('heatmapLegend', function() {
|
|||||||
let legendElem = $(elem).find('svg');
|
let legendElem = $(elem).find('svg');
|
||||||
let legendWidth = Math.floor(legendElem.outerWidth());
|
let legendWidth = Math.floor(legendElem.outerWidth());
|
||||||
|
|
||||||
|
// let maxValue = ctrl.data.cardStats.max || legendWidth;
|
||||||
|
let rangeFrom = ctrl.data.cardStats.min;
|
||||||
|
let rangeTo = ctrl.data.cardStats.max;
|
||||||
|
let maxValue = panel.color.max || rangeTo;
|
||||||
|
let minValue = panel.color.min || 0;
|
||||||
|
|
||||||
if (panel.color.mode === 'spectrum') {
|
if (panel.color.mode === 'spectrum') {
|
||||||
let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
|
let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
|
||||||
let colorScale = getColorScale(colorScheme, legendWidth);
|
let colorScale = getColorScale(colorScheme, maxValue, minValue);
|
||||||
drawColorLegend(elem, colorScale);
|
drawColorLegend(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue);
|
||||||
} else if (panel.color.mode === 'opacity') {
|
} else if (panel.color.mode === 'opacity') {
|
||||||
let colorOptions = panel.color;
|
let colorOptions = panel.color;
|
||||||
drawOpacityLegend(elem, colorOptions);
|
drawOpacityLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -64,14 +71,95 @@ module.directive('heatmapLegend', function() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function drawColorLegend(elem, colorScale) {
|
function drawColorLegend(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue) {
|
||||||
let legendElem = $(elem).find('svg');
|
let legendElem = $(elem).find('svg');
|
||||||
legendElem.find("rect").remove();
|
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 legend = d3.select(legendElem.get(0));
|
||||||
|
var legendRects = legend.selectAll(".heatmap-color-legend-rect").data(valuesRange);
|
||||||
|
|
||||||
|
legendRects.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');
|
||||||
|
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 legend = d3.select(legendElem.get(0));
|
||||||
|
var legendRects = legend.selectAll(".heatmap-opacity-legend-rect").data(valuesRange);
|
||||||
|
|
||||||
|
let legendOpacityScale = getOpacityScale(options, maxValue, minValue);
|
||||||
|
legendRects.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 => legendOpacityScale(d));
|
||||||
|
|
||||||
|
drawLegendValues(elem, legendOpacityScale, 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));
|
||||||
|
|
||||||
|
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(3);
|
||||||
|
|
||||||
|
let legendElemHeight = legendElem.height();
|
||||||
|
let posY = legendElemHeight - 23;
|
||||||
|
let posX = getSvgElemX(legendElem.find(":first-child"));
|
||||||
|
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 legendWidth = Math.floor(legendElem.outerWidth());
|
||||||
let legendHeight = legendElem.attr("height");
|
let legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
let rangeStep = 2;
|
if (legendWidth) {
|
||||||
|
let valuesNumber = Math.floor(legendWidth / 2);
|
||||||
|
let rangeStep = Math.floor(legendWidth / valuesNumber);
|
||||||
let valuesRange = d3.range(0, legendWidth, rangeStep);
|
let valuesRange = d3.range(0, legendWidth, rangeStep);
|
||||||
|
|
||||||
let legend = d3.select(legendElem.get(0));
|
let legend = d3.select(legendElem.get(0));
|
||||||
@@ -84,14 +172,15 @@ function drawColorLegend(elem, colorScale) {
|
|||||||
.attr("height", legendHeight)
|
.attr("height", legendHeight)
|
||||||
.attr("stroke-width", 0)
|
.attr("stroke-width", 0)
|
||||||
.attr("fill", d => colorScale(d));
|
.attr("fill", d => colorScale(d));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearLegend(elem) {
|
function clearLegend(elem) {
|
||||||
let legendElem = $(elem).find('svg');
|
let legendElem = $(elem).find('svg');
|
||||||
legendElem.find("rect").remove();
|
legendElem.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawOpacityLegend(elem, options) {
|
function drawSimpleOpacityLegend(elem, options) {
|
||||||
let legendElem = $(elem).find('svg');
|
let legendElem = $(elem).find('svg');
|
||||||
clearLegend(elem);
|
clearLegend(elem);
|
||||||
|
|
||||||
@@ -99,6 +188,7 @@ function drawOpacityLegend(elem, options) {
|
|||||||
let legendWidth = Math.floor(legendElem.outerWidth());
|
let legendWidth = Math.floor(legendElem.outerWidth());
|
||||||
let legendHeight = legendElem.attr("height");
|
let legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
|
if (legendWidth) {
|
||||||
let legendOpacityScale;
|
let legendOpacityScale;
|
||||||
if (options.colorScale === 'linear') {
|
if (options.colorScale === 'linear') {
|
||||||
legendOpacityScale = d3.scaleLinear()
|
legendOpacityScale = d3.scaleLinear()
|
||||||
@@ -110,7 +200,7 @@ function drawOpacityLegend(elem, options) {
|
|||||||
.range([0, 1]);
|
.range([0, 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let rangeStep = 1;
|
let rangeStep = 10;
|
||||||
let valuesRange = d3.range(0, legendWidth, rangeStep);
|
let valuesRange = d3.range(0, legendWidth, rangeStep);
|
||||||
var legendRects = legend.selectAll(".heatmap-opacity-legend-rect").data(valuesRange);
|
var legendRects = legend.selectAll(".heatmap-opacity-legend-rect").data(valuesRange);
|
||||||
|
|
||||||
@@ -122,6 +212,7 @@ function drawOpacityLegend(elem, options) {
|
|||||||
.attr("stroke-width", 0)
|
.attr("stroke-width", 0)
|
||||||
.attr("fill", options.cardColor)
|
.attr("fill", options.cardColor)
|
||||||
.style("opacity", d => legendOpacityScale(d));
|
.style("opacity", d => legendOpacityScale(d));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getColorScale(colorScheme, maxValue, minValue = 0) {
|
function getColorScale(colorScheme, maxValue, minValue = 0) {
|
||||||
@@ -134,3 +225,57 @@ function getColorScale(colorScheme, maxValue, minValue = 0) {
|
|||||||
|
|
||||||
return d3.scaleSequential(colorInterpolator).domain([start, end]);
|
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) {
|
||||||
|
return elem.get(0).x.baseVal.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) {
|
||||||
|
let range = rangeTo - rangeFrom;
|
||||||
|
let tickStepSize = tickStep(rangeFrom, rangeTo, 3);
|
||||||
|
let ticksNum = Math.floor(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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
<heatmap-legend></heatmap-legend>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|||||||
@@ -46,3 +46,40 @@
|
|||||||
stroke-width: 1;
|
stroke-width: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.heatmap-legend-wrapper {
|
||||||
|
@include clearfix();
|
||||||
|
margin: 0 $spacer;
|
||||||
|
padding-top: 10px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
height: 38px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user