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
相关产品推荐
相关产品推荐

