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

如何在D3.js中实现环形(Arc)的重叠偏移效果?

问题描述

我正在尝试用D3.js创建环形组件,需求是当环形(Arc)完成整圈绘制后,产生小幅偏移,呈现环形自身重叠的视觉效果。目前新生成的弧无法与原有弧衔接,效果不符合预期。

效果对比

  • 预期效果:预期效果
  • 当前效果:当前效果

实现代码

JavaScript代码

/* Define properties */
const width = 500;
const height = 500;
const margin = 40;
const circleStroke = 50;
const circleSpace = 2;
const radius = (width - margin) / 2;
const fullCircle = Math.PI * 2;

const configurations = [
    {color: 'hotpink',percentage:0.5}, 
    {color: 'limegreen',percentage:0.6}, 
    {color: 'turquoise',percentage:0.9}
                             ];
/* Add SVG inside <div id="activity"></div> */
const canvas = d3.select('#activity')
  .append('svg')
  .attr('width', width)
  .attr('height', height)

/* Chart configuration. */

filter = canvas.append("defs")
                .append("filter")
                .attr("id", "drop-shadow")
                .attr("height", "130%"); // Adjust this value to control the shadow spread

filter.append("feGaussianBlur")
      .attr("in", "SourceAlpha")
      .attr("stdDeviation", 5); // Adjust the stdDeviation value to control the blur intensity

filter.append("feOffset")
      .attr("dx", 0)
      .attr("dy", 5)
      .attr("result", "offsetblur");

var feMerge = filter.append("feMerge");

feMerge.append("feMergeNode");
feMerge.append("feMergeNode")
      .attr("in", "SourceGraphic");


const rings = canvas.append('g')
.attr('transform', `translate(${width / 2}, ${height / 2})`);

/* Chart configuration. */
arcs = [];
configurations.forEach((ring,index) =>{
    var arc = d3.arc()
    .innerRadius(radius - circleStroke * (index + 1) - circleSpace * index)
    .outerRadius(radius - circleStroke * index - circleSpace * index)
    .cornerRadius(50)
    .startAngle(0)
    .endAngle(fullCircle*ring.percentage);

    arcs.push(arc); // Store arc generators in the array

    rings.append("path")
    .attr("fill",ring.color)
    .attr("d",arc);

});

// Dynamically chaninging the the value of the rings

// Array of input elements
const inputElements = [
    document.getElementById("ring1"),
    document.getElementById("ring2"),
    document.getElementById("ring3")
];

inputElements.forEach((inputElement,index)=>{
    inputElement.addEventListener("input",()=>{
        var ringPercentage = +(inputElement.value/100);
        arcs[index].endAngle(fullCircle * ringPercentage); // Update the end angle of the first arc
        console.log(ringPercentage);
        rings.select(`path:nth-child(${index + 1})`)
        .attr("fill",configurations[index].color)
        .style("filter", "url(#drop-shadow)")
        .attr("d", arcs[index]);

        // console.log(arcs[index].endAngle());
        if(ringPercentage==1){
            // create and append a new path to the ring
            rings.append("path");
        }

        if(ringPercentage>1){
            // create a new arc 
            var arc_new = d3.arc()
                    .innerRadius(radius - circleStroke * (index + 1) - circleSpace * index)
                    .outerRadius(radius - circleStroke * index - circleSpace * index)
                    .cornerRadius(50)
                    .startAngle(0)
                    .endAngle(fullCircle*(ringPercentage-1));

                    // get only needed path by 1 ring => 4, 2 ring=> 5,3 ring 6

            switch (inputElement.id) {
                case "ring1":
                    rings.select(`path:nth-child(4)`)
                         .attr("fill",configurations[index].color)
                         .attr("d", arc_new)
                         .style("filter", "url(#drop-shadow)");
                    break;
            
                case "ring2":
                    rings.select(`path:nth-child(5)`)
                    .attr("fill",configurations[index].color)
                    .attr("d", arc_new)
                    .style("filter", "url(#drop-shadow)");
                    break;

                case "ring3":
                    rings.select(`path:nth-child(6)`)
                    .attr("fill",configurations[index].color)
                    .attr("d", arc_new)
                    .style("filter", "url(#drop-shadow)");

                     break;
            }
            
            
        }
    
    })
})

HTML代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ring v2</title>
    <script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
    <div>
        <label for="ring1">Ring 1:</label>
        <input type="range" id="ring1" class="ring-range" min="0" max="150" step="1" value="70" />
    </div>
    <div>
        <label for="ring2">Ring 2:</label>
        <input type="range" id="ring2" class="ring-range" min="0" max="100" step="1" value="90" />
    </div>
    <div>
        <label for="ring3">Ring 3:</label>
        <input type="range" id="ring3" class="ring-range" min="0" max="150" step="1" value="40" />
    </div>
    <div id="activity"></div>
    <script src="test.js"></script>
</body>
</html>
解决方案

问题核心是新增弧未做偏移处理,且路径选择逻辑依赖硬编码索引,导致衔接失效。以下是针对性修改:

1. 给重叠弧添加偏移

当百分比超过100%时,新增弧需要设置微小偏移(如x轴偏移2px),同时主弧保持完整一圈角度:

// 修正主弧角度:超过100%时保持整圈
const mainEndAngle = ringPercentage >= 1 ? fullCircle : fullCircle * ringPercentage;
arcs[index].endAngle(mainEndAngle);
rings.select(`path:nth-child(${index + 1})`)
    .attr("d", arcs[index]);

// 处理重叠弧
if(ringPercentage > 1){
    let overlayPath = rings.select(`#overlay-${index}`);
    // 不存在则创建
    if(overlayPath.empty()){
        overlayPath = rings.append("path")
            .attr("id", `overlay-${index}`)
            .attr("fill", configurations[index].color)
            .style("filter", "url(#drop-shadow)");
    }
    // 生成重叠弧并添加偏移
    var arc_new = d3.arc()
            .innerRadius(radius - circleStroke * (index + 1) - circleSpace * index)
            .outerRadius(radius - circleStroke * index - circleSpace * index)
            .cornerRadius(50)
            .startAngle(0)
            .endAngle(fullCircle*(ringPercentage-1));
    overlayPath.attr("d", arc_new)
        .attr("transform", "translate(2, 0)"); // 偏移量可根据需求调整
} else {
    // 百分比低于100%时移除重叠弧
    rings.select(`#overlay-${index}`).remove();
}

2. 替换硬编码路径选择

用id替代nth-child索引,避免路径顺序变化导致选择错误,代码中已通过#overlay-${index}实现精准定位。

3. 移除冗余逻辑

删除ringPercentage==1时创建空路径的代码,改为在百分比超过100%时按需创建重叠弧,减少不必要的DOM操作。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 11:54:52