有一组线条,通过一个三层的数组传递数据,第一层是线条,第二层是点,第三层是x、y坐标值,根据之前的
绘制这个图形是很容易的。但是新的需求是:上游数据更新,在中间插入了一条新的线,为了实现这个需求,遇到了一些弯路。
第一种错误方法:继续使用data()绑定,试图让d3自动更新,简单来说就是最初绘制这条线时使用的代码,在数据points更新后,再次用同样的代码来划线
linesObject.node.selectAll('path')
.data(points)
.enter()
.append('path')
.attr('d', (d) => linesObject.generator(d))
.attr('linenum', (d, i) => i)
.attr('stroke', linesObject.color)
.attr('stroke-width', '2px')
.attr('fill', 'none')
.exit().remove()
这里涉及到d3的一个机制,就是enter().append()的机制,它会查询points的length,然后根据这个length补足append()的元素,而且对于那些已经有了d属性的<path>单元,d3不会更新其d属性。也就是说,即使上游的points数据已经更新,但是d3不会再次更新已经存在的<path>里面的d属性,也就是说,浏览器里的图形不会更新。最后的结果是:因为points增加了,所以最后会append()一些<path>,并且把points里面最后的一些数据写到后面追加的<path>里,呈现到浏览器上的图形就是排列在最后的几条线被叠加绘制,如果不是通过浏览器的调试工具去查看dom节点的话,在视觉上和更新之前没有差异。
第二种正确方法:既然d3对已经存在的<path>不做更新,那么干脆把这些<path>全删掉,然后重新绘制就好了
linesObject.node.selectAll('*').remove()
linesObject.node.selectAll('path')
.data(points)
.enter()
.append('path')
.attr('d', (d) => linesObject.generator(d))
.attr('linenum', (d, i) => i)
.attr('stroke', linesObject.color)
.attr('stroke-width', '2px')
.attr('fill', 'none')
.exit().remove()
事实证明,这种方法达到了效果。
第三种正确方法:利用d3.insert(type[, before])方法
该方法会在before这个位置插入一个type,并把原来位于before这个位置的以及更靠后的节点,全部向后推
什么?你问我如果一定要在最后插入怎么办?用append()
https://d3js.org.cn/api/d3-selection/
上面的链接有insert方法的介绍,其中要注意的是
1)type仅接受两种类型的实参:一是字符串,d3会直接插入一个tag为type的标签;二是函数,这个函数和d3.js学习笔记(4)匿名函数类似,接受3个输入参数,但是区别在于返回值必须是一个dom节点。因为这里我们要插入的一个新的图形,而不是简单的文字,所以需要通过函数返回一个dom节点。
如果要绘制图形,不可避免要使用d3.select()然后绑定数据和生成器才能生成,不过d3.selection()返回值中没有dom节点的属性,所以必须要多做一步。
How to access the DOM element that correlates to a D3 SVG object? - Stack Overflow
只要在d3.select()后面加一个node()就可以返回dom节点了
d3.select()要选择一个父节点才能用append()或者insert()生成节点,而为了避免导致问题,所以最好是直接生成一个新的dom节点,这个dom节点直接就是<path>标签,然后d3.select(newDOM).attr('d', (d) => linesObject.generator(d))
在这里有一点要特别留意,如果用document.createElement(),创建的是一个html节点,在svg中不会被正确处理,所以必须要使用
document.createElementNS('http://www.w3.org/2000/svg', 'path')
把这个节点放置到svg命名空间中,svg中才能正确的绘制图形,具体请参考MDN
Document.createElementNS() - Web API 接口参考 | MDN
2)before仅接受两种类型的实参:一是字符串,该字符串必须是一个css选择器,实际上可能会有多个子节点满足该选择器,但是d3只会在第一个满足要求的子节点处插入,后面的就不会管了,如果你需要在所有满足该选择器的元素处插入节点或者做什么操作的话,需要使用each()
javascript - d3.js - how to insert new sibling elements - Stack Overflow
另外一种可接受的实参是一个函数,而且这个函数和d3.js学习笔记(4)匿名函数类似,接受3个输入参数,区别在于返回值必须是一个dom节点(不可以是d3.select()的返回值),而且必须是d3.selection().node()的子节点
对于我实际面临的问题来讲,因为这个程序需要记录线条的编号,这个编号在后面做用户交互要用,所以必须要在所有的<path>节点加上自定义的linenum属性用来记录,所以如果插入线条,则还需要对后面所有被推后一位的元素的linenum属性做处理,出于程序完整性的考虑,我就直接在before函数里做了一个遍历,把所有要做的工作处理之后,再把对应的插入位置返回。这样就不怕调用insert之后忘记处理linenum的事情
linesObject.node.insert(() => {
return d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'path'))
.datum(points)
.attr('d', (d) => linesObject.generator(d))
.attr('linenum', position)
.attr('stroke-width', '2px')
.attr('fill', 'none')
.node()
}, (d, i, a) => {
let n
for (let k = 0; k < a[i].childNodes.length; k++) {
let t = Number.parseInt(a[i].childNodes[k].getAttribute('linenum'))
if (t === position) {
a[i].childNodes[k].setAttribute('linenum', t + 1)
n = k
}
if (t > position) {
a[i].childNodes[k].setAttribute('linenum', t + 1)
}
}
return a[i].childNodes[n]
})
选哪一种方法?
第二种正确方法是先删除节点,再重新生成,如果节点数量非常大,则计算量会大。但是如果采用insert方法,只需要处理一小部分数据就可以了。不管怎么样浏览器需要reflow,所以这里还是推荐采用insert方法