d3.js 入門學習記錄(十) 力圖

WanFengZ發表於2019-12-08

引力和相互作用力

先上程式碼:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<div class="control-group">
    <button onclick="noForce()">no force</button>
    <button onclick="repulsion()">repulsion</button>
    <button onclick="gravity()">gravity</button>
    <button onclick="positioningWithGravity()">positioning with gravity</button>
    <button onclick="positioningWithRepulsion()">positioning with repulsion</button>
</div>
<script src="../d3.js"></script>
<script>
    const width = 1280,
          height = 800,
          r = 4.5,
          nodes = [],
          colors = d3.scaleOrdinal(d3.schemeCategory10),
          force = d3.forceSimulation()
            .velocityDecay(0.8)
            .alphaDecay(0)
            .force('collision', d3.forceCollide(r + 0.5).strength(1))

    const svg = d3.select('body')
        .append('svg')
            .attr('width', width)
            .attr('height', height)

    force.on('tick', function () {
        svg.selectAll('circle')
          .attr('cx', d => d.x)
          .attr('cy', d => d.y)
    })

    svg.on('mousemove', function () {
        const point = d3.mouse(this),
              node = {x: point[0], y: point[1]}

        svg.append('circle')
          .data([node])
          .attr('class', 'node')
          .attr('cx', d => d.x)
          .attr('cy', d => d.y)
          .attr('r', 1e-6)
          .style('fill', d => colors(Math.round(Math.random() * 10)))
          .transition()
          .attr('r', r)
          .transition().delay(10000)
          .attr('r', 1e-6)
          .on('end', function () {
                nodes.shift()
                force.nodes(nodes)
          })
          .remove()

        nodes.push(node)
        force.nodes(nodes)
    })

    function noForce() {
        force.force('charge', null)
        force.force('x', null)
        force.force('y', null)
        force.restart()
    }

    function repulsion() {
        force.force('charge', d3.forceManyBody().strength(-10))
        force.force('x', null)
        force.force('y', null)
        force.restart()
    }

    function gravity() {
      force.force('charge', d3.forceManyBody().strength(1))
      force.force('x', null)
      force.force('y', null)
      force.restart()
    }

    function positioningWithGravity() {
        force.force('charge', d3.forceManyBody().strength(1))
        force.force('x', d3.forceX().x(width / 2))
        force.force('y', d3.forceY().y(height / 2))
        force.restart()
    }

    function positioningWithRepulsion() {
      force.force('charge', d3.forceManyBody().strength(-10))
      force.force('x', d3.forceX().x(width / 2))
      force.force('y', d3.forceY().y(height / 2))
      force.restart()
    }
</script>
</body>
</html>
複製程式碼

效果如下:

d3.js 入門學習記錄(十) 力圖

可以看見我們生成的小圓點互相之間有了引力和斥力,d3 內建的 force 模組幫助我們在 web 頁面中通過演算法模擬實現了物理的力學效果。

d3 提供了許多單力給我們使用,但是在實際使用中,我們通常是使用多種效果複合的模擬力的(就像例項中的粒子有碰撞,有引力,還有向中心力),我們就要藉助於 forceSimulation 來幫助我們實現模擬力,其api如下:

  1. d3.forceSimulation - 建立一個新的力學模擬.
  2. simulation.restart - 重新啟動模擬的定時器.
  3. simulation.stop - 停止模擬的定時器.
  4. simulation.tick - 進行一步模擬模擬.
  5. simulation.nodes - 設定模擬的節點.
  6. simulation.alpha - 設定當前的 alpha 值.
  7. simulation.alphaMin - 設定最小 alpha 閾值.
  8. simulation.alphaDecay - 設定 alpha 衰減率.
  9. simulation.alphaTarget - 設定目標 alpha 值.
  10. simulation.velocityDecay - 設定速度衰減率. 1 對應於無摩擦環境, 0 對應凍結所有粒子.
  11. simulation.force - 新增或移除一個力模型.
  12. simulation.find - 根據指定的位置找出最近的節點.
  13. simulation.on - 新增或移除事件監聽器.

在示例中,我們就建立了一個模擬力:

force = d3.forceSimulation()
    .velocityDecay(0.8)
    .alphaDecay(0)
    .force('collision', d3.forceCollide(r + 0.5).strength(1))
複製程式碼

我們將速度衰減定為 0.8,alpha 衰減定為 0 (力一直持續下去,便於 demo 觀察),然後我們使用 force 方法給模擬力集合新增各種力,這裡初始化時新增了一個碰撞力,讓粒子具有實際的體積。

隨後我們要指定力的每一個 tick 中對粒子的處理:

force.on('tick', function () {
    svg.selectAll('circle')
      .attr('cx', d => d.x)
      .attr('cy', d => d.y)
})
複製程式碼

然後通過 force.nodes(nodes) 給所有的節點資料應用力。

在變換力的函式中,我們也是通過 force 方法新增原力,然後再 restart,forceManyBody返回一個作用力,strength 的正負值決定了是引力還是斥力。

簡單的力如下:

d3.forceCollide - 建立一個圓形區域的碰撞檢測力模型.

collide.radius - 設定碰撞半徑.

collide.strength - 設定碰撞檢測力模型的強度. [0, 1], 預設 0.7

collide.iterations - 設定迭代次數, 數值越大,效果越優,但是會加大消耗, 預設為 1

d3.forceX - 建立一個 x -方向的一維作用力.

x.strength - 設定力強度. [0, 1], 預設 0.1

x.x - 設定目標 x -座標.

d3.forceY - 建立一個 y -方向的一維作用力.

y.strength - 設定力強度. [0, 1], 預設 0.1

y.y - 設定目標 y -座標.

d3.forceCenter - 建立一箇中心作用力.

center.x - 設定中心作用力的 x -座標.

center.y - 設定中心作用力的 y -座標.

d3.forceManyBody - 建立一個電荷作用力模型.

manyBody.strength - 設定電荷力模型的強度,正值則表示節點之間相互吸引,負值表示節點之間相互排斥,預設-30

manyBody.theta - 設定 Barnes–Hut 演算法的精度.

manyBody.distanceMin - 限制節點之間的最小距離.

manyBody.distanceMax - 限制節點之間的最大距離.

d3.forceRadial - 建立一個環形佈局的作用力.

radial.strength - 設定力強度. [0, 1] 預設 0.1

radial.radius - 設定目標半徑.

radial.x - 設定環形作用力的目標中心 x -座標.

radial.y - 設定環形作用力的目標中心 y -座標.

d3.forceLink - 建立一個 link 作用力.

link.links - 設定彈簧作用力的邊.

link.id - 設定邊元素中節點的查詢方式是索引還是 id 字串.

link.distance - 設定 link 的距離.

link.strength - 設定 link 的強度.

link.iterations - 設定迭代次數.

另外,力佈局節點物件的屬性如下:

index:節點陣列中的索引值

x:當前節點位置的 x 座標

y:當前節點位置的 y 座標

vx:節點當前在 x 軸上的速度

vy:節點當前在 y 軸上的速度

fx:節點固定的 x 位置

fy:節點固定的 y 位置

使用連線約束

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        .line {
            fill: none;
            stroke: steelblue;
            stroke-width: 2;
        }

    </style>
</head>
<body>
<script src="../d3.js"></script>
<script>
    const width = 1280,
          height = 800,
          r = 4.5,
          nodes = [],
          links = [],
          force = d3.forceSimulation()
            .velocityDecay(0.5)
            .alphaDecay(0)
            .force('collision', d3.forceCollide(r + 0.5).strength(1))
            .force('charge', d3.forceManyBody().strength(-50).distanceMax(height / 10))

    const svg = d3.select('body').append('svg')
      .attr('width', width)
      .attr('height', height)

    force.on('tick', function () {
        svg.selectAll('circle')
          .attr('cx', d => boundX(d.x))
          .attr('cy', d => boundY(d.y))

        svg.selectAll('line')
          .attr('x1', d => boundX(d.source.x))
          .attr('y1', d => boundY(d.source.y))
          .attr('x2', d => boundX(d.target.x))
          .attr('y2', d => boundY(d.target.y))
    })

    function boundX(x) {
        return x > r ? (x < width - r ? x : width - r) : r
    }

    function boundY(y) {
        return y > r ? (y < height - r ? y : height - r) : r
    }

    function offset() {
        return Math.random() * 10
    }

    function createNodes(point) {
        const numberOfNodes = Math.round(Math.random() * 10),
              newNodes = []

        for (let i  = 0; i < numberOfNodes; i++) {
            newNodes.push({
                x: point[0] + offset(),
                y: point[1] + offset()
            })
        }

        newNodes.forEach(p => nodes.push(p))

        return newNodes
    }

    function createLinks(nodes) {
        const newLinks = []

        for (let i = 0; i < nodes.length; i++) {
            if (i === nodes.length - 1) {
                newLinks.push({
                    source: nodes[i],
                    target: nodes[0]
                })
            } else {
                newLinks.push({
                    source: nodes[i],
                    target: nodes[i + 1]
                })
            }
        }

        newLinks.forEach(l => links.push(l))

        return newLinks
    }

    svg.on('click', function () {
        const point = d3.mouse(this),
              newNodes = createNodes(point),
              newLinks = createLinks(newNodes)

        newNodes.forEach(node => {
            svg.append('circle')
                .data([node])
                    .classed('node', true)
                    .attr('cx', d => d.x)
                    .attr('cy', d => d.y)
                    .attr('r', 1e-6)
                    .call(d3.drag()
                        .on('start', d => {
                            d.fx = d.x
                            d.fy = d.y
                        })
                        .on('drag', d => {
                            d.fx = d3.event.x
                            d.fy = d3.event.y
                        })
                        .on('end', d => {
                            d.fx = null
                            d.fy = null
                        }))
                .transition()
                    .attr('r', 7)
                .transition()
                    .delay(10000)
                    .attr('r', 1e-6)
                    .on('end', d => nodes.shift())
                    .remove()
        })

        newLinks.forEach(link => {
            svg.append('line')
                .data([link])
                    .classed('line', true)
                    .attr('x1', d => d.source.x)
                    .attr('y1', d => d.source.y)
                    .attr('x2', d => d.target.x)
                    .attr('y2', d => d.target.y)
                .transition().delay(10000)
                    .style('stroke-opacity', 1e-6)
                    .on('end', d => links.shift())
                    .remove()
        })

        force.nodes(nodes)
        force.force('link', d3.forceLink(links).strength(1).distance(20))

        force.restart()
    })

</script>
</body>
</html>
複製程式碼

效果如下:

d3.js 入門學習記錄(十) 力圖

我們在每次點選時都根據點選的位置生成個數隨機的圓點和線條,根據圓點資料生成的線條資料是首尾相連的(線條資料中的資料和節點資料是相同引用地址的), 隨後我們給模擬力新增 link 力,然後應用到節點上。在 tick 中我們需要新增額外的對線條的渲染處理(限制了不超出svg區域):

svg.selectAll('line')
  .attr('x1', d => boundX(d.source.x))
  .attr('y1', d => boundY(d.source.y))
  .attr('x2', d => boundX(d.target.x))
  .attr('y2', d => boundY(d.target.y))
複製程式碼

並且我們還給圓點新增了拖拽:

.call(d3.drag()
    .on('start', d => {
        d.fx = d.x
        d.fy = d.y
    })
    .on('drag', d => {
        d.fx = d3.event.x
        d.fy = d3.event.y
    })
    .on('end', d => {
        d.fx = null
        d.fy = null
    }))
複製程式碼

在效果中可以看見,當我們拖動圓點時, 與之相連的圓點也會隨著一起被拖動,這就是 link 力在作用。

力氣泡圖

在上面的例子中,我們只需要把渲染dom的部分變為根據節點渲染封閉的 path 曲線,其實就是力氣泡圖了。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style type="text/css">
        html, body {
            height: 100%;
        }
        body {
            margin: 0;
        }
        svg {
            width: 100%;
            height: 100%;
        }
        .bubble {
            stroke: grey;
            stroke-width: 1;
        }
    </style>
</head>
<body>
<svg>
    <defs>
        <radialGradient id="gradient" cx="50%" cy="50%" r="100%" fx="50%" fy="50%">
            <stop offset="0%" style="stop-color:blue;stop-opacity:0"/>
            <stop offset="100%" style="stop-color:rgb(255,255,255);stop-opacity:1"/>
        </radialGradient>
    </defs>
</svg>
<script src="../d3.js"></script>
<script>
  const width = 1280,
    height = 800,
    r = 4.5,
    nodes = [],
    links = [],
    force = d3.forceSimulation()
      .velocityDecay(0.5)
      .alphaDecay(0)
      .force('collision', d3.forceCollide(r + 0.5).strength(1))
      .force('charge', d3.forceManyBody().strength(-50).distanceMax(height / 10))

  const svg = d3.select('svg')
    .attr('width', width)
    .attr('height', height)

  const line = d3.line()
    .x(d => d.x)
    .y(d => d.y)
    .curve(d3.curveBasisClosed)

  force.on('tick', function () {
    svg.selectAll('path.bubble')
      .attr('d', d => line(d))
  })

  function boundX(x) {
    return x > r ? (x < width - r ? x : width - r) : r
  }

  function boundY(y) {
    return y > r ? (y < height - r ? y : height - r) : r
  }

  function offset() {
    return Math.random() * 10
  }

  function createNodes(point) {
    const numberOfNodes = Math.round(Math.random() * 10),
      newNodes = []

    for (let i  = 0; i < numberOfNodes; i++) {
      newNodes.push({
        x: point[0] + offset(),
        y: point[1] + offset()
      })
    }

    newNodes.forEach(p => nodes.push(p))

    return newNodes
  }

  function createLinks(nodes) {
    const newLinks = []

    for (let i = 0; i < nodes.length; i++) {
      if (i === nodes.length - 1) {
        newLinks.push({
          source: nodes[i],
          target: nodes[0]
        })
      } else {
        newLinks.push({
          source: nodes[i],
          target: nodes[i + 1]
        })
      }
    }

    newLinks.forEach(l => links.push(l))

    return newLinks
  }

  svg.on('click', function () {
    const point = d3.mouse(this),
      newNodes = createNodes(point),
      newLinks = createLinks(newNodes)

      svg.append('path')
        .data([newNodes])
        .classed('bubble', true)
        .attr('fill', 'url(#gradient)')
        .attr('d', d => {
          console.log(d)
          return line(d)
        })
        .transition().delay(10000)
        .attr('fill-opacity', 0)
        .attr('stroke-opacity', 0)
        .on('end', function () {
          d3.select(this).remove()
        })


    force.nodes(nodes)
    force.force('link', d3.forceLink(links).strength(1).distance(20))

    force.restart()
  })
</script>
</body>
</html>
複製程式碼

效果如下:

d3.js 入門學習記錄(十) 力圖

改變的程式碼如下:

const line = d3.line()
    .x(d => d.x)
    .y(d => d.y)
    .curve(d3.curveBasisClosed)

force.on('tick', function () {
svg.selectAll('path.bubble')
  .attr('d', d => line(d))
})

svg.append('path')
    .data([newNodes])
    .classed('bubble', true)
    .attr('fill', 'url(#gradient)')
    .attr('d', d => {
      console.log(d)
      return line(d)
    })
    .transition().delay(10000)
    .attr('fill-opacity', 0)
    .attr('stroke-opacity', 0)
    .on('end', function () {
      d3.select(this).remove()
    })
複製程式碼

力導向圖

資料使用之前的treeData,見 juejin.im/post/5de08c…

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<script src="../d3.js"></script>
<script>
    function render(data) {
        const width = 1280,
              height = 800,
              r = 4.5,
              colors = d3.scaleOrdinal(d3.schemeCategory10),
              force = d3.forceSimulation()
                .velocityDecay(0.8)
                .alphaDecay(0)
                .force('charge', d3.forceManyBody().strength(-50))
                .force('x', d3.forceX())
                .force('y', d3.forceY()),
              svg = d3.select('body').append('svg')
                .attr('width', width)
                .attr('height', height)
                .attr("viewBox", [-width / 2, -height / 2, width, height]),
              root = d3.hierarchy(data),
              nodes = root.descendants(),
              links =  root.links()

        force.nodes(nodes)
        force.force('link', d3.forceLink(links).strength(1).distance(20))

        force.on('tick', function () {
            svg.selectAll('line')
                .attr('x1', d => d.source.x)
                .attr('y1', d => d.source.y)
                .attr('x2', d => d.target.x)
                .attr('y2', d => d.target.y)

            svg.selectAll('circle')
                .attr('cx', d => d.x)
                .attr('cy', d => d.y)
        })

        svg.selectAll('line')
            .data(links)
            .enter()
            .append('line')
            .style('stroke', '#999')
            .style('stroke-width', '1px')

        svg.selectAll('circle')
            .data(nodes)
            .enter()
            .append('circle')
                .attr('r', r)
            .attr('fill', d => colors(d.parent && d.parent.data.name))
                .call(d3.drag()
                    .on('start', d => {
                        d.fx = d.x
                        d.fy = d.y
                    })
                    .on('drag', d => {
                        d.fx = d3.event.x
                        d.fy = d3.event.y
                    })
                    .on('end', d => {
                        d.fx = null
                        d.fy = null
                    })
                )
    }

    d3.json('./treeData.json').then(data => {
        render(data)
    })
</script>
</body>
</html>
複製程式碼

效果如下:

d3.js 入門學習記錄(十) 力圖

我們使用 hierarchy.descendants() hierarchy.links() 來獲取我們要渲染的節點資料和連結資料,然後就是正常的按照資料進行渲染。對於節點,我們將同父節點下的節點設定為同顏色。

相關文章