heatmap: initial legend

This commit is contained in:
Alexander Zobnin 2017-07-31 20:11:55 +03:00
parent c79a68dcd1
commit 2aa26c98b6
3 changed files with 227 additions and 42 deletions

View File

@ -4,12 +4,13 @@ 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');
module.directive('colorLegend', function() {
return {
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) {
let ctrl = scope.ctrl;
let panel = scope.ctrl.panel;
@ -27,10 +28,10 @@ module.directive('colorLegend', function() {
if (panel.color.mode === 'spectrum') {
let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
let colorScale = getColorScale(colorScheme, legendWidth);
drawColorLegend(elem, colorScale);
drawSimpleColorLegend(elem, colorScale);
} else if (panel.color.mode === 'opacity') {
let colorOptions = panel.color;
drawOpacityLegend(elem, colorOptions);
drawSimpleOpacityLegend(elem, colorOptions);
}
}
}
@ -40,7 +41,7 @@ module.directive('colorLegend', function() {
module.directive('heatmapLegend', function() {
return {
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) {
let ctrl = scope.ctrl;
let panel = scope.ctrl.panel;
@ -50,13 +51,19 @@ module.directive('heatmapLegend', function() {
let legendElem = $(elem).find('svg');
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') {
let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
let colorScale = getColorScale(colorScheme, legendWidth);
drawColorLegend(elem, colorScale);
let colorScale = getColorScale(colorScheme, maxValue, minValue);
drawColorLegend(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue);
} else if (panel.color.mode === 'opacity') {
let colorOptions = panel.color;
drawOpacityLegend(elem, colorOptions);
drawOpacityLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue);
}
}
});
@ -64,34 +71,116 @@ module.directive('heatmapLegend', function() {
};
});
function drawColorLegend(elem, colorScale) {
function drawColorLegend(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue) {
let legendElem = $(elem).find('svg');
legendElem.find("rect").remove();
clearLegend(elem);
let legendWidth = Math.floor(legendElem.outerWidth());
let legendWidth = Math.floor(legendElem.outerWidth()) - 30;
let legendHeight = legendElem.attr("height");
let rangeStep = 2;
let valuesRange = d3.range(0, legendWidth, rangeStep);
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)
.attr("y", 0)
.attr("width", rangeStep + 1) // Overlap rectangles to prevent gaps
.attr("height", legendHeight)
.attr("stroke-width", 0)
.attr("fill", d => colorScale(d));
.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 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 clearLegend(elem) {
let legendElem = $(elem).find('svg');
legendElem.find("rect").remove();
legendElem.empty();
}
function drawOpacityLegend(elem, options) {
function drawSimpleOpacityLegend(elem, options) {
let legendElem = $(elem).find('svg');
clearLegend(elem);
@ -99,29 +188,31 @@ function drawOpacityLegend(elem, options) {
let legendWidth = Math.floor(legendElem.outerWidth());
let legendHeight = legendElem.attr("height");
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]);
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));
}
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", options.cardColor)
.style("opacity", d => legendOpacityScale(d));
}
function getColorScale(colorScheme, maxValue, minValue = 0) {
@ -134,3 +225,57 @@ function getColorScale(colorScheme, maxValue, minValue = 0) {
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;
}

View File

@ -7,5 +7,8 @@
<div class="heatmap-panel" ng-dblclick="ctrl.zoomOut()"></div>
</div>
<div class="heatmap-legend-wrapper">
<heatmap-legend></heatmap-legend>
</div>
</div>
<div class="clearfix"></div>

View File

@ -46,3 +46,40 @@
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;
}
}
}