From 1b0cddfa7205759c5fac72093cdafd40d9c964e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 6 Oct 2014 10:20:26 -0400 Subject: [PATCH] Graph: Tooltip refactoring for testability --- src/app/components/kbn.js | 16 ++ src/app/components/timeSeries.js | 18 +- src/app/directives/grafanaGraph.js | 5 +- src/app/directives/grafanaGraph.tooltip.js | 226 +++++++++++---------- src/app/panels/graph/module.js | 2 +- src/css/less/graph.less | 2 + src/test/specs/graph-tooltip-specs.js | 118 +++++++---- 7 files changed, 232 insertions(+), 155 deletions(-) diff --git a/src/app/components/kbn.js b/src/app/components/kbn.js index cb084b1c166..567e7c78f23 100644 --- a/src/app/components/kbn.js +++ b/src/app/components/kbn.js @@ -316,6 +316,10 @@ function($, _, moment) { kbn.formatFuncCreator = function(factor, extArray) { return function(size, decimals, scaledDecimals) { + if (size === null) { + return ""; + } + var steps = 0; while (Math.abs(size) >= factor) { @@ -331,6 +335,10 @@ function($, _, moment) { }; kbn.toFixed = function(value, decimals) { + if (value === null) { + return ""; + } + var factor = decimals ? Math.pow(10, decimals) : 1; var formatted = String(Math.round(value * factor) / factor); @@ -359,6 +367,8 @@ function($, _, moment) { kbn.valueFormats.none = kbn.toFixed; kbn.valueFormats.ms = function(size, decimals, scaledDecimals) { + if (size === null) { return ""; } + if (Math.abs(size) < 1000) { return kbn.toFixed(size, decimals) + " ms"; } @@ -383,6 +393,8 @@ function($, _, moment) { }; kbn.valueFormats.s = function(size, decimals, scaledDecimals) { + if (size === null) { return ""; } + if (Math.abs(size) < 600) { return kbn.toFixed(size, decimals) + " s"; } @@ -407,6 +419,8 @@ function($, _, moment) { }; kbn.valueFormats['µs'] = function(size, decimals, scaledDecimals) { + if (size === null) { return ""; } + if (Math.abs(size) < 1000) { return kbn.toFixed(size, decimals) + " µs"; } @@ -419,6 +433,8 @@ function($, _, moment) { }; kbn.valueFormats.ns = function(size, decimals, scaledDecimals) { + if (size === null) { return ""; } + if (Math.abs(size) < 1000) { return kbn.toFixed(size, decimals) + " ns"; } diff --git a/src/app/components/timeSeries.js b/src/app/components/timeSeries.js index a448a93649c..b05e9333364 100644 --- a/src/app/components/timeSeries.js +++ b/src/app/components/timeSeries.js @@ -5,10 +5,15 @@ define([ function (_, kbn) { 'use strict'; + function defaultValueFormater(value) { + return kbn.valueFormats.none(value, 2, 2); + } + function TimeSeries(opts) { this.datapoints = opts.datapoints; this.info = opts.info; this.label = opts.info.alias; + this.valueFormater = defaultValueFormater; } function matchSeriesOverride(aliasOrRegex, seriesAlias) { @@ -108,11 +113,14 @@ function (_, kbn) { }; 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; + this.valueFormater = function(value) { + return formater(value, decimals, scaledDecimals); + }; + this.info.avg = this.valueFormater(this.info.avg); + this.info.current = this.valueFormater(this.info.current); + this.info.min = this.valueFormater(this.info.min); + this.info.max = this.valueFormater(this.info.max); + this.info.total = this.valueFormater(this.info.total); }; return TimeSeries; diff --git a/src/app/directives/grafanaGraph.js b/src/app/directives/grafanaGraph.js index 685ac7e0b83..a267255a540 100755 --- a/src/app/directives/grafanaGraph.js +++ b/src/app/directives/grafanaGraph.js @@ -6,7 +6,7 @@ define([ 'lodash', './grafanaGraph.tooltip' ], -function (angular, $, kbn, moment, _, graphTooltip) { +function (angular, $, kbn, moment, _, GraphTooltip) { 'use strict'; var module = angular.module('grafana.directives'); @@ -105,6 +105,7 @@ function (angular, $, kbn, moment, _, graphTooltip) { function updateLegendValues(plot) { var yaxis = plot.getYAxes(); + console.log("value"); for (var i = 0; i < data.length; i++) { var series = data[i]; @@ -416,7 +417,7 @@ function (angular, $, kbn, moment, _, graphTooltip) { elem.html(''); } - graphTooltip.register(elem, dashboard, scope, $rootScope); + new GraphTooltip(elem, dashboard, scope); elem.bind("plotselected", function (event, ranges) { scope.$apply(function() { diff --git a/src/app/directives/grafanaGraph.tooltip.js b/src/app/directives/grafanaGraph.tooltip.js index 49f02e86980..6d10fbb89c0 100644 --- a/src/app/directives/grafanaGraph.tooltip.js +++ b/src/app/directives/grafanaGraph.tooltip.js @@ -1,14 +1,105 @@ define([ 'jquery', - 'kbn', ], -function ($, kbn) { +function ($) { 'use strict'; - function registerTooltipFeatures(elem, dashboard, scope) { + function GraphTooltip(elem, dashboard, scope) { + var self = this; var $tooltip = $('
'); + this.findHoverIndexFromDataPoints = function(posX, series,last) { + var ps = series.datapoints.pointsize; + var initial = last*ps; + var len = series.datapoints.points.length; + for (var j = initial; j < len; j += ps) { + if (series.datapoints.points[j] > posX) { + return Math.max(j - ps, 0)/ps; + } + } + return j/ps - 1; + }; + + this.findHoverIndexFromData = function(posX, series) { + var len = series.data.length; + for (var j = 0; j < len; j++) { + if (series.data[j][0] > posX) { + return Math.max(j - 1, 0); + } + } + return j - 1; + }; + + this.showTooltip = function(title, innerHtml, pos) { + var body = '
'+ title + '
' ; + body += innerHtml + '
'; + $tooltip.html(body).place_tt(pos.pageX + 20, pos.pageY); + }; + + this.getMultiSeriesPlotHoverInfo = function(seriesList, pos) { + var value, seriesInfo, i, series, hoverIndex; + var results = []; + + var pointCount = seriesList[0].data.length; + for (i = 1; i < seriesList.length; i++) { + if (seriesList[i].data.length !== pointCount) { + results.pointCountMismatch = true; + return results; + } + } + + series = seriesList[0]; + hoverIndex = this.findHoverIndexFromData(pos.x, series); + var lasthoverIndex = 0; + if(!scope.panel.steppedLine) { + lasthoverIndex = hoverIndex; + } + + //now we know the current X (j) position for X and Y values + results.time = series.data[hoverIndex][0]; + var last_value = 0; //needed for stacked values + + for (i = 0; i < seriesList.length; i++) { + series = seriesList[i]; + seriesInfo = series.info; + + if (scope.panel.stack) { + if (scope.panel.tooltip.value_type === 'individual') { + value = series.data[hoverIndex][1]; + } else { + last_value += series.data[hoverIndex][1]; + value = last_value; + } + } else { + value = series.data[hoverIndex][1]; + } + + // Highlighting multiple Points depending on the plot type + if (scope.panel.steppedLine || (scope.panel.stack && scope.panel.nullPointMode == "null")) { + // stacked and steppedLine plots can have series with different length. + // Stacked series can increase its length on each new stacked serie if null points found, + // to speed the index search we begin always on the las found hoverIndex. + var newhoverIndex = this.findHoverIndexFromDataPoints(pos.x, series,lasthoverIndex); + // update lasthoverIndex depends also on the plot type. + if(!scope.panel.steppedLine) { + // on stacked graphs new will be always greater than last + lasthoverIndex = newhoverIndex; + } else { + // if steppeLine, not always series increases its length, so we should begin + // to search correct index from the original hoverIndex on each serie. + lasthoverIndex = hoverIndex; + } + + results.push({ value: value, hoverIndex: newhoverIndex }); + } else { + results.push({ value: value, hoverIndex: hoverIndex }); + } + } + + return results; + }; + elem.mouseleave(function () { if (scope.panel.tooltip.shared || dashboard.sharedCrosshair) { var plot = elem.data().plot; @@ -20,36 +111,10 @@ function ($, kbn) { } }); - function findHoverIndexFromDataPoints(posX, series,last) { - var ps=series.datapoints.pointsize; - var initial=last*ps; - for (var j = initial; j < series.datapoints.points.length; j+=ps) { - if (series.datapoints.points[j] > posX) { - return Math.max(j - ps, 0)/ps; - } - } - return j/ps - 1; - } - - function findHoverIndexFromData(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; - } - - function showTooltip(title, innerHtml, pos) { - var body = '
'+ title + '
' ; - body += innerHtml + '
'; - $tooltip.html(body).place_tt(pos.pageX + 20, pos.pageY); - } - 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; + var group, value, timestamp, hoverInfo, i, series, seriesHtml; if(dashboard.sharedCrosshair){ scope.appEvent('setCrosshair', { pos: pos, scope: scope }); @@ -58,86 +123,34 @@ function ($, kbn) { 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) { - showTooltip('Shared tooltip error', '', pos); - return; - } + var seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(data, pos); + if (seriesHoverInfo.pointCountMismatch) { + self.showTooltip('Shared tooltip error', '', pos); + return; } seriesHtml = ''; - series = data[0]; - hoverIndex = findHoverIndexFromData(pos.x, series); - var lasthoverIndex=0; - if(!scope.panel.steppedLine) { - lasthoverIndex=hoverIndex; - } + timestamp = dashboard.formatDate(seriesHoverInfo.time); - //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 = 0; i < data.length; i++) { + for (i = 0; i < seriesHoverInfo.length; i++) { series = data[i]; - seriesInfo = series.info; - format = scope.panel.y_formats[seriesInfo.yaxis - 1]; + hoverInfo = seriesHoverInfo[i]; + value = series.valueFormater(hoverInfo.value); - if (scope.panel.stack) { - if (scope.panel.tooltip.value_type === 'individual') { - value = series.data[hoverIndex][1]; - } else { - last_value += series.data[hoverIndex][1]; - value = last_value; - } - } else { - value = series.data[hoverIndex][1]; - } - - 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 + group = ' ' + series.label; seriesHtml = group + ': ' + value + '
' + seriesHtml; - - //Highlighting multiple Points depending on the plot type - if (scope.panel.steppedLine || (scope.panel.stack && scope.panel.nullPointMode == "null")) { - //stacked and steppedLine plots can have series with different length. - //Stacked series can increase its length on each new stacked serie if null points found, - //to speed the index search we begin always on the las found hoverIndex. - var newhoverIndex=findHoverIndexFromDataPoints(pos.x, series,lasthoverIndex); - //update lasthoverIndex depends also on the plot type. - if(!scope.panel.steppedLine) { - //on stacked graphs new will be always greater than last - lasthoverIndex=newhoverIndex; - } else { - //if steppeLine, not always series increases its length, so we should begin - //to search correct index from the original hoverIndex on each serie. - lasthoverIndex=hoverIndex; - } - plot.highlight(i, newhoverIndex); - } else { - plot.highlight(i, hoverIndex); - } + plot.highlight(i, hoverInfo.hoverIndex); } - showTooltip(timestamp, seriesHtml, pos); - return; + self.showTooltip(timestamp, seriesHtml, pos); } - if (item) { - seriesInfo = item.series.info; - format = scope.panel.y_formats[seriesInfo.yaxis - 1]; - group = ' ' + seriesInfo.alias; + // single series tooltip + else if (item) { + series = item.series; + group = ' ' + series.label; if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') { value = item.datapoint[1] - item.datapoint[2]; @@ -146,19 +159,18 @@ function ($, kbn) { value = item.datapoint[1]; } - value = kbn.valueFormats[format](value, item.series.yaxis.tickDecimals); + value = series.valueFormater(value); timestamp = dashboard.formatDate(item.datapoint[0]); group += ': ' + value + ''; - showTooltip(timestamp, group, pos); - } else { + self.showTooltip(timestamp, group, pos); + } + // no hit + else { $tooltip.detach(); } }); - } - return { - register: registerTooltipFeatures - }; + return GraphTooltip; }); diff --git a/src/app/panels/graph/module.js b/src/app/panels/graph/module.js index bc15b851f9d..6f5a174056f 100644 --- a/src/app/panels/graph/module.js +++ b/src/app/panels/graph/module.js @@ -244,7 +244,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries) { var seriesInfo = { alias: alias, - color: color, + color: color, }; $scope.legend.push(seriesInfo); diff --git a/src/css/less/graph.less b/src/css/less/graph.less index 01a697ddb2e..c0c3f371325 100644 --- a/src/css/less/graph.less +++ b/src/css/less/graph.less @@ -169,6 +169,8 @@ } .graph-tooltip { + white-space: nowrap; + .graph-tooltip-time { text-align: center; font-weight: bold; diff --git a/src/test/specs/graph-tooltip-specs.js b/src/test/specs/graph-tooltip-specs.js index 7f20ad8dbc9..2277426bc81 100644 --- a/src/test/specs/graph-tooltip-specs.js +++ b/src/test/specs/graph-tooltip-specs.js @@ -1,55 +1,93 @@ define([ 'jquery', 'directives/grafanaGraph.tooltip' -], function($, tooltip) { +], function($, GraphTooltip) { 'use strict'; - describe('graph tooltip', function() { - var elem = $('
'); - var dashboard = { - formatDate: sinon.stub().returns('date'), - }; - var scope = { - appEvent: sinon.spy(), - onAppEvent: sinon.spy(), - panel: { - tooltip: { - shared: true - }, - y_formats: ['ms', 'none'], - stack: true - } - }; + var scope = { + appEvent: sinon.spy(), + onAppEvent: sinon.spy(), + }; - var data = [ - { - data: [[10,10], [12,20]], - info: { yaxis: 1 }, - yaxis: { tickDecimals: 2 }, + var elem = $('
'); + var dashboard = { }; + + function describeSharedTooltip(desc, fn) { + var ctx = {}; + ctx.scope = scope; + ctx.scope.panel = { + tooltip: { + shared: true }, - { - data: [[10,10], [12,20]], - info: { yaxis: 1 }, - yaxis: { tickDecimals: 2 }, - } - ]; - - var plot = { - getData: sinon.stub().returns(data), - highlight: sinon.stub(), - unhighlight: sinon.stub() + stack: false }; - elem.data('plot', plot); + ctx.setup = function(setupFn) { + ctx.setupFn = setupFn; + }; - beforeEach(function() { - tooltip.register(elem, dashboard, scope); - elem.trigger('plothover', [{}, {x: 13}, {}]); + describe(desc, function() { + beforeEach(function() { + ctx.setupFn(); + var tooltip = new GraphTooltip(elem, dashboard, scope); + ctx.results = tooltip.getMultiSeriesPlotHoverInfo(ctx.data, ctx.pos); + }); + + fn(ctx); + }); + } + + describeSharedTooltip("steppedLine false, stack false", function(ctx) { + ctx.setup(function() { + ctx.data = [ + { data: [[10, 15], [12, 20]], }, + { data: [[10, 2], [12, 3]], } + ]; + ctx.pos = { x: 11 }; }); - it('should add tooltip', function() { - var tooltipHtml = $(".graph-tooltip").text(); - expect(tooltipHtml).to.be('date : 40.00 ms : 20.00 ms'); + it('should return 2 series', function() { + expect(ctx.results.length).to.be(2); + }); + it('should add time to results array', function() { + expect(ctx.results.time).to.be(10); + }); + it('should set value and hoverIndex', function() { + expect(ctx.results[0].value).to.be(15); + expect(ctx.results[1].value).to.be(2); + expect(ctx.results[0].hoverIndex).to.be(0); + }); + }); + + describeSharedTooltip("steppedLine false, stack true, individual false", function(ctx) { + ctx.setup(function() { + ctx.data = [ + { data: [[10, 15], [12, 20]], }, + { data: [[10, 2], [12, 3]], } + ]; + ctx.scope.panel.stack = true; + ctx.pos = { x: 11 }; + }); + + it('should show stacked value', function() { + expect(ctx.results[1].value).to.be(17); + }); + + }); + + describeSharedTooltip("steppedLine false, stack true, individual true", function(ctx) { + ctx.setup(function() { + ctx.data = [ + { data: [[10, 15], [12, 20]], }, + { data: [[10, 2], [12, 3]], } + ]; + ctx.scope.panel.stack = true; + ctx.scope.panel.tooltip.value_type = 'individual'; + ctx.pos = { x: 11 }; + }); + + it('should not show stacked value', function() { + expect(ctx.results[1].value).to.be(2); }); });