1. html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>d3力导向图</title>
<script src='http://d3js.org/d3.v4.min.js' charset='utf-8'></script>
<script src='./draw.js' charset='utf-8'></script>
<script src='./dataCollation.js' charset='utf-8'></script>
</head>
<body>
<div id="canvas" style="height:600px; width: 1000px;"></div>
<script>
var json = {
nodes: [
{ id: 1, labels: 'WIFI', name: 'WIFI1' },
{ id: 2, labels: 'WIFI', name: 'WIFI2' },
{ id: 3, labels: 'WIFI', name: 'WIFI3' },
{ id: 4, labels: 'WIFI', name: 'WIFI4' },
{ id: 5, labels: 'WIFI', name: 'WIFI5' },
{ id: 6, labels: 'WIFI', name: 'WIFI6' }
],
edges: [
{ id: 101, source: 1, target: 2, types: 'GPS' },
{ id: 102, source: 1, target: 3, types: 'GPS' },
{ id: 103, source: 1, target: 4, types: 'GPS' },
{ id: 104, source: 4, target: 5, types: 'GPS' },
{ id: 105, source: 6, target: 3, types: 'GPS' },
{ id: 106, source: 6, target: 3, types: 'GPS' },
{ id: 107, source: 6, target: 3, types: 'GPS' }
]
}
initgraph()
function initgraph() {
var vis = buildVis()
var force = buildForce()
var linkGroup = vis.append('g').attr('class', 'linkGroup')
var linktextGroup = vis.append('g').attr('class', 'linktextGroup')
var nodeGroup = vis.append('g').attr('class', 'nodeGroup')
for (const i in json.nodes) {
json.nodes[i].entire = JSON.parse(JSON.stringify(json.nodes[i]))
}
var linkmap = {}
var lGroup = {}
json.edges = collationLinksData(json.edges, linkmap, lGroup)
update(json)
function update(json) {
var lks = json.edges
var nodes = json.nodes
var links = []
lks.forEach((m) => {
var sourceNode = nodes.filter((n) => { return n.id === m.entire.source })[0]
if (typeof (sourceNode) === 'undefined') return
var targetNode = nodes.filter((n) => { return n.id === m.entire.target })[0]
if (typeof (targetNode) === 'undefined') return
links.push({ source: sourceNode.id, target: targetNode.id, entire: m.entire, id: m.id, linknum: m.linknum })
})
json.edges = links
force.nodes(json.nodes)
force.force('link').links(json.edges)
var link = buildLink(json, linkGroup)
var linetext = buildLinktext(json, linktextGroup)
var node = buildNode(json, nodeGroup)
var nodeClick = bindNodeClick(node)
node.call(nodeDrag(force))
force.on('tick', () => (buildTick(link, node, linetext)))
force.alphaTarget(0).restart()
advance(force, 1000)
force.stop()
buildTick(link, node, linetext)
}
}
</script>
</body>
</html>
2. draw.js
function buildVis() {
d3.select('#canvas').select('*').remove()
const zoom = d3.zoom().scaleExtent([0.01, 5]).on('zoom', () => { vis.attr('transform', () => (d3.event.transform)) })
const vis = d3.select('#canvas')
.append('svg')
.attr('width', '100%')
.attr('height', '100%')
.call(zoom)
.on('dblclick.zoom', null)
.append('g')
.attr('class', 'all')
vis.append('marker')
.attr('id', 'arrow')
.attr('markerWidth', 5)
.attr('markerHeight', 5)
.attr('viewBox', '0 -5 10 10')
.attr('refX', 8)
.attr('refY', 0)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#fce6d4')
const path = 'm2.37978,3.80176c-0.71942,-0.38369 -0.33573,-1.10311 0,-1.43883c0.43165,-0.43165 0.95922,-1.91845 0.95922,-1.91845c0.8633,-0.38369 0.95922,-1.00718 1.05515,-1.43883c0.38369,-1.24699 -0.57553,-1.43883 -0.57553,-1.43883s0.76738,-2.06233 0.14388,-3.64505c-0.81534,-2.06233 -4.12466,-2.82971 -4.70019,-0.91126c-3.93281,-0.8633 -3.11747,4.55631 -3.11747,4.55631s-0.95922,0.19184 -0.57553,1.43883c0.09592,0.43165 0.19184,1.05515 1.05515,1.43883c0,0 0.52757,1.4868 0.95922,1.91845c0.33573,0.33573 0.71942,1.05515 0,1.43883c-1.43883,0.76738 -5.75534,0.95922 -5.75534,4.3165l16.30679,0c0,-3.35728 -4.3165,-3.54912 -5.75534,-4.3165z'
vis.append('defs').append('g').attr('id', 'user').append('path').attr('d', path)
return vis
}
function buildForce() {
return d3.forceSimulation()
.force('link', d3.forceLink().distance(200).id((d) => { return d.id }))
.force('charge', d3.forceManyBody().strength(-400))
.force('center', d3.forceCenter(500 / 2, 500 / 2))
.force('collide', d3.forceCollide().strength(-30))
}
function buildLink(json, linkGroup) {
let link = linkGroup.selectAll('.line').data(json.edges, (d) => { return d.entire.id })
link.exit().remove()
link = link.enter().append('path')
.attr('stroke-width', 2)
.attr('class', 'line')
.style('stroke', '#ccc')
.style('cursor', 'pointer')
.attr('fill', 'none')
.attr('id', (d) => { return 'line-' + d.entire.id + '-source-' + d.entire.source + '-target-' + d.entire.target })
.attr('marker-end', 'url(#arrow)')
.merge(link)
return link
}
function buildLinktext(json, linktextGroup) {
let linkText = linktextGroup.selectAll('text').data(json.edges, (d) => { return d.entire.id })
linkText.exit().remove()
linkText = linkText.enter().append('text')
.attr('class', (d) => { return 'linkText-' + d.entire.types })
.attr('id', (d) => { return 'linkText-' + d.entire.id + '-source-' + d.entire.source + '-target-' + d.entire.target })
.attr('dy', -5)
.style('font-size', 12)
.merge(linkText)
linkText.selectAll('.textPath').remove()
linkText.append('textPath')
.attr('startOffset', '45%')
.attr('class', (d) => { return 'textPath linetext-' + d.entire.types })
.attr('xlink:href', (d) => { return '#line-' + d.entire.id + '-source-' + d.entire.source + '-target-' + d.entire.target })
.text((d) => { return d.entire.id })
return linkText
}
function buildNode(json, nodeGroup) {
let node = nodeGroup.selectAll('.node')
node = node.data(json.nodes, (d) => { return d.id })
node.exit().remove()
node = node.enter()
.append('g')
.attr('class', 'node')
.attr('id', (d) => { return 'node-' + d.id })
.merge(node)
node.selectAll('.node-bg').remove()
node.selectAll('.node-icon').remove()
node.selectAll('.node-text').remove()
node.append('circle')
.attr('class', 'node-bg')
.attr('id', (d) => { return 'nodeBg-' + d.id })
.attr('r', 24)
.style('fill', '#C1C1C1')
node.append('use')
.attr('class', 'node-icon')
.attr('id', (d) => { return 'nodeIcon-' + d.id })
.attr('r', 24)
.style('fill', '#FFFFFF')
.attr('xlink:href', '#user')
node.append('text')
.attr('class', 'node-text')
.attr('id', (d) => { return 'nodeText-' + d.id })
.attr('dy', 40)
.attr('text-anchor', 'middle')
.text((d) => { return d.entire.id })
return node
}
function nodeDrag(force) {
const dragstart = () => {
if (!d3.event.active) force.alphaTarget(0.3).restart()
d3.event.subject.fx = d3.event.subject.x
d3.event.subject.fy = d3.event.subject.y
}
const dragmove = () => {
d3.event.subject.fx = d3.event.x
d3.event.subject.fy = d3.event.y
}
const dragend = () => {
if (!d3.event.active) force.stop()
}
return d3.drag().on('start', dragstart).on('drag', dragmove).on('end', dragend)
}
function buildTick(link, node, linetext) {
link.attr('d', (d) => {
if (d.target === d.source) {
const dr = 30 / d.linknum
return 'M' + d.source.x + ',' + d.source.y + 'A' + dr + ',' + dr + ' 0 1,1 ' + d.target.x + ',' + (d.target.y + 1)
} else if (d.size % 2 !== 0 && d.linknum === 1) {
const tan = Math.abs((d.target.y - d.source.y) / (d.target.x - d.source.x))
const x1 = d.target.x - d.source.x > 0 ? Math.sqrt(24 * 24 / (tan * tan + 1)) + d.source.x : d.source.x - Math.sqrt(24 * 24 / (tan * tan + 1))
const y1 = d.target.y - d.source.y > 0 ? Math.sqrt(24 * 24 * tan * tan / (tan * tan + 1)) + d.source.y : d.source.y - Math.sqrt(24 * 24 * tan * tan / (tan * tan + 1))
const x2 = d.target.x - d.source.x > 0 ? d.target.x - Math.sqrt(24 * 24 / (1 + tan * tan)) : d.target.x + Math.sqrt(24 * 24 / (1 + tan * tan))
const y2 = d.target.y - d.source.y > 0 ? d.target.y - Math.sqrt(24 * 24 * tan * tan / (1 + tan * tan)) : d.target.y + Math.sqrt(24 * 24 * tan * tan / (1 + tan * tan))
return 'M' + x1 + ' ' + y1 + ' L ' + x2 + ' ' + y2
}
const curve = 3
const homogeneous = 1.2
const dx = d.target.x - d.source.x
const dy = d.target.y - d.source.y
let dr = Math.sqrt(dx * dx + dy * dy) * (d.linknum + homogeneous) / (curve * homogeneous)
const tan = Math.abs(dy / dx)
const x1 = dx > 0 ? Math.sqrt(24 * 24 / (tan * tan + 1)) + d.source.x : d.source.x - Math.sqrt(24 * 24 / (tan * tan + 1))
const y1 = dy > 0 ? Math.sqrt(24 * 24 * tan * tan / (tan * tan + 1)) + d.source.y : d.source.y - Math.sqrt(24 * 24 * tan * tan / (tan * tan + 1))
const x2 = dx > 0 ? d.target.x - Math.sqrt(24 * 24 / (1 + tan * tan)) : d.target.x + Math.sqrt(24 * 24 / (1 + tan * tan))
const y2 = dy > 0 ? d.target.y - Math.sqrt(24 * 24 * tan * tan / (1 + tan * tan)) : d.target.y + Math.sqrt(24 * 24 * tan * tan / (1 + tan * tan))
if (d.linknum < 0) {
dr = Math.sqrt(dx * dx + dy * dy) * (-1 * d.linknum + homogeneous) / (curve * homogeneous)
return 'M' + x1 + ',' + y1 + 'A' + dr + ',' + dr + ' 0 0,0 ' + x2 + ',' + y2
}
return 'M' + x1 + ',' + y1 + 'A' + dr + ',' + dr + ' 0 0,1 ' + x2 + ',' + y2
})
node.attr('transform', (d) => ('translate(' + d.x + ',' + d.y + ')'))
linetext.attr('transform', (d) => {
if (d.target.x < d.source.x) {
const { x, y, width, height } = document.getElementById('linkText-' + d.entire.id + '-source-' + d.entire.source + '-target-' + d.entire.target).getBBox()
const rx = x + width / 2
const ry = y + height / 2
return 'rotate(180 ' + rx + ' ' + ry + ')'
} else {
return 'rotate(0)'
}
})
}
function advance(force, num) {
for (let i = 0, n = num; i < n; ++i) {
force.tick()
}
}
function bindNodeClick(node) {
node.on('click', (d) => {
console.log('单击')
})
node.on('dblclick', (d) => {
console.log('双击')
})
node.on('contextmenu', (d) => {
console.log('右击')
})
}
3. dataCollation.js
function collationLinksData(links, linkmap, lGroup) {
const arr = []
for (const i in links) {
arr.push({
source: links[i].source,
target: links[i].target,
id: links[i].id,
entire: links[i]
})
}
for (var i = 0; i < arr.length; i++) {
const key = arr[i].source < arr[i].target ? arr[i].source + '-' + arr[i].target : arr[i].target + '-' + arr[i].source
if (!linkmap.hasOwnProperty(key)) {
linkmap[key] = 0
}
linkmap[key] += 1
if (!lGroup.hasOwnProperty(key)) {
lGroup[key] = []
}
lGroup[key].push(arr[i])
}
for (var i = 0; i < arr.length; i++) {
const key = arr[i].source < arr[i].target ? arr[i].source + '-' + arr[i].target : arr[i].target + '-' + arr[i].source
arr[i].size = linkmap[key]
const group = lGroup[key]
const keyPair = key.split('-')
let type = 'noself'
if (keyPair[0] === keyPair[1]) {
type = 'self'
}
setLinkNumber(group, type)
}
return arr
}
function setLinkNumber(group, type) {
if (group.length === 0) return
const linksA = []
const linksB = []
for (let i = 0; i < group.length; i++) {
const link = group[i]
if (link.source < link.target) {
linksA.push(link)
} else {
linksB.push(link)
}
}
let maxLinkNumber = 0
if (type === 'self') {
maxLinkNumber = group.length
} else {
maxLinkNumber = group.length % 2 === 0 ? group.length / 2 : (group.length + 1) / 2
}
if (linksA.length === linksB.length) {
let startLinkNumber = 1
for (let i = 0; i < linksA.length; i++) {
linksA[i].linknum = startLinkNumber++
}
startLinkNumber = 1
for (let i = 0; i < linksB.length; i++) {
linksB[i].linknum = startLinkNumber++
}
} else {
var biggerLinks, smallerLinks
if (linksA.length > linksB.length) {
biggerLinks = linksA
smallerLinks = linksB
} else {
biggerLinks = linksB
smallerLinks = linksA
}
let startLinkNumber = maxLinkNumber
for (let i = 0; i < smallerLinks.length; i++) {
smallerLinks[i].linknum = startLinkNumber--
}
const tmpNumber = startLinkNumber
startLinkNumber = 1
let p = 0
while (startLinkNumber <= maxLinkNumber) {
biggerLinks[p++].linknum = startLinkNumber++
}
startLinkNumber = 0 - tmpNumber
for (let i = p; i < biggerLinks.length; i++) {
biggerLinks[i].linknum = startLinkNumber++
}
}
}
效果图
![在这里插入图片描述](https://img-blog.csdnimg.cn/20191226092402389.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzgwNTQzOQ==,size_16,color_FFFFFF,t_70)