From e78c48620fc4a9e0f6d63fae05205afd1fc50a96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 19 Sep 2014 13:24:15 +0200 Subject: [PATCH 01/11] Trying to improve yaxis precision --- src/app/components/kbn.js | 76 ++++-------------------------- src/app/components/timeSeries.js | 16 +++---- src/app/directives/grafanaGraph.js | 22 ++++++--- 3 files changed, 33 insertions(+), 81 deletions(-) diff --git a/src/app/components/kbn.js b/src/app/components/kbn.js index 7c61b22c83b..e41722747fb 100644 --- a/src/app/components/kbn.js +++ b/src/app/components/kbn.js @@ -7,6 +7,7 @@ function($, _, moment) { 'use strict'; var kbn = {}; + kbn.formatFunctions = {}; kbn.round_interval = function(interval) { switch (true) { @@ -497,52 +498,7 @@ function($, _, moment) { return (size.toFixed(decimals) + ext); }; - 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); - }; - } - }; - - kbn.noneFormat = function(value, decimals) { + kbn.toFixed = function(value, decimals) { var factor = decimals ? Math.pow(10, decimals) : 1; var formatted = String(Math.round(value * factor) / factor); @@ -553,7 +509,6 @@ function($, _, moment) { // If tickDecimals was specified, ensure that we have exactly that // much precision; otherwise default to the value's own precision. - if (decimals != null) { var decimalPos = formatted.indexOf("."); var precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1; @@ -565,17 +520,13 @@ function($, _, moment) { 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.formatFunctions.ms = function(size, decimals) { + if (Math.abs(size) < 1000) { + return kbn.toFixed(size, decimals) + " ms"; } // Less than 1 min else if (Math.abs(size) < 60000) { - return (size / 1000).toFixed(decimals) + " s"; + return kbn.toFixed(size / 1000, decimals) + " s"; } // Less than 1 hour, devide in minutes else if (Math.abs(size) < 3600000) { @@ -594,12 +545,7 @@ function($, _, moment) { }; 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) { + if (Math.abs(size) < 600) { return size.toFixed(decimals) + " s"; } // Less than 1 hour, devide in minutes @@ -623,12 +569,8 @@ function($, _, moment) { }; 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"; + if (Math.abs(size) < 1000) { + return kbn.toFixed(size, decimals) + " µs"; } else if (Math.abs(size) < 1000000) { return (size / 1000).toFixed(decimals) + " ms"; diff --git a/src/app/components/timeSeries.js b/src/app/components/timeSeries.js index 4c58c211cc3..576e8be8e8a 100644 --- a/src/app/components/timeSeries.js +++ b/src/app/components/timeSeries.js @@ -100,21 +100,21 @@ function (_, kbn) { } if (result.length) { - this.info.avg = (this.info.total / result.length); 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; }; + TimeSeries.prototype.updateLegendValues = function(formater, decimals) { + this.info.avg = this.info.avg != null ? formater(this.info.avg, decimals) : null; + this.info.current = this.info.current != null ? formater(this.info.current, decimals) : null; + this.info.min = this.info.min != null ? formater(this.info.min, decimals) : null; + this.info.max = this.info.max != null ? formater(this.info.max, decimals) : null; + this.info.total = this.info.total != null ? formater(this.info.total, decimals) : null; + }; + return TimeSeries; }); diff --git a/src/app/directives/grafanaGraph.js b/src/app/directives/grafanaGraph.js index ee6b0fb7dd7..53edcf1bc13 100755 --- a/src/app/directives/grafanaGraph.js +++ b/src/app/directives/grafanaGraph.js @@ -88,6 +88,18 @@ function (angular, $, kbn, moment, _) { } } + function updateLegendValues(plot) { + var yaxis = plot.getYAxes(); + console.log('drawSeries', yaxis); + + for (var i = 0; i < data.length; i++) { + var series = data[i]; + var formater = kbn.formatFunctions[scope.panel.y_formats[series.yaxis - 1]]; + series.updateLegendValues(formater, yaxis[series.yaxis - 1].tickDecimals); + } + + } + // Function for rendering panel function render_panel() { if (shouldAbortRender()) { @@ -110,11 +122,7 @@ function (angular, $, kbn, moment, _) { // Populate element var options = { - hooks: { - drawSeries: [function() { - console.log('drawSeries', arguments); - }] - }, + hooks: { draw: [updateLegendValues] }, legend: { show: false }, series: { stackpercent: panel.stack ? panel.percentage : false, @@ -318,7 +326,9 @@ function (angular, $, kbn, moment, _) { } function configureAxisMode(axis, format) { - axis.tickFormatter = kbn.getFormatFunction(format, 1); + axis.tickFormatter = function(val, axis) { + return kbn.formatFunctions[format](val, axis.tickDecimals); + }; } function time_format(interval, ticks, min, max) { From f59bb6461aad6afe757d0400f264ba8e685fc928 Mon Sep 17 00:00:00 2001 From: toni-moreno Date: Tue, 23 Sep 2014 13:51:59 +0200 Subject: [PATCH 02/11] added shared tooltips to graphs --- src/app/components/require.config.js | 2 + src/app/directives/grafanaGraph.js | 73 ++++++++- src/app/panels/graph/module.js | 3 +- src/app/panels/graph/styleEditor.html | 8 + src/test/specs/grafanaGraph-specs.js | 5 +- src/test/test-main.js | 2 + src/vendor/jquery/jquery.flot.crosshair.js | 176 +++++++++++++++++++++ 7 files changed, 264 insertions(+), 5 deletions(-) create mode 100644 src/vendor/jquery/jquery.flot.crosshair.js diff --git a/src/app/components/require.config.js b/src/app/components/require.config.js index 6a17f9635a2..61689fedc3c 100644 --- a/src/app/components/require.config.js +++ b/src/app/components/require.config.js @@ -40,6 +40,7 @@ require.config({ 'jquery.flot.stack': '../vendor/jquery/jquery.flot.stack', 'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent', 'jquery.flot.time': '../vendor/jquery/jquery.flot.time', + 'jquery.flot.crosshair': '../vendor/jquery/jquery.flot.crosshair', modernizr: '../vendor/modernizr-2.6.1', @@ -83,6 +84,7 @@ require.config({ 'jquery.flot.stack': ['jquery', 'jquery.flot'], 'jquery.flot.stackpercent':['jquery', 'jquery.flot'], 'jquery.flot.time': ['jquery', 'jquery.flot'], + 'jquery.flot.crosshair':['jquery', 'jquery.flot'], 'angular-cookies': ['angular'], 'angular-dragdrop': ['jquery','jquery-ui','angular'], 'angular-loader': ['angular'], diff --git a/src/app/directives/grafanaGraph.js b/src/app/directives/grafanaGraph.js index a2987213cb4..0942f4c80ef 100755 --- a/src/app/directives/grafanaGraph.js +++ b/src/app/directives/grafanaGraph.js @@ -130,6 +130,9 @@ function (angular, $, kbn, moment, _) { selection: { mode: "x", color: '#666' + }, + crosshair: { + mode: panel.tooltip.shared ? "x" : null } }; @@ -157,7 +160,7 @@ function (angular, $, kbn, moment, _) { function callPlot() { try { - $.plot(elem, sortedSeries, options); + elem.flot=$.plot(elem, sortedSeries, options); } catch (e) { console.log('flotcharts error', e); } @@ -326,9 +329,73 @@ function (angular, $, kbn, moment, _) { var $tooltip = $('
'); - elem.bind("plothover", function (event, pos, item) { - var group, value, timestamp, seriesInfo, format; + //this event will erase tooltip and crosshair once leaved the graph + elem.mouseleave(function () { + console.log('onmouse out:'); + if(scope.panel.tooltip.shared) { + $tooltip.detach(); + elem.flot.clearCrosshair(); + } + }); + elem.bind("plothover", function (event, pos, item) { + var group, value, timestamp, seriesInfo, format, i, j, s, s_final; + + //if tooltip shared we'll show a crosshair and will look for X and all Y series values + //else we will take from item. + if(scope.panel.tooltip.shared){ + //check if all series has same length if so, only one x index will + //be checked and only for exact timestamp values + var l = []; + var series; + for (i = 0; i < data.length; ++i) { + series = data[i]; + l.push(series.data.length); + } + //if all series has the same length it is because of they share time axis + if(_.uniq(l).length === 1) { + s=''; + series = data[0]; + j=0; + do { + ++j; + } while (series.data[j][0] < pos.x); + j--; //we take previous value in time. + //now we know the current X (j) position for X and Y values + timestamp = dashboard.formatDate(series.data[j][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[j][1]; + } else { + last_value+=series.data[j][1]; + value = last_value; + } + value = kbn.getFormatFunction(format, 2)(value,series.yaxis); + if (seriesInfo.alias) { + group = '' + + ' ' + seriesInfo.alias; + } else { + group = kbn.query_color_dot(series.color, 15) + ' '; + } + //pre-pending new values + s_final= group+ ": "+value +'
'+ s; + s=s_final; + } + + $tooltip.html('Time@ '+ + timestamp + '
' + s + '
').place_tt(pos.pageX, pos.pageY); + return; + }else { + console.log('WARNING: tootltip shared can not be shown becouse of from ' + +data.length+' series has different length '+_.uniq(l)); + $tooltip.detach(); + } + } if (item) { seriesInfo = item.series.info; format = scope.panel.y_formats[seriesInfo.yaxis - 1]; diff --git a/src/app/panels/graph/module.js b/src/app/panels/graph/module.js index aa6667ee154..bbe64d10c37 100644 --- a/src/app/panels/graph/module.js +++ b/src/app/panels/graph/module.js @@ -15,7 +15,8 @@ define([ 'jquery.flot.selection', 'jquery.flot.time', 'jquery.flot.stack', - 'jquery.flot.stackpercent' + 'jquery.flot.stackpercent', + 'jquery.flot.crosshair' ], function (angular, app, $, _, kbn, moment, TimeSeries) { 'use strict'; diff --git a/src/app/panels/graph/styleEditor.html b/src/app/panels/graph/styleEditor.html index cd83f23f197..d2ff50f77ea 100644 --- a/src/app/panels/graph/styleEditor.html +++ b/src/app/panels/graph/styleEditor.html @@ -61,8 +61,16 @@
+ +
+
Tooltip
+
+ +
+
+
Series specific overrides Regex match example: /server[0-3]/i
diff --git a/src/test/specs/grafanaGraph-specs.js b/src/test/specs/grafanaGraph-specs.js index faf19119d27..159a923cc7c 100644 --- a/src/test/specs/grafanaGraph-specs.js +++ b/src/test/specs/grafanaGraph-specs.js @@ -27,7 +27,10 @@ define([ legend: {}, grid: {}, y_formats: [], - seriesOverrides: [] + seriesOverrides: [], + tooltip: { + shared: true + } }; scope.hiddenSeries = {}; scope.dashboard = { timezone: 'browser' }; diff --git a/src/test/test-main.js b/src/test/test-main.js index 342f3143a9e..f60cdb87da4 100644 --- a/src/test/test-main.js +++ b/src/test/test-main.js @@ -43,6 +43,7 @@ require.config({ 'jquery.flot.stack': '../vendor/jquery/jquery.flot.stack', 'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent', 'jquery.flot.time': '../vendor/jquery/jquery.flot.time', + 'jquery.flot.crosshair': '../vendor/jquery/jquery.flot.crosshair', modernizr: '../vendor/modernizr-2.6.1', }, @@ -77,6 +78,7 @@ require.config({ 'jquery.flot.stack': ['jquery', 'jquery.flot'], 'jquery.flot.stackpercent':['jquery', 'jquery.flot'], 'jquery.flot.time': ['jquery', 'jquery.flot'], + 'jquery.flot.crosshair':['jquery', 'jquery.flot'], 'angular-route': ['angular'], 'angular-cookies': ['angular'], diff --git a/src/vendor/jquery/jquery.flot.crosshair.js b/src/vendor/jquery/jquery.flot.crosshair.js new file mode 100644 index 00000000000..5111695e3d1 --- /dev/null +++ b/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); From 68adaea12846130bfd5855175733df0c4c1dca42 Mon Sep 17 00:00:00 2001 From: Toby Matejovsky Date: Wed, 24 Sep 2014 19:01:23 -0400 Subject: [PATCH 03/11] Fix typo --- src/config.sample.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.sample.js b/src/config.sample.js index 740c0d4b8c9..ab60995d251 100644 --- a/src/config.sample.js +++ b/src/config.sample.js @@ -87,7 +87,7 @@ function (Settings) { // Example: "1m", "1h" 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 admin: { password: '' From 3ea94c34849fe351714a21dee97409b61a94883b Mon Sep 17 00:00:00 2001 From: toni-moreno Date: Thu, 25 Sep 2014 06:35:49 +0200 Subject: [PATCH 04/11] little fix when searching x index time --- src/app/directives/grafanaGraph.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/directives/grafanaGraph.js b/src/app/directives/grafanaGraph.js index 0942f4c80ef..193d0101b76 100755 --- a/src/app/directives/grafanaGraph.js +++ b/src/app/directives/grafanaGraph.js @@ -359,7 +359,7 @@ function (angular, $, kbn, moment, _) { j=0; do { ++j; - } while (series.data[j][0] < pos.x); + } while (series.data[j][0] < pos.x && j Date: Thu, 25 Sep 2014 15:45:18 +0200 Subject: [PATCH 05/11] fixing the previous fix, now working fine --- src/app/directives/grafanaGraph.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/directives/grafanaGraph.js b/src/app/directives/grafanaGraph.js index 193d0101b76..1eaacb098a6 100755 --- a/src/app/directives/grafanaGraph.js +++ b/src/app/directives/grafanaGraph.js @@ -356,10 +356,11 @@ function (angular, $, kbn, moment, _) { if(_.uniq(l).length === 1) { s=''; series = data[0]; - j=0; - do { - ++j; - } while (series.data[j][0] < pos.x && j pos.x){ + break; + } + } j--; //we take previous value in time. //now we know the current X (j) position for X and Y values timestamp = dashboard.formatDate(series.data[j][0]); From cc31a12b8c08cf9b10dae1da49564db37189397f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 29 Sep 2014 11:37:56 +0200 Subject: [PATCH 06/11] Smart decimal precision when using scaled unit format, Closes #877 --- src/app/components/kbn.js | 253 +++++------------------------ src/app/components/timeSeries.js | 14 +- src/app/directives/grafanaGraph.js | 10 +- src/test/specs/kbn-format-specs.js | 83 +++------- src/vendor/jquery/jquery.flot.js | 2 + 5 files changed, 77 insertions(+), 285 deletions(-) diff --git a/src/app/components/kbn.js b/src/app/components/kbn.js index e41722747fb..cb084b1c166 100644 --- a/src/app/components/kbn.js +++ b/src/app/components/kbn.js @@ -7,7 +7,7 @@ function($, _, moment) { 'use strict'; var kbn = {}; - kbn.formatFunctions = {}; + kbn.valueFormats = {}; kbn.round_interval = function(interval) { switch (true) { @@ -310,192 +310,24 @@ function($, _, moment) { ].join(';') + '">
'; }; - 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; + kbn.formatFuncCreator = function(factor, extArray) { + return function(size, decimals, scaledDecimals) { + var steps = 0; - if(_.isUndefined(decimals)) { - decimals = 2; - } else if (decimals === 0) { - decimals = undefined; - } + while (Math.abs(size) >= factor) { + steps++; + size /= factor; + } + if (steps > 0) { + decimals = scaledDecimals + (3 * steps); + } - 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.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); + return kbn.toFixed(size, decimals) + extArray[steps]; + }; }; kbn.toFixed = function(value, decimals) { @@ -520,84 +352,87 @@ function($, _, moment) { return formatted; }; - kbn.formatFunctions.ms = function(size, decimals) { + 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 else if (Math.abs(size) < 60000) { - return kbn.toFixed(size / 1000, decimals) + " s"; + return kbn.toFixed(size / 1000, scaledDecimals + 3) + " s"; } // Less than 1 hour, devide in minutes 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 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 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) { + kbn.valueFormats.s = function(size, decimals, scaledDecimals) { if (Math.abs(size) < 600) { - return size.toFixed(decimals) + " s"; + return kbn.toFixed(size, decimals) + " s"; } // Less than 1 hour, devide in minutes 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 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 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 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) { + kbn.valueFormats['µs'] = function(size, decimals, scaledDecimals) { if (Math.abs(size) < 1000) { return kbn.toFixed(size, decimals) + " µs"; } else if (Math.abs(size) < 1000000) { - return (size / 1000).toFixed(decimals) + " ms"; + return kbn.toFixed(size / 1000, scaledDecimals + 3) + " ms"; } 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) { - return (size / 1000).toFixed(decimals) + " µs"; + return kbn.toFixed(size / 1000, scaledDecimals + 3) + " µs"; } 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){ - return (size / 1000000000).toFixed(decimals) + " s"; + return kbn.toFixed(size / 1000000000, scaledDecimals + 9) + " s"; } else { - return (size / 60000000000).toFixed(decimals) + " m"; + return kbn.toFixed(size / 60000000000, scaledDecimals + 12) + " m"; } }; diff --git a/src/app/components/timeSeries.js b/src/app/components/timeSeries.js index 576e8be8e8a..a448a93649c 100644 --- a/src/app/components/timeSeries.js +++ b/src/app/components/timeSeries.js @@ -54,7 +54,7 @@ function (_, kbn) { } }; - TimeSeries.prototype.getFlotPairs = function (fillStyle, yFormats) { + TimeSeries.prototype.getFlotPairs = function (fillStyle) { var result = []; this.color = this.info.color; @@ -107,12 +107,12 @@ function (_, kbn) { return result; }; - TimeSeries.prototype.updateLegendValues = function(formater, decimals) { - this.info.avg = this.info.avg != null ? formater(this.info.avg, decimals) : null; - this.info.current = this.info.current != null ? formater(this.info.current, decimals) : null; - this.info.min = this.info.min != null ? formater(this.info.min, decimals) : null; - this.info.max = this.info.max != null ? formater(this.info.max, decimals) : null; - this.info.total = this.info.total != null ? formater(this.info.total, decimals) : null; + 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; diff --git a/src/app/directives/grafanaGraph.js b/src/app/directives/grafanaGraph.js index 53edcf1bc13..df48de89c6f 100755 --- a/src/app/directives/grafanaGraph.js +++ b/src/app/directives/grafanaGraph.js @@ -90,12 +90,12 @@ function (angular, $, kbn, moment, _) { function updateLegendValues(plot) { var yaxis = plot.getYAxes(); - console.log('drawSeries', yaxis); for (var i = 0; i < data.length; i++) { var series = data[i]; - var formater = kbn.formatFunctions[scope.panel.y_formats[series.yaxis - 1]]; - series.updateLegendValues(formater, yaxis[series.yaxis - 1].tickDecimals); + var axis = yaxis[series.yaxis - 1]; + var formater = kbn.valueFormats[scope.panel.y_formats[series.yaxis - 1]]; + series.updateLegendValues(formater, axis.tickDecimals, axis.scaledDecimals); } } @@ -327,7 +327,7 @@ function (angular, $, kbn, moment, _) { function configureAxisMode(axis, format) { axis.tickFormatter = function(val, axis) { - return kbn.formatFunctions[format](val, axis.tickDecimals); + return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals); }; } @@ -378,7 +378,7 @@ function (angular, $, kbn, moment, _) { value = item.datapoint[1]; } - value = kbn.getFormatFunction(format, 2)(value, item.series.yaxis); + 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); diff --git a/src/test/specs/kbn-format-specs.js b/src/test/specs/kbn-format-specs.js index 85807c277ec..e92e6d0ceec 100644 --- a/src/test/specs/kbn-format-specs.js +++ b/src/test/specs/kbn-format-specs.js @@ -3,76 +3,31 @@ define([ ], function(kbn) { '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'); - }); + 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'); + describeValueFormat('none', 2.75e-10, 0, 10, '3e-10'); + describeValueFormat('none', 0, 0, 2, '0'); - 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"); - }); - - 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"); - }); - - 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() { it('1h 100 resultion', function() { diff --git a/src/vendor/jquery/jquery.flot.js b/src/vendor/jquery/jquery.flot.js index c97ceb1bf44..87a1b17c742 100644 --- a/src/vendor/jquery/jquery.flot.js +++ b/src/vendor/jquery/jquery.flot.js @@ -1728,6 +1728,8 @@ Licensed under the MIT license. axis.delta = delta; axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); 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 // we'll add an especially friendly reminder to make sure they included it. From 51333c9eda6bbd7222e9192f588e9a01c9619e05 Mon Sep 17 00:00:00 2001 From: toni-moreno Date: Mon, 29 Sep 2014 12:48:08 +0200 Subject: [PATCH 07/11] improved tooltip styles, add multiple highlight points, and changed highligth size when graph plotted without points. --- src/app/directives/grafanaGraph.js | 16 ++++++++++++---- src/css/less/grafana.less | 12 ++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/app/directives/grafanaGraph.js b/src/app/directives/grafanaGraph.js index 1eaacb098a6..32454a1f4d7 100755 --- a/src/app/directives/grafanaGraph.js +++ b/src/app/directives/grafanaGraph.js @@ -113,7 +113,8 @@ function (angular, $, kbn, moment, _) { show: panel.points, fill: 1, fillColor: false, - radius: panel.pointradius + radius: panel.points ? panel.pointradius : 2 + // little points when highlight points }, shadowSize: 1 }, @@ -335,6 +336,7 @@ function (angular, $, kbn, moment, _) { if(scope.panel.tooltip.shared) { $tooltip.detach(); elem.flot.clearCrosshair(); + elem.flot.unhighlight(); } }); @@ -344,6 +346,8 @@ function (angular, $, kbn, moment, _) { //if tooltip shared we'll show a crosshair and will look for X and all Y series values //else we will take from item. if(scope.panel.tooltip.shared){ + //unhighligh previous points. + elem.flot.unhighlight(); //check if all series has same length if so, only one x index will //be checked and only for exact timestamp values var l = []; @@ -361,7 +365,9 @@ function (angular, $, kbn, moment, _) { break; } } - j--; //we take previous value in time. + if(j>0) { + j--; //we take previous value in time. + } //now we know the current X (j) position for X and Y values timestamp = dashboard.formatDate(series.data[j][0]); var last_value=0; //needed for stacked values @@ -384,12 +390,14 @@ function (angular, $, kbn, moment, _) { group = kbn.query_color_dot(series.color, 15) + ' '; } //pre-pending new values - s_final= group+ ": "+value +'
'+ s; + s_final= group+ ": "+value +'
'+ s; s=s_final; + //higligth point + elem.flot.highlight(i,j); } $tooltip.html('Time@ '+ - timestamp + '
' + s + '
').place_tt(pos.pageX, pos.pageY); + timestamp + '

' + s + '').place_tt(pos.pageX, pos.pageY); return; }else { console.log('WARNING: tootltip shared can not be shown becouse of from ' diff --git a/src/css/less/grafana.less b/src/css/less/grafana.less index b291b548219..d009f1eff89 100644 --- a/src/css/less/grafana.less +++ b/src/css/less/grafana.less @@ -438,6 +438,18 @@ select.grafana-target-segment-input { max-width: 800px; max-height: 600px; 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 { From 2473ae3b47d637f534b04465244c3273dd0d45cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 29 Sep 2014 16:57:05 +0200 Subject: [PATCH 08/11] Graph: shared multi series tooltip, refactoring PR #850 --- src/app/directives/grafanaGraph.js | 117 ++------------------ src/app/directives/grafanaGraph.tooltip.js | 122 +++++++++++++++++++++ src/app/panels/graph/module.js | 2 +- src/css/less/graph.less | 16 +++ src/test/specs/graph-tooltip-specs.js | 56 ++++++++++ src/test/test-main.js | 1 + 6 files changed, 203 insertions(+), 111 deletions(-) create mode 100644 src/app/directives/grafanaGraph.tooltip.js create mode 100644 src/test/specs/graph-tooltip-specs.js diff --git a/src/app/directives/grafanaGraph.js b/src/app/directives/grafanaGraph.js index c7c937d8bd6..42ef1b91cb5 100755 --- a/src/app/directives/grafanaGraph.js +++ b/src/app/directives/grafanaGraph.js @@ -3,9 +3,10 @@ define([ 'jquery', 'kbn', 'moment', - 'lodash' + 'lodash', + './grafanaGraph.tooltip' ], -function (angular, $, kbn, moment, _) { +function (angular, $, kbn, moment, _, graphTooltip) { 'use strict'; var module = angular.module('grafana.directives'); @@ -15,8 +16,8 @@ function (angular, $, kbn, moment, _) { restrict: 'A', template: '
', link: function(scope, elem) { - var data, annotations; var dashboard = scope.dashboard; + var data, annotations; var legendSideLastValue = null; scope.$on('refresh',function() { @@ -174,7 +175,7 @@ function (angular, $, kbn, moment, _) { function callPlot() { try { - elem.flot=$.plot(elem, sortedSeries, options); + $.plot(elem, sortedSeries, options); } catch (e) { console.log('flotcharts error', e); } @@ -343,112 +344,6 @@ function (angular, $, kbn, moment, _) { return "%H:%M"; } - var $tooltip = $('
'); - - //this event will erase tooltip and crosshair once leaved the graph - elem.mouseleave(function () { - console.log('onmouse out:'); - if(scope.panel.tooltip.shared) { - $tooltip.detach(); - elem.flot.clearCrosshair(); - elem.flot.unhighlight(); - } - }); - - elem.bind("plothover", function (event, pos, item) { - var group, value, timestamp, seriesInfo, format, i, j, s, s_final; - - //if tooltip shared we'll show a crosshair and will look for X and all Y series values - //else we will take from item. - if(scope.panel.tooltip.shared){ - //unhighligh previous points. - elem.flot.unhighlight(); - //check if all series has same length if so, only one x index will - //be checked and only for exact timestamp values - var l = []; - var series; - for (i = 0; i < data.length; ++i) { - series = data[i]; - l.push(series.data.length); - } - //if all series has the same length it is because of they share time axis - if(_.uniq(l).length === 1) { - s=''; - series = data[0]; - for(j=0;j pos.x){ - break; - } - } - if(j>0) { - j--; //we take previous value in time. - } - //now we know the current X (j) position for X and Y values - timestamp = dashboard.formatDate(series.data[j][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[j][1]; - } else { - last_value+=series.data[j][1]; - value = last_value; - } - value = kbn.getFormatFunction(format, 2)(value,series.yaxis); - if (seriesInfo.alias) { - group = '' + - ' ' + seriesInfo.alias; - } else { - group = kbn.query_color_dot(series.color, 15) + ' '; - } - //pre-pending new values - s_final= group+ ": "+value +'
'+ s; - s=s_final; - //higligth point - elem.flot.highlight(i,j); - } - - $tooltip.html('Time@ '+ - timestamp + '

' + s + '
').place_tt(pos.pageX, pos.pageY); - return; - }else { - console.log('WARNING: tootltip shared can not be shown becouse of from ' - +data.length+' series has different length '+_.uniq(l)); - $tooltip.detach(); - } - } - if (item) { - seriesInfo = item.series.info; - format = scope.panel.y_formats[seriesInfo.yaxis - 1]; - - if (seriesInfo.alias) { - group = '' + - '' + ' ' + - seriesInfo.alias + - '
'; - } 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(); - } - }); - function render_panel_as_graphite_png(url) { url += '&width=' + elem.width(); url += '&height=' + elem.css('height').replace('px', ''); @@ -499,6 +394,8 @@ function (angular, $, kbn, moment, _) { elem.html(''); } + graphTooltip.register(elem, dashboard, scope); + elem.bind("plotselected", function (event, ranges) { scope.$apply(function() { timeSrv.setTime({ diff --git a/src/app/directives/grafanaGraph.tooltip.js b/src/app/directives/grafanaGraph.tooltip.js new file mode 100644 index 00000000000..513d7126985 --- /dev/null +++ b/src/app/directives/grafanaGraph.tooltip.js @@ -0,0 +1,122 @@ +define([ + 'jquery', + 'kbn', +], +function ($, kbn) { + 'use strict'; + + function registerTooltipFeatures(elem, dashboard, scope) { + + var $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 = ' ' + seriesInfo.alias; + } else { + group = kbn.query_color_dot(series.color, 15) + ' '; + } + + //pre-pending new values + seriesHtml = group + ': ' + value + '
' + seriesHtml; + + plot.highlight(i, hoverIndex); + } + + $tooltip.html('
'+ timestamp + '
' + seriesHtml + '
') + .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 = '' + + '' + ' ' + + seriesInfo.alias + + '
'; + } 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 + }; +}); diff --git a/src/app/panels/graph/module.js b/src/app/panels/graph/module.js index bbe64d10c37..bc15b851f9d 100644 --- a/src/app/panels/graph/module.js +++ b/src/app/panels/graph/module.js @@ -161,7 +161,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries) { tooltip : { value_type: 'cumulative', - query_as_alias: true + shared: false, }, targets: [{}], diff --git a/src/css/less/graph.less b/src/css/less/graph.less index d53c55345c7..92f57dbfc71 100644 --- a/src/css/less/graph.less +++ b/src/css/less/graph.less @@ -166,3 +166,19 @@ 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; + } + +} diff --git a/src/test/specs/graph-tooltip-specs.js b/src/test/specs/graph-tooltip-specs.js new file mode 100644 index 00000000000..2ca3ec85cac --- /dev/null +++ b/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 = $('
'); + 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'); + }); + + }); + +}); + + diff --git a/src/test/test-main.js b/src/test/test-main.js index f60cdb87da4..6862c9fa8ae 100644 --- a/src/test/test-main.js +++ b/src/test/test-main.js @@ -129,6 +129,7 @@ require([ 'specs/influxdb-datasource-specs', 'specs/graph-ctrl-specs', 'specs/grafanaGraph-specs', + 'specs/graph-tooltip-specs', 'specs/seriesOverridesCtrl-specs', 'specs/timeSrv-specs', 'specs/templateSrv-specs', From 5f164d99ace9fd48c026259817f50d60952910e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 29 Sep 2014 16:59:17 +0200 Subject: [PATCH 09/11] Updated changelog with merged PR #850, shared multi series graph tooltip & crosshair --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95cf8a52a5b..ad6ab03e0c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # 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) From 64f3303711758b47f0ef6915d5a03d2e51660206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 30 Sep 2014 09:02:25 +0200 Subject: [PATCH 10/11] InfluxDB: save dashboard issue, another fix for #859 --- src/app/services/influxdb/influxdbDatasource.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/services/influxdb/influxdbDatasource.js b/src/app/services/influxdb/influxdbDatasource.js index 8ad003cc632..757c2ef0d32 100644 --- a/src/app/services/influxdb/influxdbDatasource.js +++ b/src/app/services/influxdb/influxdbDatasource.js @@ -215,9 +215,9 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { if (id === title) { return; } var self = this; - self._getDashboardInternal(id, isTemp).then(function(dashboard) { + self._getDashboardInternal(title, isTemp).then(function(dashboard) { if (dashboard !== null) { - self.deleteDashboard(id); + self.deleteDashboard(title); } }); }; From bf361d2b02a350b7439a04cf12e2e9f94ac33d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 30 Sep 2014 09:03:14 +0200 Subject: [PATCH 11/11] Updated package.json version and latest.json, preparation for 1.8.1 release --- latest.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/latest.json b/latest.json index 6e7dc07bae2..30dd2d3127b 100644 --- a/latest.json +++ b/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" } diff --git a/package.json b/package.json index 8de8f05cf8a..8369d640c62 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Coding Instinct AB" }, "name": "grafana", - "version": "1.8.0", + "version": "1.8.1", "repository": { "type": "git", "url": "http://github.com/torkelo/grafana.git"