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

React中D3热力图Tooltip不显示问题求助

React中D3热力图Tooltip不显示问题解决

问题描述

我用D3在HTML里实现的基础热力图,Tooltip能正常显示。迁移到React后,热力图可正常渲染,mouseover、mousemove、mouseleave事件也能正常触发(控制台能正确打印数据),但Tooltip始终无法显示。相关代码如下:

import rd3 from 'react-d3-library';
import * as d3 from 'd3';
import React from 'react';
import data2 from '../data/hmap_current_year.csv';

const node = document.createElement('div');

 // set the dimensions and margins of the graph
 const margin = { top: 30, right: 60, bottom: 29, left: 60 },
 width = 1400 - margin.left - margin.right,
 height = 630 - margin.top - margin.bottom;

// append the svg object to the body of the page
const svg = d3
 .select(node)
 .append("svg")
 //add mobile-friendly
 .attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`)
//  .attr("width", width + margin.left + margin.right)
//  .attr("height", height + margin.top + margin.bottom)
 .append("g")
 .attr("transform", `translate(${margin.left},${margin.top})`);

d3.csv(data2).then(function (data) {
 const var0 = data.map(function (d0) {
     return {
         month: d3.timeParse("%Y-%m-%d")(d0.time).getMonth(),
         day: d3.timeParse("%Y-%m-%d")(d0.time).getDate(),
         value: d0.tmed
     }
 })

 const myGroups = Array.from({ length: 31 }, (_, i) => i + 1)
 const myMonths = [
    "Jan", 
    "Feb", 
    "Mar", 
    "Apr", 
    "May", 
    "Jun", 
    "Jul", 
    "Aug", 
    "Sep", 
    "Oct", 
    "Nov", 
    "Dec"
    ]

 // Build X scales and axis:
 const x = d3.scaleBand()
     .range([0, width])
     .domain(myGroups)
     .padding(0.01);
     svg.append("g")
     .attr("transform", `translate(0, ${height})`)
     .call(d3.axisBottom(x))
     .call(g => g.select(".domain").remove());

     svg.append("g")
     .call(d3.axisTop(x))
     .call(g => g.select(".domain").remove());

 // Build X scales and axis:
 const y = d3.scaleBand()
     .range([0, height])
     .domain(myMonths)
     .padding(0.01);
     svg.append("g")
     .call(d3.axisLeft(y))
     .call(g => g.select(".domain").remove());
     svg.append("g")
     .call(d3.axisRight(y))
     .attr("transform", `translate(${width}, 0)`)
     .call(g => g.select(".domain").remove());

 // Build color scale
 const myColor = d3.scaleQuantile()
     .domain([0, 5, 20, 40, 60, 80, 95, 100])
     .range([
        "#08306B", 
        "#2171B5", 
        "#6BAED6", 
        "#FFFFFF", 
        "#FCBBA1", 
        "#FA6A4A", 
        "#CB181D"
    ]);

 // create a tooltip
 const tooltip = d3.select(node)
     .append("div")
     .style("opacity", 0)
     .attr("class", "tooltip")
     .style("background-color", "white")
     .style("border", "solid")
     .style("border-width", "2px")
     .style("border-radius", "5px")
     .style("padding", "5px")
     .style("margin-bottom", '51px')

 // Three function that change the tooltip when user hover / move / leave a cell
 const mouseover = function (event, d) {
         tooltip
         .style("opacity", 1)
         d3.select(this)
         .style("stroke", "black")
         .style("opacity", 1)
 }
 const mousemove = function (event, d) {
         tooltip
         .html("The exact value of<br>this cell is: " + d.value)
         //accessing values correctly
         console.log(d.value, 'HELLO')
         .style("left", (event.x) / 2 + "px")
         .style("top", (event.y) / 2 + "px")
 };
 const mouseleave = function (event, d) {
         tooltip
         .style("opacity", 0)
         d3.select(this)
         .style("stroke", "none")
         .style("opacity", 1)
 }

 svg.selectAll()
     .data(var0)
     .join("rect")
     .attr("x", function (d) { return x(d.day) })
     .attr("y", function (d) { return y(myMonths[d.month]) })
     .attr("width", x.bandwidth())
     .attr("height", y.bandwidth())
     .style("fill", function (d) { return myColor(d.value) })
     .style("stroke", "none")
     .on("mouseover", mouseover)
     .on("mousemove", mousemove)
     .on("mouseleave", mouseleave)
});

const RD3Component = rd3.Component;
class HeatmapInteractive extends React.Component {
    constructor(props) {
        super(props);
        this.state = {d3: ''}
    }
    componentDidMount() {
        this.setState({d3: node});
    }
    render() {
        return (
            <div>
                <RD3Component data={this.state.d3}/>
            </div>
        )
    }
}
export default HeatmapInteractive;

问题根源

  1. DOM挂载问题:你创建的临时node脱离了React的DOM树,Tooltip作为其子元素无法被正确挂载到React组件的渲染区域中
  2. 定位错误:直接使用event.x/event.y是基于整个窗口的坐标,在React组件内会导致Tooltip位置偏移甚至超出可视区域
  3. 样式缺失:Tooltip未设置position: absolute,无法实现跟随鼠标的浮动定位

解决方案

修改后的完整代码

import rd3 from 'react-d3-library';
import * as d3 from 'd3';
import React from 'react';
import data2 from '../data/hmap_current_year.csv';

const RD3Component = rd3.Component;

class HeatmapInteractive extends React.Component {
    constructor(props) {
        super(props);
        this.state = { d3: '' };
        this.nodeRef = React.createRef();
    }

    componentDidMount() {
        const node = this.nodeRef.current;
        if (!node) return;

        // 设置图表尺寸和边距
        const margin = { top: 30, right: 60, bottom: 29, left: 60 },
              width = 1400 - margin.left - margin.right,
              height = 630 - margin.top - margin.bottom;

        // 创建SVG
        const svg = d3
            .select(node)
            .append("svg")
            .attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`)
            .append("g")
            .attr("transform", `translate(${margin.left},${margin.top})`);

        // 加载数据
        d3.csv(data2).then(function (data) {
            const var0 = data.map(function (d0) {
                return {
                    month: d3.timeParse("%Y-%m-%d")(d0.time).getMonth(),
                    day: d3.timeParse("%Y-%m-%d")(d0.time).getDate(),
                    value: d0.tmed
                }
            });

            const myGroups = Array.from({ length: 31 }, (_, i) => i + 1);
            const myMonths = [
                "Jan", "Feb", "Mar", "Apr", "May", "Jun",
                "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
            ];

            // X轴比例尺和轴
            const x = d3.scaleBand()
                .range([0, width])
                .domain(myGroups)
                .padding(0.01);
            svg.append("g")
                .attr("transform", `translate(0, ${height})`)
                .call(d3.axisBottom(x))
                .call(g => g.select(".domain").remove());

            svg.append("g")
                .call(d3.axisTop(x))
                .call(g => g.select(".domain").remove());

            // Y轴比例尺和轴
            const y = d3.scaleBand()
                .range([0, height])
                .domain(myMonths)
                .padding(0.01);
            svg.append("g")
                .call(d3.axisLeft(y))
                .call(g => g.select(".domain").remove());
            svg.append("g")
                .call(d3.axisRight(y))
                .attr("transform", `translate(${width}, 0)`)
                .call(g => g.select(".domain").remove());

            // 颜色比例尺
            const myColor = d3.scaleQuantile()
                .domain([0, 5, 20, 40, 60, 80, 95, 100])
                .range([
                    "#08306B", "#2171B5", "#6BAED6",
                    "#FFFFFF", "#FCBBA1", "#FA6A4A", "#CB181D"
                ]);

            // 创建Tooltip - 挂载到React组件容器
            const tooltip = d3.select(node)
                .append("div")
                .style("opacity", 0)
                .attr("class", "tooltip")
                .style("position", "absolute")
                .style("background-color", "white")
                .style("border", "solid")
                .style("border-width", "2px")
                .style("border-radius", "5px")
                .style("padding", "5px")
                .style("pointer-events", "none")
                .style("z-index", 1000);

            // 鼠标事件处理函数
            const mouseover = function (event, d) {
                tooltip.style("opacity", 1);
                d3.select(this)
                    .style("stroke", "black")
                    .style("opacity", 1);
            };

            const mousemove = function (event, d) {
                // 获取相对于SVG的坐标
                const [xPos, yPos] = d3.pointer(event, svg.node());
                tooltip
                    .html(`The exact value of<br>this cell is: ${d.value}`)
                    .style("left", `${xPos + margin.left + 10}px`)
                    .style("top", `${yPos + margin.top - 30}px`);
            };

            const mouseleave = function (event, d) {
                tooltip.style("opacity", 0);
                d3.select(this)
                    .style("stroke", "none")
                    .style("opacity", 1);
            };

            // 绘制热力图矩形
            svg.selectAll("rect")
                .data(var0)
                .join("rect")
                .attr("x", function (d) { return x(d.day) })
                .attr("y", function (d) { return y(myMonths[d.month]) })
                .attr("width", x.bandwidth())
                .attr("height", y.bandwidth())
                .style("fill", function (d) { return myColor(d.value) })
                .style("stroke", "none")
                .on("mouseover", mouseover)
                .on("mousemove", mousemove)
                .on("mouseleave", mouseleave);
        });

        this.setState({ d3: node });
    }

    render() {
        return (
            <div ref={this.nodeRef} style={{ position: 'relative' }}>
                {this.state.d3 && <RD3Component data={this.state.d3} />}
            </div>
        )
    }
}

export default HeatmapInteractive;

关键修改点

  • 使用React的ref获取组件的DOM容器,替代临时node,确保Tooltip和SVG都在React的DOM树中
  • 给Tooltip添加position: absolute、pointer-events: none和z-index样式,解决定位和事件干扰问题
  • 用d3.pointer获取相对于SVG的坐标,替代窗口坐标,确保Tooltip位置准确
  • 给组件容器设置position: relative,让Tooltip的绝对定位基于组件区域
  • 增加渲染判断,避免初始状态下的空值渲染错误

内容的提问来源于stack exchange,提问作者andy spears

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 18:30:54