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插值路径参数。
修复步骤
- 保存与更新节点状态:在每次更新时,先获取节点的旧状态(
_current),再将新状态赋值给它,为过渡提供起始基准。 - 使用
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
相关产品推荐
相关产品推荐

