D3基于多组独立数组创建SVG路径避免元素覆盖的方法
问题原因
你的判断完全正确:D3的数据绑定是基于选择集匹配的,你两次都调用svg.selectAll('path'),第二次选择时会把第一次生成的3条path全部纳入待匹配集合,和index2的8条数据做join时,会先更新前3个已有DOM,只新增5个,最终总共有8条path,和预期的11条不符。
符合D3设计规范的实现方案
D3本身推荐的做法是给不同数据集对应的元素做独立分组,或者通过类名限定选择范围,避免跨数据集的元素匹配,不需要用forEach遍历,有两种标准实现:
方案1:用<g>标签给不同数据集的元素分组(最推荐)
把两组路径分别放在不同的分组容器里,后续选择时直接在对应分组下选择,完全不会互相干扰,这也是D3做多系列图表时的通用写法:
// 先创建两个独立的分组,分别对应两个数据集 const group1 = svg.append('g').attr('class', 'index1-paths'); const group2 = svg.append('g').attr('class', 'index2-paths'); // 处理index1时,只在group1下选择path group1.selectAll('path') .data(index1) .join('path') // 后面的属性设置和你原来的代码完全一致 .attr('class', (d) => `ln${d}`) .attr('d', pathVal) .attr('stroke', () => { return `hsla(${Math.random() * 360} , ${((Math.random() + Math.random()/2)*100).toFixed(2)}%, 50%, 1)`; }) .style('transform', (d) => { return `translateY(${d*multiplier}px)` }) .attr('stroke-width', '2') // 处理index2时,只在group2下选择path,初始选择集为空,会全部新建元素 group2.selectAll('path') .data(index2) .join('path') // 属性设置和原代码一致 .attr('class', (d) => `ln${d}`) .attr('d', pathVal) .attr('stroke', () => { return `hsla(${Math.random() * 360} , ${((Math.random() + Math.random()/2)*100).toFixed(2)}%, 50%, 1)`; }) .style('transform', (d) => { return `translateY(${d*multiplier}px)` }) .attr('stroke-width', '2')
这种方式的好处是后续如果要对某一组路径做统一操作(比如整体隐藏、调整透明度、批量绑定交互),直接操作对应g元素即可,维护成本很低。
方案2:通过类名限定选择范围
如果不想加额外的g标签,也可以在选择时指定更精确的类名,只选择当前数据集对应的元素,避免匹配到其他数据集生成的path:
// 处理index1时,选择类名匹配index1规则的path svg.selectAll('path.index1') .data(index1) .join('path') .attr('class', (d) => `index1 ln${d}`) // 给index1的path加上统一的类标记 // 其余属性和原代码一致 .attr('d', pathVal) .attr('stroke', () => { return `hsla(${Math.random() * 360} , ${((Math.random() + Math.random()/2)*100).toFixed(2)}%, 50%, 1)`; }) .style('transform', (d) => { return `translateY(${d*multiplier}px)` }) .attr('stroke-width', '2') // 处理index2时,只选择类名匹配index2规则的path,不会选中index1生成的元素 svg.selectAll('path.index2') .data(index2) .join('path') .attr('class', (d) => `index2 ln${d}`) // 给index2的path加上统一类标记 // 其余属性和原代码一致 .attr('d', pathVal) .attr('stroke', () => { return `hsla(${Math.random() * 360} , ${((Math.random() + Math.random()/2)*100).toFixed(2)}%, 50%, 1)`; }) .style('transform', (d) => { return `translateY(${d*multiplier}px)` }) .attr('stroke-width', '2')
关于你提到的forEach实现
这种方式虽然能跑通,但不符合D3数据驱动的设计思路:手动放弃了D3的enter/update/exit生命周期能力,后续如果数据集变化需要更新、删除元素时,需要自己手动管理DOM,代码冗余度会很高,不推荐长期使用。
内容的提问来源于stack exchange,提问作者smpa01
相关产品推荐
相关产品推荐

