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

React+D3.js下钻环形图小数值标签重叠及引导线添加问题

D3.js 下钻环形图标签重叠问题求助

我实现了支持下钻交互的环形图表,默认展示一级主分类,点击对应分类可跳转查看其子分类,功能整体运行符合预期,仅小数值对应的标签存在重叠问题。
效果截图

实现代码

import * as d3 from 'd3';
import { useEffect, useRef, useState } from 'react';
import { deepFilter, deepSearch } from '../../utilz';
const graphData = (data) => {
  return data.map((item, index) => {
    return {
      ...item,
      color: d3.schemePastel1[index],
      children: item.children.map((item, index) => {
        return {
          ...item,
          color: d3.schemePastel1[index],
        };
      }),
    };
  });
};

const Drilldown = ({ data, x, y }) => {
  const [level, setLevel] = useState({ level: 0, index: null });
  const graph = useRef(null);

  useEffect(() => {
    let isCancelled = false;
    const drawChart = (graph, width, height) => {
      const legendRectSize = 18;
      const legendSpacing = 4;
      const radius = 100;
      const donutWidth = 40;
      const textOffset = 10;

      const svg = d3
        .select(graph.current)
        .append('svg')
        .attr('width', width)
        .attr('height', height)
        .append('g')
        .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');

      const pie = d3
        .pie()
        .value(function (d) {
          return d.value;
        })
        .sort(null);

      const arc = d3
        .arc()
        .innerRadius(radius - donutWidth)
        .outerRadius(radius);

      const arcs = svg
        .selectAll('g.slice')
        .data(pie(deepFilter(graphData(data), 'level', level.level, 'index', level.index)))
        .enter()
        .append('g')
        .attr('class', 'slice');

      const arcOver = d3
        .arc()
        .outerRadius(radius * 1.1)
        .innerRadius(radius - donutWidth);

      arcs
        .append('path')
        .attr('fill', function (d, i) {
          return d.data.color;
        })
        .attr('d', function (d) {
          return arc(d);
        })
        .on('mouseover', (e, d) => {
          d3.select(e.originalTarget)
            .transition()
            .duration(100)
            .attr('d', arcOver)
            .attr('fill', () => {
              return d3.hsl(d.data.color).darker(0.5);
            })
            .style('cursor', 'pointer');
        })
        .on('mouseout', (d) => {
          d3.select(d.originalTarget)
            .transition()
            .duration(500)
            .ease(d3.easeBounce)
            .attr('fill', (d, i) => {
              return d.data.color;
            })
            .attr('d', arc);
        })
        .on('click', (e, d) => {
          if (d.data.level === 0) {
            if (d.data.children.length === 0) {
            } else {
              setLevel({ level: d.data.level + 1, index: d.data.index });
            }
          }
        });

      const labels = svg
        .selectAll('g.slice')
        .append('text')
        .attr('transform', function (d) {
          var pos = arcOver.centroid(d);
          pos[0] = radius * (midAngle(d) < Math.PI ? 1.5 : -1) + (midAngle(d) < Math.PI ? -20 : -20);
          return 'translate(' + pos + ')';
        })
        .attr('text-anchor', 'middle')
        .text(function (d, i) {
          return d.data.value + ' MB';
        })
        .style('fill', 'gray')
        .style('font-size', '18px')
        .style('font-weight', '600');

      let prev;
      labels.each(function (d, i) {
        console.log(i, d);
        if (i > 0) {
          var thisbb = this.getBoundingClientRect(),
            prevbb = prev.getBoundingClientRect();
          // move if they overlap
          if (!(thisbb.right < prevbb.left || thisbb.left > prevbb.right || thisbb.bottom < prevbb.top || thisbb.top > prevbb.bottom)) {
            const matrix = prev.transform.baseVal.consolidate().matrix;
            d3.select(this).attr('transform', `translate(${matrix.e}, ${matrix.f + prevbb.bottom - prevbb.top})`);
          }
        }

        prev = this;
      });

      const legend = svg
        .selectAll('.legend')
        .data(deepFilter(graphData(data), 'level', level.level, 'index', level.index))
        .enter()
        .append('g')
        .attr('class', 'legend')
        .attr('transform', function (d, i) {
          const legendHeight = legendRectSize + legendSpacing;
          if (i % 2 === 0) {
            const vert = height / 2 + legendHeight * (i > 0 && i - 1);
            return 'translate(' + (width / 2) * -1 + ',' + vert + ')';
          } else {
            const vert = height / 2 + legendHeight * (i > 1 && i - 2);
            return 'translate(' + 0 + ',' + vert + ')';
          }
        });
      legend
        .append('rect')
        .attr('width', legendRectSize)
        .attr('height', legendRectSize)
        .style('fill', (d, i) => {
          return d.color;
        })
        .style('stroke', (d, i) => {
          return d.color;
        });

      legend
        .append('text')
        .attr('x', legendRectSize + legendSpacing)
        .attr('y', legendRectSize - legendSpacing)
        .text(function (d) {
          return d.name.substring(0, 30);
        });

      function midAngle(d) {
        return d.startAngle + (d.endAngle - d.startAngle) / 2;
      }

      // const polylines = svg
      //   .selectAll('g.slice')
      //   .append('polyline')
      //   .attr('points', function (d) {
      //     const pos = arcOver.centroid(d);
      //     pos[0] = radius * 1 * (midAngle(d) < Math.PI ? 1 : -1) + (midAngle(d) < Math.PI ? 10 : -20);
      //     return [arc.centroid(d), arcOver.centroid(d), pos];
      //   })
      //   .style('fill', 'none')
      //   .style('stroke', 'gray')
      //   .style('stroke-width', '1px');

      d3.select('svg').attr('height', height + d3.selectAll('.legend').size() * legendRectSize);
    };

    if (!isCancelled) {
      d3.selectAll('svg').remove();
      drawChart(graph, 500, 250);
    }
    return () => {
      isCancelled = true;
    };
  }, [data, level]);

  return <div ref={graph}> {level.level > 0 && <button onClick={() => setLevel({ level: 0, index: null })}>Back</button>}</div>;
};

export default Drilldown;

测试数据

[
  {
    "level": 0,
    "index": 0,
    "name": "Business Broadband",
    "value": 50,
    "children": [],
    "color": "#fbb4ae"
  },
  {
    "level": 0,
    "index": 1,
    "name": "Dedicated",
    "value": 650,
    "children": [
      {
        "_id": "60e6c6177258f94970bd2d07",
        "index": 1,
        "level": 1,
        "value": 600,
        "color": "#fbb4ae"
      },
      {
        "_id": "60e6c6e363d1e60cb03f3a57",
        "index": 1,
        "level": 1,
        "value": 10,
        "color": "#b3cde3"
      },
      {
        "_id": "60e6cd20d29baa52f801318a",
        "index": 1,
        "level": 1,
        "value": 10,
        "color": "#ccebc5"
      },
      {
        "_id": "60e6c6883cd7b54e4889580c",
        "index": 1,
        "level": 1,
        "value": 10,
        "color": "#decbe4"
      },
      {
        "_id": "60e6c3785b41f64e2c4f3940",
        "index": 1,
        "level": 1,
        "value": 10,
        "color": "#fed9a6"
      },
      {
        "_id": "60c2332a827d5d09e071f6cc",
        "index": 1,
        "level": 1,
        "value": 10,
        "color": "#ffffcc"
      }
    ],
    "color": "#b3cde3"
  },
  {
    "level": 0,
    "index": 2,
    "name": "Core Ports",
    "value": 40,
    "children": [
      {
        "index": 2,
        "level": 1,
        "value": 40,
        "color": "#fbb4ae"
      }
    ],
    "color": "#ccebc5"
  },
  {
    "level": 0,
    "index": 3,
    "name": "Remaining",
    "value": 650,
    "children": [],
    "color": "#decbe4"
  }
]

补充说明

目前我还在学习D3.js阶段,代码还有较大的简化空间。我参考了多个标签重叠问题的解决方案都没有正常生效,另外我还希望为每个标签添加折线引导线,相关代码目前已注释,需要先解决标签偏移问题后再启用。
演示地址:codesandbox demo


内容的提问来源于stack exchange,提问作者Jakub Koudela

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.03 21:54:01