Collectives™ on Stack Overflow

Find centralized, trusted content and collaborate around the technologies you use most.

Learn more about Collectives

Teams

Q&A for work

Connect and share knowledge within a single location that is structured and easy to search.

Learn more about Teams

I have a working world map built with D3 based on this one . Here is my version on CodePen .

Edit: CodePen has now been updated with the fix. Code snippet below is the original, broken code.

I have tooltips properly displaying on hover over 15 specific countries and I have a sidebar legend listing the countries.

The Goal:

I'd like to display each country's tooltip, in its current place on the map, when that country's name is hovered in the legend.

All the JavaScript after // start remote tooltip attempt is my attempt at doing so but I'm having trouble passing the data into the tooltip in the same way it gets passed while hovering over the country on the map. Any ideas on how this could be accomplished?

Thanks!

Relevant code copied from the CodePen here...

Original broken code:

const stat1Variable = "stat1";
const stat2Variable = "stat2";
const stat3Variable = "stat3";
const stat4Variable = "stat4";
const geoIDVariable = 'id';
const format = d3.format(',');
// Set tooltips
const tip = d3
  .tip()
  .attr('class', 'd3-tip')
  .offset([-10, 0])
  .html(
      `<h4>${d.properties.name}</h4>
       <div class='tooltip-top'>
         <p>1: <span>${d.id}</span></p>
         <p>2: <span>${format(d[stat1Variable])}</span></p>
       <div class='tooltip-bottom'>
         <ul class='tooltip-list'>
           <li>3: <span>${d[stat2Variable]}</span></li>
           <li>4: <span>${d[stat3Variable]}</span></li>
           <li>5: <span>${d[stat4Variable]}</span></li>
       </div>`
tip.direction(function(d) {
  if (d.properties.name === 'Antarctica') return 'n'
  // Americas
  if (d.properties.name === 'Greenland') return 's'
  if (d.properties.name === 'Canada') return 'e'
  if (d.properties.name === 'USA') return 'e'
  if (d.properties.name === 'Mexico') return 'e'
  // Europe
  if (d.properties.name === 'Iceland') return 's'
  if (d.properties.name === 'Norway') return 's'
  if (d.properties.name === 'Sweden') return 's'
  if (d.properties.name === 'Finland') return 's'
  if (d.properties.name === 'Russia') return 'w'
  // Asia
  if (d.properties.name === 'China') return 'w'
  if (d.properties.name === 'Japan') return 's'
  // Oceania
  if (d.properties.name === 'Indonesia') return 'w'
  if (d.properties.name === 'Papua New Guinea') return 'w'
  if (d.properties.name === 'Australia') return 'w'
  if (d.properties.name === 'New Zealand') return 'w'
  // otherwise if not specified
  return 'n'
tip.offset(function(d) {
  // [top, left]
  if (d.properties.name === 'Antarctica') return [0, 0]
  // Americas
  if (d.properties.name === 'Greenland') return [10, -10]
  if (d.properties.name === 'Canada') return [24, -28]
  if (d.properties.name === 'USA') return [-5, 8]
  if (d.properties.name === 'Mexico') return [12, 10]
  if (d.properties.name === 'Chile') return [0, -15]
  // Europe
  if (d.properties.name === 'Iceland') return [15, 0]
  if (d.properties.name === 'Norway') return [10, -28]
  if (d.properties.name === 'Sweden') return [10, -8]
  if (d.properties.name === 'Finland') return [10, 0]
  if (d.properties.name === 'France') return [-9, 66]
  if (d.properties.name === 'Italy') return [-8, -6]
  if (d.properties.name === 'Russia') return [5, 385]
  // Africa
  if (d.properties.name === 'Madagascar') return [-10, 10]
  // Asia
  if (d.properties.name === 'China') return [-16, -8]
  if (d.properties.name === 'Mongolia') return [-5, 0]
  if (d.properties.name === 'Pakistan') return [-10, 13]
  if (d.properties.name === 'India') return [-11, -18]
  if (d.properties.name === 'Nepal') return [-8, 1]
  if (d.properties.name === 'Myanmar') return [-12, 0]
  if (d.properties.name === 'Laos') return [-12, -8]
  if (d.properties.name === 'Vietnam') return [-12, -4]
  if (d.properties.name === 'Japan') return [5, 5]
  // Oceania
  if (d.properties.name === 'Indonesia') return [0, -5]
  if (d.properties.name === 'Papua New Guinea') return [-5, -10]
  if (d.properties.name === 'Australia') return [-15, 0]
  if (d.properties.name === 'New Zealand') return [-15, 0]
  // otherwise if not specified
  return [-10, 0]
d3.select('body').style('overflow', 'hidden')
const parentWidth = d3
  .select('body')
  .node()
  .getBoundingClientRect().width
const margin = {
  top: 0,
  right: 0,
  bottom: 0,
  left: 0
const width = 960 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
const color = d3
  .scaleQuantile()
  .range([
    '#bbb',
    '#bbb'
const svg = d3
  .select("#vis")
  .append('svg')
  .attr('class', 'svg-map')
  .attr('width', width)
  .attr('height', height)
  .append('g')
  .attr('class', 'map');
const projection = d3
  .geoRobinson()
  .scale(148)
  .rotate([352, 0, 0])
  .translate([width / 2, height / 2]);
const path = d3
  .geoPath()
  .projection(projection);
svg.call(tip);
queue()
  .defer(d3.json, 'https://raw.githubusercontent.com/ChrisBup/quick-files/master/world_countries.json')
  .defer(d3.tsv, 'https://raw.githubusercontent.com/ChrisBup/quick-files/master/country_data.tsv')
  .await(ready);
function ready(error, geography, data) {
  // console.log('geography: ' + geography);
  // console.log('data: ' + data);
  data.forEach((d) => {
    d[stat1Variable] = Number(d[stat1Variable].replace(",", ""));
    d[stat2Variable] = d[stat2Variable].replace(",", "");
    d[stat3Variable] = d[stat3Variable].replace(",", "");
    d[stat4Variable] = d[stat4Variable].replace(",", "");
  const stat1VariableValueByID = {};
  const stat2VariableValueByID = {};
  const stat3VariableValueByID = {};
  const stat4VariableValueByID = {};
  data.forEach((d) => {
    stat1VariableValueByID[d[geoIDVariable]] = d[stat1Variable];
    stat2VariableValueByID[d[geoIDVariable]] = d[stat2Variable];
    stat3VariableValueByID[d[geoIDVariable]] = d[stat3Variable];
    stat4VariableValueByID[d[geoIDVariable]] = d[stat4Variable];
  geography.features.forEach((d) => {
    d[stat1Variable] = stat1VariableValueByID[d.id];
    d[stat2Variable] = stat2VariableValueByID[d.id];
    d[stat3Variable] = stat3VariableValueByID[d.id];
    d[stat4Variable] = stat4VariableValueByID[d.id];
  // calculate ckmeans clusters
  // then use the max value of each cluster
  // as a break
  const numberOfClasses = color.range().length - 1;
  const ckmeansClusters = ss.ckmeans(
    data.map(d => d[stat1Variable]),
    numberOfClasses
  const ckmeansBreaks = ckmeansClusters.map(d => d3.max(d));
  // console.log('numberOfClasses', numberOfClasses);
  // console.log('ckmeansClusters', ckmeansClusters);
  // console.log('ckmeansBreaks', ckmeansBreaks);
  // set the domain of the color scale based on our data
  color.domain(ckmeansBreaks);
    .append('g')
    .attr('class', 'countries')
    .selectAll('path')
    .data(geography.features)
    .enter()
    .append('path')
    .attr('class', d => {
      return d.id.toLowerCase();
    .attr('d', path)
    .style('fill', d => {
      if (typeof stat1VariableValueByID[d.id] !== 'undefined') {
        // return color(stat1VariableValueByID[d.id]);
        return "lightgreen"
      return 'white';
    .style('fill-opacity', 0.8).style('stroke', d => {
      if (d[stat1Variable] !== 0) {
        return '#666';
      return '#666';
    .style('stroke-width', 1)
    .style('stroke-opacity', 0.5)
    // tooltips
    .on('mouseover', function (d) {
      console.log("mouseover d: " + d);
      if (typeof stat1VariableValueByID[d.id] !== "undefined") {
        tip.show(d);
        d3.select(this)
          .style('fill-opacity', 1)
          .style('stroke-opacity', 1)
          .style('stroke-width', 2);
    .on('mouseout', function (d) {
      console.log("mouseout d: " + d);
      if (typeof stat1VariableValueByID[d.id] !== "undefined") {
        tip.hide(d);
        d3.select(this)
          .style('fill-opacity', 0.8)
          .style('stroke-opacity', 0.5)
          .style('stroke-width', 1);
    .append('path')
    .datum(topojson.mesh(geography.features, (a, b) => a.id !== b.id))
    .attr('class', 'names')
    .attr('d', path);
// start remote tooltip attempt
queue()
  .defer(d3.json, "https://raw.githubusercontent.com/ChrisBup/quick-files/master/world_countries.json")
  .defer(d3.tsv, "https://raw.githubusercontent.com/ChrisBup/quick-files/master/country_data.tsv?1")
  .await(remoteTooltip);
function remoteTooltip(error, geography, data) {
  let listItems = d3.selectAll(".country-li");
  listItems.each(function (index) {
    // marker 1
    console.log("index: " + index + " - d: " + d);
    // not working: trying to show China's tooltip when any li is hovered
    d3.select(this)
      .data(geography.features)
      .on("mouseover", function (d, i) {
        console.log("mouseover test - d: " + d);
        tip.show(d, document.querySelector(".chn"));
      .on("mouseout", function (d) {
        console.log("mouseout test - d : " + d);
        tip.hide(d, document.querySelector(".chn"));
    // is working (when all code directly above is removed starting with "marker 1") but not what I want: when any of first 3 countries are clicked, country changes color
    d3.select(this).on("click", function () {
      let text = d3.select(this).text();
      console.log("text = " + text);
      if (text === "China") {
        svg.select(".chn").style("fill", "Tomato");
      } else if (text === "Cambodia") {
        svg.select(".khm").style("fill", "Teal");
      } else if (text === "Indonesia") {
        svg.select(".idn").style("fill", "Purple");
// end remote tooltip attempt
font-family: monaco, sans-serif; body { margin: 0; .wrapper { position: relative; min-height: 100vh; display: flex; flex-direction: column; justify-content: center; align-items: center; background: #ddd; margin: 2rem 1rem; text-align: center; color: DarkSlateGray; .legend { position: absolute; top: 50%; left: 10px; transform: translateY(-50%); z-index: 2; min-width: 150px; padding: 10px; background: lightyellow; .legend ul { margin: 0; padding: 0; list-style-type: none; .country-li { margin-bottom: 4px; padding: 5px; font-size: 12px; background: Khaki; .country-li:hover { background: Tomato; /* tooltip */ .d3-tip { padding: 10px; line-height: 1; border-radius: 2px; background: rgba(255,255,255,0.9); .d3-tip h4, .d3-tip p { color: DarkSlateGray; margin-bottom: 0; .d3-tip h4 { margin-top: 10px; .d3-tip h6, .d3-tip li { color: DarkSlateGray; .d3-tip p { font-size: 12px; .d3-tip span { font-weight: bold; color: salmon; .d3-tip .tooltip-top .tooltip-subheader { margin-top: 10px; margin-bottom: 10px; font-size: 10px; font-style: italic; color: DarkSlateGray; .d3-tip .tooltip-top p:not(.tooltip-subheader):nth-of-type(3) { margin-top: 6px; .d3-tip .tooltip-bottom { width: 100%; margin-top: 10px; padding: 6px 0 0 0; border-top: 1px solid #ddd; .d3-tip .tooltip-bottom .tooltip-list-header { display: block; margin-top: 15px; margin-bottom: 10px; text-transform: uppercase; .d3-tip .tooltip-bottom .tooltip-list { margin: 0; padding-left: 0; font-size: 12px; line-height: 1.8; list-style: none; /* creates a small triangle glyph for the tooltip */ .d3-tip:after { position: absolute; display: inline; width: 100%; font-size: 16px; line-height: 0.25; color: rgba(255,255,255,0.9); pointer-events: none; box-sizing: border-box; /* northward tooltips */ .d3-tip.n:after { content: "\25BC"; top: 100%; left: 0; margin: -1px 0 0 0; text-align: center; /* eastward tooltips */ .d3-tip.e:after { content: "\25C0"; top: 50%; left: -8px; margin: -4px 0 0 0; /* southward tooltips */ .d3-tip.s:after { content: "\25B2"; top: -8px; left: 0; margin: 0 0 1px 0; text-align: center; /* westward tooltips */ .d3-tip.w:after { content: "\25B6"; top: 50%; left: 100%; margin: -4px 0 0 -1px;
<!-- https://bl.ocks.org/micahstubbs/c7f17dcbdc728e0d579d84e47c33dfa6 -->
<!DOCTYPE html>
<html lang="en">
  <meta charset="UTF-8" />
  <title>D3 World Map</title>
  <link rel="stylesheet" href="style.css">
</head>
  <div class="wrapper">
    <h1>D3 World Map</h1>
    <div class="legend">
        <li class="country-li country-chn">China</li>
        <li class="country-li country-khm">Cambodia</li>
        <li class="country-li country-idn">Indonesia</li>
        <li class="country-li country-vnm">Vietnam</li>
        <li class="country-li country-mmr">Myanmar</li>
        <li class="country-li country-lao">Laos</li>
        <li class="country-li country-gha">Ghana</li>
        <li class="country-li country-ken">Kenya</li>
        <li class="country-li country-lso">Lesotho</li>
        <li class="country-li country-rwa">Rwanda</li>
        <li class="country-li country-zaf">South Africa</li>
        <li class="country-li country-swz">Eswatini</li>
        <li class="country-li country-tza">Tanzania</li>
        <li class="country-li country-zmb">Zambia</li>
        <li class="country-li country-hnd">Honduras</li>
    <div class="fadeable" id="vis"></div>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
  <script src='https://d3js.org/d3.v4.min.js'></script>
  <script src="https://d3js.org/queue.v1.min.js"></script>
  <script src="https://d3js.org/topojson.v1.min.js"></script>
  <script src="https://d3js.org/d3-geo-projection.v1.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.9.1/d3-tip.min.js"></script>
  <script src='https://unpkg.com/[email protected]/dist/simple-statistics.min.js'></script>
  <script src="script.js"></script>
</body>
</html>
Your pen is read-only and the snippet does not work. Anyway, you can find a <path> element of a specific country by its class name: path = d3.select('.country-name'), get its datum and toggle the tip with it: tip.show(path.datum()) Michael Rovinsky Mar 26, 2021 at 15:36 Thanks @MichaelRovinsky, I updated the snippet with a CDN for D3-tip so should be working now (aside from the question-related error) and I think the CodePen is read only because it’s a project or maybe because you're not logged in. But thanks for the suggestion. I tried implementing it on line 293 of the CodePen (attempt 2B) but get the error: Uncaught TypeError: targetel.getScreenCTM is not a function So trying to figure that out now. Chris_Heu Mar 29, 2021 at 23:03

For a solution, I refactored the remoteTooltip function utilizing D3's .datum method on line 736 of this CodePen .

Here's the isolated refactored function:

function remoteTooltip(error, geography, data) {
  console.log("remoteTooltip fired");
  d3.selectAll('.country-li')
    .datum(function() {
      return geography.features.find(feature => feature.properties.name === this.innerText);
    .on('mouseover', function(d) {
      const pathClass = `.${d.id.toLowerCase()}`;
      tip.show(d, document.querySelector(pathClass));
      d3.select(pathClass)
        .style('fill-opacity', 1)
        .style('stroke-opacity', 1)
        .style('stroke-width', 2);
    .on('mouseout', function(d) {
      const pathClass = `.${d.id.toLowerCase()}`;
      tip.hide(d, document.querySelector(pathClass))
      d3.select(pathClass)
        .style('fill-opacity', 0.8)
        .style('stroke-opacity', 0.5)
        .style('stroke-width', 1);
        

Thanks for contributing an answer to Stack Overflow!

  • Please be sure to answer the question. Provide details and share your research!

But avoid

  • Asking for help, clarification, or responding to other answers.
  • Making statements based on opinion; back them up with references or personal experience.

To learn more, see our tips on writing great answers.