Skip to content
Advertisement

Tooltip in worldmap created via d3.js

I have created a worldmap using the d3 and now able to create the specific countries to have hover effect , however I have also created the tooltip what I want to do now is to get the country map in the tooltip (the country which is hovered) i have used d3 v4 to do all this.

I have made changes suggested by CodeSmit but it seems I’m missing a lot of things.

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <script src="https://d3js.org/d3.v4.min.js"></script>
  <script src="https://d3js.org/topojson.v2.min.js"></script>
  <script src="https://d3js.org/d3-queue.v3.min.js"></script>
  <style>
  
#country_name{
    border:none;
}  
.bigmap {
    width: 100%;
    height: 100%;
    position: center;
    background-color: #080e1e;
    float:right;
}
.hidden {
      display: none;
}
div.tooltip {
      color: #222; 
      background: #19202d;
      border-radius: 3px; 
      box-shadow: 0px 0px 2px 0px #a6a6a6; 
      padding: .2em; 
      text-shadow: #f5f5f5 0 1px 0;
      opacity: 0.9; 
      position: absolute;
      }
      
div {
    color: #fff;
    font-family: Tahoma, Verdana, Segoe, sans-serif;
    padding: 5px;
}
.container {
    display:flex;
}
.fixed {
    text-align:center;
     border-width: 1px;
     vertical-align:middle;
    border-style: solid;
    border-color:#55a4bf ;
    width: 80px;
    margin-right:10px;
}
.flex-item {
 border-width: 1px;
 text-align:center;
 vertical-align:middle;
    border-style: solid;
    border-color:#55a4bf ;
    //background-color:#7887AB;
    width: 120px;
}
  </style>
</head>
<svg class= "bigmap"width="760" height="340"></svg>
<div class="tooltip"></div>
<body>
<script>
var margin = {top: 0, right: 10, bottom: 10, left: 10};
var width = 760 ;
var height = 400 ;
var projection = d3.geoNaturalEarth1()
                   .center([0, 15]) 
                   .rotate([-11,0])
                   .scale([150]) 
                   .translate([750,350]);
var path = d3.geoPath()
             .projection(projection);;
var svg = d3.select(".bigmap")
            .append("g")
            .attr("width", width)
            .attr("height", height);
        

var tooltip = d3.select("div.tooltip");
d3.queue()
  .defer(d3.json, "https://cdn.rawgit.com/mbostock/4090846/raw/d534aba169207548a8a3d670c9c2cc719ff05c47/world-110m.json")
    .defer(d3.tsv, "https://cdn.rawgit.com/mbostock/4090846/raw/d534aba169207548a8a3d670c9c2cc719ff05c47/world-country-names.tsv")
  .await(ready);
function ready(error, world, names) {
  if (error) throw error;
  var countries1 = topojson.feature(world, world.objects.countries).features;
    countries = countries1.filter(function(d) {
    return names.some(function(n) {
      if (d.id == n.id) return d.name = n.name;
    })});
    console.log("countries",countries);
  var arr = ["India","Sri Lanka","Afghanistan","Russian Federation"];
  svg.selectAll("path")
            .data(countries.filter(d => d.name !== "Antarctica"))
            .enter()
            .append("path")
            .attr("stroke","#080e1e")
            .attr("stroke-width",1)
            .attr("fill", "#0d2331")
            .attr("d", path )
            .on("mouseover",function(d,i){
                if (arr.includes(d.name)){
                var tipSVG = tooltip.append("svg")
                                    .attr("width", 220)
                                    .attr("height", 55);
                var bbox =tipSVG.append("path")
                        .attr("stroke","#080e1e")
                        .attr("stroke-width",1)
                        .attr("fill", "#0d2331")
                        .attr("d", path(d) )
                        .node().getBBox()
                tipSVG.attr("viewBox", `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`);
                tooltip.classed("hidden", false)
                       .attr("fill","#19202d")
                       .style("top", 150+ "px")
                       .style("left", 20 + "px")
                       .html('<div class="container"><div class="fixed" id="country_name">'+d.name+'</div><div class="flex-item">Fixed 2</div></div><div class="container"><div class="fixed">Fixed 3</div><div class="flex-item">Fixed 4</div></div><div class="container"><div class="fixed">Fixed 5</div><div class="flex-item">Fixed 6</div></div><div class="container"><div class="fixed">Fixed 7</div><div class="flex-item">Fixed 8</div></div>');

                d3.select(this).attr("fill","#0d2331").attr("stroke-width",2).style("stroke","#b72c2f")
                return tooltip.style("hidden", false).html(d.name);
                }
            })
            .on("mousemove",function(d){
                
            })
            .on("mouseout",function(d,i){
                if (arr.includes(d.name)){
                d3.select(this).attr("fill","#0d2331").attr("stroke-width",1).style("stroke","#080e1e")
                tooltip.classed("hidden", true);
                }
            });
    var g = svg.append('g');
    g.selectAll('circle')
            .data(countries)
          .enter().append('circle')
          .attr("fill","white")
            .attr('transform', function(d) { return 'translate(' + path.centroid(d) + ')'; })
            .attr('r',function(d){
                if (arr.includes(d.name)){
                    return "3"
                }
                return "0";
                }
            
            
            );
};

</script>
</body>

Any guidance or help is greatly appreciated and thanks in advance

Advertisement

Answer

TL;DR:

The .html method on D3 selections first deletes anything that’s already inside those elements before setting the new contents. Thus, to initiate an element’s base HTML content with .html, be sure to call it first before adding anything else to the element, and also do not call .html later on, or risk it overwriting anything that was added to it.


You’re close. You’ve got a number of issues though.

1. d3-tip Not Used

You’re including the d3-tip library, but you’re not making real use of it at all. Because of this, it’s adding to the confusion. You have your own <div class="tooltip"></div> which is what actually appears. If you don’t need the tooltip to float where the cursor is (which is what d3-tip is for), then I’d highly recommend starting by stripping out all your code making use of this library.

2. <svg> Doesn’t Make It Into Tooltip

Your “mouseover” event fails to add the country SVG element for two reasons:

First, because you’re selecting the #tipDiv element which never appears since it’s part of the d3-tip code that doesn’t get used. To fix this, I think you want to select the div.tooltip element instead. Since you already have the ‘tooltip’ variable set to this, you don’t need d3.select; you can simply do:

var tipSVG = tooltip.append("svg")
    .attr("width", 220)
    .attr("height", 55);

At this point, it will add the <svg>, but the issue is that, following this, it is immediately written over. This happens in the “mousemove” event:

tooltip.classed("hidden", false)
    ...
    .html('<div class="container">...</div>');

As soon as the mouse moves over the element, the .html call overwrites the newly added svg, deleting it. I was able to fix this by moving this block of code out of the “mousemove” event and to the start of the “mouseover” event.

After this, the SVG successfully appears in the tooltip.

3. Tooltip SVG Contains Entire Map (But Looks Empty)

Before you’re done, though, at this point the newly appearing SVG looks blank. It’s actually not blank, but all the contents are appearing outside the SVG’s rendering region. Before fixing this, though, first note you’re loading the entire map into the SVG in your “mouseover” event:

tipSVG.selectAll("path")
    .data(countries.filter(d => d.name !== "Antarctica"))
    .enter()
    .append("path")
    .attr("stroke","#080e1e")
    .attr("stroke-width",1)
    .attr("fill", "#0d2331")
    .attr("d", path )

I’m guessing you want to load just the country being hovered over? Since you already have that data with ‘d’ in the function, you can do that with something like:

tipSVG.append("path")
    .attr("stroke","#080e1e")
    .attr("stroke-width",1)
    .attr("fill", "#0d2331")
    .attr("d", path(d) )

Note now I’m calling ‘path’ right on the data.

4. Country Path Way Out Of SVG View

So now the SVG only contains the country being hovered over, which makes fixing the last issue much easier: it’s way outside the bounding box. Because it’s a single path now, a simple fix is to set the SVG’s ‘viewBox’ to the path’s bounding box, which you can get by calling getBBox right on the path element (not the d3 selection of it though).

You can do this like so:

var bbox = tipSVG.append("path")
    ...
    .attr("d", path(d) )
    .node().getBBox(); // get the bbox from the path we just added

tipSVG.attr("viewBox", `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`);

This effectively copies the bbox into the viewBox, and fortunately for us, due to the default value of the preserveAspectRatio <svg> attribute, this nicely centers the country in the SVG box:

Centered country on hover


EDIT:

So, now you’re very close! You’ve just got a couple things wrong:

1. .html() Called After SVG Insert

So, the code you moved out of the “mousemove” should be placed at the beginning of the “mouseover” event. Here’s what’s happening:

// code appends the new SVG element
var tipSVG = tooltip.append("svg")
  .attr("width", 220)
  .attr("height", 55);
// creates the path in the SVG and gets the bounding box
var bbox = tipSVG.append("path")
  // ...
  .node().getBBox()
// makes the SVG view the path
tipSVG.attr("viewBox", `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`);

// DELETES the SVG once .html is called
tooltip.classed("hidden", false)
  // ...
  .html('<div class="container">...</div>');

Again, .html calls replace everything in the element. Generally you want to use the method sparingly. In this case, it’s replacing the content and the newly added SVG with content that doesn’t have the SVG. You can still keep the .html call, but it must be moved above the SVG append call so it doesn’t delete the SVG:

// replaces all contents with tooltip html
tooltip.classed("hidden", false)
  // ...
  .html('<div class="container">...</div>');

// code appends the new SVG element
var tipSVG = tooltip.append("svg")
  .attr("width", 220)
  .attr("height", 55);
// creates the path in the SVG and gets the bounding box
var bbox = tipSVG.append("path")
  // ...
  .node().getBBox()
// makes the SVG view the path
tipSVG.attr("viewBox", `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`);

// now SVG exists

2. .html Called After SVG Insert

So, the second issue is, yes, the same issue:

return tooltip.style("hidden", false).html(d.name);

This line, at the end, re-calls .html on your tooltip, which has the same effect. In this case, though, it replaces everything with just the country name.

To be honest, I’m not exactly sure of the intent of this line.

  1. Unless I’m mistaken, returning a value in an event listener in D3 has no specific effect.
  2. .style is used for applying CSS styles, but “hidden” is not a CSS style. Perhaps you meant .classed?
  3. The .html call clears the well-setup tooltip with just the name. If you were looking to replace just the country name, remember it’s under the subelement #country_name. So, you could do so with, e.g., tooltip.select("#country_name").text(d.name). Note, I used .text which safely escapes HTML code.

Also note you’re already embedding the country name in the original .html call, making it unnecessary. The safest option, though, would be to actually remove the d.name from the other .html call, just leaving it <div class="fixed" id="country_name"></div>, and then right after the .html call, include the .text code I have above. You could even link it in D3 style like so,

tooltip.classed("hidden", false)
  .attr("fill","#19202d")
  .style("top", 150+ "px")
  .style("left", 20 + "px")
  // .html line does not contain d.name embed:
  .html('<div class="container">...</div>')
  .select("#country_name")
    .text(d.name);

Using your updated code, I was able to get this to work for me.

Keep up the good work!

User contributions licensed under: CC BY-SA
2 People found this is helpful
Advertisement