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

在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 15:05:02