From 2aa26c98b6ba41bebb0671cb0c57c356d8040ae4 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 31 Jul 2017 20:11:55 +0300 Subject: [PATCH] heatmap: initial legend --- .../app/plugins/panel/heatmap/color_legend.ts | 229 ++++++++++++++---- public/app/plugins/panel/heatmap/module.html | 3 + public/sass/components/_panel_heatmap.scss | 37 +++ 3 files changed, 227 insertions(+), 42 deletions(-) diff --git a/public/app/plugins/panel/heatmap/color_legend.ts b/public/app/plugins/panel/heatmap/color_legend.ts index 572d26c7330..0b23a10e000 100644 --- a/public/app/plugins/panel/heatmap/color_legend.ts +++ b/public/app/plugins/panel/heatmap/color_legend.ts @@ -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: '
', + template: '
', 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: '
', + template: '
', 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; +} diff --git a/public/app/plugins/panel/heatmap/module.html b/public/app/plugins/panel/heatmap/module.html index 6cb89f2e1f2..ac9652a5afc 100644 --- a/public/app/plugins/panel/heatmap/module.html +++ b/public/app/plugins/panel/heatmap/module.html @@ -7,5 +7,8 @@
+
+ +
diff --git a/public/sass/components/_panel_heatmap.scss b/public/sass/components/_panel_heatmap.scss index 9d07e6a363c..9ca7b6f4bba 100644 --- a/public/sass/components/_panel_heatmap.scss +++ b/public/sass/components/_panel_heatmap.scss @@ -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; + } + } +}