Просмотр исходного кода

Merge branch 'feat-8539' of https://github.com/alexanderzobnin/grafana

Torkel Ödegaard 8 лет назад
Родитель
Сommit
1507c02ebb

+ 300 - 0
public/app/plugins/panel/heatmap/color_legend.ts

@@ -0,0 +1,300 @@
+///<reference path="../../../headers/common.d.ts" />
+import angular from 'angular';
+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');
+
+/**
+ * Color legend for heatmap editor.
+ */
+module.directive('colorLegend', function() {
+  return {
+    restrict: 'E',
+    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;
+
+      render();
+
+      ctrl.events.on('render', function() {
+        render();
+      });
+
+      function render() {
+        let legendElem = $(elem).find('svg');
+        let legendWidth = Math.floor(legendElem.outerWidth());
+
+        if (panel.color.mode === 'spectrum') {
+          let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
+          let colorScale = getColorScale(colorScheme, legendWidth);
+          drawSimpleColorLegend(elem, colorScale);
+        } else if (panel.color.mode === 'opacity') {
+          let colorOptions = panel.color;
+          drawSimpleOpacityLegend(elem, colorOptions);
+        }
+      }
+    }
+  };
+});
+
+/**
+ * Heatmap legend with scale values.
+ */
+module.directive('heatmapLegend', function() {
+  return {
+    restrict: 'E',
+    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;
+
+      render();
+      ctrl.events.on('render', function() {
+        render();
+      });
+
+      function render() {
+        clearLegend(elem);
+        if (!_.isEmpty(ctrl.data) && !_.isEmpty(ctrl.data.cards)) {
+          let rangeFrom = 0;
+          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});
+            drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue);
+          } else if (panel.color.mode === 'opacity') {
+            let colorOptions = panel.color;
+            drawOpacityLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue);
+          }
+        }
+      }
+    }
+  };
+});
+
+function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue) {
+  let legendElem = $(elem).find('svg');
+  let legend = d3.select(legendElem.get(0));
+  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 colorScale = getColorScale(colorScheme, maxValue, minValue);
+  legend.selectAll(".heatmap-color-legend-rect")
+    .data(valuesRange)
+    .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');
+  let legend = d3.select(legendElem.get(0));
+  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 opacityScale = getOpacityScale(options, maxValue, minValue);
+  legend.selectAll(".heatmap-opacity-legend-rect")
+    .data(valuesRange)
+    .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 => opacityScale(d));
+
+  drawLegendValues(elem, opacityScale, 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));
+
+  if (legendWidth <= 0 || legendElem.get(0).childNodes.length === 0) {
+    return;
+  }
+
+  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(2);
+
+  let colorRect = legendElem.find(":first-child");
+  let posY = colorRect.height() + 2;
+  let posX = getSvgElemX(colorRect);
+  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 drawSimpleOpacityLegend(elem, options) {
+  let legendElem = $(elem).find('svg');
+  clearLegend(elem);
+
+  let legend = d3.select(legendElem.get(0));
+  let legendWidth = Math.floor(legendElem.outerWidth());
+  let legendHeight = legendElem.attr("height");
+
+  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));
+  }
+}
+
+function clearLegend(elem) {
+  let legendElem = $(elem).find('svg');
+  legendElem.empty();
+}
+
+function getColorScale(colorScheme, maxValue, minValue = 0) {
+  let colorInterpolator = d3[colorScheme.value];
+  let colorScaleInverted = colorScheme.invert === 'always' ||
+    (colorScheme.invert === 'dark' && !contextSrv.user.lightTheme);
+
+  let start = colorScaleInverted ? maxValue : minValue;
+  let end = colorScaleInverted ? minValue : maxValue;
+
+  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) {
+  let svgElem = elem.get(0);
+  if (svgElem && svgElem.x && svgElem.x.baseVal) {
+    return elem.get(0).x.baseVal.value;
+  } else {
+    return 0;
+  }
+}
+
+function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) {
+  let range = rangeTo - rangeFrom;
+  let tickStepSize = tickStep(rangeFrom, rangeTo, 3);
+  let ticksNum = Math.round(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;
+}

+ 6 - 0
public/app/plugins/panel/heatmap/display_editor.ts

@@ -1,4 +1,10 @@
 ///<reference path="../../../headers/common.d.ts" />
 ///<reference path="../../../headers/common.d.ts" />
+import _ from 'lodash';
+import $ from 'jquery';
+import d3 from 'd3';
+import {contextSrv} from 'app/core/core';
+
+const COLOR_LEGEND_SELECTOR = '.heatmap-color-legend';
 
 
 export class HeatmapDisplayEditorCtrl {
 export class HeatmapDisplayEditorCtrl {
   panel: any;
   panel: any;

+ 9 - 2
public/app/plugins/panel/heatmap/heatmap_ctrl.ts

@@ -7,7 +7,7 @@ import TimeSeries from 'app/core/time_series';
 import {axesEditor} from './axes_editor';
 import {axesEditor} from './axes_editor';
 import {heatmapDisplayEditor} from './display_editor';
 import {heatmapDisplayEditor} from './display_editor';
 import rendering from './rendering';
 import rendering from './rendering';
-import { convertToHeatMap, elasticHistogramToHeatmap, calculateBucketSize, getMinLog} from './heatmap_data_converter';
+import {convertToHeatMap, convertToCards, elasticHistogramToHeatmap, calculateBucketSize, getMinLog} from './heatmap_data_converter';
 
 
 let X_BUCKET_NUMBER_DEFAULT = 30;
 let X_BUCKET_NUMBER_DEFAULT = 30;
 let Y_BUCKET_NUMBER_DEFAULT = 10;
 let Y_BUCKET_NUMBER_DEFAULT = 10;
@@ -26,6 +26,9 @@ let panelDefaults = {
     exponent: 0.5,
     exponent: 0.5,
     colorScheme: 'interpolateOranges',
     colorScheme: 'interpolateOranges',
   },
   },
+  legend: {
+    show: false
+  },
   dataFormat: 'timeseries',
   dataFormat: 'timeseries',
   xAxis: {
   xAxis: {
     show: true,
     show: true,
@@ -188,11 +191,15 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
       yBucketSize = 1;
       yBucketSize = 1;
     }
     }
 
 
+    let {cards, cardStats} = convertToCards(bucketsData);
+
     this.data = {
     this.data = {
       buckets: bucketsData,
       buckets: bucketsData,
       heatmapStats: heatmapStats,
       heatmapStats: heatmapStats,
       xBucketSize: xBucketSize,
       xBucketSize: xBucketSize,
-      yBucketSize: yBucketSize
+      yBucketSize: yBucketSize,
+      cards: cards,
+      cardStats: cardStats
     };
     };
   }
   }
 
 

+ 11 - 1
public/app/plugins/panel/heatmap/heatmap_data_converter.ts

@@ -51,6 +51,7 @@ function elasticHistogramToHeatmap(seriesList) {
  * @return {Array}          Array of "card" objects
  * @return {Array}          Array of "card" objects
  */
  */
 function convertToCards(buckets) {
 function convertToCards(buckets) {
+  let min = 0, max = 0;
   let cards = [];
   let cards = [];
   _.forEach(buckets, xBucket => {
   _.forEach(buckets, xBucket => {
     _.forEach(xBucket.buckets, yBucket=> {
     _.forEach(xBucket.buckets, yBucket=> {
@@ -62,10 +63,19 @@ function convertToCards(buckets) {
         count: yBucket.count,
         count: yBucket.count,
       };
       };
       cards.push(card);
       cards.push(card);
+
+      if (cards.length === 1) {
+        min = yBucket.count;
+        max = yBucket.count;
+      }
+
+      min = yBucket.count < min ? yBucket.count : min;
+      max = yBucket.count > max ? yBucket.count : max;
     });
     });
   });
   });
 
 
-  return cards;
+  let cardStats = {min, max};
+  return {cards, cardStats};
 }
 }
 
 
 /**
 /**

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

@@ -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" ng-if="ctrl.panel.legend.show">
+		<heatmap-legend></heatmap-legend>
+	</div>
 </div>
 </div>
 <div class="clearfix"></div>
 <div class="clearfix"></div>

+ 1 - 1
public/app/plugins/panel/heatmap/module.ts

@@ -1,5 +1,5 @@
 ///<reference path="../../../headers/common.d.ts" />
 ///<reference path="../../../headers/common.d.ts" />
-
+import './color_legend';
 import {HeatmapCtrl} from './heatmap_ctrl';
 import {HeatmapCtrl} from './heatmap_ctrl';
 
 
 export {
 export {

+ 24 - 6
public/app/plugins/panel/heatmap/partials/display_editor.html

@@ -25,9 +25,6 @@
         <label class="gf-form-label width-9">Exponent</label>
         <label class="gf-form-label width-9">Exponent</label>
         <input type="number" class="gf-form-input width-8" placeholder="auto" data-placement="right" bs-tooltip="''" ng-model="ctrl.panel.color.exponent" ng-change="ctrl.refresh()" ng-model-onblur>
         <input type="number" class="gf-form-input width-8" placeholder="auto" data-placement="right" bs-tooltip="''" ng-model="ctrl.panel.color.exponent" ng-change="ctrl.refresh()" ng-model-onblur>
       </div>
       </div>
-      <div class="gf-form">
-        <svg id="heatmap-opacity-legend" width="19em" height="2em"></svg>
-      </div>
     </div>
     </div>
 
 
     <div ng-show="ctrl.panel.color.mode === 'spectrum'">
     <div ng-show="ctrl.panel.color.mode === 'spectrum'">
@@ -37,10 +34,31 @@
           <select class="input-small gf-form-input" ng-model="ctrl.panel.color.colorScheme" ng-options="s.value as s.name for s in ctrl.colorSchemes" ng-change="ctrl.render()"></select>
           <select class="input-small gf-form-input" ng-model="ctrl.panel.color.colorScheme" ng-options="s.value as s.name for s in ctrl.colorSchemes" ng-change="ctrl.render()"></select>
         </div>
         </div>
       </div>
       </div>
-      <div class="gf-form">
-        <svg id="heatmap-color-legend" width="19em" height="2em"></svg>
-      </div>
     </div>
     </div>
+
+    <div class="gf-form">
+      <color-legend></color-legend>
+    </div>
+  </div>
+
+  <div class="section gf-form-group">
+    <h5 class="section-heading">Color scale</h5>
+    <div class="gf-form">
+      <label class="gf-form-label width-8">Min</label>
+      <input type="number" ng-model="ctrl.panel.color.min" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-change="ctrl.refresh()" ng-model-onblur>
+    </div>
+    <div class="gf-form">
+      <label class="gf-form-label width-8">Max</label>
+      <input type="number" ng-model="ctrl.panel.color.max" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-change="ctrl.refresh()" ng-model-onblur>
+    </div>
+  </div>
+
+  <div class="section gf-form-group">
+    <h5 class="section-heading">Legend</h5>
+    <gf-form-switch class="gf-form" label-class="width-8"
+      label="Show legend"
+      checked="ctrl.panel.legend.show" on-change="ctrl.render()">
+    </gf-form-switch>
   </div>
   </div>
 
 
   <div class="section gf-form-group">
   <div class="section gf-form-group">

+ 9 - 74
public/app/plugins/panel/heatmap/rendering.ts

@@ -8,7 +8,7 @@ import {appEvents, contextSrv} from 'app/core/core';
 import {tickStep, getScaledDecimals, getFlotTickSize} from 'app/core/utils/ticks';
 import {tickStep, getScaledDecimals, getFlotTickSize} from 'app/core/utils/ticks';
 import d3 from 'd3';
 import d3 from 'd3';
 import {HeatmapTooltip} from './heatmap_tooltip';
 import {HeatmapTooltip} from './heatmap_tooltip';
-import {convertToCards, mergeZeroBuckets} from './heatmap_data_converter';
+import {mergeZeroBuckets} from './heatmap_data_converter';
 
 
 let MIN_CARD_SIZE = 1,
 let MIN_CARD_SIZE = 1,
     CARD_PADDING = 1,
     CARD_PADDING = 1,
@@ -384,10 +384,12 @@ export default function link(scope, elem, attrs, ctrl) {
       data.buckets = mergeZeroBuckets(data.buckets, _.min(tick_values));
       data.buckets = mergeZeroBuckets(data.buckets, _.min(tick_values));
     }
     }
 
 
-    let cardsData = convertToCards(data.buckets);
-    let maxValue = d3.max(cardsData, card => card.count);
+    let cardsData = data.cards;
+    let maxValueAuto = data.cardStats.max;
+    let maxValue = panel.color.max || maxValueAuto;
+    let minValue = panel.color.min || 0;
 
 
-    colorScale = getColorScale(maxValue);
+    colorScale = getColorScale(maxValue, minValue);
     setOpacityScale(maxValue);
     setOpacityScale(maxValue);
     setCardSize();
     setCardSize();
 
 
@@ -434,14 +436,14 @@ export default function link(scope, elem, attrs, ctrl) {
     .style("stroke-width", 0);
     .style("stroke-width", 0);
   }
   }
 
 
-  function getColorScale(maxValue) {
+  function getColorScale(maxValue, minValue = 0) {
     let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
     let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
     let colorInterpolator = d3[colorScheme.value];
     let colorInterpolator = d3[colorScheme.value];
     let colorScaleInverted = colorScheme.invert === 'always' ||
     let colorScaleInverted = colorScheme.invert === 'always' ||
       (colorScheme.invert === 'dark' && !contextSrv.user.lightTheme);
       (colorScheme.invert === 'dark' && !contextSrv.user.lightTheme);
 
 
-    let start = colorScaleInverted ? maxValue : 0;
-    let end = colorScaleInverted ? 0 : maxValue;
+    let start = colorScaleInverted ? maxValue : minValue;
+    let end = colorScaleInverted ? minValue : maxValue;
 
 
     return d3.scaleSequential(colorInterpolator).domain([start, end]);
     return d3.scaleSequential(colorInterpolator).domain([start, end]);
   }
   }
@@ -704,78 +706,11 @@ export default function link(scope, elem, attrs, ctrl) {
     }
     }
   }
   }
 
 
-  function drawColorLegend() {
-    d3.select("#heatmap-color-legend").selectAll("rect").remove();
-
-    let legend = d3.select("#heatmap-color-legend");
-    let legendWidth = Math.floor($(d3.select("#heatmap-color-legend").node()).outerWidth());
-    let legendHeight = d3.select("#heatmap-color-legend").attr("height");
-
-    let legendColorScale = getColorScale(legendWidth);
-
-    let rangeStep = 2;
-    let valuesRange = d3.range(0, legendWidth, rangeStep);
-    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 => {
-      return legendColorScale(d);
-    });
-  }
-
-  function drawOpacityLegend() {
-    d3.select("#heatmap-opacity-legend").selectAll("rect").remove();
-
-    let legend = d3.select("#heatmap-opacity-legend");
-    let legendWidth = Math.floor($(d3.select("#heatmap-opacity-legend").node()).outerWidth());
-    let legendHeight = d3.select("#heatmap-opacity-legend").attr("height");
-
-    let legendOpacityScale;
-    if (panel.color.colorScale === 'linear') {
-      legendOpacityScale = d3.scaleLinear()
-      .domain([0, legendWidth])
-      .range([0, 1]);
-    } else if (panel.color.colorScale === 'sqrt') {
-      legendOpacityScale = d3.scalePow().exponent(panel.color.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);
-
-    legendRects.enter().append("rect")
-    .attr("x", d => d)
-    .attr("y", 0)
-    .attr("width", rangeStep)
-    .attr("height", legendHeight)
-    .attr("stroke-width", 0)
-    .attr("fill", panel.color.cardColor)
-    .style("opacity", d => {
-      return legendOpacityScale(d);
-    });
-  }
-
   function render() {
   function render() {
     data = ctrl.data;
     data = ctrl.data;
     panel = ctrl.panel;
     panel = ctrl.panel;
     timeRange = ctrl.range;
     timeRange = ctrl.range;
 
 
-    // Draw only if color editor is opened
-    if (!d3.select("#heatmap-color-legend").empty()) {
-      drawColorLegend();
-    }
-
-    if (!d3.select("#heatmap-opacity-legend").empty()) {
-      drawOpacityLegend();
-    }
-
     if (!setElementHeight() || !data) {
     if (!setElementHeight() || !data) {
       return;
       return;
     }
     }

+ 43 - 1
public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts

@@ -3,7 +3,8 @@
 import _ from 'lodash';
 import _ from 'lodash';
 import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common';
 import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common';
 import TimeSeries from 'app/core/time_series2';
 import TimeSeries from 'app/core/time_series2';
-import { convertToHeatMap, elasticHistogramToHeatmap, calculateBucketSize, isHeatmapDataEqual } from '../heatmap_data_converter';
+import {convertToHeatMap, convertToCards, elasticHistogramToHeatmap,
+        calculateBucketSize, isHeatmapDataEqual} from '../heatmap_data_converter';
 
 
 describe('isHeatmapDataEqual', () => {
 describe('isHeatmapDataEqual', () => {
   let ctx: any = {};
   let ctx: any = {};
@@ -244,6 +245,47 @@ describe('ES Histogram converter', () => {
   });
   });
 });
 });
 
 
+describe('convertToCards', () => {
+  let buckets = {};
+
+  beforeEach(() => {
+    buckets = {
+      '1422774000000': {
+        x: 1422774000000,
+        buckets: {
+          '1': { y: 1, values: [1], count: 1, bounds: {} },
+          '2': { y: 2, values: [2], count: 1, bounds: {} }
+        }
+      },
+      '1422774060000': {
+        x: 1422774060000,
+        buckets: {
+          '2': { y: 2, values: [2, 3], count: 2, bounds: {} }
+        }
+      },
+    };
+  });
+
+  it('should build proper cards data', () => {
+    let expectedCards = [
+      {x: 1422774000000, y: 1, count: 1, values: [1], yBounds: {}},
+      {x: 1422774000000, y: 2, count: 1, values: [2], yBounds: {}},
+      {x: 1422774060000, y: 2, count: 2, values: [2, 3], yBounds: {}}
+    ];
+    let {cards, cardStats} = convertToCards(buckets);
+    expect(cards).to.eql(expectedCards);
+  });
+
+  it('should build proper cards stats', () => {
+    let expectedStats = {
+      min: 1,
+      max: 2
+    };
+    let {cards, cardStats} = convertToCards(buckets);
+    expect(cardStats).to.eql(expectedStats);
+  });
+});
+
 /**
 /**
  * Compare two numbers with given precision. Suitable for compare float numbers after conversions with precision loss.
  * Compare two numbers with given precision. Suitable for compare float numbers after conversions with precision loss.
  * @param a
  * @param a

+ 4 - 4
public/app/plugins/panel/heatmap/specs/renderer_specs.ts

@@ -11,8 +11,7 @@ import TimeSeries from 'app/core/time_series2';
 import moment from 'moment';
 import moment from 'moment';
 import { Emitter } from 'app/core/core';
 import { Emitter } from 'app/core/core';
 import rendering from '../rendering';
 import rendering from '../rendering';
-import { convertToHeatMap } from '../heatmap_data_converter';
-// import d3 from 'd3';
+import {convertToHeatMap, convertToCards} from '../heatmap_data_converter';
 
 
 describe('grafanaHeatmap', function () {
 describe('grafanaHeatmap', function () {
 
 
@@ -115,8 +114,9 @@ describe('grafanaHeatmap', function () {
           let bucketsData = convertToHeatMap(ctx.series, ctx.data.yBucketSize, ctx.data.xBucketSize, logBase);
           let bucketsData = convertToHeatMap(ctx.series, ctx.data.yBucketSize, ctx.data.xBucketSize, logBase);
           ctx.data.buckets = bucketsData;
           ctx.data.buckets = bucketsData;
 
 
-          // console.log("bucketsData", bucketsData);
-          // console.log("series", ctrl.panel.yAxis.logBase, ctx.series.length);
+          let {cards, cardStats} = convertToCards(bucketsData);
+          ctx.data.cards = cards;
+          ctx.data.cardStats = cardStats;
 
 
           let elemHtml = `
           let elemHtml = `
           <div class="heatmap-wrapper">
           <div class="heatmap-wrapper">

+ 43 - 0
public/sass/components/_panel_heatmap.scss

@@ -46,3 +46,46 @@
     stroke-width: 1;
     stroke-width: 1;
   }
   }
 }
 }
+
+.heatmap-selection {
+  stroke-width: 1;
+  fill: rgba(102, 102, 102, 0.4);
+  stroke: rgba(102, 102, 102, 0.8);
+}
+
+.heatmap-legend-wrapper {
+  @include clearfix();
+  margin: 0 $spacer;
+  padding-top: 10px;
+
+  svg {
+    width: 100%;
+    max-width: 300px;
+    height: 33px;
+    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;
+    }
+  }
+}