|
|
@@ -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 = 1;
|
|
|
- let valuesRange = d3.range(0, legendWidth, rangeStep);
|
|
|
- var legendRects = legend.selectAll(".heatmap-opacity-legend-rect").data(valuesRange);
|
|
|
+ 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));
|
|
|
+ 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;
|
|
|
+}
|