@@ -60,6 +60,7 @@ function updateUrl() {
6060
6161function destroyPlots ( ) {
6262 if ( plotInstance ) { plotInstance . destroy ( ) ; plotInstance = null ; }
63+ $ ( "#plot" ) . off ( "mousemove.tooltip" ) ;
6364 miniplotInstances . forEach ( function ( g ) { g . destroy ( ) ; } ) ;
6465 miniplotInstances = [ ] ;
6566}
@@ -107,7 +108,8 @@ function buildGraphData(branches, median, equidistant) {
107108
108109 dateIndex [ dateKey ] = new Date ( dateKey . trim ( ) ) ;
109110 seriesRaw [ exe_id ] [ dateKey ] = { low : low , mid : mid , high : high ,
110- commit : commit , tag : tag } ;
111+ commit : commit , tag : tag ,
112+ dateKey : dateKey } ;
111113 }
112114 }
113115 }
@@ -126,12 +128,15 @@ function buildGraphData(branches, median, equidistant) {
126128
127129 // commitMap[dateKey][exe_id] = {commit, tag}
128130 var commitMap = { } ;
131+ var tsMap = { } ; // timestamp (ms) -> dateKey, for legend lookup
129132 sortedDateKeys . forEach ( function ( dk ) {
133+ tsMap [ dateIndex [ dk ] . getTime ( ) ] = dk ;
130134 commitMap [ dk ] = { } ;
131135 seriesIds . forEach ( function ( id ) {
132136 if ( seriesRaw [ id ] [ dk ] ) {
133- commitMap [ dk ] [ id ] = { commit : seriesRaw [ id ] [ dk ] . commit ,
134- tag : seriesRaw [ id ] [ dk ] . tag } ;
137+ var pt = seriesRaw [ id ] [ dk ] ;
138+ commitMap [ dk ] [ id ] = { commit : pt . commit , tag : pt . tag ,
139+ low : pt . low , high : pt . high } ;
135140 }
136141 } ) ;
137142 } ) ;
@@ -148,7 +153,7 @@ function buildGraphData(branches, median, equidistant) {
148153
149154 return { labels : labels , colors : colors , data : graphData ,
150155 commitMap : commitMap , sortedDateKeys : sortedDateKeys ,
151- seriesIds : seriesIds } ;
156+ seriesIds : seriesIds , tsMap : tsMap } ;
152157}
153158
154159function renderPlot ( data ) {
@@ -186,8 +191,58 @@ function renderPlot(data) {
186191 var commitMap = built . commitMap ;
187192 var sortedDateKeys = built . sortedDateKeys ;
188193 var seriesIds = built . seriesIds ;
194+ var tsMap = built . tsMap ;
189195 var env = $ ( "input[name='environments']:checked" ) . val ( ) ;
190196
197+ // Map label -> color and label -> exeId for tooltip
198+ var colorMap = { } ;
199+ var labelToExeId = { } ;
200+ built . labels . slice ( 1 ) . forEach ( function ( lbl , i ) {
201+ colorMap [ lbl ] = built . colors [ i ] ;
202+ labelToExeId [ lbl ] = seriesIds [ i ] ;
203+ } ) ;
204+
205+ function lookupDateKey ( x ) {
206+ if ( equidistant ) { return sortedDateKeys [ Math . round ( x ) ] ; }
207+ var dk = tsMap [ x ] ;
208+ if ( ! dk ) {
209+ // find closest
210+ dk = sortedDateKeys . reduce ( function ( best , curr ) {
211+ return Math . abs ( new Date ( curr . trim ( ) ) - x ) <
212+ Math . abs ( new Date ( best . trim ( ) ) - x ) ? curr : best ;
213+ } , sortedDateKeys [ 0 ] ) ;
214+ }
215+ return dk ;
216+ }
217+
218+ function legendFormatter ( ld ) {
219+ if ( ld . x === undefined ) { return '' ; }
220+ var dk = lookupDateKey ( ld . x ) ;
221+ var html = '<div style="font-weight:bold;margin-bottom:3px">' +
222+ ( dk ? dk . slice ( 0 , 16 ) : '' ) + '</div>' ;
223+ ld . series . forEach ( function ( s ) {
224+ if ( ! s . isVisible || s . y === undefined ) { return ; }
225+ var exeId = labelToExeId [ s . labelHTML ] || labelToExeId [ s . name ] ;
226+ var color = colorMap [ s . labelHTML ] || colorMap [ s . name ] || '#333' ;
227+ html += '<div style="margin:2px 0;color:' + color + '">' +
228+ '<b>' + s . labelHTML + ': ' + s . y . toPrecision ( 4 ) + '</b>' ;
229+ if ( dk && commitMap [ dk ] && commitMap [ dk ] [ exeId ] ) {
230+ var info = commitMap [ dk ] [ exeId ] ;
231+ if ( info . low !== info . high ) {
232+ html += ' <span style="font-weight:normal">[' +
233+ info . low . toPrecision ( 3 ) + ' \u2013 ' +
234+ info . high . toPrecision ( 3 ) + ']</span>' ;
235+ }
236+ html += '<br><span style="font-size:0.85em;font-weight:normal;color:#666">' +
237+ 'commit: ' + info . commit ;
238+ if ( info . tag ) { html += ' tag: ' + info . tag ; }
239+ html += '</span>' ;
240+ }
241+ html += '</div>' ;
242+ } ) ;
243+ return html ;
244+ }
245+
191246 // Per-series options: baseline gets thinner line, no points
192247 var seriesOpts = { } ;
193248 built . labels . slice ( 1 ) . forEach ( function ( lbl , i ) {
@@ -211,6 +266,8 @@ function renderPlot(data) {
211266 }
212267 } : { } ;
213268
269+ var lastHighlightPoints = null , lastHighlightX = null ;
270+
214271 plotInstance = new Dygraph (
215272 document . getElementById ( 'plot' ) ,
216273 built . data ,
@@ -221,14 +278,23 @@ function renderPlot(data) {
221278 colors : built . colors ,
222279 customBars : true ,
223280 series : seriesOpts ,
224- legend : 'always ' ,
281+ legend : 'never ' ,
225282 ylabel : data . units + data . lessisbetter ,
226283 axes : {
227284 x : xAxisOpts ,
228285 y : { valueRange : [ 0 , null ] }
229286 } ,
230287 connectSeparatedPoints : true ,
231288 highlightSeriesOpts : { strokeWidth : 3 } ,
289+ highlightCircleSize : 5 ,
290+ highlightCallback : function ( e , x , points ) {
291+ lastHighlightX = x ;
292+ lastHighlightPoints = points ;
293+ } ,
294+ unhighlightCallback : function ( ) {
295+ lastHighlightPoints = null ;
296+ $ ( "#dygraph-tooltip" ) . hide ( ) ;
297+ } ,
232298 // Navigate to changes page on point click
233299 clickCallback : function ( e , x , points ) {
234300 var dk ;
@@ -250,6 +316,38 @@ function renderPlot(data) {
250316 }
251317 }
252318 ) ;
319+
320+ $ ( "#plot" ) . on ( "mousemove.tooltip" , function ( e ) {
321+ var $tip = $ ( "#dygraph-tooltip" ) ;
322+ if ( ! lastHighlightPoints ) { $tip . hide ( ) ; return ; }
323+ var THRESHOLD = 0.015 ;
324+ var area = plotInstance . getArea ( ) ;
325+ var mx = e . offsetX !== undefined ? e . offsetX : e . layerX ;
326+ var my = e . offsetY !== undefined ? e . offsetY : e . layerY ;
327+ var close = lastHighlightPoints . some ( function ( pt ) {
328+ if ( pt . canvasx === undefined ) { return false ; }
329+ var dx = ( mx - pt . canvasx ) / area . w ;
330+ var dy = ( my - pt . canvasy ) / area . w ;
331+ return Math . sqrt ( dx * dx + dy * dy ) < THRESHOLD ;
332+ } ) ;
333+ if ( close ) {
334+ var chartOffset = $ ( "#plot" ) . offset ( ) ;
335+ var absX = chartOffset . left + mx ;
336+ var onRight = absX > $ ( window ) . width ( ) / 2 ;
337+ $tip . html ( legendFormatter ( { x : lastHighlightX , series : lastHighlightPoints . map ( function ( pt ) {
338+ return { color : colorMap [ pt . name ] || '#333' ,
339+ labelHTML : pt . name , name : pt . name ,
340+ y : pt . yval , isVisible : true } ;
341+ } ) } ) ) ;
342+ var tipW = $tip . outerWidth ( ) ;
343+ $tip . css ( {
344+ left : onRight ? ( absX - tipW - 18 ) + 'px' : ( absX + 18 ) + 'px' ,
345+ top : ( chartOffset . top + my - 10 ) + 'px'
346+ } ) . show ( ) ;
347+ } else {
348+ $tip . hide ( ) ;
349+ }
350+ } ) ;
253351}
254352
255353function renderMiniplot ( plotid , data ) {
0 commit comments