Fork me on GitHub

D3力导图的canvas实现

之前一直默认的d3-svg画图,使用svg的好处是方便操作dom元素, 添加用户交互,操作起来很方便,但后期发现展示大量数据时用svg渲染会造成页面卡顿。因此最近研究了下d3画canvas,在需要展示的数据量很大且交互少的时候适合用canvas。

canvas和svg都能实现同样的效果,如图:

force

简单的示例:

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
<div id="graph"></div>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script type="text/javascript" src="myforce.js"></script>
<script type="text/javascript">
const data = {
nodes:[{id:1,name:'a'},{id:2,name:'b'},{id:3,name:'c'},
{id:4,name:'d'},{id:5,name:'e'},{id:6,name:'f'},
{id:7,name:'g'},{id:8,name:'h'},{id:9,name:'i'},
{id:10,name:'j'},{id:11,name:'k'},{id:12,name:'l'},
{id:13,name:'m'},{id:14,name:'n'}],
edges:[{source:1,target:5},{source:2,target:3},
{source:1,target:3},{source:4,target:12},
{source:5,target:7},{source:1,target:4},
{source:1,target:10},{source:1,target:9},
{source:6,target:3},{source:4,target:14},
{source:5,target:8}
]
};
var force = new MyForce({
el:'#graph',
height:window.innerHeight - 50,
width:window.innerWidth - 50,
nodes: data.nodes,
edges: data.edges,
type:"canvas"
})
</script>

myforce.js

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
let MyForce = function(options){
this.options = options;
this.width = options.width||500;
this.height = options.height||400;
this.el = options.el || "body";

this.transform = d3.zoomIdentity;
this.scale = 1;
this.radius = 6;
this.selectNode = null;
this.selectEdge = null;
this.defaultColor = options.defaultColor || "#52BAFF";
this.lightColor = "#fec10e";

this.nodes = options.nodes||[];
this.edges = options.edges||[];

this.type = options.type||'svg';

this.simulation = d3.forceSimulation()
.force("center", d3.forceCenter(this.width / 2, this.height / 2))
.force("x", d3.forceX(this.width / 2).strength(0.1))
.force("y", d3.forceY(this.height / 2).strength(0.1))
.force("charge", d3.forceManyBody().strength(-100))
.force("link", d3.forceLink().strength(1).id(function(d) { return d.id; }))
.alphaTarget(0)
.alphaDecay(0.05);

// type
if(this.type == 'svg'){
this.forceBySvg();
}else{
this.forceByCanvas();
}
}

canvas渲染:

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
MyForce.prototype.forceByCanvas = function(){
let myForce = this;
//append canvas
myForce.canvas = d3.select(myForce.el).append("canvas")
.attr("height", myForce.height+'px')
.attr("width", myForce.width+'px').node();
myForce.context = myForce.canvas.getContext('2d');

// add tick and links
myForce.simulation.nodes(myForce.nodes).on("tick",forceTick);
myForce.simulation.force("link").links(myForce.edges);

//drag
d3.select(myForce.canvas)
.call(d3.drag().container(myForce.canvas).subject(function(){
console.log("dragsubject")
var i,
x = myForce.transform.invertX(d3.event.x),
y = myForce.transform.invertY(d3.event.y),
dx,
dy;
for (i = myForce.nodes.length - 1; i >= 0; --i) {
var node = myForce.nodes[i];
dx = x - node.x;
dy = y - node.y;

if (dx * dx + dy * dy < myForce.radius * myForce.radius) {

node.x = myForce.transform.applyX(node.x);
node.y = myForce.transform.applyY(node.y);
return node;
}
}
}).on("start", function(){
console.log("start")
if (!d3.event.active) myForce.simulation.alphaTarget(0.3).restart();
d3.event.subject.fx = myForce.transform.invertX(d3.event.x);
d3.event.subject.fy = myForce.transform.invertY(d3.event.y);
}).on("drag", function(){
// console.log("drag",d3.event)
d3.event.subject.fx = myForce.transform.invertX(d3.event.x);
d3.event.subject.fy = myForce.transform.invertY(d3.event.y);
}).on("end",function(){
console.log("end")
if (!d3.event.active) myForce.simulation.alphaTarget(0);
d3.event.subject.fx = null;
d3.event.subject.fy = null;
}))
.call(d3.zoom().scaleExtent([1 / 10, 8]).on("zoom", function(){
myForce.transform = d3.event.transform;
forceTick();
}))
.on("click",function(){
// console.log("click")
var point = d3.mouse(this),node=null,minDistance = Infinity,edge = null;
console.log(point,myForce.transform)
myForce.nodes.forEach(function(d){
var dx = d.x - point[0],
dy = d.y - point[1];
var distance = Math.sqrt((dx * dx) + (dy * dy));
if (distance < minDistance && distance < myForce.radius + 10) {
minDistance = distance;
node = d;
}
})
myForce.selectNode = node;
myForce.selectEdge = edge;
})

// tick
function forceTick() {
myForce.context.save();

myForce.context.clearRect(0, 0, myForce.width, myForce.height);
myForce.context.translate(myForce.transform.x, myForce.transform.y);
myForce.context.scale(myForce.transform.k, myForce.transform.k);

myForce.drawCanvasEdges();
myForce.drawCanvasNodes();

myForce.context.restore();
}
}
// 画线
MyForce.prototype.drawCanvasEdges = function(){
let myForce = this;
myForce.context.beginPath();
myForce.edges.forEach(function(d) {
myForce.context.moveTo(d.source.x, d.source.y);
myForce.context.lineTo(d.target.x, d.target.y);

if (myForce.selectEdge && d === myForce.selectEdge) {
myForce.context.strokeStyle = myForce.lightColor;
} else {
myForce.context.strokeStyle = "#aaa";
}
});
myForce.context.stroke();
}
// 画点
MyForce.prototype.drawCanvasNodes = function(){
let myForce = this;
myForce.nodes.forEach(function(d, i) {
myForce.context.beginPath();
// p.moveTo(d.x, d.y);
myForce.context.arc(d.x, d.y, myForce.radius, 0, 2 * Math.PI, true);
if (myForce.selectNode && d === myForce.selectNode) {
console.log(myForce.selectNode)
myForce.context.fillStyle = myForce.lightColor;
} else {
myForce.context.fillStyle = myForce.defaultColor;
}
myForce.context.fill();
// add text
myForce.context.fillStyle = "#333";
myForce.context.font = "8px";
myForce.context.fillText(d.name,d.x-myForce.radius*2+3,d.y+myForce.radius*2+3);
});
}

svg渲染:

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
MyForce.prototype.forceBySvg = function(){
let myForce = this;

// append svg
myForce.svg = d3.select(myForce.el).append("svg")
.attr("height", myForce.height).attr("width", myForce.width);
myForce.svgG = myForce.svg.append('g').attr("transform", "translate(0,0)");
myForce.svgTransformX = 0;
myForce.svgTransformY = 0;

//drag svg and zoom
this.svg.call(d3.drag().on("drag", function(d) {
myForce.svgTransformX += d3.event.x - d3.event.subject.x;
myForce.svgTransformY += d3.event.y - d3.event.subject.y;
d3.event.subject.x = d3.event.x;
d3.event.subject.y = d3.event.y;

myForce.translateX= myForce.svgTransformX;
myForce.translateY= myForce.svgTransformY;
myForce.svgG.attr("transform", "translate(" + myForce.svgTransformX + " " + myForce.svgTransformY + ")scale(" + myForce.scale + ")");
}).on("end",function(d){

}))
.call(d3.zoom().scaleExtent([ 0.2, 5 ])
.on("zoom",function(){
var scale = 1;
var removeX = 0;
var removeY = 0;

scale = d3.event.transform.k;
removeX = d3.event.transform.x;
removeY = d3.event.transform.y;

myForce.translateX= myForce.svgTransformX;
myForce.translateY= myForce.svgTransformY;

myForce.scale = scale;

myForce.svgG.attr("transform", "translate(" + myForce.svgTransformX + " " + myForce.svgTransformY + ")scale(" + myForce.scale + ")");
}))
.on("click",function(){
if (d3.event == null || d3.event.target.nodeName == "svg" ) {
console.log('svg')
myForce.selectNode = null;
}
})

myForce.drawLinksAndNodes();
}
MyForce.prototype.drawLinksAndNodes = function(){
let myForce = this;
// add links
let links = myForce.svgG.append("g")
.attr("class", "links")
.selectAll("line")
.data(myForce.edges)
.enter().append("line")
.attr("stroke","#aaa")
.attr("stroke-width", 1);
// add nodes
let nodes = myForce.svgG.append("g")
.attr("class", "nodes")
.selectAll("g")
.data(myForce.nodes)
.enter()
.append("g")
.attr("class", "node")
.style("cursor","pointer")
.call(d3.drag()
.on("start", function(d){
d3.select(this).raise();
if (!d3.event.active) myForce.simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
})
.on("drag", function(d){
d.fx = d3.event.x;
d.fy = d3.event.y;
})
.on("end", function(d){
if (!d3.event.active) myForce.simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}))
.on("click",function(d){
myForce.selectNode = d;
if(circles){
circles.attr("fill",function(node){
if(node == myForce.selectNode){
return myForce.lightColor;
}else{
return myForce.defaultColor;
}
})
}
});

let circles = nodes.append("circle").attr("r", myForce.radius)
.attr("fill",myForce.defaultColor);
let texts = nodes.append("text").attr("text-anchor","middle")
.append("tspan").text(function(d){return d.name;})
.attr("fill","#333")
.style("font-size",10);

// simulation tick
myForce.simulation.nodes(myForce.nodes)
.on("tick", function(){
links.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });

circles.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });

texts.attr("dx",function(d){return d.x; })
.attr("dy",function(d){return d.y + myForce.radius*2+3; });
});
myForce.simulation.force("link").links(myForce.edges);
}
-------------完结撒花 -------------