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

Merge branch 'develop-graph-legend' into develop

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

+ 6 - 2
public/app/core/core.ts

@@ -52,6 +52,8 @@ import {gfPageDirective} from './components/gf_page';
 import {orgSwitcher} from './components/org_switcher';
 import {profiler} from './profiler';
 import {registerAngularDirectives} from './angular_wrappers';
+import {updateLegendValues} from './time_series2';
+import TimeSeries from './time_series2';
 import {searchResultsDirective} from './components/search/search_results';
 import {manageDashboardsDirective} from './components/manage_dashboards/manage_dashboards';
 
@@ -86,6 +88,8 @@ export {
   geminiScrollbar,
   gfPageDirective,
   orgSwitcher,
-  searchResultsDirective,
-  manageDashboardsDirective
+  manageDashboardsDirective,
+  TimeSeries,
+  updateLegendValues,
+  searchResultsDirective
 };

+ 43 - 0
public/app/core/time_series2.ts

@@ -1,4 +1,5 @@
 import kbn from 'app/core/utils/kbn';
+import {getFlotTickDecimals} from 'app/core/utils/ticks';
 import _ from 'lodash';
 
 function matchSeriesOverride(aliasOrRegex, seriesAlias) {
@@ -16,6 +17,48 @@ function translateFillOption(fill) {
   return fill === 0 ? 0.001 : fill/10;
 }
 
+/**
+ * Calculate decimals for legend and update values for each series.
+ * @param data series data
+ * @param panel
+ */
+export function updateLegendValues(data: TimeSeries[], panel) {
+  for (let i = 0; i < data.length; i++) {
+    let series = data[i];
+    let yaxes = panel.yaxes;
+    let axis = yaxes[series.yaxis - 1];
+    let {tickDecimals, scaledDecimals} = getFlotTickDecimals(data, axis);
+    let formater = kbn.valueFormats[panel.yaxes[series.yaxis - 1].format];
+
+    // decimal override
+    if (_.isNumber(panel.decimals)) {
+      series.updateLegendValues(formater, panel.decimals, null);
+    } else {
+      // auto decimals
+      // legend and tooltip gets one more decimal precision
+      // than graph legend ticks
+      tickDecimals = (tickDecimals || -1) + 1;
+      series.updateLegendValues(formater, tickDecimals, scaledDecimals + 2);
+    }
+  }
+}
+
+export function getDataMinMax(data: TimeSeries[]) {
+  let datamin = null;
+  let datamax = null;
+
+  for (let series of data) {
+    if (datamax === null || datamax < series.stats.max) {
+      datamax = series.stats.max;
+    }
+    if (datamin === null || datamin > series.stats.min) {
+      datamin = series.stats.min;
+    }
+  }
+
+  return {datamin, datamax};
+}
+
 export default class TimeSeries {
   datapoints: any;
   id: string;

+ 91 - 0
public/app/core/utils/ticks.ts

@@ -1,3 +1,5 @@
+import {getDataMinMax} from 'app/core/time_series2';
+
 /**
  * Calculate tick step.
  * Implementation from d3-array (ticks.js)
@@ -32,6 +34,7 @@ export function getScaledDecimals(decimals, tick_size) {
 
 /**
  * Calculate tick size based on min and max values, number of ticks and precision.
+ * Implementation from Flot.
  * @param min           Axis minimum
  * @param max           Axis maximum
  * @param noTicks       Number of ticks
@@ -65,3 +68,91 @@ export function getFlotTickSize(min: number, max: number, noTicks: number, tickD
 
   return size;
 }
+
+/**
+ * Calculate axis range (min and max).
+ * Implementation from Flot.
+ */
+export function getFlotRange(panelMin, panelMax, datamin, datamax) {
+  const autoscaleMargin = 0.02;
+
+  let min = +(panelMin != null ? panelMin : datamin);
+  let max = +(panelMax != null ? panelMax : datamax);
+  let delta = max - min;
+
+  if (delta === 0.0) {
+      // Grafana fix: wide Y min and max using increased wideFactor
+      // when all series values are the same
+      var wideFactor = 0.25;
+      var widen = Math.abs(max === 0 ? 1 : max * wideFactor);
+
+      if (panelMin === null) {
+        min -= widen;
+      }
+      // always widen max if we couldn't widen min to ensure we
+      // don't fall into min == max which doesn't work
+      if (panelMax == null || panelMin != null) {
+        max += widen;
+      }
+  } else {
+    // consider autoscaling
+    var margin = autoscaleMargin;
+    if (margin != null) {
+      if (panelMin == null) {
+        min -= delta * margin;
+        // make sure we don't go below zero if all values
+        // are positive
+        if (min < 0 && datamin != null && datamin >= 0) {
+          min = 0;
+        }
+      }
+      if (panelMax == null) {
+        max += delta * margin;
+        if (max > 0 && datamax != null && datamax <= 0) {
+          max = 0;
+        }
+      }
+    }
+  }
+  return {min, max};
+}
+
+/**
+ * Calculate tick decimals.
+ * Implementation from Flot.
+ */
+export function getFlotTickDecimals(data, axis) {
+  let {datamin, datamax} = getDataMinMax(data);
+  let {min, max} = getFlotRange(axis.min, axis.max, datamin, datamax);
+  let noTicks = 3;
+  let tickDecimals, maxDec;
+  let delta = (max - min) / noTicks;
+  let dec = -Math.floor(Math.log(delta) / Math.LN10);
+
+  let magn = Math.pow(10, -dec);
+  // norm is between 1.0 and 10.0
+  let norm = delta / magn;
+  let size;
+
+  if (norm < 1.5) {
+    size = 1;
+  } else if (norm < 3) {
+    size = 2;
+    // special case for 2.5, requires an extra decimal
+    if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
+      size = 2.5;
+      ++dec;
+    }
+  } else if (norm < 7.5) {
+    size = 5;
+  } else {
+    size = 10;
+  }
+
+  size *= magn;
+
+  tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
+  // grafana addition
+  const scaledDecimals = tickDecimals - Math.floor(Math.log(size) / Math.LN10);
+  return {tickDecimals, scaledDecimals};
+}

+ 1 - 1
public/app/features/panel/panel_directive.ts

@@ -20,7 +20,7 @@ var panelTemplate = `
     </div>
 
     <div class="panel-content">
-      <ng-transclude></ng-transclude>
+      <ng-transclude class="panel-height-helper"></ng-transclude>
     </div>
   </div>
 

+ 108 - 157
public/app/plugins/panel/graph/graph.ts

@@ -22,7 +22,7 @@ import {EventManager} from 'app/features/annotations/all';
 import {convertValuesToHistogram, getSeriesValues} from './histogram';
 
 /** @ngInject **/
-function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
+function graphDirective(timeSrv, popoverSrv, contextSrv) {
   return {
     restrict: 'A',
     template: '',
@@ -34,8 +34,6 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
       var data;
       var plot;
       var sortedSeries;
-      var legendSideLastValue = null;
-      var rootScope = scope.$root;
       var panelWidth = 0;
       var eventManager = new EventManager(ctrl);
       var thresholdManager = new ThresholdManager(ctrl);
@@ -53,17 +51,28 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
         }
       });
 
-      ctrl.events.on('render', function(renderData) {
+      /**
+       * Split graph rendering into two parts.
+       * First, calculate series stats in buildFlotPairs() function. Then legend rendering started
+       * (see ctrl.events.on('render') in legend.ts).
+       * When legend is rendered it emits 'legend-rendering-complete' and graph rendered.
+       */
+      ctrl.events.on('render', (renderData) => {
         data = renderData || data;
         if (!data) {
           return;
         }
         annotations = ctrl.annotations || [];
+        buildFlotPairs(data);
+        ctrl.events.emit('render-legend');
+      });
+
+      ctrl.events.on('legend-rendering-complete', () => {
         render_panel();
       });
 
       // global events
-      appEvents.on('graph-hover', function(evt) {
+      appEvents.on('graph-hover', (evt) => {
         // ignore other graph hover events if shared tooltip is disabled
         if (!dashboard.sharedTooltipModeEnabled()) {
           return;
@@ -77,47 +86,17 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
         tooltip.show(evt.pos);
       }, scope);
 
-      appEvents.on('graph-hover-clear', function(event, info) {
+      appEvents.on('graph-hover-clear', (event, info) => {
         if (plot) {
           tooltip.clear(plot);
         }
       }, scope);
 
-      function getLegendHeight(panelHeight) {
-        if (!panel.legend.show || panel.legend.rightSide) {
-          return 0;
-        }
-
-        if (panel.legend.alignAsTable) {
-          var legendSeries = _.filter(data, function(series) {
-            return series.hideFromLegend(panel.legend) === false;
-          });
-          var total = 23 + (21 * legendSeries.length);
-          return Math.min(total, Math.floor(panelHeight/2));
-        } else {
-          return 26;
-        }
-      }
-
-      function setElementHeight() {
-        try {
-          var height = ctrl.height - getLegendHeight(ctrl.height);
-          elem.css('height', height + 'px');
-
-          return true;
-        } catch (e) { // IE throws errors sometimes
-          console.log(e);
-          return false;
-        }
-      }
-
       function shouldAbortRender() {
         if (!data) {
           return true;
         }
 
-        if (!setElementHeight()) { return true; }
-
         if (panelWidth === 0) {
           return true;
         }
@@ -126,27 +105,6 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
       }
 
       function drawHook(plot) {
-        // Update legend values
-        var yaxis = plot.getYAxes();
-        for (var i = 0; i < data.length; i++) {
-          var series = data[i];
-          var axis = yaxis[series.yaxis - 1];
-          var formater = kbn.valueFormats[panel.yaxes[series.yaxis - 1].format];
-
-          // decimal override
-          if (_.isNumber(panel.decimals)) {
-            series.updateLegendValues(formater, panel.decimals, null);
-          } else {
-            // auto decimals
-            // legend and tooltip gets one more decimal precision
-            // than graph legend ticks
-            var tickDecimals = (axis.tickDecimals || -1) + 1;
-            series.updateLegendValues(formater, tickDecimals, axis.scaledDecimals + 2);
-          }
-
-          if (!rootScope.$$phase) { scope.$digest(); }
-        }
-
         // add left axis labels
         if (panel.yaxes[0].label && panel.yaxes[0].show) {
           $("<div class='axisLabel left-yaxis-label flot-temp-elem'></div>").text(panel.yaxes[0].label).appendTo(elem);
@@ -157,6 +115,10 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
           $("<div class='axisLabel right-yaxis-label flot-temp-elem'></div>").text(panel.yaxes[1].label).appendTo(elem);
         }
 
+        if (ctrl.dataWarning) {
+          $(`<div class="datapoints-warning flot-temp-elem">${ctrl.dataWarning.title}</div>`).appendTo(elem);
+        }
+
         thresholdManager.draw(plot);
       }
 
@@ -207,7 +169,6 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
       // Function for rendering panel
       function render_panel() {
         panelWidth =  elem.width();
-
         if (shouldAbortRender()) {
           return;
         }
@@ -218,67 +179,18 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
         // un-check dashes if lines are unchecked
         panel.dashes = panel.lines ? panel.dashes : false;
 
-        var stack = panel.stack ? true : null;
-
         // Populate element
-        var options: any = {
-          hooks: {
-            draw: [drawHook],
-            processOffset: [processOffsetHook],
-          },
-          legend: { show: false },
-          series: {
-            stackpercent: panel.stack ? panel.percentage : false,
-            stack: panel.percentage ? null : stack,
-            lines:  {
-              show: panel.lines,
-              zero: false,
-              fill: translateFillOption(panel.fill),
-              lineWidth: panel.dashes ? 0 : panel.linewidth,
-              steps: panel.steppedLine
-            },
-            dashes: {
-              show: panel.dashes,
-              lineWidth: panel.linewidth,
-              dashLength: [panel.dashLength, panel.spaceLength]
-            },
-            bars: {
-              show: panel.bars,
-              fill: 1,
-              barWidth: 1,
-              zero: false,
-              lineWidth: 0
-            },
-            points: {
-              show: panel.points,
-              fill: 1,
-              fillColor: false,
-              radius: panel.points ? panel.pointradius : 2
-            },
-            shadowSize: 0
-          },
-          yaxes: [],
-          xaxis: {},
-          grid: {
-            minBorderMargin: 0,
-            markings: [],
-            backgroundColor: null,
-            borderWidth: 0,
-            hoverable: true,
-            clickable: true,
-            color: '#c8c8c8',
-            margin: { left: 0, right: 0 },
-            labelMarginX: 0,
-          },
-          selection: {
-            mode: "x",
-            color: '#666'
-          },
-          crosshair: {
-            mode: 'x'
-          }
-        };
+        let options: any = buildFlotOptions(panel);
+        prepareXAxis(options, panel);
+        configureYAxisOptions(data, options);
+        thresholdManager.addFlotOptions(options, panel);
+        eventManager.addFlotEvents(annotations, options);
 
+        sortedSeries = sortSeries(data, panel);
+        callPlot(options, true);
+      }
+
+      function buildFlotPairs(data) {
         for (let i = 0; i < data.length; i++) {
           let series = data[i];
           series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode);
@@ -289,7 +201,9 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
             series.stack = false;
           }
         }
+      }
 
+      function prepareXAxis(options, panel) {
         switch (panel.xaxis.mode) {
           case 'series': {
             options.series.bars.barWidth = 0.7;
@@ -334,42 +248,89 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
             break;
           }
         }
+      }
 
-        thresholdManager.addFlotOptions(options, panel);
-        eventManager.addFlotEvents(annotations, options);
-        configureAxisOptions(data, options);
-
-        sortedSeries = sortSeries(data, ctrl.panel);
-
-        function callPlot(incrementRenderCounter) {
-          try {
-            plot = $.plot(elem, sortedSeries, options);
-            if (ctrl.renderError) {
-              delete ctrl.error;
-              delete ctrl.inspector;
-            }
-          } catch (e) {
-            console.log('flotcharts error', e);
-            ctrl.error = e.message || "Render Error";
-            ctrl.renderError = true;
-            ctrl.inspector = {error: e};
-          }
-
-          if (incrementRenderCounter) {
-            ctrl.renderingCompleted();
+      function callPlot(options, incrementRenderCounter) {
+        try {
+          plot = $.plot(elem, sortedSeries, options);
+          if (ctrl.renderError) {
+            delete ctrl.error;
+            delete ctrl.inspector;
           }
+        } catch (e) {
+          console.log('flotcharts error', e);
+          ctrl.error = e.message || "Render Error";
+          ctrl.renderError = true;
+          ctrl.inspector = {error: e};
         }
 
-        if (shouldDelayDraw(panel)) {
-          // temp fix for legends on the side, need to render twice to get dimensions right
-          callPlot(false);
-          setTimeout(function() { callPlot(true); }, 50);
-          legendSideLastValue = panel.legend.rightSide;
-        } else {
-          callPlot(true);
+        if (incrementRenderCounter) {
+          ctrl.renderingCompleted();
         }
       }
 
+      function buildFlotOptions(panel) {
+        const stack = panel.stack ? true : null;
+        let options = {
+          hooks: {
+            draw: [drawHook],
+            processOffset: [processOffsetHook],
+          },
+          legend: { show: false },
+          series: {
+            stackpercent: panel.stack ? panel.percentage : false,
+            stack: panel.percentage ? null : stack,
+            lines:  {
+              show: panel.lines,
+              zero: false,
+              fill: translateFillOption(panel.fill),
+              lineWidth: panel.dashes ? 0 : panel.linewidth,
+              steps: panel.steppedLine
+            },
+            dashes: {
+              show: panel.dashes,
+              lineWidth: panel.linewidth,
+              dashLength: [panel.dashLength, panel.spaceLength]
+            },
+            bars: {
+              show: panel.bars,
+              fill: 1,
+              barWidth: 1,
+              zero: false,
+              lineWidth: 0
+            },
+            points: {
+              show: panel.points,
+              fill: 1,
+              fillColor: false,
+              radius: panel.points ? panel.pointradius : 2
+            },
+            shadowSize: 0
+          },
+          yaxes: [],
+          xaxis: {},
+          grid: {
+            minBorderMargin: 0,
+            markings: [],
+            backgroundColor: null,
+            borderWidth: 0,
+            hoverable: true,
+            clickable: true,
+            color: '#c8c8c8',
+            margin: { left: 0, right: 0 },
+            labelMarginX: 0,
+          },
+          selection: {
+            mode: "x",
+            color: '#666'
+          },
+          crosshair: {
+            mode: 'x'
+          }
+        };
+        return options;
+      }
+
       function sortSeries(series, panel) {
         var sortBy = panel.legend.sort;
         var sortOrder = panel.legend.sortDesc;
@@ -410,16 +371,6 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
         }
       }
 
-      function shouldDelayDraw(panel) {
-        if (panel.legend.rightSide) {
-          return true;
-        }
-        if (legendSideLastValue !== null && panel.legend.rightSide !== legendSideLastValue) {
-          return true;
-        }
-        return false;
-      }
-
       function addTimeAxis(options) {
         var ticks = panelWidth / 100;
         var min = _.isUndefined(ctrl.range.from) ? null : ctrl.range.from.valueOf();
@@ -519,7 +470,7 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
         };
       }
 
-      function configureAxisOptions(data, options) {
+      function configureYAxisOptions(data, options) {
         var defaults = {
           position: 'left',
           show: panel.yaxes[0].show,

+ 0 - 215
public/app/plugins/panel/graph/legend.js

@@ -1,215 +0,0 @@
-define([
-  'angular',
-  'lodash',
-  'jquery',
-],
-function (angular, _, $) {
-  'use strict';
-
-  var module = angular.module('grafana.directives');
-
-  module.directive('graphLegend', function(popoverSrv, $timeout) {
-
-    return {
-      link: function(scope, elem) {
-        var $container = $('<section class="graph-legend"></section>');
-        var firstRender = true;
-        var ctrl = scope.ctrl;
-        var panel = ctrl.panel;
-        var data;
-        var seriesList;
-        var i;
-
-        ctrl.events.on('render', function() {
-          data = ctrl.seriesList;
-          if (data) {
-            render();
-          }
-        });
-
-        function getSeriesIndexForElement(el) {
-          return el.parents('[data-series-index]').data('series-index');
-        }
-
-        function openColorSelector(e) {
-          // if we clicked inside poup container ignore click
-          if ($(e.target).parents('.popover').length) {
-            return;
-          }
-
-          var el = $(e.currentTarget).find('.fa-minus');
-          var index = getSeriesIndexForElement(el);
-          var series = seriesList[index];
-
-          $timeout(function() {
-            popoverSrv.show({
-              element: el[0],
-              position: 'bottom center',
-              template: '<series-color-picker series="series" onToggleAxis="toggleAxis" onColorChange="colorSelected">' +
-                '</series-color-picker>',
-              openOn: 'hover',
-              model: {
-                series: series,
-                toggleAxis: function() {
-                  ctrl.toggleAxis(series);
-                },
-                colorSelected: function(color) {
-                  ctrl.changeSeriesColor(series, color);
-                }
-              },
-            });
-          });
-        }
-
-        function toggleSeries(e) {
-          var el = $(e.currentTarget);
-          var index = getSeriesIndexForElement(el);
-          var seriesInfo = seriesList[index];
-          var scrollPosition = $($container.children('tbody')).scrollTop();
-          ctrl.toggleSeries(seriesInfo, e);
-          $($container.children('tbody')).scrollTop(scrollPosition);
-        }
-
-        function sortLegend(e) {
-          var el = $(e.currentTarget);
-          var stat = el.data('stat');
-
-          if (stat !== panel.legend.sort) { panel.legend.sortDesc = null; }
-
-          // if already sort ascending, disable sorting
-          if (panel.legend.sortDesc === false) {
-            panel.legend.sort = null;
-            panel.legend.sortDesc = null;
-            ctrl.render();
-            return;
-          }
-
-          panel.legend.sortDesc = !panel.legend.sortDesc;
-          panel.legend.sort = stat;
-          ctrl.render();
-        }
-
-        function getTableHeaderHtml(statName) {
-          if (!panel.legend[statName]) { return ""; }
-          var html = '<th class="pointer" data-stat="' + statName + '">' + statName;
-
-          if (panel.legend.sort === statName) {
-            var cssClass = panel.legend.sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up' ;
-            html += ' <span class="' + cssClass + '"></span>';
-          }
-
-          return html + '</th>';
-        }
-
-        function render() {
-          if (!ctrl.panel.legend.show) {
-            elem.empty();
-            firstRender = true;
-            return;
-          }
-
-          if (firstRender) {
-            elem.append($container);
-            $container.on('click', '.graph-legend-icon', openColorSelector);
-            $container.on('click', '.graph-legend-alias', toggleSeries);
-            $container.on('click', 'th', sortLegend);
-            firstRender = false;
-          }
-
-          seriesList = data;
-
-          $container.empty();
-
-          // Set min-width if side style and there is a value, otherwise remove the CSS propery
-          var width = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + "px" : "";
-          $container.css("min-width", width);
-
-          $container.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
-
-          var tableHeaderElem;
-          if (panel.legend.alignAsTable) {
-            var header = '<tr>';
-            header += '<th colspan="2" style="text-align:left"></th>';
-            if (panel.legend.values) {
-              header += getTableHeaderHtml('min');
-              header += getTableHeaderHtml('max');
-              header += getTableHeaderHtml('avg');
-              header += getTableHeaderHtml('current');
-              header += getTableHeaderHtml('total');
-            }
-            header += '</tr>';
-            tableHeaderElem = $(header);
-          }
-
-          if (panel.legend.sort) {
-            seriesList = _.sortBy(seriesList, function(series) {
-              return series.stats[panel.legend.sort];
-            });
-            if (panel.legend.sortDesc) {
-              seriesList = seriesList.reverse();
-            }
-          }
-
-          var seriesShown = 0;
-          var seriesElements = [];
-
-          for (i = 0; i < seriesList.length; i++) {
-            var series = seriesList[i];
-
-            if (series.hideFromLegend(panel.legend)) {
-              continue;
-            }
-
-            var html = '<div class="graph-legend-series';
-
-            if (series.yaxis === 2) { html += ' graph-legend-series--right-y'; }
-            if (ctrl.hiddenSeries[series.alias]) { html += ' graph-legend-series-hidden'; }
-            html += '" data-series-index="' + i + '">';
-            html += '<div class="graph-legend-icon">';
-            html += '<i class="fa fa-minus pointer" style="color:' + series.color + '"></i>';
-            html += '</div>';
-
-            html += '<a class="graph-legend-alias pointer" title="' + series.aliasEscaped + '">' + series.aliasEscaped + '</a>';
-
-            if (panel.legend.values) {
-              var avg = series.formatValue(series.stats.avg);
-              var current = series.formatValue(series.stats.current);
-              var min = series.formatValue(series.stats.min);
-              var max = series.formatValue(series.stats.max);
-              var total = series.formatValue(series.stats.total);
-
-              if (panel.legend.min) { html += '<div class="graph-legend-value min">' + min + '</div>'; }
-              if (panel.legend.max) { html += '<div class="graph-legend-value max">' + max + '</div>'; }
-              if (panel.legend.avg) { html += '<div class="graph-legend-value avg">' + avg + '</div>'; }
-              if (panel.legend.current) { html += '<div class="graph-legend-value current">' + current + '</div>'; }
-              if (panel.legend.total) { html += '<div class="graph-legend-value total">' + total + '</div>'; }
-            }
-
-            html += '</div>';
-            seriesElements.push($(html));
-
-            seriesShown++;
-          }
-
-          if (panel.legend.alignAsTable) {
-            var maxHeight = ctrl.height;
-
-            if (!panel.legend.rightSide) {
-              maxHeight = maxHeight/2;
-            }
-
-            var topPadding = 6;
-            var tbodyElem = $('<tbody></tbody>');
-            tbodyElem.css("max-height", maxHeight - topPadding);
-            tbodyElem.append(tableHeaderElem);
-            tbodyElem.append(seriesElements);
-            $container.append(tbodyElem);
-          } else {
-            $container.append(seriesElements);
-          }
-        }
-      }
-    };
-  });
-
-});

+ 233 - 0
public/app/plugins/panel/graph/legend.ts

@@ -0,0 +1,233 @@
+import angular from 'angular';
+import _ from 'lodash';
+import $ from 'jquery';
+import PerfectScrollbar from 'perfect-scrollbar';
+import {updateLegendValues} from 'app/core/core';
+
+var module = angular.module('grafana.directives');
+
+module.directive('graphLegend', function(popoverSrv, $timeout) {
+
+  return {
+    link: function(scope, elem) {
+      var firstRender = true;
+      var ctrl = scope.ctrl;
+      var panel = ctrl.panel;
+      var data;
+      var seriesList;
+      var i;
+      var legendScrollbar;
+
+      scope.$on("$destroy", function() {
+        if (!legendScrollbar) {
+          legendScrollbar.destroy();
+        }
+      });
+
+      ctrl.events.on('render-legend', () => {
+        data = ctrl.seriesList;
+        if (data) {
+          render();
+        }
+        ctrl.events.emit('legend-rendering-complete');
+      });
+
+      function updateLegendDecimals() {
+        updateLegendValues(data, panel);
+      }
+
+      function getSeriesIndexForElement(el) {
+        return el.parents('[data-series-index]').data('series-index');
+      }
+
+      function openColorSelector(e) {
+        // if we clicked inside poup container ignore click
+        if ($(e.target).parents('.popover').length) {
+          return;
+        }
+
+        var el = $(e.currentTarget).find('.fa-minus');
+        var index = getSeriesIndexForElement(el);
+        var series = seriesList[index];
+
+        $timeout(function() {
+          popoverSrv.show({
+            element: el[0],
+            position: 'bottom center',
+            template: '<series-color-picker series="series" onToggleAxis="toggleAxis" onColorChange="colorSelected">' +
+              '</series-color-picker>',
+            openOn: 'hover',
+            model: {
+              series: series,
+              toggleAxis: function() {
+                ctrl.toggleAxis(series);
+              },
+              colorSelected: function(color) {
+                ctrl.changeSeriesColor(series, color);
+              }
+            },
+          });
+        });
+      }
+
+      function toggleSeries(e) {
+        var el = $(e.currentTarget);
+        var index = getSeriesIndexForElement(el);
+        var seriesInfo = seriesList[index];
+        var scrollPosition = $(elem.children('tbody')).scrollTop();
+        ctrl.toggleSeries(seriesInfo, e);
+        $(elem.children('tbody')).scrollTop(scrollPosition);
+      }
+
+      function sortLegend(e) {
+        var el = $(e.currentTarget);
+        var stat = el.data('stat');
+
+        if (stat !== panel.legend.sort) { panel.legend.sortDesc = null; }
+
+        // if already sort ascending, disable sorting
+        if (panel.legend.sortDesc === false) {
+          panel.legend.sort = null;
+          panel.legend.sortDesc = null;
+          ctrl.render();
+          return;
+        }
+
+        panel.legend.sortDesc = !panel.legend.sortDesc;
+        panel.legend.sort = stat;
+        ctrl.render();
+      }
+
+      function getTableHeaderHtml(statName) {
+        if (!panel.legend[statName]) { return ""; }
+        var html = '<th class="pointer" data-stat="' + statName + '">' + statName;
+
+        if (panel.legend.sort === statName) {
+          var cssClass = panel.legend.sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up' ;
+          html += ' <span class="' + cssClass + '"></span>';
+        }
+
+        return html + '</th>';
+      }
+
+      function render() {
+        if (!ctrl.panel.legend.show) {
+          elem.empty();
+          firstRender = true;
+          return;
+        }
+
+        if (firstRender) {
+          elem.on('click', '.graph-legend-icon', openColorSelector);
+          elem.on('click', '.graph-legend-alias', toggleSeries);
+          elem.on('click', 'th', sortLegend);
+          firstRender = false;
+        }
+
+        seriesList = data;
+
+        elem.empty();
+
+        // Set min-width if side style and there is a value, otherwise remove the CSS propery
+        var width = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + "px" : "";
+        elem.css("min-width", width);
+
+        elem.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
+
+        var tableHeaderElem;
+        if (panel.legend.alignAsTable) {
+          var header = '<tr>';
+          header += '<th colspan="2" style="text-align:left"></th>';
+          if (panel.legend.values) {
+            header += getTableHeaderHtml('min');
+            header += getTableHeaderHtml('max');
+            header += getTableHeaderHtml('avg');
+            header += getTableHeaderHtml('current');
+            header += getTableHeaderHtml('total');
+          }
+          header += '</tr>';
+          tableHeaderElem = $(header);
+        }
+
+        if (panel.legend.sort) {
+          seriesList = _.sortBy(seriesList, function(series) {
+            return series.stats[panel.legend.sort];
+          });
+          if (panel.legend.sortDesc) {
+            seriesList = seriesList.reverse();
+          }
+        }
+
+        // render first time for getting proper legend height
+        if (!panel.legend.rightSide) {
+          renderLegendElement(tableHeaderElem);
+          updateLegendDecimals();
+          elem.empty();
+        } else {
+          updateLegendDecimals();
+        }
+
+        renderLegendElement(tableHeaderElem);
+      }
+
+      function renderSeriesLegendElements() {
+        let seriesElements = [];
+        for (i = 0; i < seriesList.length; i++) {
+          var series = seriesList[i];
+
+          if (series.hideFromLegend(panel.legend)) {
+            continue;
+          }
+
+          var html = '<div class="graph-legend-series';
+
+          if (series.yaxis === 2) { html += ' graph-legend-series--right-y'; }
+          if (ctrl.hiddenSeries[series.alias]) { html += ' graph-legend-series-hidden'; }
+          html += '" data-series-index="' + i + '">';
+          html += '<div class="graph-legend-icon">';
+          html += '<i class="fa fa-minus pointer" style="color:' + series.color + '"></i>';
+          html += '</div>';
+
+          html += '<a class="graph-legend-alias pointer" title="' + series.aliasEscaped + '">' + series.aliasEscaped + '</a>';
+
+          if (panel.legend.values) {
+            var avg = series.formatValue(series.stats.avg);
+            var current = series.formatValue(series.stats.current);
+            var min = series.formatValue(series.stats.min);
+            var max = series.formatValue(series.stats.max);
+            var total = series.formatValue(series.stats.total);
+
+            if (panel.legend.min) { html += '<div class="graph-legend-value min">' + min + '</div>'; }
+            if (panel.legend.max) { html += '<div class="graph-legend-value max">' + max + '</div>'; }
+            if (panel.legend.avg) { html += '<div class="graph-legend-value avg">' + avg + '</div>'; }
+            if (panel.legend.current) { html += '<div class="graph-legend-value current">' + current + '</div>'; }
+            if (panel.legend.total) { html += '<div class="graph-legend-value total">' + total + '</div>'; }
+          }
+
+          html += '</div>';
+          seriesElements.push($(html));
+        }
+        return seriesElements;
+      }
+
+      function renderLegendElement(tableHeaderElem) {
+        var seriesElements = renderSeriesLegendElements();
+
+        if (panel.legend.alignAsTable) {
+          var tbodyElem = $('<tbody></tbody>');
+          tbodyElem.append(tableHeaderElem);
+          tbodyElem.append(seriesElements);
+          elem.append(tbodyElem);
+        } else {
+          elem.append(seriesElements);
+
+          if (!legendScrollbar) {
+            legendScrollbar = new PerfectScrollbar(elem[0]);
+          } else {
+            legendScrollbar.update();
+          }
+        }
+      }
+    }
+  };
+});

+ 2 - 0
public/app/plugins/panel/graph/specs/graph_specs.ts

@@ -87,6 +87,8 @@ describe('grafanaGraph', function() {
 
           $.plot = ctx.plotSpy = sinon.spy();
           ctrl.events.emit('render', ctx.data);
+          ctrl.events.emit('render-legend');
+          ctrl.events.emit('legend-rendering-complete');
           ctx.plotData = ctx.plotSpy.getCall(0).args[1];
           ctx.plotOptions = ctx.plotSpy.getCall(0).args[2];
         }));

+ 4 - 16
public/app/plugins/panel/graph/template.ts

@@ -1,22 +1,10 @@
 var template = `
-<div class="graph-wrapper" ng-class="{'graph-legend-rightside': ctrl.panel.legend.rightSide}">
-  <div class="graph-canvas-wrapper">
-
-    <div class="datapoints-warning" ng-if="ctrl.dataWarning">
-      <span class="small" bs-tooltip="ctrl.dataWarning.tip">{{ctrl.dataWarning.title}}</span>
-    </div>
-
-    <div grafana-graph class="histogram-chart" ng-dblclick="ctrl.zoomOut()">
-    </div>
-
+<div class="graph-panel" ng-class="{'graph-panel--legend-right': ctrl.panel.legend.rightSide}">
+  <div class="graph-panel__chart" grafana-graph ng-dblclick="ctrl.zoomOut()">
   </div>
 
-  <div class="graph-legend-wrapper" graph-legend></div>
-  </div>
-
-<div class="clearfix"></div>
+  <div class="graph-legend" graph-legend></div>
+</div>
 `;
 
 export default template;
-
-

+ 30 - 39
public/sass/components/_panel_graph.scss

@@ -1,10 +1,31 @@
-.graph-canvas-wrapper {
-  position: relative;
-  cursor: crosshair;
+.graph-panel {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+
+  &--legend-right {
+    flex-direction: row;
+
+    .graph-legend {
+      flex: 0 1 10px;
+      max-height: 100%;
+    }
+
+    .graph-legend-series {
+      display: block;
+      padding-left: 0px;
+    }
+
+    .graph-legend-table .graph-legend-series {
+      display: table-row;
+    }
+  }
 }
 
-.histogram-chart {
+.graph-panel__chart {
   position: relative;
+  cursor: crosshair;
+  flex-grow: 1;
 }
 
 .datapoints-warning {
@@ -22,11 +43,12 @@
 }
 
 .graph-legend {
-  @include clearfix();
+  flex: 0 1 auto;
+  max-height: 30%;
   margin: 0 $spacer;
   text-align: center;
-  width: calc(100% - $spacer);
   padding-top: 6px;
+  position: relative;
 
   .popover-content {
     padding: 0;
@@ -89,7 +111,9 @@
     display: block;
     overflow-y: auto;
     overflow-x: hidden;
+    height: 100%;
     padding-bottom: 1px;
+    padding-right: 5px;
   }
 
   .graph-legend-series {
@@ -160,39 +184,6 @@
   }
 }
 
-.graph-legend-rightside {
-
-  &.graph-wrapper {
-    display: table;
-    width: 100%;
-  }
-
-  .graph-canvas-wrapper {
-    display: table-cell;
-    width: 100%;
-    position: relative;
-  }
-
-  .graph-legend-wrapper {
-    display: table-cell;
-    vertical-align: top;
-    position: relative;
-    left: 4px;
-  }
-
-  .graph-legend {
-    margin: 0 0 0 1rem;
-  }
-
-  .graph-legend-series {
-    display: block;
-    padding-left: 0px;
-  }
-
-  .graph-legend-table .graph-legend-series {
-    display: table-row;
-  }
-}
 
 .graph-legend-series-hidden {
   .graph-legend-value,