0

thanks in advance for checking out this question. I have created a D3 choropleth map and I am having trouble trying figure out why the hover tooltip is having some issues in regards to where the map is located within the browser (e.g. scrolling up and down the page and moving the map upwards/downwards). To begin, here is the code that I am working with, "choropleth.tag":

<%@ include file="/WEB-INF/jsp/site/taglib.jsp"%>
<%@ attribute name="baseDataUrl" required="true" %>
<%@ attribute name="domain" required="true" %>
<%@ attribute name="id" required="true" %>
<%@ attribute name="dataInputLabel" required="false" %>

<div class="container">
 <div class="row">
  <div class="chropleth-map">
   <svg id="${id}-d3-choroplethmap" viewBox="-120 -150 1200 700" class="d3-map-animation" preserveAspectRatio="xMidYMid meet"></svg>
   <!--<svg id="${id}-svgkey" viewBox="650 -25 150 50" class="fade-in-slow-2 svgkey"></svg>-->
  </div>
  <div class="col-xs-6 tooltip" style="opacity: 0;"></div>
 </div>
</div>

<script src="<c:url value="/js/libs/d3v4/d3.v4.min.js"/>"></script>
<script src="<c:url value="/js/libs/d3v4/d3-scale-chromatic.v1.min.js"/>"></script>
<script src="<c:url value="/js/libs/d3v4/topojson.v2.min.js"/>"></script>
<script src="https://d3js.org/d3-axis.v1.min.js"></script>

<script type="text/javascript">

    var panZoomMaps;
    var drawMapFunctions;
    
 panZoomMaps = ( typeof panZoomMaps !== 'undefined' && panZoomMaps instanceof Array ) ? panZoomMaps : [];
 drawMapFunctions = ( typeof drawMapFunctions !== 'undefined' && drawMapFunctions instanceof Array ) ? drawMapFunctions : [];
     
 $.EventBus("currentOrganizationChange").subscribe(function(){
  /*panZoomMaps["${id}"].destroy();*/
  $("#${id}-d3-choroplethmap").empty();
        drawMapFunctions["${id}"]();
    });

    drawMapFunctions["${id}"] = function (){
        var svgMap = d3.select("#${id}-d3-choroplethmap");
   width = +svgMap
    .attr("width");
   height = +svgMap
    .attr("height");
  var cbsaMap = d3.map();
  var color = d3.scaleLinear().domain(${domain}).range(d3.schemeBlues[${domain}.length])
  var orgId = sessionStorage.getItem("currentOrganizationId");

  if (!orgId){
    orgId = -1;
   }
   d3.queue()
   .defer(d3.json, "${contextRoot}/2013_us.topojson.json")
   .defer(d3.json, "${baseDataUrl}/"+orgId)
   .await(ready);

  function ready(error, cbsa, d) {

   var width = 960,
        height = 500,
        centered;   
   var projection = d3.geoAlbersUsa()
       .scale(1070)
       .translate([width / 2, height / 2]);   
   var path = d3.geoPath().projection(projection);
   var svg = d3.select("#${id}-d3-choroplethmap");
      width = +svg
      .attr("width");
      height = +svg
      .attr("height");  
   svg.append("rect")
       .attr("class", "d3MapBackground")
       .attr("width", width)
       .attr("height", height)
       .on("click", clicked); 
   
   var g = svg.append("g");   
   
   d3.json("${contextRoot}/2013_us.topojson.json", function(error, us) {
      if (error) throw error;

   });  

   function clicked(d) {
      var x, y, k;

      if (d && centered !== d) {
        var centroid = path.centroid(d);
        x = centroid[0];
        y = centroid[1];
        k = 4;
        centered = d;
        $('#btn').click(function(){
            var btn = $(this);
            $.post(/*...*/).complete(function(){
                btn.prop('disabled', false);
            });
            btn.prop('disabled', true);

        });
        $("#dialogBox, #mobileDialogBox").css("display", "block").css("visibility", "visible").css("height", "104");
        
      } else {
        x = width / 2;
        y = height / 2;
        k = 1;
        centered = null;
        $("#dialogBoxCloseButton, #mobileDialogBoxCloseButton").click();
        $("#dialogBox, #mobileDialogBox").css("display", "table-column").css("visibility", "hidden").css("height", "0");
      }
    d3.select("#${id}-d3MapCbsa").selectAll("path")
     .classed("active", centered && function(d) { return d === centered; });
       
    d3.select("#${id}-d3MapState").transition()
     .duration(750)
     .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")")
     .attr("transform-origin", "-160px -60px 10px")
     .style("stroke-width", 1.5 / k + "px");
       
    d3.select("#${id}-d3MapCbsa").transition()
     .duration(750)
     .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")")
     .attr("transform-origin", "-160px -60px 10px")
     .style("stroke-width", 1.5 / k + "px");
       
    d3.select("#${id}-d3MapStateBorder").transition()
     .duration(750)
     .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")")
     .attr("transform-origin", "-160px -60px 10px")
     .style("stroke-width", 1.5 / k + "px");
   } 
     
      var tooltip = d3.select("body").append("div") 
           .attr("class", "tooltip")       
           .style("opacity", 0);
   var us = cbsa;
   var svgMap = d3.select("#${id}-d3-choroplethmap");
   var mouseDownTime=new Date().getTime();
   var prevMouseUp=new Date().getTime();
   var clicks=0;
   var timeout;
   var path = d3.geoPath().projection(projection);
   var stateAndMicroColor= "#cccccc";

   if (error)
    throw error;

   for (var i=0; i<d.length; i++){
       cbsaMap.set(d[i].cbsaId, +d[i].num);
   }    
   
   svgMap.append("g")
    .attr("class", "state")
    .attr("id", "${id}-d3MapState")
    .selectAll("path").data(topojson.feature(us, us.objects.states).features)
    .enter()
    .append("path")
    .attr("fill", function(){
     return stateAndMicroColor;
    })
    .attr("d", path)
    .attr("pointer-events", "none")
    .on("click", clicked);

   svgMap.append("g")
    .attr("class", "cbsa")
    .attr("id", "${id}-d3MapCbsa")
    .selectAll("path")
    .data(topojson.feature(us, us.objects.cbsas).features)
    .enter().append("path")
    .attr("fill", function(d){
      if(cbsaMap.has(d.properties.id)){
       return color(cbsaMap.get(d.properties.id));
      }
      return stateAndMicroColor;
     })
    .attr("stroke", function(){
     return "#ffffff";
    })
    .attr("stroke-width", 0.5)
    .attr("d", path)
    .on("click", clicked)
    .on("mouseover", function(d) {    
     var data = cbsaMap.get(d.properties.id)
              tooltip.transition()    
              .duration(200)    
              .style("opacity", .9);
     tooltip.html("${dataInputLabel}: "+ data)
              .style("left", (d3.event.pageX + 20) + "px")
              .style("top", (d3.event.pageY - 45) + "px");
            })          
    .on("mouseout", function(d) {   
              tooltip.transition()    
              .duration(500)    
              .style("opacity", 0); 
    })
    .on("mousedown", function(){
     mouseDownTime=new Date().getTime();
     clearTimeout(timeout);
    })
    .on("mouseup", function(d) {
     clicks++;
     
      if (clicks === 1){
       var x = d3.event.pageX - document.getElementById("${id}-d3-choroplethmap").getBoundingClientRect().x + 700;
       var y = d3.event.pageY - document.getElementById("${id}-d3-choroplethmap").getBoundingClientRect().y + 380;
       timeout = setTimeout(function () {
 
        
         if(new Date().getTime() - mouseDownTime < 250 && cbsaMap.has(d.properties.id)){
         $('#dialogText, #mobileDialogText').text("Header");
         $('#dialogText, #mobileDialogText').html("The ID of this Core Based Statistical Area (CBSA) is <b>" + d.properties.id + "</b>.<br/> There are a total of <b>" + cbsaMap.get(d.properties.id) + "</b> ${dataInputLabel}. ");
 
         /* NOTE: The following function works on smaller devices and viewports with the help of the "#dialogBox" media query.  The dialog box is stuck to the bottom of the map and is updated on CBSA clicking.  Space is made or reduced to fit the dialog box. */
         /* $('#dialogBox').show().draggable(); */
         
         /* NOTE: The following jQuery funtion works on desktop screensizes and not on mobile or tablet.  How can the .offset({}) be adjusted for different screensizes like CSS media queries? */
         $('#dialogBox, #mobileDialogBox').show().offset({top: y,left: x}).draggable();       
         
         $('#dialogBoxCloseButton, #mobileDialogBoxCloseButton').click(function() {
          $('#dialogBox, #mobileDialogBox').hide();
         });
         
        }      
        clicks=0;
       }, 100);
      } else if (clicks === 2) {
       clearTimeout(timeout);
       $("#dialogBoxCloseButton, #mobileDialogBoxCloseButton").click();
       clicks = 0;
      }
     prevMouseUp=new Date().getTime();
    })
   ;

   svgMap.append("g")
    .attr("class", "stateBorder")
    .attr("id", "${id}-d3MapStateBorder")
    .selectAll("path").data(topojson.feature(us, us.objects.states).features)   
    .enter().append("path")
    .attr("fill", function(){
     return "transparent";
   })
    .attr("stroke", "#fff")
    .attr("stroke-width", 0.8)
    .attr("d", path)
    .attr("pointer-events", "none")
    .on("click", clicked);   
  }
 }
    drawMapFunctions["${id}"]();

 /* Closing of the D3 map dialog box upon clicking each of the four CTA's. */
  $('#cardholdersCTA, #transactionsCTA, #ordersCTA, #pointsCTA' ).click(function(){
      $("#dialogBoxCloseButton, #mobileDialogBoxCloseButton").click();
  });  

</script>

So to begin, on initial page load, the map is generated and if you hover any CBSA/geographic area within the United States, the DIV tooltip is properly generated and located near the mouse cursor. The problem begins when the user scrolls down the page. At that point, if the user hovers over the same map area. the tooltip is located in the same area within the browser window and no logner within the map. It is almost like the tooltip is not being placed based on the mouse cursor and instead, the dimensions of the bowser window. I believe the issue lies in the code:

.style("left", (d3.event.pageX + 20) + "px")
.style("top", (d3.event.pageY - 45) + "px");

The whole map is fully responsive except for this one issue. Here are a few screenshots of the cursor being hovered over the CBSA area near Denver, Colorado and scrolling down the page:

Initial page load and map presentation; hovering over Denver, Colorado:

Hovering and Scrolling no. 1

I scroll down the page and then hover again over Denver, Colorado; tooltip is in the same location within the browser but it should be located near where the mouse is, Denver, Colorado:

Hovering and Scrolling no. 2

I scroll further and then hover again over Denver, Colorado:

Hovering and Scrolling no. 3

...and another screenshot of scrolling down the page and then rehovering over Denver.

Hovering and Scrolling no. 4

So basically, it seems like the tooltip is being placed based on the dimensions of the browser and not the map. What am I missing? How can I fix this? Any suggestions? Thank you so much for your help and insight. I greatly appreciate it.

UPDATE: Here is what I changed to the mouse over function:

var scrollNumber = window.pageYOffset;
var data = cbsaMap.get(d.properties.id)
  tooltip.transition()    
    .duration(200)    
    .style("opacity", .9);
  tooltip.html("${dataInputLabel}: "+ data)
    .style("left", (d3.event.pageX + 20) + "px")
    .style("top", (d3.event.pageY - scrollNumber - 45) + "px"); 
Pete Adam Bialecki
  • 183
  • 1
  • 3
  • 9
  • have you considered taking the `window.scrollOffset` into account when positioning the tooltip? the style `top` is relative to the page top. – rioV8 Oct 01 '18 at 01:10
  • Thanks @rioV8 but I am still having some trouble. I've been reading about `window.scrollOffset` but I am not sure where and how to implement it. Would it be within `drawMapFunctions["${id}"]` or at the end? How can I set the value of `top` on the hover tooltip based on where the user scrolls up/down? Thank you so much for your help. – Pete Adam Bialecki Oct 01 '18 at 20:48
  • set `top` equal to `pageY+scrollOffset` – rioV8 Oct 01 '18 at 20:54
  • @riov8 With your suggestion, in the `.on("mouseover", function(d)` I wrote the following code within `.style("top", (d3.event.pageY + scrollOffset - 45) + "px");`. This gave me an error _info:1652 Uncaught ReferenceError: scrollOffset is not defined_. – Pete Adam Bialecki Oct 02 '18 at 15:24
  • So I defined `var d3MapContainer = $( "#d3-maps-container" );` and `var scrollOffset = d3MapContainer.offset();` after the mouseover function that I mentioned before and now there is no console error and the tooltip no longer has a top element. I feel like it is almost there but it is still off. Any other suggestions. Thanks again @rioV8. – Pete Adam Bialecki Oct 02 '18 at 15:30
  • I also tried `.style("top", (d3.event.pageY + document.documentElement.scrollTop - 45) + "px");` and `(d3.event.pageY + window.pageYOffset - 45)` but these two methods also did not work (no console errors as well). – Pete Adam Bialecki Oct 02 '18 at 16:29
  • maybe it is `pageYOffset` https://stackoverflow.com/a/3464890/9938317 – rioV8 Oct 02 '18 at 16:37
  • I found a solution. It is `window.pageYOffset`. I wrote out the updated code above. Thank you so much for your help. You're awesome. – Pete Adam Bialecki Oct 02 '18 at 17:06

0 Answers0