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;
问题根源
- DOM挂载问题:你创建的临时
node脱离了React的DOM树,Tooltip作为其子元素无法被正确挂载到React组件的渲染区域中 - 定位错误:直接使用
event.x/event.y是基于整个窗口的坐标,在React组件内会导致Tooltip位置偏移甚至超出可视区域 - 样式缺失: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
相关产品推荐
相关产品推荐

