From f59bb6461aad6afe757d0400f264ba8e685fc928 Mon Sep 17 00:00:00 2001 From: toni-moreno Date: Tue, 23 Sep 2014 13:51:59 +0200 Subject: [PATCH] added shared tooltips to graphs --- src/app/components/require.config.js | 2 + src/app/directives/grafanaGraph.js | 71 ++++++++- 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, 263 insertions(+), 4 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 = $('
'); + //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; + 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);