在网上有很多教程讲如何用drag拖拽操作图形元素的,但是他们都没说如何操作数据。
因为我要写的东西是一个通过在图形界面通过拖拽调整几何形状的程序,所以特别注意了这一点。
首先在zoom里已经有了拖拽功能了,可以让整个场景平移,但是原始数据不变。
而我需要在特定单元上添加拖拽功能,具体来说就是拖拽线条上的标记点,改变线条的数据。
所以必须要让特定的拖拽覆盖zoom的拖拽,也就是说在程序执行时必须先执行zoom,后执行特定的drag
drag的定义。要特别说明,在以前的版本中,需要用d3.behavior.drag(),但是现在的新版把d3打碎成很多的小模块,引用时可以按需引用,可以减少生成的文件的体积,减少网络传输的负担,现在v5可以直接用d3.drag()
this.dragHandler = d3.drag()
.on('start', (d, i, a) => this.dragStart(d, i, a))
.on('drag', (d, i, a) => this.dragDrag(d, i, a))
.on('end', (d, i, a) => this.dragEnd(d, i, a))
this.dragHandler(this.symbols.node.selectAll('path')) // this.symbols.node.selectAll('path').call(this.dragHandler)
先定义一个d3.drag(),然后给start/drag/end分别赋予不同的响应函数
比较常见的start是改变拖拽元素的颜色,然后end再改回来,这样用户可以清楚地看到拖拽了哪个元素。如果在拖拽完成后有存储或者其他需求,可以添加。很少见到start需要做特殊处理的情况。
dragStart (d, i, a) {
d3.select(a[i])
.attr('stroke', '#00FFFF')
},
dragEnd (d, i, a) {
d3.select(a[i])
.attr('stroke', '#FF0000')
}
在这个函数中需要找到拖拽的元素,本来d3提供了下面的方法来找
d3.select(this)
vue对this有特殊定义,所以在vue组件中这种方法不能用了,只能另辟蹊径
这个匿名函数有3个形参,其中
d=q[i] // 每个元素对应的数据
i // 数据的索引
q // 数据的父节点
直接用匿名函数的第2个和第3个元素来选取
let target = d3.select(a[i])
就可以直接找到拖拽的元素。
拖动的函数是最复杂的
dragDrag (d, i, a) {
let target = d3.select(a[i])
let [linenum, pointnum] = [target.attr('linenum'), target.attr('pointnum')]
this.$store.state.lineControlPoints[linenum][pointnum][0] = this.axises.xScale.invert(d3.event.x)
this.$store.state.lineControlPoints[linenum][pointnum][1] = this.axises.yScale.invert(d3.event.y)
this.$store.commit('lineUpdate')
this.polylines.node.selectAll('path')
.data(this.$store.state.lineControlPoints)
.attr('d', (d) => this.polylines.lineGenerator(d))
this.splines.node.selectAll('path')
.data(this.$store.state.linePoints)
.attr('d', (d) => this.splines.lineGenerator(d))
target.attr('transform', `translate(${d3.event.x},${d3.event.y})`)
},
想在drag同时对图形进行实时更新来进行数据操作就要在dragDrag函数里添加数据操作 。
因为我为了操作方便,所以在当初生成这个元素时添加了linenum和pointnum属性,这里直接用attr取出来就可以了。
let [linenum, pointnum] = [target.attr('linenum'), target.attr('pointnum')]
这里的lineControlpoints和linePoints可以不是一致的,比如lineControlPoints是控制点,linePoints是样条线或者Bezier曲线,需要一些运算,而且这些数据还和程序中其他部分关联,所以使用vuex。因为是拖拽过程中实时更新,所以采用了同步的commit,而不是异步的dispatch
this.$store.state.lineControlPoints[linenum][pointnum][0] = this.axises.xScale.invert(d3.event.x)
this.$store.state.lineControlPoints[linenum][pointnum][1] = this.axises.yScale.invert(d3.event.y)
this.$store.commit('lineUpdate')
数据处理完成以后,从lineControlPoints得到了linePoints
要实时呈现出来,就要设定线条的data,还要处理d属性,因为svg中的元素归根结底是通过d属性来呈现的,仅仅处理到data是不会刷新图形的
这个案例用的是用d3.line()处理的,此处的处理方法和设定线条数据的方法一样
this.polylines.node.selectAll('path')
.data(this.$store.state.impeller.stage.lineControlPoints)
.attr('d', (d) => this.polylines.lineGenerator(d))
this.splines.node.selectAll('path')
.data(this.$store.state.impeller.stage.linePoints)
.attr('d', (d) => this.splines.lineGenerator(d))
如果vuex中存储的数据和这里要呈现的数据之间还有较多运算,也可以引入其他函数
因为目标点是一个symbol,而且symbol的数据也是直接关联的vuex数据,所以不做过多处理,而是直接移动symbol的位置即可
target.attr('transform', `translate(${d3.event.x},${d3.event.y})`)
这样,在拖动的同时,标记点随鼠标移动,vuex中的数据随时更新,svg中呈现的线条图形也跟着更新
这里要特别提示,这里移动的是symbol,而symbol默认的初始位置是原点,也就是svg的左上角。所以这里translate直接到鼠标位置。如果是其他元素,需要根据元素自身的实际情况来设定。
对于大多数元素,可以设定drag的origin,然后
dragHandler.origin(target.x, target.y)
target.attr('transform', `translate(${d3.event.dx},${d3.event.dy})`)
一定要设定origin,不仅仅是因为这样有助于计算鼠标的实际移动距离,而且在鼠标刚刚点中目标元素时,目标元素不会突然跳动使得目标元素的定位点和鼠标位置重合。比如点击饼图的单元时,饼图单元的中心位置在半径均值、角相位均值的扇形中心位置,如果鼠标点击到饼图扇形的其他位置,饼图会突然跳动使得中心位置跳动到鼠标位置,这样的跳动会改变操作者的预期,使得操作不方便。
如果拖拽的目标不仅仅是改变图形呈现的位置,还要改变图形切实的定位,那么
target.attr('x', d3.event.x)
.attr('y', d3.event.y)
这里使用的是x、y属性,涉及到具体的图形元素,可能有不同的属性,比如圆形就是cx、cy,可以根据实际情况调整
如果绑定是简单的数据其实也可以用
d3.select(this).data()[0]=xxx
来直接操作绑定的数据,但是这样不是很灵活。比如说我这里的点数据是3维数组,第0维是线条编号。这样我在把数据绑定到标记点的时候就要做一次转化。而且处理完节点以后,还要处理Bezier曲线,这需要较复杂的函数。还不如直接处理原始数据,然后重新给图形赋值。
该示例代码是vue单文件组件中的代码,所以this指的是上下文组件,this里的数据是在data里定义的