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>
–
–
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.