Bladeren bron

Merge branch 'master' into panel_edit_menu_poc

Torkel Ödegaard 11 jaren geleden
bovenliggende
commit
022cbdda31

+ 2 - 0
CHANGELOG.md

@@ -1,4 +1,6 @@
 # 1.9.0 (unreleased)
 # 1.9.0 (unreleased)
+- [Issue #877](https://github.com/grafana/grafana/issues/877). Graph: Smart auto decimal precision when using scaled unit formats
+- [Issue #850](https://github.com/grafana/grafana/issues/850). Graph: Shared tooltip that shows multiple series & crosshair line, thx @toni-moreno
 
 
 # 1.8.1 (unreleased)
 # 1.8.1 (unreleased)
 
 

+ 2 - 2
latest.json

@@ -1,4 +1,4 @@
 {
 {
-	"version": "1.8.0",
-	"url": "http://grafanarel.s3.amazonaws.com/grafana-1.8.0.tar.gz"
+	"version": "1.8.1",
+	"url": "http://grafanarel.s3.amazonaws.com/grafana-1.8.1.tar.gz"
 }
 }

+ 1 - 1
package.json

@@ -4,7 +4,7 @@
     "company": "Coding Instinct AB"
     "company": "Coding Instinct AB"
   },
   },
   "name": "grafana",
   "name": "grafana",
-  "version": "1.8.0",
+  "version": "1.8.1",
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",
     "url": "http://github.com/torkelo/grafana.git"
     "url": "http://github.com/torkelo/grafana.git"

+ 50 - 273
src/app/components/kbn.js

@@ -7,6 +7,7 @@ function($, _, moment) {
   'use strict';
   'use strict';
 
 
   var kbn = {};
   var kbn = {};
+  kbn.valueFormats = {};
 
 
   kbn.round_interval = function(interval) {
   kbn.round_interval = function(interval) {
     switch (true) {
     switch (true) {
@@ -309,240 +310,27 @@ function($, _, moment) {
     ].join(';') + '"></div>';
     ].join(';') + '"></div>';
   };
   };
 
 
-  kbn.byteFormat = function(size, decimals) {
-    var ext, steps = 0;
-
-    if(_.isUndefined(decimals)) {
-      decimals = 2;
-    } else if (decimals === 0) {
-      decimals = undefined;
-    }
-
-    while (Math.abs(size) >= 1024) {
-      steps++;
-      size /= 1024;
-    }
-
-    switch (steps) {
-    case 0:
-      ext = " B";
-      break;
-    case 1:
-      ext = " KiB";
-      break;
-    case 2:
-      ext = " MiB";
-      break;
-    case 3:
-      ext = " GiB";
-      break;
-    case 4:
-      ext = " TiB";
-      break;
-    case 5:
-      ext = " PiB";
-      break;
-    case 6:
-      ext = " EiB";
-      break;
-    case 7:
-      ext = " ZiB";
-      break;
-    case 8:
-      ext = " YiB";
-      break;
-    }
-
-    return (size.toFixed(decimals) + ext);
+  kbn.valueFormats.percent = function(size, decimals) {
+    return kbn.toFixed(size, decimals) + '%';
   };
   };
 
 
-  kbn.bitFormat = function(size, decimals) {
-    var ext, steps = 0;
-
-    if(_.isUndefined(decimals)) {
-      decimals = 2;
-    } else if (decimals === 0) {
-      decimals = undefined;
-    }
-
-    while (Math.abs(size) >= 1024) {
-      steps++;
-      size /= 1024;
-    }
-
-    switch (steps) {
-    case 0:
-      ext = " b";
-      break;
-    case 1:
-      ext = " Kib";
-      break;
-    case 2:
-      ext = " Mib";
-      break;
-    case 3:
-      ext = " Gib";
-      break;
-    case 4:
-      ext = " Tib";
-      break;
-    case 5:
-      ext = " Pib";
-      break;
-    case 6:
-      ext = " Eib";
-      break;
-    case 7:
-      ext = " Zib";
-      break;
-    case 8:
-      ext = " Yib";
-      break;
-    }
-
-    return (size.toFixed(decimals) + ext);
-  };
-
-  kbn.bpsFormat = function(size, decimals) {
-    var ext, steps = 0;
-
-    if(_.isUndefined(decimals)) {
-      decimals = 2;
-    } else if (decimals === 0) {
-      decimals = undefined;
-    }
-
-    while (Math.abs(size) >= 1000) {
-      steps++;
-      size /= 1000;
-    }
-
-    switch (steps) {
-    case 0:
-      ext = " bps";
-      break;
-    case 1:
-      ext = " Kbps";
-      break;
-    case 2:
-      ext = " Mbps";
-      break;
-    case 3:
-      ext = " Gbps";
-      break;
-    case 4:
-      ext = " Tbps";
-      break;
-    case 5:
-      ext = " Pbps";
-      break;
-    case 6:
-      ext = " Ebps";
-      break;
-    case 7:
-      ext = " Zbps";
-      break;
-    case 8:
-      ext = " Ybps";
-      break;
-    }
-
-    return (size.toFixed(decimals) + ext);
-  };
+  kbn.formatFuncCreator = function(factor, extArray) {
+    return function(size, decimals, scaledDecimals) {
+      var steps = 0;
 
 
-  kbn.shortFormat = function(size, decimals) {
-    var ext, steps = 0;
-
-    if(_.isUndefined(decimals)) {
-      decimals = 2;
-    } else if (decimals === 0) {
-      decimals = undefined;
-    }
-
-    while (Math.abs(size) >= 1000) {
-      steps++;
-      size /= 1000;
-    }
-
-    switch (steps) {
-    case 0:
-      ext = "";
-      break;
-    case 1:
-      ext = " K";
-      break;
-    case 2:
-      ext = " Mil";
-      break;
-    case 3:
-      ext = " Bil";
-      break;
-    case 4:
-      ext = " Tri";
-      break;
-    case 5:
-      ext = " Quadr";
-      break;
-    case 6:
-      ext = " Quint";
-      break;
-    case 7:
-      ext = " Sext";
-      break;
-    case 8:
-      ext = " Sept";
-      break;
-    }
-
-    return (size.toFixed(decimals) + ext);
-  };
+      while (Math.abs(size) >= factor) {
+        steps++;
+        size /= factor;
+      }
+      if (steps > 0) {
+        decimals = scaledDecimals + (3 * steps);
+      }
 
 
-  kbn.getFormatFunction = function(formatName, decimals) {
-    switch(formatName) {
-    case 'short':
-      return function(val) {
-        return kbn.shortFormat(val, decimals);
-      };
-    case 'bytes':
-      return function(val) {
-        return kbn.byteFormat(val, decimals);
-      };
-    case 'bits':
-      return function(val) {
-        return kbn.bitFormat(val, decimals);
-      };
-    case 'bps':
-      return function(val) {
-        return kbn.bpsFormat(val, decimals);
-      };
-    case 's':
-      return function(val) {
-        return kbn.sFormat(val, decimals);
-      };
-    case 'ms':
-      return function(val) {
-        return kbn.msFormat(val, decimals);
-      };
-    case 'µs':
-      return function(val) {
-        return kbn.microsFormat(val, decimals);
-      };
-    case 'ns':
-      return function(val) {
-        return kbn.nanosFormat(val, decimals);
-      };
-    case 'percent':
-      return function(val, axis) {
-        return kbn.noneFormat(val, axis ? axis.tickDecimals : null) + ' %';
-      };
-    default:
-      return function(val, axis) {
-        return kbn.noneFormat(val, axis ? axis.tickDecimals : null);
-      };
-    }
+      return kbn.toFixed(size, decimals) + extArray[steps];
+    };
   };
   };
 
 
-  kbn.noneFormat = function(value, decimals) {
+  kbn.toFixed = function(value, decimals) {
     var factor = decimals ? Math.pow(10, decimals) : 1;
     var factor = decimals ? Math.pow(10, decimals) : 1;
     var formatted = String(Math.round(value * factor) / factor);
     var formatted = String(Math.round(value * factor) / factor);
 
 
@@ -553,7 +341,6 @@ function($, _, moment) {
 
 
     // If tickDecimals was specified, ensure that we have exactly that
     // If tickDecimals was specified, ensure that we have exactly that
     // much precision; otherwise default to the value's own precision.
     // much precision; otherwise default to the value's own precision.
-
     if (decimals != null) {
     if (decimals != null) {
       var decimalPos = formatted.indexOf(".");
       var decimalPos = formatted.indexOf(".");
       var precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1;
       var precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1;
@@ -565,97 +352,87 @@ function($, _, moment) {
     return formatted;
     return formatted;
   };
   };
 
 
-  kbn.msFormat = function(size, decimals) {
-    // Less than 1 milli, downscale to micro
-    if (size !== 0 && Math.abs(size) < 1) {
-      return kbn.microsFormat(size * 1000, decimals);
-    }
-    else if (Math.abs(size) < 1000) {
-      return size.toFixed(decimals) + " ms";
+  kbn.valueFormats.bits = kbn.formatFuncCreator(1024, [' b', ' Kib', ' Mib', ' Gib', ' Tib', ' Pib', ' Eib', ' Zib', ' Yib']);
+  kbn.valueFormats.bytes = kbn.formatFuncCreator(1024, [' B', ' KiB', ' MiB', ' GiB', ' TiB', ' PiB', ' EiB', ' ZiB', ' YiB']);
+  kbn.valueFormats.bps = kbn.formatFuncCreator(1000, [' bps', ' Kbps', ' Mbps', ' Gbps', ' Tbps', ' Pbps', ' Ebps', ' Zbps', ' Ybps']);
+  kbn.valueFormats.short = kbn.formatFuncCreator(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Qaudr', ' Quint', ' Sext', ' Sept']);
+  kbn.valueFormats.none = kbn.toFixed;
+
+  kbn.valueFormats.ms = function(size, decimals, scaledDecimals) {
+    if (Math.abs(size) < 1000) {
+      return kbn.toFixed(size, decimals) + " ms";
     }
     }
     // Less than 1 min
     // Less than 1 min
     else if (Math.abs(size) < 60000) {
     else if (Math.abs(size) < 60000) {
-      return (size / 1000).toFixed(decimals) + " s";
+      return kbn.toFixed(size / 1000, scaledDecimals + 3) + " s";
     }
     }
     // Less than 1 hour, devide in minutes
     // Less than 1 hour, devide in minutes
     else if (Math.abs(size) < 3600000) {
     else if (Math.abs(size) < 3600000) {
-      return (size / 60000).toFixed(decimals) + " min";
+      return kbn.toFixed(size / 60000, scaledDecimals + 5) + " min";
     }
     }
     // Less than one day, devide in hours
     // Less than one day, devide in hours
     else if (Math.abs(size) < 86400000) {
     else if (Math.abs(size) < 86400000) {
-      return (size / 3600000).toFixed(decimals) + " hour";
+      return kbn.toFixed(size / 3600000, scaledDecimals + 7) + " hour";
     }
     }
     // Less than one year, devide in days
     // Less than one year, devide in days
     else if (Math.abs(size) < 31536000000) {
     else if (Math.abs(size) < 31536000000) {
-      return (size / 86400000).toFixed(decimals) + " day";
+      return kbn.toFixed(size / 86400000, scaledDecimals + 8) + " day";
     }
     }
 
 
-    return (size / 31536000000).toFixed(decimals) + " year";
+    return kbn.toFixed(size / 31536000000, scaledDecimals + 10) + " year";
   };
   };
 
 
-  kbn.sFormat = function(size, decimals) {
-    // Less than 1 sec, downscale to milli
-    if (size !== 0 && Math.abs(size) < 1) {
-      return kbn.msFormat(size * 1000, decimals);
-    }
-    // Less than 10 min, use seconds
-    else if (Math.abs(size) < 600) {
-      return size.toFixed(decimals) + " s";
+  kbn.valueFormats.s = function(size, decimals, scaledDecimals) {
+    if (Math.abs(size) < 600) {
+      return kbn.toFixed(size, decimals) + " s";
     }
     }
     // Less than 1 hour, devide in minutes
     // Less than 1 hour, devide in minutes
     else if (Math.abs(size) < 3600) {
     else if (Math.abs(size) < 3600) {
-      return (size / 60).toFixed(decimals) + " min";
+      return kbn.toFixed(size / 60, scaledDecimals + 1) + " min";
     }
     }
     // Less than one day, devide in hours
     // Less than one day, devide in hours
     else if (Math.abs(size) < 86400) {
     else if (Math.abs(size) < 86400) {
-      return (size / 3600).toFixed(decimals) + " hour";
+      return kbn.toFixed(size / 3600, scaledDecimals + 4) + " hour";
     }
     }
     // Less than one week, devide in days
     // Less than one week, devide in days
     else if (Math.abs(size) < 604800) {
     else if (Math.abs(size) < 604800) {
-      return (size / 86400).toFixed(decimals) + " day";
+      return kbn.toFixed(size / 86400, scaledDecimals + 5) + " day";
     }
     }
     // Less than one year, devide in week
     // Less than one year, devide in week
     else if (Math.abs(size) < 31536000) {
     else if (Math.abs(size) < 31536000) {
-      return (size / 604800).toFixed(decimals) + " week";
+      return kbn.toFixed(size / 604800, scaledDecimals + 6) + " week";
     }
     }
 
 
-    return (size / 3.15569e7).toFixed(decimals) + " year";
+    return kbn.toFixed(size / 3.15569e7, scaledDecimals + 7) + " year";
   };
   };
 
 
-  kbn.microsFormat = function(size, decimals) {
-    // Less than 1 micro, downscale to nano
-    if (size !== 0 && Math.abs(size) < 1) {
-      return kbn.nanosFormat(size * 1000, decimals);
-    }
-    else if (Math.abs(size) < 1000) {
-      return size.toFixed(decimals) + " µs";
+  kbn.valueFormats['µs'] = function(size, decimals, scaledDecimals) {
+    if (Math.abs(size) < 1000) {
+      return kbn.toFixed(size, decimals) + " µs";
     }
     }
     else if (Math.abs(size) < 1000000) {
     else if (Math.abs(size) < 1000000) {
-      return (size / 1000).toFixed(decimals) + " ms";
+      return kbn.toFixed(size / 1000, scaledDecimals + 3) + " ms";
     }
     }
     else {
     else {
-      return (size / 1000000).toFixed(decimals) + " s";
+      return kbn.toFixed(size / 1000000, scaledDecimals + 6) + " s";
     }
     }
   };
   };
 
 
-  kbn.nanosFormat = function(size, decimals) {
-    if (Math.abs(size) < 1) {
-      return size.toFixed(decimals) + " ns";
-    }
-    else if (Math.abs(size) < 1000) {
-      return size.toFixed(0) + " ns";
+  kbn.valueFormats.ns = function(size, decimals, scaledDecimals) {
+    if (Math.abs(size) < 1000) {
+      return kbn.toFixed(size, decimals) + " ns";
     }
     }
     else if (Math.abs(size) < 1000000) {
     else if (Math.abs(size) < 1000000) {
-      return (size / 1000).toFixed(decimals) + " µs";
+      return kbn.toFixed(size / 1000, scaledDecimals + 3) + " µs";
     }
     }
     else if (Math.abs(size) < 1000000000) {
     else if (Math.abs(size) < 1000000000) {
-      return (size / 1000000).toFixed(decimals) + " ms";
+      return kbn.toFixed(size / 1000000, scaledDecimals + 6) + " ms";
     }
     }
     else if (Math.abs(size) < 60000000000){
     else if (Math.abs(size) < 60000000000){
-      return (size / 1000000000).toFixed(decimals) + " s";
+      return kbn.toFixed(size / 1000000000, scaledDecimals + 9) + " s";
     }
     }
     else {
     else {
-      return (size / 60000000000).toFixed(decimals) + " m";
+      return kbn.toFixed(size / 60000000000, scaledDecimals + 12) + " m";
     }
     }
   };
   };
 
 

+ 2 - 0
src/app/components/require.config.js

@@ -40,6 +40,7 @@ require.config({
     'jquery.flot.stack':      '../vendor/jquery/jquery.flot.stack',
     'jquery.flot.stack':      '../vendor/jquery/jquery.flot.stack',
     'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
     'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
     'jquery.flot.time':       '../vendor/jquery/jquery.flot.time',
     'jquery.flot.time':       '../vendor/jquery/jquery.flot.time',
+    'jquery.flot.crosshair':  '../vendor/jquery/jquery.flot.crosshair',
 
 
     modernizr:                '../vendor/modernizr-2.6.1',
     modernizr:                '../vendor/modernizr-2.6.1',
 
 
@@ -83,6 +84,7 @@ require.config({
     'jquery.flot.stack':    ['jquery', 'jquery.flot'],
     'jquery.flot.stack':    ['jquery', 'jquery.flot'],
     'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
     'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
     'jquery.flot.time':     ['jquery', 'jquery.flot'],
     'jquery.flot.time':     ['jquery', 'jquery.flot'],
+    'jquery.flot.crosshair':['jquery', 'jquery.flot'],
     'angular-cookies':      ['angular'],
     'angular-cookies':      ['angular'],
     'angular-dragdrop':     ['jquery','jquery-ui','angular'],
     'angular-dragdrop':     ['jquery','jquery-ui','angular'],
     'angular-loader':       ['angular'],
     'angular-loader':       ['angular'],

+ 9 - 9
src/app/components/timeSeries.js

@@ -54,7 +54,7 @@ function (_, kbn) {
     }
     }
   };
   };
 
 
-  TimeSeries.prototype.getFlotPairs = function (fillStyle, yFormats) {
+  TimeSeries.prototype.getFlotPairs = function (fillStyle) {
     var result = [];
     var result = [];
 
 
     this.color = this.info.color;
     this.color = this.info.color;
@@ -100,21 +100,21 @@ function (_, kbn) {
     }
     }
 
 
     if (result.length) {
     if (result.length) {
-
       this.info.avg = (this.info.total / result.length);
       this.info.avg = (this.info.total / result.length);
       this.info.current = result[result.length-1][1];
       this.info.current = result[result.length-1][1];
-
-      var formater = kbn.getFormatFunction(yFormats[this.yaxis - 1], 2);
-      this.info.avg = this.info.avg != null ? formater(this.info.avg) : null;
-      this.info.current = this.info.current != null ? formater(this.info.current) : null;
-      this.info.min = this.info.min != null ? formater(this.info.min) : null;
-      this.info.max = this.info.max != null ? formater(this.info.max) : null;
-      this.info.total = this.info.total != null ? formater(this.info.total) : null;
     }
     }
 
 
     return result;
     return result;
   };
   };
 
 
+  TimeSeries.prototype.updateLegendValues = function(formater, decimals, scaledDecimals) {
+    this.info.avg = this.info.avg != null ? formater(this.info.avg, decimals, scaledDecimals) : null;
+    this.info.current = this.info.current != null ? formater(this.info.current, decimals, scaledDecimals) : null;
+    this.info.min = this.info.min != null ? formater(this.info.min, decimals, scaledDecimals) : null;
+    this.info.max = this.info.max != null ? formater(this.info.max, decimals, scaledDecimals) : null;
+    this.info.total = this.info.total != null ? formater(this.info.total, decimals, scaledDecimals) : null;
+  };
+
   return TimeSeries;
   return TimeSeries;
 
 
 });
 });

+ 27 - 39
src/app/directives/grafanaGraph.js

@@ -3,9 +3,10 @@ define([
   'jquery',
   'jquery',
   'kbn',
   'kbn',
   'moment',
   'moment',
-  'lodash'
+  'lodash',
+  './grafanaGraph.tooltip'
 ],
 ],
-function (angular, $, kbn, moment, _) {
+function (angular, $, kbn, moment, _, graphTooltip) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('grafana.directives');
   var module = angular.module('grafana.directives');
@@ -15,8 +16,8 @@ function (angular, $, kbn, moment, _) {
       restrict: 'A',
       restrict: 'A',
       template: '<div> </div>',
       template: '<div> </div>',
       link: function(scope, elem) {
       link: function(scope, elem) {
-        var data, annotations;
         var dashboard = scope.dashboard;
         var dashboard = scope.dashboard;
+        var data, annotations;
         var legendSideLastValue = null;
         var legendSideLastValue = null;
 
 
         scope.$on('refresh',function() {
         scope.$on('refresh',function() {
@@ -80,6 +81,18 @@ function (angular, $, kbn, moment, _) {
           }
           }
         }
         }
 
 
+        function updateLegendValues(plot) {
+          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[scope.panel.y_formats[series.yaxis - 1]];
+            series.updateLegendValues(formater, axis.tickDecimals, axis.scaledDecimals);
+          }
+
+        }
+
         // Function for rendering panel
         // Function for rendering panel
         function render_panel() {
         function render_panel() {
           if (shouldAbortRender()) {
           if (shouldAbortRender()) {
@@ -91,6 +104,7 @@ function (angular, $, kbn, moment, _) {
 
 
           // Populate element
           // Populate element
           var options = {
           var options = {
+            hooks: { draw: [updateLegendValues] },
             legend: { show: false },
             legend: { show: false },
             series: {
             series: {
               stackpercent: panel.stack ? panel.percentage : false,
               stackpercent: panel.stack ? panel.percentage : false,
@@ -113,7 +127,8 @@ function (angular, $, kbn, moment, _) {
                 show: panel.points,
                 show: panel.points,
                 fill: 1,
                 fill: 1,
                 fillColor: false,
                 fillColor: false,
-                radius: panel.pointradius
+                radius: panel.points ? panel.pointradius : 2
+                // little points when highlight points
               },
               },
               shadowSize: 1
               shadowSize: 1
             },
             },
@@ -130,6 +145,9 @@ function (angular, $, kbn, moment, _) {
             selection: {
             selection: {
               mode: "x",
               mode: "x",
               color: '#666'
               color: '#666'
+            },
+            crosshair: {
+              mode: panel.tooltip.shared ? "x" : null
             }
             }
           };
           };
 
 
@@ -299,7 +317,9 @@ function (angular, $, kbn, moment, _) {
         }
         }
 
 
         function configureAxisMode(axis, format) {
         function configureAxisMode(axis, format) {
-          axis.tickFormatter = kbn.getFormatFunction(format, 1);
+          axis.tickFormatter = function(val, axis) {
+            return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
+          };
         }
         }
 
 
         function time_format(interval, ticks, min, max) {
         function time_format(interval, ticks, min, max) {
@@ -324,40 +344,6 @@ function (angular, $, kbn, moment, _) {
           return "%H:%M";
           return "%H:%M";
         }
         }
 
 
-        var $tooltip = $('<div id="tooltip">');
-
-        elem.bind("plothover", function (event, pos, item) {
-          var group, value, timestamp, seriesInfo, format;
-
-          if (item) {
-            seriesInfo = item.series.info;
-            format = scope.panel.y_formats[seriesInfo.yaxis - 1];
-
-            if (seriesInfo.alias) {
-              group = '<small style="font-size:0.9em;">' +
-                '<i class="icon-circle" style="color:'+item.series.color+';"></i>' + ' ' +
-                seriesInfo.alias +
-              '</small><br>';
-            } else {
-              group = kbn.query_color_dot(item.series.color, 15) + ' ';
-            }
-
-            if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') {
-              value = item.datapoint[1] - item.datapoint[2];
-            }
-            else {
-              value = item.datapoint[1];
-            }
-
-            value = kbn.getFormatFunction(format, 2)(value, item.series.yaxis);
-            timestamp = dashboard.formatDate(item.datapoint[0]);
-
-            $tooltip.html(group + value + " @ " + timestamp).place_tt(pos.pageX, pos.pageY);
-          } else {
-            $tooltip.detach();
-          }
-        });
-
         function render_panel_as_graphite_png(url) {
         function render_panel_as_graphite_png(url) {
           url += '&width=' + elem.width();
           url += '&width=' + elem.width();
           url += '&height=' + elem.css('height').replace('px', '');
           url += '&height=' + elem.css('height').replace('px', '');
@@ -408,6 +394,8 @@ function (angular, $, kbn, moment, _) {
           elem.html('<img src="' + url + '"></img>');
           elem.html('<img src="' + url + '"></img>');
         }
         }
 
 
+        graphTooltip.register(elem, dashboard, scope);
+
         elem.bind("plotselected", function (event, ranges) {
         elem.bind("plotselected", function (event, ranges) {
           scope.$apply(function() {
           scope.$apply(function() {
             timeSrv.setTime({
             timeSrv.setTime({

+ 122 - 0
src/app/directives/grafanaGraph.tooltip.js

@@ -0,0 +1,122 @@
+define([
+  'jquery',
+  'kbn',
+],
+function ($, kbn) {
+  'use strict';
+
+  function registerTooltipFeatures(elem, dashboard, scope) {
+
+    var $tooltip = $('<div id="tooltip">');
+
+    elem.mouseleave(function () {
+      if(scope.panel.tooltip.shared) {
+        var plot = elem.data().plot;
+        $tooltip.detach();
+        plot.clearCrosshair();
+        plot.unhighlight();
+      }
+    });
+
+    function findHoverIndex(posX, series) {
+      for (var j = 0; j < series.data.length; j++) {
+        if (series.data[j][0] > posX) {
+          return Math.max(j - 1,  0);
+        }
+      }
+      return j - 1;
+    }
+
+    elem.bind("plothover", function (event, pos, item) {
+      var plot = elem.data().plot;
+      var data = plot.getData();
+      var group, value, timestamp, seriesInfo, format, i, series, hoverIndex, seriesHtml;
+
+      if (scope.panel.tooltip.shared) {
+        plot.unhighlight();
+
+        //check if all series has same length if so, only one x index will
+        //be checked and only for exact timestamp values
+        var pointCount = data[0].data.length;
+        for (i = 1; i < data.length; i++) {
+          if (data[i].data.length !== pointCount) {
+            console.log('WARNING: tootltip shared can not be shown becouse of series points do not align, different point counts');
+            $tooltip.detach();
+            return;
+          }
+        }
+
+        seriesHtml = '';
+        series = data[0];
+        hoverIndex = findHoverIndex(pos.x, series);
+
+        //now we know the current X (j) position for X and Y values
+        timestamp = dashboard.formatDate(series.data[hoverIndex][0]);
+        var last_value = 0; //needed for stacked values
+
+        for (i = data.length-1; i >= 0; --i) {
+          //stacked values should be added in reverse order
+          series = data[i];
+          seriesInfo = series.info;
+          format = scope.panel.y_formats[seriesInfo.yaxis - 1];
+
+          if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') {
+            value = series.data[hoverIndex][1];
+          } else {
+            last_value += series.data[hoverIndex][1];
+            value = last_value;
+          }
+
+          value = kbn.valueFormats[format](value, series.yaxis.tickDecimals);
+
+          if (seriesInfo.alias) {
+            group = '<i class="icon-minus" style="color:' + series.color +';"></i> ' + seriesInfo.alias;
+          } else {
+            group = kbn.query_color_dot(series.color, 15) + ' ';
+          }
+
+          //pre-pending new values
+          seriesHtml = group + ': <span class="graph-tooltip-value">' + value + '</span><br>' + seriesHtml;
+
+          plot.highlight(i, hoverIndex);
+        }
+
+        $tooltip.html('<div class="graph-tooltip small"><div class="graph-tooltip-time">'+ timestamp + '</div> ' + seriesHtml + '</div>')
+          .place_tt(pos.pageX + 20, pos.pageY);
+        return;
+      }
+      if (item) {
+        seriesInfo = item.series.info;
+        format = scope.panel.y_formats[seriesInfo.yaxis - 1];
+
+        if (seriesInfo.alias) {
+          group = '<small style="font-size:0.9em;">' +
+            '<i class="icon-circle" style="color:'+item.series.color+';"></i>' + ' ' +
+            seriesInfo.alias +
+            '</small><br>';
+        } else {
+          group = kbn.query_color_dot(item.series.color, 15) + ' ';
+        }
+
+        if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') {
+          value = item.datapoint[1] - item.datapoint[2];
+        }
+        else {
+          value = item.datapoint[1];
+        }
+
+        value = kbn.valueFormats[format](value, item.series.yaxis.tickDecimals);
+        timestamp = dashboard.formatDate(item.datapoint[0]);
+
+        $tooltip.html(group + value + " @ " + timestamp).place_tt(pos.pageX, pos.pageY);
+      } else {
+        $tooltip.detach();
+      }
+    });
+
+  }
+
+  return {
+    register: registerTooltipFeatures
+  };
+});

+ 3 - 2
src/app/panels/graph/module.js

@@ -15,7 +15,8 @@ define([
   'jquery.flot.selection',
   'jquery.flot.selection',
   'jquery.flot.time',
   'jquery.flot.time',
   'jquery.flot.stack',
   'jquery.flot.stack',
-  'jquery.flot.stackpercent'
+  'jquery.flot.stackpercent',
+  'jquery.flot.crosshair'
 ],
 ],
 function (angular, app, $, _, kbn, moment, TimeSeries) {
 function (angular, app, $, _, kbn, moment, TimeSeries) {
   'use strict';
   'use strict';
@@ -160,7 +161,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
 
 
       tooltip       : {
       tooltip       : {
         value_type: 'cumulative',
         value_type: 'cumulative',
-        query_as_alias: true
+        shared: false,
       },
       },
 
 
       targets: [{}],
       targets: [{}],

+ 8 - 0
src/app/panels/graph/styleEditor.html

@@ -51,8 +51,16 @@
       <input type="radio" class="input-small" ng-model="panel.renderer" value="png" ng-change="get_data()" />
       <input type="radio" class="input-small" ng-model="panel.renderer" value="png" ng-change="get_data()" />
     </div>
     </div>
   </div>
   </div>
+
+  <div class="section">
+    <h5>Tooltip</h5>
+    <div class="editor-option">
+      <label class="small">shared <tip> Show all series values on the same time in the same tooltip and a x croshair to help follow all series</tip> </label><input type="checkbox" ng-model="panel.tooltip.shared" ng-checked="panel.tooltip.shared" ng-change="render()">
+    </div>
+  </div>
 </div>
 </div>
 
 
+
 <div class="editor-row">
 <div class="editor-row">
   <div class="section">
   <div class="section">
 		<h5>Series specific overrides <tip>Regex match example: /server[0-3]/i </tip></h5>
 		<h5>Series specific overrides <tip>Regex match example: /server[0-3]/i </tip></h5>

+ 2 - 2
src/app/services/influxdb/influxdbDatasource.js

@@ -215,9 +215,9 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
       if (id === title) { return; }
       if (id === title) { return; }
 
 
       var self = this;
       var self = this;
-      self._getDashboardInternal(id, isTemp).then(function(dashboard) {
+      self._getDashboardInternal(title, isTemp).then(function(dashboard) {
         if (dashboard !== null) {
         if (dashboard !== null) {
-          self.deleteDashboard(id);
+          self.deleteDashboard(title);
         }
         }
       });
       });
     };
     };

+ 1 - 1
src/config.sample.js

@@ -87,7 +87,7 @@ function (Settings) {
     // Example: "1m", "1h"
     // Example: "1m", "1h"
     playlist_timespan: "1m",
     playlist_timespan: "1m",
 
 
-    // If you want to specify password before saving, please specify it bellow
+    // If you want to specify password before saving, please specify it below
     // The purpose of this password is not security, but to stop some users from accidentally changing dashboards
     // The purpose of this password is not security, but to stop some users from accidentally changing dashboards
     admin: {
     admin: {
       password: ''
       password: ''

+ 12 - 0
src/css/less/grafana.less

@@ -444,6 +444,18 @@ select.grafana-target-segment-input {
   max-width: 800px;
   max-width: 800px;
   max-height: 600px;
   max-height: 600px;
   overflow: hidden;
   overflow: hidden;
+  line-height: 14px;
+}
+
+
+
+.grafana-tooltip hr {
+ padding: 2px;
+ color: #c8c8c8;
+ margin: 0px;
+ border-bottom:0px solid #c8c8c8;
+ /*height:0px;
+ background-color: rgb(58, 57, 57);*/
 }
 }
 
 
 .tooltip.in {
 .tooltip.in {

+ 16 - 0
src/css/less/graph.less

@@ -166,3 +166,19 @@
     float: left;
     float: left;
   }
   }
 }
 }
+
+.graph-tooltip {
+  .graph-tooltip-time {
+    text-align: center;
+    font-weight: bold;
+    position: relative;
+    top: -3px;
+  }
+
+  .graph-tooltip-value {
+    font-weight: bold;
+    float: right;
+    padding-left: 10px;
+  }
+
+}

+ 4 - 1
src/test/specs/grafanaGraph-specs.js

@@ -27,7 +27,10 @@ define([
               legend: {},
               legend: {},
               grid: {},
               grid: {},
               y_formats: [],
               y_formats: [],
-              seriesOverrides: []
+              seriesOverrides: [],
+	      tooltip: {
+                shared: true
+              }
             };
             };
             scope.hiddenSeries = {};
             scope.hiddenSeries = {};
             scope.dashboard = { timezone: 'browser' };
             scope.dashboard = { timezone: 'browser' };

+ 56 - 0
src/test/specs/graph-tooltip-specs.js

@@ -0,0 +1,56 @@
+define([
+  'jquery',
+  'directives/grafanaGraph.tooltip'
+], function($, tooltip) {
+  'use strict';
+
+  describe('graph tooltip', function() {
+    var elem = $('<div></div>');
+    var dashboard = {
+      formatDate: sinon.stub().returns('date'),
+    };
+    var scope =  {
+      panel: {
+        tooltip:  {
+          shared: true
+        },
+        y_formats: ['ms', 'none'],
+      }
+    };
+
+    var data = [
+      {
+        data: [[10,10], [12,20]],
+        info: { yaxis: 1 },
+        yaxis: { tickDecimals: 2 },
+      },
+      {
+        data: [[10,10], [12,20]],
+        info: { yaxis: 1 },
+        yaxis: { tickDecimals: 2 },
+      }
+    ];
+
+    var plot = {
+      getData: sinon.stub().returns(data),
+      highlight: sinon.stub(),
+      unhighlight: sinon.stub()
+    };
+
+    elem.data('plot', plot);
+
+    beforeEach(function() {
+      tooltip.register(elem, dashboard, scope);
+      elem.trigger('plothover', [{}, {x: 13}, {}]);
+    });
+
+    it('should add tooltip', function() {
+      var tooltipHtml = $(".graph-tooltip").text();
+      expect(tooltipHtml).to.be('date  : 40.00 ms : 20.00 ms');
+    });
+
+  });
+
+});
+
+

+ 19 - 64
src/test/specs/kbn-format-specs.js

@@ -3,76 +3,31 @@ define([
 ], function(kbn) {
 ], function(kbn) {
   'use strict';
   'use strict';
 
 
-  describe('millisecond formating', function() {
+  function describeValueFormat(desc, value, tickSize, tickDecimals, result) {
 
 
-    it('should translate 4378634603 as 1.67 years', function() {
-      var str = kbn.msFormat(4378634603, 2);
-      expect(str).to.be('50.68 day');
+    describe('value format: ' + desc, function() {
+      it('should translate ' + value + ' as ' + result, function() {
+        var scaledDecimals = tickDecimals - Math.floor(Math.log(tickSize) / Math.LN10);
+        var str = kbn.valueFormats[desc](value, tickDecimals, scaledDecimals);
+        expect(str).to.be(result);
+      });
     });
     });
 
 
-    it('should translate 3654454 as 1.02 hour', function() {
-      var str = kbn.msFormat(3654454, 2);
-      expect(str).to.be('1.02 hour');
-    });
-
-    it('should not downscale when value is zero', function() {
-      var str = kbn.msFormat(0, 2);
-      expect(str).to.be('0.00 ms');
-    });
-
-
-    it('should translate 365445 as 6.09 min', function() {
-      var str = kbn.msFormat(365445, 2);
-      expect(str).to.be('6.09 min');
-    });
-
-  });
-
-  describe('high negative exponent, issue #696', function() {
-    it('should ignore decimal correction if exponent', function() {
-      var str = kbn.getFormatFunction('')(2.75e-10, { tickDecimals: 12 });
-      expect(str).to.be('2.75e-10');
-    });
-    it('should format 0 correctly', function() {
-      var str = kbn.getFormatFunction('')(0.0, { tickDecimals: 12 });
-      expect(str).to.be('0');
-    });
-  });
+  }
 
 
-  describe('none format tests', function() {
-    it('should translate 2 as 2.0000 if axis decimals is 4', function() {
-      var str = kbn.getFormatFunction('')(2, { tickDecimals: 4 });
-      expect(str).to.be('2.0000');
-    });
-  });
-
-  describe('nanosecond formatting', function () {
-    it('should translate 25 to 25 ns', function () {
-      var str = kbn.nanosFormat(25, 2);
-      expect(str).to.be("25 ns");
-    });
-
-    it('should translate 2558 to 2.56 µs', function () {
-      var str = kbn.nanosFormat(2558, 2);
-      expect(str).to.be("2.56 µs");
-    });
+  describeValueFormat('ms', 0.0024, 0.0005, 4, '0.0024 ms');
+  describeValueFormat('ms', 100, 1, 0, '100 ms');
+  describeValueFormat('ms', 1250, 10, 0, '1.25 s');
+  describeValueFormat('ms', 1250, 300, 0, '1.3 s');
+  describeValueFormat('ms', 65150, 10000, 0, '1.1 min');
+  describeValueFormat('ms', 6515000, 1500000, 0, '1.8 hour');
+  describeValueFormat('ms', 651500000, 150000000, 0, '8 day');
 
 
-    it('should translate 2558000 to 2.56 ms', function () {
-      var str = kbn.nanosFormat(2558000, 2);
-      expect(str).to.be("2.56 ms");
-    });
-
-    it('should translate 2019962000 to 2.02 s', function () {
-      var str = kbn.nanosFormat(2049962000, 2);
-      expect(str).to.be("2.05 s");
-    });
+  describeValueFormat('none', 2.75e-10, 0, 10, '3e-10');
+  describeValueFormat('none', 0, 0, 2, '0');
 
 
-    it('should translate 95199620000 to 1.59 m', function () {
-      var str = kbn.nanosFormat(95199620000, 2);
-      expect(str).to.be("1.59 m");
-    });
-
-  });
+  describeValueFormat('ns', 25, 1, 0, '25 ns');
+  describeValueFormat('ns', 2558, 50, 0, '2.56 µs');
 
 
   describe('calculateInterval', function() {
   describe('calculateInterval', function() {
     it('1h 100 resultion', function() {
     it('1h 100 resultion', function() {

+ 3 - 0
src/test/test-main.js

@@ -43,6 +43,7 @@ require.config({
     'jquery.flot.stack':      '../vendor/jquery/jquery.flot.stack',
     'jquery.flot.stack':      '../vendor/jquery/jquery.flot.stack',
     'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
     'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
     'jquery.flot.time':       '../vendor/jquery/jquery.flot.time',
     'jquery.flot.time':       '../vendor/jquery/jquery.flot.time',
+    'jquery.flot.crosshair':  '../vendor/jquery/jquery.flot.crosshair',
 
 
     modernizr:                '../vendor/modernizr-2.6.1',
     modernizr:                '../vendor/modernizr-2.6.1',
   },
   },
@@ -77,6 +78,7 @@ require.config({
     'jquery.flot.stack':    ['jquery', 'jquery.flot'],
     'jquery.flot.stack':    ['jquery', 'jquery.flot'],
     'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
     'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
     'jquery.flot.time':     ['jquery', 'jquery.flot'],
     'jquery.flot.time':     ['jquery', 'jquery.flot'],
+    'jquery.flot.crosshair':['jquery', 'jquery.flot'],
 
 
     'angular-route':        ['angular'],
     'angular-route':        ['angular'],
     'angular-cookies':      ['angular'],
     'angular-cookies':      ['angular'],
@@ -127,6 +129,7 @@ require([
     'specs/influxdb-datasource-specs',
     'specs/influxdb-datasource-specs',
     'specs/graph-ctrl-specs',
     'specs/graph-ctrl-specs',
     'specs/grafanaGraph-specs',
     'specs/grafanaGraph-specs',
+    'specs/graph-tooltip-specs',
     'specs/seriesOverridesCtrl-specs',
     'specs/seriesOverridesCtrl-specs',
     'specs/timeSrv-specs',
     'specs/timeSrv-specs',
     'specs/templateSrv-specs',
     'specs/templateSrv-specs',

+ 176 - 0
src/vendor/jquery/jquery.flot.crosshair.js

@@ -0,0 +1,176 @@
+/* Flot plugin for showing crosshairs when the mouse hovers over the plot.
+
+Copyright (c) 2007-2014 IOLA and Ole Laursen.
+Licensed under the MIT license.
+
+The plugin supports these options:
+
+	crosshair: {
+		mode: null or "x" or "y" or "xy"
+		color: color
+		lineWidth: number
+	}
+
+Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical
+crosshair that lets you trace the values on the x axis, "y" enables a
+horizontal crosshair and "xy" enables them both. "color" is the color of the
+crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of
+the drawn lines (default is 1).
+
+The plugin also adds four public methods:
+
+  - setCrosshair( pos )
+
+    Set the position of the crosshair. Note that this is cleared if the user
+    moves the mouse. "pos" is in coordinates of the plot and should be on the
+    form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple
+    axes), which is coincidentally the same format as what you get from a
+    "plothover" event. If "pos" is null, the crosshair is cleared.
+
+  - clearCrosshair()
+
+    Clear the crosshair.
+
+  - lockCrosshair(pos)
+
+    Cause the crosshair to lock to the current location, no longer updating if
+    the user moves the mouse. Optionally supply a position (passed on to
+    setCrosshair()) to move it to.
+
+    Example usage:
+
+	var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } };
+	$("#graph").bind( "plothover", function ( evt, position, item ) {
+		if ( item ) {
+			// Lock the crosshair to the data point being hovered
+			myFlot.lockCrosshair({
+				x: item.datapoint[ 0 ],
+				y: item.datapoint[ 1 ]
+			});
+		} else {
+			// Return normal crosshair operation
+			myFlot.unlockCrosshair();
+		}
+	});
+
+  - unlockCrosshair()
+
+    Free the crosshair to move again after locking it.
+*/
+
+(function ($) {
+    var options = {
+        crosshair: {
+            mode: null, // one of null, "x", "y" or "xy",
+            color: "rgba(170, 0, 0, 0.80)",
+            lineWidth: 1
+        }
+    };
+    
+    function init(plot) {
+        // position of crosshair in pixels
+        var crosshair = { x: -1, y: -1, locked: false };
+
+        plot.setCrosshair = function setCrosshair(pos) {
+            if (!pos)
+                crosshair.x = -1;
+            else {
+                var o = plot.p2c(pos);
+                crosshair.x = Math.max(0, Math.min(o.left, plot.width()));
+                crosshair.y = Math.max(0, Math.min(o.top, plot.height()));
+            }
+            
+            plot.triggerRedrawOverlay();
+        };
+        
+        plot.clearCrosshair = plot.setCrosshair; // passes null for pos
+        
+        plot.lockCrosshair = function lockCrosshair(pos) {
+            if (pos)
+                plot.setCrosshair(pos);
+            crosshair.locked = true;
+        };
+
+        plot.unlockCrosshair = function unlockCrosshair() {
+            crosshair.locked = false;
+        };
+
+        function onMouseOut(e) {
+            if (crosshair.locked)
+                return;
+
+            if (crosshair.x != -1) {
+                crosshair.x = -1;
+                plot.triggerRedrawOverlay();
+            }
+        }
+
+        function onMouseMove(e) {
+            if (crosshair.locked)
+                return;
+                
+            if (plot.getSelection && plot.getSelection()) {
+                crosshair.x = -1; // hide the crosshair while selecting
+                return;
+            }
+                
+            var offset = plot.offset();
+            crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width()));
+            crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height()));
+            plot.triggerRedrawOverlay();
+        }
+        
+        plot.hooks.bindEvents.push(function (plot, eventHolder) {
+            if (!plot.getOptions().crosshair.mode)
+                return;
+
+            eventHolder.mouseout(onMouseOut);
+            eventHolder.mousemove(onMouseMove);
+        });
+
+        plot.hooks.drawOverlay.push(function (plot, ctx) {
+            var c = plot.getOptions().crosshair;
+            if (!c.mode)
+                return;
+
+            var plotOffset = plot.getPlotOffset();
+            
+            ctx.save();
+            ctx.translate(plotOffset.left, plotOffset.top);
+
+            if (crosshair.x != -1) {
+                var adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0;
+
+                ctx.strokeStyle = c.color;
+                ctx.lineWidth = c.lineWidth;
+                ctx.lineJoin = "round";
+
+                ctx.beginPath();
+                if (c.mode.indexOf("x") != -1) {
+                    var drawX = Math.floor(crosshair.x) + adj;
+                    ctx.moveTo(drawX, 0);
+                    ctx.lineTo(drawX, plot.height());
+                }
+                if (c.mode.indexOf("y") != -1) {
+                    var drawY = Math.floor(crosshair.y) + adj;
+                    ctx.moveTo(0, drawY);
+                    ctx.lineTo(plot.width(), drawY);
+                }
+                ctx.stroke();
+            }
+            ctx.restore();
+        });
+
+        plot.hooks.shutdown.push(function (plot, eventHolder) {
+            eventHolder.unbind("mouseout", onMouseOut);
+            eventHolder.unbind("mousemove", onMouseMove);
+        });
+    }
+    
+    $.plot.plugins.push({
+        init: init,
+        options: options,
+        name: 'crosshair',
+        version: '1.0'
+    });
+})(jQuery);

+ 2 - 0
src/vendor/jquery/jquery.flot.js

@@ -1728,6 +1728,8 @@ Licensed under the MIT license.
             axis.delta = delta;
             axis.delta = delta;
             axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
             axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
             axis.tickSize = opts.tickSize || size;
             axis.tickSize = opts.tickSize || size;
+            // grafana addition
+            axis.scaledDecimals = axis.tickDecimals - Math.floor(Math.log(axis.tickSize) / Math.LN10);
 
 
             // Time mode was moved to a plug-in in 0.8, and since so many people use it
             // Time mode was moved to a plug-in in 0.8, and since so many people use it
             // we'll add an especially friendly reminder to make sure they included it.
             // we'll add an especially friendly reminder to make sure they included it.