소스 검색

heatmap: initial legend

Alexander Zobnin 8 년 전
부모
커밋
2aa26c98b6
3개의 변경된 파일226개의 추가작업 그리고 41개의 파일을 삭제
  1. 186 41
      public/app/plugins/panel/heatmap/color_legend.ts
  2. 3 0
      public/app/plugins/panel/heatmap/module.html
  3. 37 0
      public/sass/components/_panel_heatmap.scss

+ 186 - 41
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: '<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;
+}

+ 3 - 0
public/app/plugins/panel/heatmap/module.html

@@ -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>

+ 37 - 0
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;
+    }
+  }
+}