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

D3.js过渡动画仅对删除元素生效,更新元素无过渡求助

问题描述

更新数据源时,transition仅对被删除的元素生效,更新元素没有过渡效果。已在删除和更新部分都加入.transition(),怀疑更新部分的transition使用有误,无报错信息。


完整代码

JavaScript

// Data
const data1 = {
"name": "TOPICS",
"id": 1,
"children": [{
    "name": "Topic A",
    "id": 2,
    "children": [{
        "name": "Sub A1",
        "id": 5,
        "size": 4
        }, {
        "name": "Sub A2",
        "id": 6,
        "size": 4
        }]
}, {
    "name": "Topic B",
    "id": 3,
    "children": [{
        "name": "Sub B1",
        "id": 7,
        "size": 3
        }, {
        "name": "Sub B2",
        "id": 8,
        "size": 3
        }, {
        "name": "Sub B3",
        "id": 9,
        "size": 3
        }]
}, {
    "name": "Topic C",
    "id": 4,
    "children": [{
        "name": "Sub A3",
        "id": 10,
        "size": 4
        }, {
        "name": "Sub A4",
        "id": 11,
        "size": 4
        }]
}]
};

const data2 = {
"name": "TOPICS",
"id": 1,
"children": [{
    "name": "Topic A",
    "id": 2,
    "children": [{
        "name": "Sub A1",
        "id": 5,
        "size": 4
        }, {
        "name": "Sub A2",
        "id": 6,
        "size": 4
        }]
}, {
    "name": "Topic B",
    "id": 3,
    "children": [{
        "name": "Sub B1",
        "id": 7,
        "size": 3
        }, {
        "name": "Sub B2",
        "id": 8,
        "size": 3
        }, {
        "name": "Sub B3",
        "id": 9,
        "size": 3
        }]
}]
};

//-------------------------------------------------------------------------------------------            
// Declare variables
let i_region_static_id = "sunburst",
    parentDiv = document.getElementById(i_region_static_id),
    width = parentDiv.clientWidth,
    height = 450,
    root,
    x,
    y,
    color = d3.scaleOrdinal(d3.schemeCategory10);
    maxRadius = (Math.min(width, height) / 2) - 5;


const partition = d3.partition();
//-----------------------------------------------------------------------------------
// SVG-Element
let svg = d3.select('#' + i_region_static_id).append('svg')
                .style('width', width)
                .style('height', height)
                .attr('viewBox', `${-width / 2} ${-height / 2} ${width} ${height}`)

//-----------------------------------------------------------------------------------
// X-Scale
x = d3.scaleLinear()
        .range([0, 2 * Math.PI])
        .clamp(true);

//-----------------------------------------------------------------------------------
// Y-Scale
y = d3.scaleSqrt()
        .range([maxRadius * .1, maxRadius]);

//-----------------------------------------------------------------------------------
// Create Arc generator
const arc = d3.arc()
    .startAngle(d => x(d.x0))
    .endAngle(d => x(d.x1))
    .innerRadius(d => Math.max(0, y(d.y0)))
    .outerRadius(d => Math.max(0, y(d.y1)))

//-------------------------------------------------------------------------------------------
// Initialize and Update sun 
function update_sun(pData) {
    const valueAccessor = (d) => d.size;

    root = d3.hierarchy(pData); //set data
    
    valueAccessor == null ? root.count() : root.sum((d) => Math.max(0, valueAccessor(d)));

    // Sort Nodes
    root.sort((d) => d3.descending(d.value))

    const slice = svg.selectAll('g.slice')
        .data(
            partition(root).descendants(),
            function(d) { return d.data.id; }
        );

    //-------------------------------------------------------------------------------------------             

    // Update Section
    const newSlice = slice.enter() //rect_node_grp
                        .append('g').attr('class', 'slice')
                        .attr('display', d => d.depth < 2 ? null : 'none') // Hide levels lower depth
                        .each(function(d) { this._current = d; })
                        .each(function(d) {
                                //Append Elemente an g
                                    d3.select(this).append('path')   
                                        // .attr('class', 'main-arc')
                                        .style('fill', d => (d.data.color == undefined) ? color((d.children ? d : d.parent).data.name) : d.data.color) //set source color, otherwise default color
                                        // .attr('d', arc); 
                                })
                        .merge(slice)
                        .each(function(d) {
                                    // Update meine Elemente in g tag
                                    d3.select(this).select('path')
                                    .attr('class', 'main-arc')
                                        .transition()
                                        .delay(100)
                                        .attr('d', arc);  
                                        // .attrTween("d", arcTween)
                            })

    // Delete Section
    slice.exit().transition().duration(500).style("fill-opacity", 0.2).remove();
}

//-------------------------------------------------------------------------------------------

update_sun(data1)

let i = 0;
d3.interval(() => {
if (i++ % 2 === 0) {
    console.log("data2")
    update_sun(data2);
} else {
    console.log("data1")
    update_sun(data1);
}

}, 4000)

CSS

.slice .main-arc {
    stroke: #fff;
    stroke-width: 1px;
}

HTML

<!DOCTYPE html>
<html>             
    <head>               
        <link rel="stylesheet" href="sun.css">
        <meta charset="utf-8" /> 
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    </head>
    <body>             
        <div id="sunburst"></div>
        <script src="https://d3js.org/d3.v7.js" charset="utf-8"></script> 
    </body>
</html>

问题原因与修复方案

核心问题

当前代码中,更新元素时没有保存节点的当前状态,导致D3无法计算过渡的起始值与目标值,过渡效果无法触发。另外,直接使用.attr('d', arc)无法实现平滑的弧度过渡,需要用attrTween插值路径参数。

修复步骤

  1. 保存与更新节点状态:在每次更新时,先获取节点的旧状态(_current),再将新状态赋值给它,为过渡提供起始基准。
  2. 使用attrTween实现弧度过渡:自定义tween函数,插值弧的x0/x1/y0/y1参数,生成平滑的路径变化。

修改后的update_sun函数

function update_sun(pData) {
    const valueAccessor = (d) => d.size;

    root = d3.hierarchy(pData);
    valueAccessor == null ? root.count() : root.sum((d) => Math.max(0, valueAccessor(d)));
    root.sort((d) => d3.descending(d.value))

    const slice = svg.selectAll('g.slice')
        .data(partition(root).descendants(), d => d.data.id);

    // Enter Section
    const newSlice = slice.enter()
        .append('g').attr('class', 'slice')
        .attr('display', d => d.depth < 2 ? null : 'none')
        .each(function(d) { 
            this._current = d; // 初始化新节点状态
        })
        .append('path')
        .attr('class', 'main-arc')
        .style('fill', d => (d.data.color == undefined) ? color((d.children ? d : d.parent).data.name) : d.data.color)
        .attr('d', arc);

    // Update + Merge Section
    slice.merge(newSlice)
        .each(function(d) {
            const oldData = this._current;
            this._current = d; // 更新为新状态
            d3.select(this).select('path')
                .transition()
                .delay(100)
                .duration(500) // 统一过渡时长
                .attrTween('d', function() {
                    // 插值四个关键参数
                    const interpolateX0 = d3.interpolate(oldData.x0, d.x0);
                    const interpolateX1 = d3.interpolate(oldData.x1, d.x1);
                    const interpolateY0 = d3.interpolate(oldData.y0, d.y0);
                    const interpolateY1 = d3.interpolate(oldData.y1, d.y1);
                    return function(t) {
                        // 生成过渡中的临时数据
                        const tempData = {
                            x0: interpolateX0(t),
                            x1: interpolateX1(t),
                            y0: interpolateY0(t),
                            y1: interpolateY1(t)
                        };
                        return arc(tempData);
                    };
                });
        });

    // Exit Section
    slice.exit()
        .transition()
        .duration(500)
        .style("fill-opacity", 0)
        .remove();
}

额外优化点

  • 给更新过渡添加明确的duration,与删除过渡时长保持一致,视觉更统一。
  • 调整enter部分代码结构,将path创建直接放入enter链,逻辑更清晰。
  • 退出时将透明度设为0,过渡效果更自然。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 08:08:13