Zhihao's Studio.

drop and drag in d3

Word count: 1,699 / Reading time: 9 min
2014/11/15 Share

Behavior in d3

在d3中,行为被设计成了一种组件,可以创建一系列的事件,并绑定到调用这些行为的元素上。最常见的行为有拖拽与缩放,使用这些行为可以很方便地在拖拽或者缩放的开始、过程中以及结束时达到想要的效果。

这篇博客中,就以拖拽为例,并借助之前实现的热力图,实现拖拽dimension的label以达到reoder维度的效果。为了使用三个阶段,我们会让开始拖拽的时候,维度名变成红色、字体变大来表示强调的效果;在拖拽过程中,让label紧跟鼠标的位置;并且判断最终的位置,交换两个维度,并重新绘制整个热力图。

理想情况下,开始移动的时候的,运行情况应该像下面这样。

drag

创建一个新的拖拽行为非常简单,var drag = d3.behavior.drag()就创建好了。在这个行为上,可以指定拖拽事件类型的监听器,支持的事件类型有三种,dragstart/drag/dragend,分别是上面提到的拖动开始时触发的函数,拖动时触发和拖动结束时触发。
对应到上面的要求,维度名变成红色、字体变大来表示强调的效果(开始拖拽时),让label紧跟鼠标的位置(拖拽过程中),并且判断最终的位置,交换两个维度,并重新绘制整个热力图(拖拽结束后)。

实现的细节

需要注意的有几点,

  1. 由于热力图的实现方式,横着的dimension和竖着的dimension的drag是不同的。但是他们也有可以共用的地方,dragEnd是可以共用的,但是另外两个是不可以的,所以需要建立两个drag的事件。
  2. 重新绘制涉及到了行和列,变换是需要考虑如何实现的。
  3. 如何判断label拖拽之后是到哪里了,是否超出了边界(超过上界认为和第一个换,超过下界认为认为和最后一个换。) 查看辅助函数changeLabelOrder()
  4. 真正意义上的交换,不仅仅是两个label之间的交换,还有相关性的交换。
  5. Math.ceil()和Math.floor()的区别。

代码见文末的附录。

附录(代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
var colorScales = d3.scale.linear()
.domain([-1,0,1]())
.range(["#31a354","#ffffff","#e6550d"]());
var w = element[0]().offsetWidth;
var h = element[0]().offsetHeight;
var width = w - options.margin.left - options.margin.right;
var height = h - options.margin.top - options.margin.bottom;
d3.select(element[0]()).select("svg").remove();
var svg = d3.select(element[0]()).append("svg")
.attr("width", width*2 + options.margin.left + options.margin.right)
.attr("height", height + options.margin.top + options.margin.bottom)
.append("g")
.attr('class','heat')
.attr("transform", "translate(" + options.margin.left + "," + options.margin.top + ")")
// .append('g')
// .attr('class','rad');
var xu = {};
var x = []();
var yu = {};
var y = []();
var theLabel = 0;
for (var i in scope.data) {
if (typeof(xu[scope.data\[i]().x]) == "undefined") {
x.push(scope.data[i]().x);
}
xu[scope.data\[i]().x] = 0;
if (typeof(yu[scope.data\[i]().y]) == "undefined") {
y.push(scope.data[i]().y);
}
yu[scope.data\[i]().y] = 0;
}
for (d in scope.data) {
scope.data[d]().xIndex = x.indexOf(scope.data[d]().x);
scope.data[d]().yIndex = y.indexOf(scope.data[d]().y);
}
var drag = d3.behavior.drag()
.on('drag',dragText)
.on('dragstart',dragStart)
.on('dragend',dragEnd);
var dragX = d3.behavior.drag()
.on('drag',dragTextX)
.on('dragstart',dragStartX)
.on('dragend',dragEnd);
function dragStartX () {
d3.select(this).attr('fill', 'red').attr('font-weight', '900')
}
function dragTextX (d,i){
// console.log(i);
var y= d3.event.x-i*xGridSize;
var x= -d3.event.y;
// console.log("d3.event.y:"+d3.event.y);
// console.log("d3.event.x:"+d3.event.x);
// console.log(xGridSize+" == "+yGridSize);
// var tmp=(d3.event.x-xGridSize/2)/xGridSize;
var tmp = Math.ceil(d3.event.x/xGridSize)-1;
// console.log(tmp+" tmp")
if(tmp\<=0)
{
theLabel=0;
}else if(tmp\>=Math.sqrt(data.length))
{
theLabel=Math.sqrt(data.length)-1;
}else
{
theLabel=tmp;
}
d3.select(this)
.attr('transform',function(d){
return "rotate(-90) translate("+x+','+y+')';
})
}
function dragStart (){
d3.select(this).attr('fill','red').attr('font-weight','900');
}
function dragText (d,i){
// console.log(i);
var x= d3.event.x;
var y= d3.event.y-yGridSize*i;
// console.log("d3.event.y:"+d3.event.y);
var tmp = cal((y-yGridSize)/yGridSize);
// console.log(tmp+" tmp");
if(tmp+i\>=Math.sqrt(data.length))
{
theLabel=Math.sqrt(data.length);
}else if(tmp+i\<=0){
theLabel=0;
}else{
theLabel=tmp+i;
}
d3.select(this)
.attr('transform',function(d){
return "translate("+x+','+y+')';
})
}
function dragEnd(d,i){
d3.select(this)
.attr('fill','black');
if(theLabel!=i)
changeLabelOrder(data,theLabel,i);
scope.$apply(order); //very import
// console.log(order);
render();
renderRadViz();
}
var xGridSize = Math.floor(width / x.length);
var yGridSize = Math.floor(height / y.length);
var legendElementWidth = Math.floor(width * options.legendWidth / (options.buckets));
var legendElementHeight = height / 20;
var yLabels = svg.selectAll(".yLabel")
.data(y)
.enter().append("text")
.text(function (d) { return d; })
.attr("x", 0)
.attr("y", function (d, i) { return i * yGridSize; })
.style("text-anchor", "end")
.attr("transform", "translate(-6," + yGridSize / 1.5 + ")")
.attr("class", function (d, i) { return ("yLabel axis"); })
.call(drag);
var xLabels = svg.selectAll(".xLabel")
.data(x)
.enter().append("text")
.text(function(d) { return d; })
.attr("y", function(d, i) { return i * xGridSize; })
.attr("x", 0)
.style("text-anchor", "start")
.attr("transform", "rotate(-90) translate(10, " + xGridSize / 2 + ")")
.attr("class", function(d, i) { return ("xLabel axis"); })
.call(dragX);
// var colorScales = []();
// if (options.breaks != null && options.breaks.length \> 0) {
// for (b in options.colors) {
// colorScales.push(d3.scale.quantile()
// .domain([0, options.buckets - 1, d3.max(scope.data, function(d) { return d.value; })]())
// .range(options.colors[b]()));
// }
// } else {
// colorScales.push(d3.scale.quantile()
// .domain([0, options.buckets - 1, d3.max(scope.data, function(d) { return d.value; })]())
// .range(options.colors));
// }
var cards = svg.selectAll(".square")
.data(scope.data);
// console.log(scope.data[0]())
cards.enter().append("rect")
.filter(function(d) { return d.value != null })
.attr("x", function(d) { return d.xIndex * xGridSize; })
.attr("y", function(d) { return d.yIndex * yGridSize; })
.attr("class", "square")
.attr("width", xGridSize)
.attr("height", yGridSize)
.on("click", function(d) { scope.dispatch.click(d); })
.on("mouseover", function(d) { scope.dispatch.mouseover(d); })
.on("mouseout", function(d) { scope.dispatch.mouseout(d); })
.on("mousemove", function(d) { scope.dispatch.mousemove(d); })
.style("fill", "#ffffff");
cards.transition().duration(options.duration).style("fill", function(d) {
if (options.customColors && options.customColors.hasOwnProperty(d.value)) {
return options.customColors[d.value]();
} else if (options.breaks != null && options.breaks.length \> 0) {
for (b in options.breaks) {
if (d.xIndex \< options.breaks[b]()) {
return colorScales[b]()(d.value);
}
}
return colorScales[options.breaks.length]()(d.value);
} else {
return colorScales(d.value);
}
});
cards.exit().remove();
if (options.legend) {
var lenendData = []();
for(var i=0;i\<11;i++)
{
lenendData.push(i/5-1);
}
var legend = svg.selectAll(".legend")
.data(lenendData)
legend.enter().append("g").attr("class", "legend");
legend.append("rect")
.attr("x", function(d, i) { return legendElementWidth * i; })
.attr("y", height * 1.05)
.attr("width", legendElementWidth)
.attr("height", legendElementHeight)
.style("fill", function(d, i) { return colorScales(lenendData[i]()); })
.style("visibility", function(d, i) { return(i \< options.buckets ? "visible" : "hidden") });
legend.append("text")
.attr("class", "legendLabel")
.text(function(d,i) { if(i%2==0) return lenendData[i]().toFixed(1); })
.attr("x", function(d, i) { return legendElementWidth * i; })
.attr("y", height * 1.15)
.style("text-anchor", "middle");
legend.exit().remove();
}
};
scope.$watch("data", debounce(function() {
render();
},true),400);
scope.$watch("order", debounce(function() {
render();
renderRadViz();
},true),400);
// d3.select(window).on("resize", debounce(function() {
// render();
// }, 500));
function cal(x)
{
if(x\>0)
return Math.ceil(x);
else if (x==0) {
return 0;
}else{
return Math.ceil(x);
}
}
function changeLabelOrder(data,i,j)
{
var tmpLabel = order[i]();
order[i]()=order[j]();
order[j]()=tmpLabel;
// console.log(order);
var iLabel = "";
var jLabel = "";
for(var k=0;k\<data.length;k++)
{
if(data[k]().xIndex===i) {
iLabel = data[k]().x;
}
// console.log(iLabel);
}
for(var k=0;k\<data.length;k++)
{
if(data[k]().yIndex===j) {
jLabel = data[k]().y;
}
// console.log(jLabel);
}
for(var k=0;k\< data.length;k++)
{
if(data[k]().xIndex===i){
data[k]().xIndex=j;
data[k]().x=jLabel;
}else if(data[k]().xIndex===j)
{
data[k]().xIndex=i;
data[k]().x=iLabel;
}
if(data[k]().yIndex===i){
data[k]().yIndex=j;
data[k]().y=jLabel;
}else if(data[k]().yIndex===j)
{
data[k]().yIndex=i;
data[k]().y=iLabel;
}
}
var n = Math.sqrt(data.length);
for(var k=0;k\<n;k++)
{
tmp = data[i+k*n]().value;
data[i+k*n]().value=data[j+k*n]().value;
data[j+k*n]().value=tmp;
}
for(var k=0;k\<n;k++)
{
tmp = data[i*n+k]().value;
data[i*n+k]().value=data[j*n+k]().value;
data[k+j*n]().value=tmp;
}
CATALOG
  1. 1. Behavior in d3
  2. 2. drag
  3. 3. 实现的细节
  4. 4. 附录(代码)