8e1d11e9de8d0b27b4d31169e7cf2994d3821533
hiram
  Mon Jan 25 13:43:14 2021 -0800
correctly measuring size of text for popUp sizing refs #21980

diff --git src/hg/js/hgTracks.js src/hg/js/hgTracks.js
index fadcbd4..29f4f96 100644
--- src/hg/js/hgTracks.js
+++ src/hg/js/hgTracks.js
@@ -4467,30 +4467,33 @@
 ///////////////////////////////////////
 var mouseOver = {
 
     items: {},  // items[trackName][] - data for each item in this track
     visible: false,     // keeping track of popUp window visibility
     tracks: {}, // tracks[trackName] - number of data items for this track
     trackType: {},	// key is track name, value is track type from hgTracks
     jsonUrl: {},       // list of json files from hidden DIV elements
     maximumWidth: {},   // maximumWidth[trackName] - largest string to display
     popUpDelay: 200,   // 0.2 second delay before popUp appears
     popUpTimer: null,// handle from setTimeout to use in clearTimout(popUpTimer)
     delayDone: true,   // mouse has not left element, still receiving move evts
     delayInProgress: false,        // true while waiting for delay timer
     mostRecentMouseEvt: null,   // to use when mouse delay is finished
     browserTextSize: 12,        // default if not found otherwise
+    measureTextBox: null,
+    noDataString: "no data",	// message for no data at this position
+    noDataSize: 0,	// will be set to size of text 'no data'
 
     // items{} - key name is track name, value is an array of data items
     //           where the format of each item can be different for different
     //           data tracks.  For example, the wiggle track is an array of:
     //                   objects: {x1, x2, v, c}
     //           where [x1..x2) is the array index where the value 'v'
     //           is found, and 'c' is the data value count in this value
     //           i.e. when c > 1 the value is a 'mean' of 'c' data values
     // visible - keep track of window visible or not, value: true|false
     //           shouldn't need to do this here, the window knows if it
     //           is visible or not, just ask it for status
     // tracks{}  - tracks that were set up initially, key is track name
     //             value is the number of items (for debugging)
     // maximumWidth{} - key is track name, value is length of longest
     //                  number string as measured when rendered
@@ -4630,31 +4633,31 @@
     // the graphOffset is the index (x coordinate) into the 'items' definitions
     //  of the data value boxes for the graph.  The magic number three
     //   is used elsewhere in this code, note the comment on the constant
     //   LEFTADD.
     var graphOffset = Math.max(0, clientX - tdLeft - 3);
     if (hgTracks.revCmplDisp) {
        graphOffset = Math.max(0, rightSide - clientX);
     }
 
     var windowUp = false;     // see if window is supposed to become visible
     var foundIdx = -1;
     if (mouseOver.items[trackName]) {
        foundIdx = mouseOver.findRange(graphOffset, mouseOver.items[trackName]);
     }
     // can show 'no data' when not found
-    var mouseOverValue = "no data";
+    var mouseOverValue = mouseOver.noDataString;
     if (foundIdx > -1) { // value to display
       if (mouseOver.items[trackName][foundIdx].c > 1) {
         mouseOverValue = " μ " + mouseOver.items[trackName][foundIdx].v + " ";
       } else {
         mouseOverValue = " " + mouseOver.items[trackName][foundIdx].v + " ";
       }
     }
     $('#mouseOverText').html(mouseOverValue);
     var msgWidth = mouseOver.maximumWidth[trackName];
     $('#mouseOverText').width(msgWidth);
     var msgHeight = Math.ceil($('#mouseOverText').height());
     var lineHeight = Math.max(0, tdHeight - msgHeight);
     var lineTop = Math.max(0, tdTop + msgHeight);
     var msgLeft = Math.max(tdLeft, clientX - (msgWidth/2) - 3); // with magic 3
     msgLeft = Math.min(msgLeft, rightSide - msgWidth);  // right border limit
@@ -4698,30 +4701,44 @@
           mouseOver.mouseInTrackImage(evt);	// OK to trigger event now
           return;
         } else {
           return; // wait for delay to be done
         }
       }
       mouseOver.delayDone = false;
       mouseOver.delayInProgress = true;
       if (mouseOver.popUpTimer) {
          clearTimeout(mouseOver.popUpTimer);
          mouseOver.popUpTimer = null;
       }
       mouseOver.popUpTimer = setTimeout(mouseOver.delayCompleted, mouseOver.popUpDelay);
     },
 
+    // given a string of text, return width of rendered text size
+    // using an off-screen span element that is created here first time through
+    getWidthOfText: function (measureThis)
+    {
+    if(mouseOver.measureTextBox === null){  // set up first time only
+        mouseOver.measureTextBox = document.createElement('span');
+        var cssText = "position: fixed; width: auto; display: block; text-align: right; left:-999px; top:-999px; font-style:normal; font-size:" + mouseOver.browserTextSize + "px; font-family:" + jQuery('body').css('font-family');
+        mouseOver.measureTextBox.style.cssText = cssText;
+        document.body.appendChild(mouseOver.measureTextBox);
+    }
+    mouseOver.measureTextBox.innerHTML = measureThis;
+    return Math.ceil(mouseOver.measureTextBox.clientWidth);
+    },
+
     // =======================================================================
     // receiveData() callback for successful JSON request, receives incoming
     //             JSON data and gets it into global variables for use here.
     //  The incoming 'arr' is a a set of objects where the object key name is
     //      the track name, used here as an array reference: arr[trackName]
     //        (currently only one object per json file, one file for each track,
     //           this may change to have multiple tracks in one json file.)
     //  The value associated with each track name
     //  is an array of span definitions, where each element in the array is a
     //     mapBox definition object:
     //        {x1:n, x2:n, value:s}
     //        where n is an integer in the range: 0..width,
     //        and s is the value string to display
     //     Will need to get them sorted on x1 for efficient searching as
     //     they accumulate in the local data structure here.
@@ -4761,42 +4778,37 @@
          if (lenV > lengthLongestNumberString) {
 	   lengthLongestNumberString = lenV;
 	   longestNumber = arr[trackName].d[datum].v;
          }
         mouseOver.items[trackName].push(arr[trackName].d[datum]);
        ++itemCount;
       }
       mouseOver.tracks[trackName] = itemCount;	// != 0 -> indicates valid track
       var mouseOverValue = "";
       if (hasMean) {
          mouseOverValue = " μ " + longestNumber + " ";
       } else {
          mouseOverValue = " " + longestNumber + " ";
       }
       $('#mouseOverText').css('fontSize',mouseOver.browserTextSize);
-      $('#mouseOverText').html(mouseOverValue);	// see how big as rendered
-      var maximumWidth = Math.ceil($('#mouseOverText').width());
-      $('#mouseOverText').html("no data");	// might be bigger
-      if (Math.ceil($('#mouseOverText').width() > maximumWidth)) {
-          maximumWidth = Math.ceil($('#mouseOverText').width());
-      }
-      // XXX this is not working when there are two wiggle tracks in display
-      //     and the window is dragged left or right.  The track where the
-      //     drag takes place seems to be dominate somehow and it's width
-      //     calculation overrides the other track width calculation
-      //     even though the .html() has been set here correctly, the
-      //     width() comes back with the dragged track value ?
+      var maximumWidth = mouseOver.getWidthOfText(mouseOverValue);
+      if ( 0 === mouseOver.noDataSize) {  // only need to do this once
+        mouseOver.noDataSize = mouseOver.getWidthOfText(mouseOver.noDataString);
+      }
+      if (mouseOver.noDataSize > maximumWidth) {
+          maximumWidth = mouseOver.noDataSize;
+      }
       mouseOver.maximumWidth[trackName] = maximumWidth;
       }
     },  //      receiveData: function (arr)
 
     failedRequest: function(url)
     {   // failed request to get json data, remove it from the URL list
       if (mouseOver.jsonUrl[url]) {
         delete mouseOver.jsonUrl[url];
       }
     },
 
     // =========================================================================
     // fetchJsonData() sends JSON request, callback to receiveData() upon return
     // =========================================================================
     fetchJsonData: function (url)