d3力导向图

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() {
        // 创建svg视图
        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')
    // .style('fill-opacity', 0.3)

   // 图标
   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) => {
    // 如果连接线连接的是同一个实体,则对path属性进行调整,绘制的圆弧属于长圆弧,同时对终点坐标进行微调,避免因坐标一致导致弧无法绘制
    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) {
      // 如果两个节点之间的连接线数量为奇数条,则设置编号为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
    }
    // 根据连接线编号值来动态确定该条椭圆弧线的长半轴和短半轴,当两者一致时绘制的是圆弧
    // 注意A属性后面的参数,前两个为长半轴和短半轴,第三个默认为0,第四个表示弧度大于180度则为1,小于则为0,这在绘制连接到相同节点的连接线时用到;第五个参数,0表示正角,1表示负角,即用来控制弧形凹凸的方向。本文正是结合编号的正负情况来控制该条连接线的凹凸方向,从而达到连接线对称的效果
    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)
    // 圆心连线tan值
    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])
  }
  // 为每一条连接线分配size属性,同时对每一组连接线进行编号
  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
}

// 给节点分配编号 linknum
function setLinkNumber(group, type) {
  if (group.length === 0) return
  // 对该分组内的关系按照方向进行分类,此处根据连接的实体ASCII值大小分成两部分
  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 {
    // 当两个方向的关系数量不对等时,先对数量少的那组关系从最大编号值进行逆序编号,然后在对另一组数量多的关系从编号1一直编号到最大编号,再对剩余关系进行负编号
    // 如果抛开负号,可以发现,最终所有关系的编号序列一定是对称的(对称是为了保证后续绘图时曲线的弯曲程度也是对称的)
    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++
    }
  }
}

效果图

在这里插入图片描述

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的d3导向的代码示例,包含了节点、边和节点拖动等功能: ```html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>导向</title> <script src="https://d3js.org/d3.v5.min.js"></script> </head> <body> <svg width="600" height="600"></svg> <script> // 创建节点和边的数据 var nodes = [{id: 'A'}, {id: 'B'}, {id: 'C'}, {id: 'D'}, {id: 'E'}]; var links = [{source: 'A', target: 'B'}, {source: 'B', target: 'C'}, {source: 'C', target: 'D'}, {source: 'D', target: 'E'}, {source: 'E', target: 'A'}]; // 创建导向模拟器 var simulation = d3.forceSimulation(nodes) .force('link', d3.forceLink(links).id(function(d) { return d.id; })) .force('charge', d3.forceManyBody()) .force('center', d3.forceCenter(300, 300)) .on('tick', ticked); // 创建节点和边的SVG元素 var svg = d3.select('svg'); var link = svg.selectAll('line') .data(links) .enter().append('line') .attr('stroke', '#999') .attr('stroke-width', '1'); var node = svg.selectAll('circle') .data(nodes) .enter().append('circle') .attr('r', 10) .attr('fill', '#ccc') .call(d3.drag() .on('start', dragstarted) .on('drag', dragged) .on('end', dragended)); // 节点拖动相关的函数 function dragstarted(d) { if (!d3.event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(d) { d.fx = d3.event.x; d.fy = d3.event.y; } function dragended(d) { if (!d3.event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } // 每个时间段更新节点和边的位置 function ticked() { link.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; }); node.attr('cx', function(d) { return d.x; }) .attr('cy', function(d) { return d.y; }); } </script> </body> </html> ``` 在这个示例中,我们首先创建了节点和边的数据,并使用 `d3.forceSimulation()` 函数创建了导向模拟器。然后,我们创建了节点和边的SVG元素,并使用 `d3.drag()` 函数为节点添加了拖动事件。最后,在模拟器的每个时间段里,我们更新了节点和边的位置。如果你需要更详细的解释,请参考d3官方文档。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值