You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

D3 Choropleth地图悬停Tooltip不显示及位置异常求助

解决D3 Choropleth地图悬停Tooltip不显示及位置偏移问题

问题回顾

你尝试给D3世界地图添加悬停Tooltip时遇到了两个核心问题:

  • 最初将Tooltip附加在SVG内部,导致Tooltip始终无法显示
  • 移到SVG外部后,Tooltip位置要么固定在地图底部,要么偏移到页面顶部,无法跟随鼠标位置移动

问题根源分析

  1. SVG无法直接容纳HTML元素:SVG属于XML命名空间,<div>这类HTML元素不能直接嵌入SVG中,所以你最初把tooltip附加到svg内的写法,浏览器根本不会渲染这个div。
  2. 定位逻辑存在漏洞:
    • 未给Tooltip设置正确的position属性,导致它默认以流式布局显示,只能出现在SVG下方
    • 即使添加了position:absolute,d3.mouse(this)获取的是相对于SVG元素的局部坐标,而绝对定位默认参考整个页面,所以需要结合SVG在页面中的偏移量计算,才能让Tooltip精准跟随鼠标

分步解决办法

1. 正确创建Tooltip容器

将Tooltip创建在SVG所在的父容器中,而非SVG内部:

// 选择包含SVG的父容器,而非SVG本身
var tooltip = d3.select("#map-container")
  .append("div")
  .style("opacity", 0)
  .attr("class", "tooltip")
  .style("pointer-events", "none") // 防止Tooltip干扰地图的鼠标事件

2. 修复定位的CSS样式

给包含SVG的父容器设置相对定位,让Tooltip的绝对定位参考这个父容器,避免相对于整个页面偏移:

#map-container {
  position: relative; /* 关键:让子元素的绝对定位基于自身 */
}
.tooltip {
  position: absolute;
  background-color: white;
  border: solid 2px;
  border-radius: 5px;
  padding: 5px;
  pointer-events: none;
}

3. 修正鼠标位置计算

在mouseOver函数中,先获取SVG元素在页面中的偏移量,再结合鼠标相对于SVG的局部坐标,计算出Tooltip的正确位置:

let mouseOver = function(d) {
  // 保留原有高亮逻辑
  d3.selectAll(".Country")
    .transition()
    .duration(200)
    .style("opacity", .5)
  d3.select(this)
    .transition()
    .duration(200)
    .style("opacity", 1)
    .style("stroke", "black")
  
  // 计算SVG在页面中的偏移量
  const svgRect = svg.node().getBoundingClientRect();
  tooltip.style("opacity", 1)
    .html("Population: <br/>" + d.total.toLocaleString()) // 用绑定的d.total而非d.pop,同时格式化数字
    .style("left", (d3.mouse(this)[0] + svgRect.left + 10) + "px")
    .style("top", (d3.mouse(this)[1] + svgRect.top + 10) + "px")
}

注意:你的代码中d.pop是错误的,应该使用d.total——因为你在设置填充色时已经给每个国家数据绑定了d.total = data.get(d.id) || 0

完整修改后的代码

<head>
  <!-- Load d3.js and the geo projection plugin -->
  <script src="https://d3js.org/d3.v4.js"></script>
  <script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
  <script src="https://d3js.org/d3-geo-projection.v2.min.js"></script>
  <style>
    #map-container {
      position: relative;
    }
    .tooltip {
      position: absolute;
      background-color: white;
      border: solid 2px;
      border-radius: 5px;
      padding: 5px;
      pointer-events: none;
    }
  </style>
  <script>
    jQuery(document).ready(function() {
      // The svg
      var svg = d3.select("#my_dataviz"),
          width = +svg.attr("width"),
          height = +svg.attr("height");

      // Map and projection
      var path = d3.geoPath();
      var projection = d3.geoMercator()
        .scale(70)
        .center([0,20])
        .translate([width / 2, height / 2]);

      // Data and color scale
      var data = d3.map();
      var colorScale = d3.scaleThreshold()
        .domain([100000, 1000000, 10000000, 30000000, 100000000, 500000000])
        .range(d3.schemeBlues[7]);

      // 创建Tooltip:放在SVG的父容器中
      var tooltip = d3.select("#map-container")
        .append("div")
        .style("opacity", 0)
        .attr("class", "tooltip")

      // Load external data and boot
      d3.queue()
        .defer(d3.json, "https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson")
        .defer(d3.csv, "https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world_population.csv", function(d) {
          data.set(d.code, +d.pop);
        })
        .await(ready);

      function ready(error, topo) {
        let mouseOver = function(d) {
          d3.selectAll(".Country")
            .transition()
            .duration(200)
            .style("opacity", .5)
          d3.select(this)
            .transition()
            .duration(200)
            .style("opacity", 1)
            .style("stroke", "black")
          
          // 计算SVG的页面偏移量,修正Tooltip位置
          const svgRect = svg.node().getBoundingClientRect();
          tooltip.style("opacity", 1)
            .html("Population: <br/>" + d.total.toLocaleString())
            .style("left", (d3.mouse(this)[0] + svgRect.left + 10) + "px")
            .style("top", (d3.mouse(this)[1] + svgRect.top + 10) + "px")
        }

        let mouseLeave = function(d) {
          d3.selectAll(".Country")
            .transition()
            .duration(200)
            .style("opacity", .8)
          d3.select(this)
            .transition()
            .duration(200)
            .style("stroke", "transparent")
          tooltip.style("opacity", 0)
        }

        // Draw the map
        svg.append("g")
          .selectAll("path")
          .data(topo.features)
          .enter()
          .append("path")
          .attr("d", d3.geoPath().projection(projection))
          .attr("fill", function (d) {
            d.total = data.get(d.id) || 0;
            return colorScale(d.total);
          })
          .style("stroke", "transparent")
          .attr("class", "Country" )
          .style("opacity", .8)
          .on("mouseover", mouseOver )
          .on("mouseleave", mouseLeave )
      }
    })
  </script>
</head>
<body>
  <div>
    <h1>Graphs</h1>
    <!-- 给SVG添加父容器,用于放置Tooltip -->
    <div id="map-container">
      <svg id="my_dataviz" width="400" height="300"></svg>
    </div>
  </div>
</body>

额外优化点

  • 将Tooltip样式移到CSS中,便于后续维护
  • 使用toLocaleString()格式化人口数字,提升可读性
  • 添加pointer-events: none避免Tooltip干扰地图的鼠标事件

内容的提问来源于stack exchange,提问作者Juan Ignacio Durante

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:34:33