精华内容
下载资源
问答
  • D3力导向图

    2021-01-17 13:17:15
    2017年5月8日一、背景力导向图非常适合于渲染关系型信息图。二、什么是力导向图(Force-directed)?我们可以把整张 Network 想象成一个物理仿真系统(Simulation)。系统中的每个节点(Node)都是一个带有能量的粒子,...

    2017年5月8日

    一、背景

    力导向图非常适合于渲染关系型信息图。

    二、什么是力导向图(Force-directed)?

    我们可以把整张 Network 想象成一个物理仿真系统(Simulation)。系统中的每个节点(Node)都是一个带有能量的粒子,粒子与粒子之间存在斥力(如模拟库伦斥力),而被边(Link)所连结的粒子受到牵引力(如模拟胡克弹力)。系统中的粒子在斥力和引力的不断作用下,从随机无序的布局(Layout)不断发生位移,逐渐趋于平衡有序的布局。同时整个仿真系统的能量也在不断消耗,经过数次迭代后,粒子之间不再发生相对位移,整个系统达到最终稳定平衡的状态。

    d11f0d2719ad

    此动效实现的本质就是每一帧都重新渲染图中节点的位置(x,y), 节点的位置(x,y)是由节点上一帧所处的位置(x,y)+速度(vx,vy)所决定的。而速度就是通过力学模型所计算出来的。

    关键在于力(Forces),D3.js 中内置了几种经典的力模型:

    1. 中心力(Centering)

    中心力可以使得节点最终布局是围绕着某个中心的。相当于某个中心点对所有的节点都有一个制约,不会让布局的中心偏离。

    2. 碰撞力(Collision)

    d11f0d2719ad

    碰撞力为每个节点都设定一个圆形的碰撞区域,从而防止节点之间重叠。

    关键参数:radius 碰撞半径

    3. 牵引力(Links)

    d11f0d2719ad

    牵引力的强度与节点之间的距离成正比,类似于弹簧力。

    关键参数:distance。影响两个节点之间的最终距离。

    4. N 体力(Many-Body)

    N体问题是天体力学的一种力学模型,它研究 N 个质点相互之间在万有引力作用下的运动规律。

    Many-Body 力是作用于所有节点之间的,是全局的,任何两个节点之间都将受到此力的影响。(与 牵引力 Links 不同,Links 力仅仅会影响有连接关系的两个节点)

    它可以用来模拟引力(吸引力),只需设置的 strength 参数为正数;

    它也可用来模拟电荷力(排斥力),只需设置的 strength 参数为负数。

    实现算法使用了 the Barnes–Hut approximation(通过将平面不断递归地划分成四个小区域来构建一棵四叉树) 来提高性能;

    5. 方向力(Positioning)

    方向力分为 X 方向和 Y 方向,即将作用力限制在一个维度上( X 维度或者 Y 维度)

    说明

    以上这几个力学模型是 D3.js 封装的几个经典力学模型,开发者也可根据自身的业务场景,应用自定义的力模型;

    力模型是可以多重复叠加的,即可同时叠加中心力、碰撞力、牵引力等等;

    三、最终效果

    d11f0d2719ad

    通过控制右侧面板,所见即所得地为 Chart 添加不同的力,用户可灵活定制想要的效果。

    d11f0d2719ad

    四、相关资料

    展开全文
  • d3力导向图

    2019-12-26 09:16:51
    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> ...

    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++
        }
      }
    }
    

    效果图

    在这里插入图片描述

    展开全文
  • D3js力导向图搭建 d3js是一个可以基于数据来操作文档的JavaScript库。可以使用HTML,CSS,SVG以及Canvas来展示数据。力导向图能够用来表示节点间多对多的关系。 实现效果:连线有箭头,点击节点能改变该节点颜色和所...
  • 本篇文章主要介绍了如何在react中搭建d3力导向图,现在分享给大家,也给大家做个参考。D3js力导向图搭建d3js是一个可以基于数据来操作文档的JavaScript库。可以使用HTML,CSS,SVG以及Canvas来展示数据。力导向图能够...

    本篇文章主要介绍了如何在react中搭建d3力导向图,现在分享给大家,也给大家做个参考。

    D3js力导向图搭建

    d3js是一个可以基于数据来操作文档的JavaScript库。可以使用HTML,CSS,SVG以及Canvas来展示数据。力导向图能够用来表示节点间多对多的关系。

    实现效果:连线有箭头,点击节点能改变该节点颜色和所连接的线的粗细,缩放、拖拽。

    版本:4.X

    53ab43e0f18a594f4845dd34c458e31d.png

    安装和导入

    npm安装:npm install d3

    前端导入:import * as d3 from 'd3';

    一、完整代码import React, { Component } from 'react';

    import PropTypes from 'prop-types';

    import { connect } from 'react-redux';

    import { push } from 'react-router-redux';

    import * as d3 from 'd3';

    import { Row, Form } from 'antd';

    import { chartReq} from './actionCreator';

    import './Chart.less';

    const WIDTH = 1900;

    const HEIGHT = 580;

    const R = 30;

    let simulation;

    class Chart extends Component {

    constructor(props, context) {

    super(props, context);

    this.print = this.print.bind(this);

    this.forceChart = this.forceChart.bind(this);

    this.state = {

    };

    }

    componentWillMount() {

    this.props.dispatch(push('/Chart'));

    }

    componentDidMount() {

    this.print();

    }

    print() {

    let callback = (res) => { // callback获取后台返回的数据,并存入state

    let nodeData = res.data.nodes;

    let relationData = res.data.rels;

    this.setState({

    nodeData: res.data.nodes,

    relationData: res.data.rels,

    });

    let nodes = [];

    for (let i = 0; i < nodeData.length; i++) {

    nodes.push({

    id: (nodeData[i] && nodeData[i].id) || '',

    name: (nodeData[i] && nodeData[i].name) || '',

    type: (nodeData[i] && nodeData[i].type) || '',

    definition: (nodeData[i] && nodeData[i].definition) || '',

    });

    }

    let edges = [];

    for (let i = 0; i < relationData.length; i++) {

    edges.push({

    id: (relationData[i] && (relationData[i].id)) || '',

    source: (relationData[i] && relationData[i].start.id) || '',

    target: (relationData[i] && relationData[i].end.id) || '',

    tag: (relationData[i] && relationData[i].name) || '',

    });

    }

    this.forceChart(nodes, edges); // d3力导向图内容

    };

    this.props.dispatch(chartReq({ param: param }, callback));

    }

    // func

    forceChart(nodes, edges) {

    this.refs['theChart'].innerHTML = '';

    // 函数内其余代码请看拆解代码

    }

    render() {

    return (

    );

    }

    }

    Chart.propTypes = {

    dispatch: PropTypes.func.isRequired,

    };

    function mapStateToProps(state) {

    return {

    };

    }

    const WrappedChart = Form.create({})(Chart);

    export default connect(mapStateToProps)(WrappedChart);

    二、拆解代码

    1.组件

    整个图都将在p里绘制。

    2.构造节点和连线let nodes = []; // 节点

    for (let i = 0; i < nodeData.length; i++) {

    nodes.push({

    id: (nodeData[i] && nodeData[i].id) || '',

    name: (nodeData[i] && nodeData[i].name) || '', // 节点名称

    });

    }

    let edges = []; // 连线

    for (let i = 0; i < relationData.length; i++) {

    edges.push({

    id: (relationData[i] && (relationData[i].id)) || '',

    source: (relationData[i] && relationData[i].start.id) || '', // 开始节点

    target: (relationData[i] && relationData[i].end.id) || '', // 结束节点

    tag: (relationData[i] && relationData[i].name) || '', // 连线名称

    });

    }

    具体怎么构造依据你们的项目数据。

    3.定义力模型const simulation = d3.forceSimulation(nodes) // 指定被引用的nodes数组

    .force('link', d3.forceLink(edges).id(d => d.id).distance(150))

    .force('collision', d3.forceCollide(1).strength(0.1))

    .force('center', d3.forceCenter(WIDTH / 2, HEIGHT / 2))

    .force('charge', d3.forceManyBody().strength(-1000).distanceMax(800));

    通过simulation.force()设置力,可以设置这几种力:Centering:中心力,设置图中心点位置。

    Collision:节点碰撞作用力,.strength参数范围为[0,1]。

    Links:连线的作用力;.distance设置连线两端节点的距离。

    Many-Body:.strength的参数为正时,模拟重力,为负时,模拟电荷力;.distanceMax的参数设置最大距离。

    Positioning:给定向某个方向的力。

    通过simulation.on监听力图元素位置变化。

    4.绘制svgconst svg = d3.select('#theChart').append('svg') // 在id为‘theChart'的标签内创建svg

    .style('width', WIDTH)

    .style('height', HEIGHT * 0.9)

    .on('click', () => {

    console.log('click', d3.event.target.tagName);

    })

    .call(zoom); // 缩放

    const g = svg.append('g'); // 则svg中创建g

    创建svg,在svg里创建g,将节点连线等内容放在g内。select:选择第一个对应的元素

    selectAll:选择所有对应的元素

    append:创建元素

    5.绘制连线const edgesLine = svg.select('g')

    .selectAll('line')

    .data(edges) // 绑定数据

    .enter() // 添加数据到选择集edgepath

    .append('path') // 生成折线

    .attr('d', (d) => { return d && 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y; }) // 遍历所有数据,d表示当前遍历到的数据,返回绘制的贝塞尔曲线

    .attr('id', (d, i) => { return i && 'edgepath' + i; }) // 设置id,用于连线文字

    .attr('marker-end', 'url(#arrow)') // 根据箭头标记的id号标记箭头

    .style('stroke', '#000') // 颜色

    .style('stroke-width', 1); // 粗细

    连线用贝塞尔曲线绘制:(M 起点X 起点y L 终点x 终点y)

    6.绘制连线上的箭头const defs = g.append('defs'); // defs定义可重复使用的元素

    const arrowheads = defs.append('marker') // 创建箭头

    .attr('id', 'arrow')

    // .attr('markerUnits', 'strokeWidth') // 设置为strokeWidth箭头会随着线的粗细进行缩放

    .attr('markerUnits', 'userSpaceOnUse') // 设置为userSpaceOnUse箭头不受连接元素的影响

    .attr('class', 'arrowhead')

    .attr('markerWidth', 20) // viewport

    .attr('markerHeight', 20) // viewport

    .attr('viewBox', '0 0 20 20') // viewBox

    .attr('refX', 9.3 + R) // 偏离圆心距离

    .attr('refY', 5) // 偏离圆心距离

    .attr('orient', 'auto'); // 绘制方向,可设定为:auto(自动确认方向)和 角度值

    arrowheads.append('path')

    .attr('d', 'M0,0 L0,10 L10,5 z') // d: 路径描述,贝塞尔曲线

    .attr('fill', '#000'); // 填充颜色viewport:可视区域

    viewBox:实际大小,会自动缩放填充viewport

    7.绘制节点const nodesCircle = svg.select('g')

    .selectAll('circle')

    .data(nodes)

    .enter()

    .append('circle') // 创建圆

    .attr('r', 30) // 半径

    .style('fill', '#9FF') // 填充颜色

    .style('stroke', '#0CF') // 边框颜色

    .style('stroke-width', 2) // 边框粗细

    .on('click', (node) => { // 点击事件

    console.log('click');

    })

    .call(drag); // 拖拽单个节点带动整个图

    创建圆作为节点。

    .call()调用拖拽函数。

    8.节点名称const nodesTexts = svg.select('g')

    .selectAll('text')

    .data(nodes)

    .enter()

    .append('text')

    .attr('dy', '.3em') // 偏移量

    .attr('text-anchor', 'middle') // 节点名称放在圆圈中间位置

    .style('fill', 'black') // 颜色

    .style('pointer-events', 'none') // 禁止鼠标事件

    .text((d) => { // 文字内容

    return d && d.name; // 遍历nodes每一项,获取对应的name

    });

    因为文字在节点上层,如果没有设置禁止鼠标事件,点击文字将无法响应点击节点的效果,也无法拖拽节点。

    9.连线名称const edgesText = svg.select('g').selectAll('.edgelabel')

    .data(edges)

    .enter()

    .append('text') // 为每一条连线创建文字区域

    .attr('class', 'edgelabel')

    .attr('dx', 80)

    .attr('dy', 0);

    edgesText.append('textPath')// 设置文字内容

    .attr('xlink:href', (d, i) => { return i && '#edgepath' + i; }) // 文字布置在对应id的连线上

    .style('pointer-events', 'none')

    .text((d) => { return d && d.tag; });

    10.鼠标移到节点上有气泡提示nodesCircle.append('title')

    .text((node) => { // .text设置气泡提示内容

    return node.definition;

    });

    11.监听图元素的位置变化simulation.on('tick', () => {

    // 更新节点坐标

    nodesCircle.attr('transform', (d) => {

    return d && 'translate(' + d.x + ',' + d.y + ')';

    });

    // 更新节点文字坐标

    nodesTexts.attr('transform', (d) => {

    return 'translate(' + (d.x) + ',' + d.y + ')';

    });

    // 更新连线位置

    edgesLine.attr('d', (d) => {

    const path = 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;

    return path;

    });

    // 更新连线文字位置

    edgesText.attr('transform', (d, i) => {

    return 'rotate(0)';

    });

    });

    12.拖拽function onDragStart(d) {

    // console.log('start');

    // console.log(d3.event.active);

    if (!d3.event.active) {

    simulation.alphaTarget(1) // 设置衰减系数,对节点位置移动过程的模拟,数值越高移动越快,数值范围[0,1]

    .restart(); // 拖拽节点后,重新启动模拟

    }

    d.fx = d.x; // d.x是当前位置,d.fx是静止时位置

    d.fy = d.y;

    }

    function dragging(d) {

    d.fx = d3.event.x;

    d.fy = d3.event.y;

    }

    function onDragEnd(d) {

    if (!d3.event.active) simulation.alphaTarget(0);

    d.fx = null; // 解除dragged中固定的坐标

    d.fy = null;

    }

    const drag = d3.drag()

    .on('start', onDragStart)

    .on('drag', dragging) // 拖拽过程

    .on('end', onDragEnd);

    13.缩放function onZoomStart(d) {

    // console.log('start zoom');

    }

    function zooming(d) {

    // 缩放和拖拽整个g

    // console.log('zoom ing', d3.event.transform, d3.zoomTransform(this));

    g.attr('transform', d3.event.transform); // 获取g的缩放系数和平移的坐标值。

    }

    function onZoomEnd() {

    // console.log('zoom end');

    }

    const zoom = d3.zoom()

    // .translateExtent([[0, 0], [WIDTH, HEIGHT]]) // 设置或获取平移区间, 默认为[[-∞, -∞], [+∞, +∞]]

    .scaleExtent([1 / 10, 10]) // 设置最大缩放比例

    .on('start', onZoomStart)

    .on('zoom', zooming)

    .on('end', onZoomEnd);

    三、其它效果

    1.单击节点时让连接线加粗nodesCircle.on('click, (node) => {

    edges_line.style("stroke-width",function(line){

    if(line.source.name==node.name || line.target.name==node.name){

    return 4;

    }else{

    return 0.5;

    }

    });

    })

    2.被点击的节点变色nodesCircle.on('click, (node) => {

    nodesCircle.style('fill', (nodeOfSelected) => { // nodeOfSelected:所有节点, node: 选中的节点

    if (nodeOfSelected.id === node.id) { // 被点击的节点变色

    console.log('node')

    return '#36F';

    } else {

    return '#9FF';

    }

    });

    })

    四、在react中使用注意事项componentDidMount() {

    this.print();

    }

    print() {

    let callback = (res) => { // callback获取后台返回的数据,并存入state

    let nodeData = res.data.nodes;

    let relationData = res.data.rels;

    this.setState({

    nodeData: res.data.nodes,

    relationData: res.data.rels,

    });

    let nodes = [];

    for (let i = 0; i < nodeData.length; i++) {

    nodes.push({

    id: (nodeData[i] && nodeData[i].id) || '',

    name: (nodeData[i] && nodeData[i].name) || '',

    type: (nodeData[i] && nodeData[i].type) || '',

    definition: (nodeData[i] && nodeData[i].definition) || '',

    });

    }

    let edges = [];

    for (let i = 0; i < relationData.length; i++) {

    edges.push({

    id: (relationData[i] && (relationData[i].id)) || '',

    source: (relationData[i] && relationData[i].start.id) || '',

    target: (relationData[i] && relationData[i].end.id) || '',

    tag: (relationData[i] && relationData[i].name) || '',

    });

    }

    this.forceChart(nodes, edges); // d3力导向图内容

    };

    this.props.dispatch(getDataFromNeo4J({

    neo4jrun: 'match p=(()-[r]-()) return p limit 300',

    }, callback));

    }

    在哪里构造图因为图是动态的,如果渲染多次(render执行多次,渲染多次),不会覆盖前面渲染的图,反而会造成渲染多次,出现多个图的现象。把构造图的函数print()放到componentDidMount()内执行,则只会渲染一次。

    对节点和连线数据进行增删改操作后,需要再次调用print()函数,重新构造图。

    从哪里获取数据 数据不从redux获取,发送请求后callback直接获取。

    五、干货:d3项目查找网址

    D3js所有项目检索.http://blockbuilder.org/search/

    上面是我整理给大家的,希望今后会对大家有帮助。

    相关文章:

    展开全文
  • d3 力导向图

    千次阅读 2019-04-03 16:35:35
    没有研究过力导向图 仅作参考 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>1</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/j...

    代码是摘抄的,找不找网址了,

    没有研究过力导向图 仅作参考  

    <!DOCTYPE html>
    <html>
    <head>
    	<meta charset="utf-8">
    	<title>1</title>
    	<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    	<script src=https://d3js.org/d3.v4.js></script>
    	<style>
    
    		body,html{
    			cursor: pointer;
    			width: 100%;
    			height: 100%;
    			margin:0;
    			box-sizing: border-box;
    			overflow: hidden;
    		}
    		    .node {
    			cursor: pointer;
    		}
    		    .node circle {
    			fill: none;
    			stroke: #fff;
    			stroke-width: 1.5px;
    		}
    		    .node text {
    			font: 10px sans-serif;
    		}
    		.link{
    			fill:none;
    			stroke:#ccc;
    			stroke-width: 1.5px;
    		}
    		    .link {
    			fill: none;
    			stroke: #ccc;
    			stroke-width: 1.5px;
    		}
    		.tree{
    			height: 100%;
    			margin: 0 auto;
    			background: #E0E0E0;
    			box-sizing: border-box;
    		}
    
    		.tree svg{
    			width: 100%;
    			height: 100%;
    		}
    	</style>
    </head>
    <body>
    <svg height=1000 id=mindmap-svg width=1300></svg>
    <!--<svg class="chart"></svg>-->
    
    
    
    <script>
    	!function () {
    		var n = d3.select("#mindmap-svg"), t = n.attr("width"), a = n.attr("height"), d = new MindMap;
    
    		d.svg(n), d.size(t, a),
    			d3.json("tree2.json", function (n, t) {
    
    				n ? d3.json("tree2.json", function (n, t) {
    					if (n) throw n;
    					console.log(n)
    					console.log(t)
    					d.data(t), d.render()
    				}) : (d.data(t), d.render())
    			})
    	}()
    
    	function MindMap() {
    		function t() {
    			r(), a([0, 0])
    		}
    
    		function r() {
    			x = d3.tree().size([360, 120]).separation(function (t, e) {
    				return (t.parent == e.parent ? 1 : 2) / t.depth
    			});
    
    			g = d3.hierarchy(p);
    
    			g.descendants().forEach(function (t, e) {
    				t.data.closed = "false" == t.data.closed, t.data.closed && i(t)
    			});
    
    
    
    			m = u.select(".gmind");
    			m.empty() && (m = u.append("g").attr("class", "gmind"));
    
    			v = m.select(".glink");
    			v.empty() && (v = m.append("g").attr("class", "glink"));
    			k = m.select(".gnode");
    			k.empty() && (k = m.append("g").attr("class", "gnode"));
    			v.attr("transform", "translate(" + d / 2 + "," + f / 2 + ")");
    			k.attr("transform", "translate(" + d / 2 + "," + f / 2 + ")");
    
    
    			var t = d3.zoom().on("zoom", function () {
    				var t = "scale(" + d3.event.transform.k + ")", e = "translate(" + d3.event.transform.x + "," + d3.event.transform.y + ")";
    				m.attr("transform", e + t)
    			});
    			u.call(t)
    		}
    
    		function n(t, e) {
    			var r = (t - 90) / 180 * Math.PI,
    				n = e;
    
    			return [n * Math.cos(r), n * Math.sin(r)]
    		}
    
    		function a(t) {
    			x(g),
    				y = g.descendants(),
    				h = g.links(),
    				y.forEach(function (t) {
    					t.y = 120 * t.depth,
    						t.pos = n(t.x, t.y)
    				});
    			for (var e = 0; e < y.length; e++) y[e].id || (y[e].id = b, b++);
    			for (var e = 0; e < h.length; e++) h[e].id || (h[e].id = M, M++);
    
    			var r = d3.scaleLinear().domain([0, 180, 360]).range([1, .3, 1]),
    				o = d3.scaleLinear().domain([0, 1, 5, 10]).range([13, 13, 6.5, 6.5]),
    				c = k.selectAll(".node").data(y, function (t) {
    					console.log(t)
    
    					return t.id
    				}),
    				u = c.enter(), d = c.exit(),
    				f = u.append("g").attr("class", "node").attr("transform", function (e) {
    					console.log(t)
    					console.log(e)
    					return "translate(" + t[0] + "," + t[1] + ")"
    				})
    					.on("click", function (t) {
    						d3.event.defaultPrevented || (i(t), a(t.prevPos))
    					});
    
    			f.append("circle").attr("r", 0).style("fill", function (t) {
    				var e = "";
    				return t.depth > 1 ? (e = d3.hsl(t.parent.color), e = e.brighter(.5), e.l = r(t.x)) : e = t.depth > 0 ? d3.hsl(t.x, 1, .5) : "#FFFFFF", t.color = e + "", t.color + ""
    			}).style("stroke-width", 2)
    				.style("stroke", "black")
    				.style("opacity", 0);
    
    
    			var p = f.merge(c).transition().duration(600).attr("transform", function (t) {
    				return "translate(" + t.pos[0] + "," + t.pos[1] + ")"
    			});
    
    			p.select("circle").attr("r", function (t) {
    				return o(t.depth)
    			}).style("opacity", 1), p.select("text").attr("dy", function (t) {
    				return o(t.depth)
    			}).style("fill-opacity", 0);
    			var m = d.transition().duration(600).attr("transform", function (e) {
    				return "translate(" + t[0] + "," + t[1] + ")"
    			}).remove();
    			m.select("circle").style("opacity", 0).attr("r", 0), m.select("text").style("fill-opacity", 0);
    			var w = v.selectAll(".link").data(h, function (t) {
    					return t.target.id
    				}),
    				F = w.enter(),
    				z = w.exit();
    
    			F.append("path").attr("class", "link").attr("fill", "none").attr("stroke", "rgba(20,20,20,1)").attr("stroke-width", 1).attr("opacity", 0).attr("d", function (e) {
    				var r = {x: t[0], y: t[1]};
    				return s({source: r, target: r})
    			}).merge(w).transition().duration(600).attr("opacity", 1).attr("d", function (t) {
    				var e = {x: t.source.pos[0], y: t.source.pos[1]}, r = {x: t.target.pos[0], y: t.target.pos[1]};
    				return "M" + e.x + "," + e.y + "L" + r.x + "," + r.y
    			}),
    				z.transition().duration(600).attr("opacity", 0).attr("d", function (e) {
    					var r = {x: t[0], y: t[1]}, n = {x: t[0], y: t[1]};
    					return "M" + r.x + "," + r.y + "L" + n.x + "," + n.y
    				}).remove(),
    				y.forEach(function (t) {
    					t.prevPos = [t.pos[0], t.pos[1]]
    				})
    		}
    
    		function s(t) {
    			return "M" + t.source.x + "," + t.source.y + "C" + (t.source.x + t.target.x) / 2 + "," + t.source.y + " " + (t.source.x + t.target.x) / 2 + "," + t.target.y + " " + t.target.x + "," + t.target.y
    		}
    
    		function i(t) {
    			t.children ? (t._children = t.children, t.children = null, t.data.closed = !0) : (t.children = t._children, t._children = null, t.data.closed = !1)
    		}
    
    		var u, d, f, p, y, h, x, g, m, v, k, b = 0, M = 0;
    		this.svg = function (t) {
    			if (arguments.length < 1) return u;
    			u = t
    		}, this.data = function (t) {
    			if (arguments.length < 1) return p;
    			p = t
    		}, this.size = function (t, e) {
    			if (arguments.length < 2) return [d, f];
    			d = t, f = e
    		}, this.getRoot = function () {
    			return g
    		}, this.render = t, this.update = null
    	}
    </script>
    
    </body>
    </html>
    

     

    {
      "name":"中国",
      "children":
      [
        {
          "name":"3BMEXzi" ,
          "children":[
            {"name":"杭州",
             "children":[
                {"name":"宁波"},
                {"name":"温州"}
              ]
            }
          ]
        },
        {
          "name":"广西" ,
          "children":
          [
            {
              "name":"桂林"
            },
            {"name":"南宁"},
            {"name":"柳州"},
            {"name":"防城港"},
            {"name":"浙江"}
          ]
        },
        {
          "name":"黑龙江"
        },
        {
          "name":"新疆6" ,
          "children":[
            {"name":"乌鲁木齐"},
            {"name":"克拉玛依"},
            {"name":"吐鲁番"},
            {"name":"哈密"}
          ]}
      ]
    }
    

     

    展开全文
  • vue + D3 力导向图

    2021-05-29 11:40:44
    vue + D3 力导向图 这个力导向图支持 节点点击之后出现数据, 单个节点可以用图片显示, 单个节点可以拖动, 整个页面可以缩放
  • D3力导向图中突出显示所选节点,其链接及其子节点我正在研究D3中的力导向图。我想通过将所有其他节点和链接设置为较低的不透明度来突出显示鼠标悬停节点,其链接及其子节点。在这个例子中,...
  • 静态D3力导向图性能

    2021-07-19 18:39:55
    Is there a way to use d3.js to calculate a force directed graph without rendering it (making it much faster) and then just render the static graph from the pre-calculated values? My code is currently...
  • d3 力导向图 force graph

    2021-01-13 10:21:19
    背景:项目 vue.js + d3 v4力导向图可以直观看出各个元素之间的相互作用力数据:{nodes:[{id:xxx, group: xx},{},...] // nodes 是每个节点 group 是聚类后的分组 为了让每个 circle 显示不同分组的颜色links:[{...
  • D3力导向图使用总结

    千次阅读 2018-11-02 09:34:01
    一、simulation各项参数简单介绍this.simulation = d3 .forceSimulation() .alphaDecay(0.03) //设置 alpha 衰减率.迭代150,默认0.0228 .alphaMin(0.005) //需要在 [0, 1] 之间。如果没有指定 min 则返回当前的最小...
  • <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> ...
  • D3力导向图及树状布局变换

    千次阅读 2017-09-25 11:57:42
    D3力导向图及树状布局变换d3的力导向图是表现关系型数据比较方便且直观的方法,但是会遇到节点比较多且层级关系混乱的情况,这时树状布局就比较方便了,如何不破坏原来结构以最小的代价变换树状布局呢?下面将为大家...
  • D3 力导向图 固定某个节点

    千次阅读 2019-04-19 16:07:21
    很快的可以利用d3力导向图做出以下效果: 不过,我们应该要固定药物所在的节点。 查一下相关的API,传送门:https://github.com/xswei/d3-force/blob/master/README.md#simulation_nodes 由于D3最后会修改我们...
  • 这段时间再学习D3力导向图 力导向图(Force-Directed Graph),是绘图的一种算法。在二维或三维空间里配置节点,节点之间用线连接,称为连线。各连线的长度几乎相等,且尽可能不相交。节点和连线都被施加了力的...
  • 教程:在react中搭建d3力导向图

    千次阅读 2018-01-11 19:27:59
    D3js力导向图搭建 d3js是一个可以基于数据来操作文档的JavaScript库。可以使用HTML,CSS,SVG以及Canvas来展示数据。力导向图能够用来表示节点间多对多的关系。 实现效果:连线有箭头,点击节点能改变该节点颜色和...
  • D3力导向图设置线条长度

    千次阅读 2019-04-19 10:10:49
    怎么设置力导向图的线条长度API并没有给出示例,导致我一直不知道distance这个代码往哪里加,这里记录一下. const simulation = d3.forceSimulation().force('link', d3.forceLink().id(d => d.id).distance(100)...
  • 这是一款对前端框架d3封装比较完善的一个示例,简单、易懂,非常推荐给需要对neo4j数据力导向图的朋友使用,包含了节点的动态添加(这个是重点)
  • // nodes.push({ // type: d3.svg.symbolTypes[~~(Math.random() * d3.svg.symbolTypes.length)], // size: Math.random() * 300 + 100 解决方案 This is a jsfiddle that is exuivalent to the first example that...
  • d3.js实现关系,多条关系连线,关系说明居中。根据json参数自动生成关系图谱。
  • D3力导向地图制作

    千次阅读 2017-06-27 14:55:21
    制作一个中国地图的力导向地图,支持鼠标拖拽,省份作为节点,连线用三角剖分生成。访问地址:http://106.14.147.72/Graphtest/forcemap.html效果: 定义一个力导向布局、投影和地理路径生成器 创建一个...
  • 前端工程根目录下执行yarn add d3,安装 d3 依赖包。安装的版本"d3": "^5.7.0" 2. vue 文件中引入 d3 import * as d3 from 'd3' 例如一个基础的 d3.vue 文件内容,包含基本的<template><script><...
  • d3力导向图聚焦

    2017-10-18 16:22:00
    最重要的是,在双击节点后力导向模型的参数也要根据节点半径作出相应的变化。非激活状态的节点电荷力改为50。一条线有一端的节点为非激活状态,就将它的最短距离也就是distance设为50。这样就能达到一个收缩效果。 ...
  • 我的d3.js力导向图如何解决数据在首次渲染时,所有节点剧烈抖动,为什么抖动,(特别剧烈!!!</strong>)  </p>
  • 最近用到d3.js中的force力导向图,想实现效果如下,所有城市节点都在可视范围内,如果超出有滚动条也可以。遇到的问题是,当节点一多,有的节点就会跑到外面去,这边是通过加大charge相互作用力,从原本的-300改为-...
  • d.name var data = [{ population: 30, value: 'X', type: 'delete' }, { population: 30, value: '收起', type: 'showOn' }, { population: 30, value: '展开', type: 'showOff' }] var sum = d3.sum(data.map...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,098
精华内容 439
关键字:

d3力导向图

友情链接: choujiang.rar