d3力导向图

/

1. html

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <title>d3力导向图</title>
  8. <script src='http://d3js.org/d3.v4.min.js' charset='utf-8'></script>
  9. <script src='./draw.js' charset='utf-8'></script>
  10. <script src='./dataCollation.js' charset='utf-8'></script>
  11. </head>
  12. <body>
  13. <div id="canvas" style="height:600px; width: 1000px;"></div>
  14. <script>
  15. // 图表数据
  16. var json = {
  17. nodes: [
  18. { id: 1, labels: 'WIFI', name: 'WIFI1' },
  19. { id: 2, labels: 'WIFI', name: 'WIFI2' },
  20. { id: 3, labels: 'WIFI', name: 'WIFI3' },
  21. { id: 4, labels: 'WIFI', name: 'WIFI4' },
  22. { id: 5, labels: 'WIFI', name: 'WIFI5' },
  23. { id: 6, labels: 'WIFI', name: 'WIFI6' }
  24. ],
  25. edges: [
  26. { id: 101, source: 1, target: 2, types: 'GPS' },
  27. { id: 102, source: 1, target: 3, types: 'GPS' },
  28. { id: 103, source: 1, target: 4, types: 'GPS' },
  29. { id: 104, source: 4, target: 5, types: 'GPS' },
  30. { id: 105, source: 6, target: 3, types: 'GPS' },
  31. { id: 106, source: 6, target: 3, types: 'GPS' },
  32. { id: 107, source: 6, target: 3, types: 'GPS' }
  33. ]
  34. }
  35. // 初始化
  36. initgraph()
  37. function initgraph() {
  38. // 创建svg视图
  39. var vis = buildVis()
  40. // 力导向图布局
  41. var force = buildForce()
  42. // 连接线层
  43. var linkGroup = vis.append('g').attr('class', 'linkGroup')
  44. // 连接线文字层
  45. var linktextGroup = vis.append('g').attr('class', 'linktextGroup')
  46. // 节点层
  47. var nodeGroup = vis.append('g').attr('class', 'nodeGroup')
  48. // 数据备份
  49. for (const i in json.nodes) {
  50. json.nodes[i].entire = JSON.parse(JSON.stringify(json.nodes[i]))
  51. }
  52. var linkmap = {}
  53. var lGroup = {}
  54. json.edges = collationLinksData(json.edges, linkmap, lGroup)
  55. // 装载数据更新图
  56. update(json)
  57. function update(json) {
  58. var lks = json.edges
  59. var nodes = json.nodes
  60. var links = []
  61. lks.forEach((m) => {
  62. var sourceNode = nodes.filter((n) => { return n.id === m.entire.source })[0]
  63. if (typeof (sourceNode) === 'undefined') return
  64. var targetNode = nodes.filter((n) => { return n.id === m.entire.target })[0]
  65. if (typeof (targetNode) === 'undefined') return
  66. links.push({ source: sourceNode.id, target: targetNode.id, entire: m.entire, id: m.id, linknum: m.linknum })
  67. })
  68. json.edges = links
  69. // 整理节点
  70. force.nodes(json.nodes)
  71. // 整理连线
  72. force.force('link').links(json.edges)
  73. // 画连线
  74. var link = buildLink(json, linkGroup)
  75. // 画连线文字
  76. var linetext = buildLinktext(json, linktextGroup)
  77. // 画节点
  78. var node = buildNode(json, nodeGroup)
  79. // 绑定节点点击事件
  80. var nodeClick = bindNodeClick(node)
  81. // 绑定拖拽
  82. node.call(nodeDrag(force))
  83. force.on('tick', () => (buildTick(link, node, linetext)))
  84. force.alphaTarget(0).restart()
  85. // 向前推动力导向,确定节点位置坐标
  86. advance(force, 1000)
  87. // 停止力导向
  88. force.stop()
  89. buildTick(link, node, linetext)
  90. }
  91. }
  92. </script>
  93. </body>
  94. </html>

2. draw.js

  1. // 创建画布
  2. function buildVis() {
  3. d3.select('#canvas').select('*').remove()
  4. const zoom = d3.zoom().scaleExtent([0.01, 5]).on('zoom', () => { vis.attr('transform', () => (d3.event.transform)) })
  5. const vis = d3.select('#canvas')
  6. .append('svg')
  7. .attr('width', '100%')
  8. .attr('height', '100%')
  9. .call(zoom)
  10. .on('dblclick.zoom', null)
  11. .append('g')
  12. .attr('class', 'all')
  13. // 箭头
  14. vis.append('marker')
  15. .attr('id', 'arrow')
  16. .attr('markerWidth', 5)
  17. .attr('markerHeight', 5)
  18. .attr('viewBox', '0 -5 10 10')
  19. .attr('refX', 8)
  20. .attr('refY', 0)
  21. .attr('orient', 'auto')
  22. .append('path')
  23. .attr('d', 'M0,-5L10,0L0,5')
  24. .attr('fill', '#fce6d4')
  25. // 图标
  26. 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'
  27. vis.append('defs').append('g').attr('id', 'user').append('path').attr('d', path)
  28. return vis
  29. }
  30. // 创建力布局
  31. function buildForce() {
  32. return d3.forceSimulation()
  33. .force('link', d3.forceLink().distance(200).id((d) => { return d.id }))
  34. .force('charge', d3.forceManyBody().strength(-400))
  35. // 力的中心点
  36. .force('center', d3.forceCenter(500 / 2, 500 / 2))
  37. .force('collide', d3.forceCollide().strength(-30))
  38. }
  39. // 创建连线
  40. function buildLink(json, linkGroup) {
  41. let link = linkGroup.selectAll('.line').data(json.edges, (d) => { return d.entire.id })
  42. link.exit().remove()
  43. link = link.enter().append('path')
  44. .attr('stroke-width', 2)
  45. .attr('class', 'line')
  46. .style('stroke', '#ccc')
  47. .style('cursor', 'pointer')
  48. .attr('fill', 'none')
  49. .attr('id', (d) => { return 'line-' + d.entire.id + '-source-' + d.entire.source + '-target-' + d.entire.target })
  50. .attr('marker-end', 'url(#arrow)')
  51. .merge(link)
  52. return link
  53. }
  54. // 创建连线文字
  55. function buildLinktext(json, linktextGroup) {
  56. let linkText = linktextGroup.selectAll('text').data(json.edges, (d) => { return d.entire.id })
  57. linkText.exit().remove()
  58. linkText = linkText.enter().append('text')
  59. .attr('class', (d) => { return 'linkText-' + d.entire.types })
  60. .attr('id', (d) => { return 'linkText-' + d.entire.id + '-source-' + d.entire.source + '-target-' + d.entire.target })
  61. .attr('dy', -5)
  62. .style('font-size', 12)
  63. .merge(linkText)
  64. linkText.selectAll('.textPath').remove()
  65. linkText.append('textPath')
  66. .attr('startOffset', '45%')
  67. .attr('class', (d) => { return 'textPath linetext-' + d.entire.types })
  68. .attr('xlink:href', (d) => { return '#line-' + d.entire.id + '-source-' + d.entire.source + '-target-' + d.entire.target })
  69. .text((d) => { return d.entire.id })
  70. return linkText
  71. }
  72. // 创建节点
  73. function buildNode(json, nodeGroup) {
  74. let node = nodeGroup.selectAll('.node')
  75. node = node.data(json.nodes, (d) => { return d.id })
  76. node.exit().remove()
  77. node = node.enter()
  78. .append('g')
  79. .attr('class', 'node')
  80. .attr('id', (d) => { return 'node-' + d.id })
  81. .merge(node)
  82. node.selectAll('.node-bg').remove()
  83. node.selectAll('.node-icon').remove()
  84. node.selectAll('.node-text').remove()
  85. // 背景圆
  86. node.append('circle')
  87. .attr('class', 'node-bg')
  88. .attr('id', (d) => { return 'nodeBg-' + d.id })
  89. .attr('r', 24)
  90. .style('fill', '#C1C1C1')
  91. // .style('fill-opacity', 0.3)
  92. // 图标
  93. node.append('use')
  94. .attr('class', 'node-icon')
  95. .attr('id', (d) => { return 'nodeIcon-' + d.id })
  96. .attr('r', 24)
  97. .style('fill', '#FFFFFF')
  98. .attr('xlink:href', '#user')
  99. // 文字
  100. node.append('text')
  101. .attr('class', 'node-text')
  102. .attr('id', (d) => { return 'nodeText-' + d.id })
  103. .attr('dy', 40)
  104. .attr('text-anchor', 'middle')
  105. .text((d) => { return d.entire.id })
  106. return node
  107. }
  108. // 节点拖拽
  109. function nodeDrag(force) {
  110. // 开始拖拽
  111. const dragstart = () => {
  112. if (!d3.event.active) force.alphaTarget(0.3).restart()
  113. d3.event.subject.fx = d3.event.subject.x
  114. d3.event.subject.fy = d3.event.subject.y
  115. }
  116. // 正在拖拽
  117. const dragmove = () => {
  118. d3.event.subject.fx = d3.event.x
  119. d3.event.subject.fy = d3.event.y
  120. }
  121. // 结束拖拽
  122. const dragend = () => {
  123. if (!d3.event.active) force.stop()
  124. }
  125. return d3.drag().on('start', dragstart).on('drag', dragmove).on('end', dragend)
  126. }
  127. // 更新坐标
  128. function buildTick(link, node, linetext) {
  129. link.attr('d', (d) => {
  130. // 如果连接线连接的是同一个实体,则对path属性进行调整,绘制的圆弧属于长圆弧,同时对终点坐标进行微调,避免因坐标一致导致弧无法绘制
  131. if (d.target === d.source) {
  132. const dr = 30 / d.linknum
  133. return 'M' + d.source.x + ',' + d.source.y + 'A' + dr + ',' + dr + ' 0 1,1 ' + d.target.x + ',' + (d.target.y + 1)
  134. } else if (d.size % 2 !== 0 && d.linknum === 1) {
  135. // 如果两个节点之间的连接线数量为奇数条,则设置编号为1的连接线为直线,其他连接线会均分在两边
  136. const tan = Math.abs((d.target.y - d.source.y) / (d.target.x - d.source.x))
  137. 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))
  138. 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))
  139. 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))
  140. 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))
  141. return 'M' + x1 + ' ' + y1 + ' L ' + x2 + ' ' + y2
  142. }
  143. // 根据连接线编号值来动态确定该条椭圆弧线的长半轴和短半轴,当两者一致时绘制的是圆弧
  144. // 注意A属性后面的参数,前两个为长半轴和短半轴,第三个默认为0,第四个表示弧度大于180度则为1,小于则为0,这在绘制连接到相同节点的连接线时用到;第五个参数,0表示正角,1表示负角,即用来控制弧形凹凸的方向。本文正是结合编号的正负情况来控制该条连接线的凹凸方向,从而达到连接线对称的效果
  145. const curve = 3
  146. const homogeneous = 1.2
  147. const dx = d.target.x - d.source.x
  148. const dy = d.target.y - d.source.y
  149. let dr = Math.sqrt(dx * dx + dy * dy) * (d.linknum + homogeneous) / (curve * homogeneous)
  150. // 圆心连线tan值
  151. const tan = Math.abs(dy / dx)
  152. const x1 = dx > 0 ? Math.sqrt(24 * 24 / (tan * tan + 1)) + d.source.x : d.source.x - Math.sqrt(24 * 24 / (tan * tan + 1))
  153. 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))
  154. const x2 = dx > 0 ? d.target.x - Math.sqrt(24 * 24 / (1 + tan * tan)) : d.target.x + Math.sqrt(24 * 24 / (1 + tan * tan))
  155. 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))
  156. // 当节点编号为负数时,对弧形进行反向凹凸,达到对称效果
  157. if (d.linknum < 0) {
  158. dr = Math.sqrt(dx * dx + dy * dy) * (-1 * d.linknum + homogeneous) / (curve * homogeneous)
  159. return 'M' + x1 + ',' + y1 + 'A' + dr + ',' + dr + ' 0 0,0 ' + x2 + ',' + y2
  160. }
  161. return 'M' + x1 + ',' + y1 + 'A' + dr + ',' + dr + ' 0 0,1 ' + x2 + ',' + y2
  162. })
  163. node.attr('transform', (d) => ('translate(' + d.x + ',' + d.y + ')'))
  164. // 连线文字角度
  165. linetext.attr('transform', (d) => {
  166. if (d.target.x < d.source.x) {
  167. const { x, y, width, height } = document.getElementById('linkText-' + d.entire.id + '-source-' + d.entire.source + '-target-' + d.entire.target).getBBox()
  168. const rx = x + width / 2
  169. const ry = y + height / 2
  170. return 'rotate(180 ' + rx + ' ' + ry + ')'
  171. } else {
  172. return 'rotate(0)'
  173. }
  174. })
  175. }
  176. // 向前推动力导向,确定节点位置坐标
  177. function advance(force, num) {
  178. for (let i = 0, n = num; i < n; ++i) {
  179. force.tick()
  180. }
  181. }
  182. function bindNodeClick(node) {
  183. // 单击
  184. node.on('click', (d) => {
  185. console.log('单击')
  186. })
  187. // 双击
  188. node.on('dblclick', (d) => {
  189. console.log('双击')
  190. })
  191. // 右击
  192. node.on('contextmenu', (d) => {
  193. console.log('右击')
  194. })
  195. }

3. dataCollation.js

  1. // 整理连接线关系
  2. function collationLinksData(links, linkmap, lGroup) {
  3. const arr = []
  4. for (const i in links) {
  5. arr.push({
  6. source: links[i].source,
  7. target: links[i].target,
  8. id: links[i].id,
  9. entire: links[i]
  10. })
  11. }
  12. // 对连接线进行统计和分组,不区分连接线的方向,只要属于同两个实体,即认为是同一组
  13. for (var i = 0; i < arr.length; i++) {
  14. const key = arr[i].source < arr[i].target ? arr[i].source + '-' + arr[i].target : arr[i].target + '-' + arr[i].source
  15. if (!linkmap.hasOwnProperty(key)) {
  16. linkmap[key] = 0
  17. }
  18. linkmap[key] += 1
  19. if (!lGroup.hasOwnProperty(key)) {
  20. lGroup[key] = []
  21. }
  22. lGroup[key].push(arr[i])
  23. }
  24. // 为每一条连接线分配size属性,同时对每一组连接线进行编号
  25. for (var i = 0; i < arr.length; i++) {
  26. const key = arr[i].source < arr[i].target ? arr[i].source + '-' + arr[i].target : arr[i].target + '-' + arr[i].source
  27. arr[i].size = linkmap[key]
  28. // 同一组的关系进行编号
  29. const group = lGroup[key]
  30. const keyPair = key.split('-')
  31. // 标示该组关系是指向两个不同实体还是同一个实体
  32. let type = 'noself'
  33. if (keyPair[0] === keyPair[1]) {
  34. type = 'self'
  35. }
  36. // 给节点分配编号
  37. setLinkNumber(group, type)
  38. }
  39. return arr
  40. }
  41. // 给节点分配编号 linknum
  42. function setLinkNumber(group, type) {
  43. if (group.length === 0) return
  44. // 对该分组内的关系按照方向进行分类,此处根据连接的实体ASCII值大小分成两部分
  45. const linksA = []
  46. const linksB = []
  47. for (let i = 0; i < group.length; i++) {
  48. const link = group[i]
  49. if (link.source < link.target) {
  50. linksA.push(link)
  51. } else {
  52. linksB.push(link)
  53. }
  54. }
  55. // 确定关系最大编号。为了使得连接两个实体的关系曲线呈现对称,根据关系数量奇偶性进行平分。
  56. // 特殊情况:当关系都是连接到同一个实体时,不平分
  57. let maxLinkNumber = 0
  58. if (type === 'self') {
  59. maxLinkNumber = group.length
  60. } else {
  61. maxLinkNumber = group.length % 2 === 0 ? group.length / 2 : (group.length + 1) / 2
  62. }
  63. // 如果两个方向的关系数量一样多,直接分别设置编号即可
  64. if (linksA.length === linksB.length) {
  65. let startLinkNumber = 1
  66. for (let i = 0; i < linksA.length; i++) {
  67. linksA[i].linknum = startLinkNumber++
  68. }
  69. startLinkNumber = 1
  70. for (let i = 0; i < linksB.length; i++) {
  71. linksB[i].linknum = startLinkNumber++
  72. }
  73. } else {
  74. // 当两个方向的关系数量不对等时,先对数量少的那组关系从最大编号值进行逆序编号,然后在对另一组数量多的关系从编号1一直编号到最大编号,再对剩余关系进行负编号
  75. // 如果抛开负号,可以发现,最终所有关系的编号序列一定是对称的(对称是为了保证后续绘图时曲线的弯曲程度也是对称的)
  76. var biggerLinks, smallerLinks
  77. if (linksA.length > linksB.length) {
  78. biggerLinks = linksA
  79. smallerLinks = linksB
  80. } else {
  81. biggerLinks = linksB
  82. smallerLinks = linksA
  83. }
  84. let startLinkNumber = maxLinkNumber
  85. for (let i = 0; i < smallerLinks.length; i++) {
  86. smallerLinks[i].linknum = startLinkNumber--
  87. }
  88. const tmpNumber = startLinkNumber
  89. startLinkNumber = 1
  90. let p = 0
  91. while (startLinkNumber <= maxLinkNumber) {
  92. biggerLinks[p++].linknum = startLinkNumber++
  93. }
  94. // 开始负编号
  95. startLinkNumber = 0 - tmpNumber
  96. for (let i = p; i < biggerLinks.length; i++) {
  97. biggerLinks[i].linknum = startLinkNumber++
  98. }
  99. }
  100. }

效果图

20uWnO.png

转载请注明作者和出处,并添加本页链接。
原文链接: //v2ci.com/21.html