在React和D3.js中为密度图添加提示框
在React+D3密度图中实现path悬停提示框
要给D3绘制的path元素添加悬停提示框,核心是结合React状态管理和D3的鼠标事件处理,以下是具体实现方案:
步骤1:添加状态管理
在组件中引入useState,用来控制提示框的显示状态、位置和对应的数据:
import {useMemo, useRef, useState } from "react";
定义状态:
const [tooltip, setTooltip] = useState({ show: false, x: 0, y: 0, value: null, density: null });
步骤2:为path添加鼠标事件
给path元素绑定鼠标事件,同时用D3的工具函数找到鼠标位置对应的密度数据点:
// 用D3的bisector快速定位最接近的x值对应的数据 const bisect = d3.bisector(d => d[0]).left; const handleMouseMove = (e) => { const [mouseX] = d3.pointer(e); // 将SVG像素坐标转换为原始数据的x值 const xValue = xScale.invert(mouseX); // 找到最接近的数据索引 const index = bisect(density, xValue, 1); const dataPoint = density[index]; if (dataPoint) { setTooltip({ show: true, x: e.clientX + 10, // 提示框偏移鼠标位置,避免遮挡 y: e.clientY - 10, value: dataPoint[0].toFixed(2), density: dataPoint[1].toFixed(4) }); } }; const handleMouseOut = () => { setTooltip({...tooltip, show: false}); };
修改path元素,添加事件和交互样式:
<path d={path} fill="blue" opacity={0.4} stroke="black" strokeWidth={1} strokeLinejoin="round" onMouseMove={handleMouseMove} onMouseOut={handleMouseOut} style={{cursor: 'crosshair'}} // 切换鼠标样式,提升交互感知 />
步骤3:渲染提示框
在组件中添加绝对定位的提示框,根据状态控制显示:
{tooltip.show && ( <div style={{ position: 'absolute', left: tooltip.x, top: tooltip.y, background: 'rgba(0,0,0,0.8)', color: 'white', padding: '4px 8px', borderRadius: '4px', fontSize: '12px', pointerEvents: 'none' // 避免提示框干扰鼠标事件 }} > <div>数值: {tooltip.value}</div> <div>密度: {tooltip.density}</div> </div> )}
完整修改后的组件代码
import {useMemo, useRef, useState } from "react"; import * as d3 from "d3"; import AxisBottom from "./axisBottom"; import AxisLeft from "./axisTop"; import importedData from "./data" const MARGIN = { top: 50, right: 30, bottom: 50, left: 50 }; export const DensityChart = ({ width = 700, height = 400}) => { const boundsWidth = width - MARGIN.right - MARGIN.left; const boundsHeight = height - MARGIN.top - MARGIN.bottom; const refs = useRef({ dataMin: Math.min(...importedData), dataMax: Math.max(...importedData), domain: [Math.min(...importedData), Math.max(...importedData)] }); const [tooltip, setTooltip] = useState({ show: false, x: 0, y: 0, value: null, density: null }); const xScale = useMemo(() => { try { const result = d3.scaleLinear().domain([refs.current.dataMin, refs.current.dataMax]).range([10, boundsWidth - 10]); return result } catch (error) { console.error("Error creating xScale:", error); return d3.scaleLinear().domain([0, 1]).range([10, boundsWidth - 10]); } }, [importedData, width]); // 计算核密度估计 const density = useMemo(() => { const kde = kernelDensityEstimator(kernelEpanechnikov(7), xScale.ticks(40)); return kde(importedData); }, [xScale, importedData]); const yScale = useMemo(() => { try { const max = Math.max(...density.map((d) => d[1])); const result = d3.scaleLinear().range([boundsHeight, 0]).domain([0, max]) return result; } catch (error) { console.error("Error creating yScale:", error); return } }, [importedData, height, density]); const path = useMemo(() => { try { const lineGenerator = d3.line().x((d) => { return xScale(d[0]) }) .y((d) => { return yScale(d[1]) }) .curve(d3.curveBasis); return lineGenerator(density); } catch (error) { console.error("Error creating path:", error); return ""; } }, [density, xScale, yScale]); // 用D3的bisector定位最接近的数据点 const bisect = d3.bisector(d => d[0]).left; const handleMouseMove = (e) => { const [mouseX] = d3.pointer(e); const xValue = xScale.invert(mouseX); const index = bisect(density, xValue, 1); const dataPoint = density[index]; if (dataPoint) { setTooltip({ show: true, x: e.clientX + 10, y: e.clientY - 10, value: dataPoint[0].toFixed(2), density: dataPoint[1].toFixed(4) }); } }; const handleMouseOut = () => { setTooltip({...tooltip, show: false}); }; return ( <section style={{position: 'relative'}}> {/* 提示框需要相对定位的父容器 */} <svg height="500" width="700"> <g width={boundsWidth} height={boundsHeight} transform={`translate(${[MARGIN.left, MARGIN.top].join(",")})`} > <path d={path} fill="blue" opacity={0.4} stroke="black" strokeWidth={1} strokeLinejoin="round" onMouseMove={handleMouseMove} onMouseOut={handleMouseOut} style={{cursor: 'crosshair'}} /> <g transform={`translate(0, ${boundsHeight})`}> <AxisLeft yScale={yScale} pixelsPerTick={40} /> </g> <g transform={`translate(0, ${boundsHeight})`}> <AxisBottom xScale={xScale} pixelsPerTick={40} /> </g> </g> </svg> {tooltip.show && ( <div style={{ position: 'absolute', left: tooltip.x, top: tooltip.y, background: 'rgba(0,0,0,0.8)', color: 'white', padding: '4px 8px', borderRadius: '4px', fontSize: '12px', pointerEvents: 'none' }} > <div>数值: {tooltip.value}</div> <div>密度: {tooltip.density}</div> </div> )} </section> ); } export default DensityChart; // 核密度估计相关函数 function kernelDensityEstimator(kernel, X) { return function(V) { return X.map(function(x) { return [x, d3.mean(V, function(v) { return kernel(x - +v )})]; }); }; } function kernelEpanechnikov(bandwidth) { return function(v) { const result = Math.abs(v /= bandwidth) <= 1 ? 0.75 * (1 - v * v) / bandwidth : 0; return result }; }
关键说明
- 使用
d3.pointer获取鼠标在SVG内的坐标,通过xScale.invert转换为原始数据值 d3.bisector能高效定位最接近鼠标位置的数据点,保证提示内容的准确性- 提示框设置
pointerEvents: none,避免遮挡鼠标事件导致交互中断 - 父容器设置相对定位,确保提示框的绝对定位基于图表区域
内容的提问来源于stack exchange,提问作者arengit
相关产品推荐
相关产品推荐

