引力和相互作用力
先上程式碼:
<!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 內建的 force 模組幫助我們在 web 頁面中通過演算法模擬實現了物理的力學效果。
d3 提供了許多單力給我們使用,但是在實際使用中,我們通常是使用多種效果複合的模擬力的(就像例項中的粒子有碰撞,有引力,還有向中心力),我們就要藉助於 forceSimulation 來幫助我們實現模擬力,其api如下:
- d3.forceSimulation - 建立一個新的力學模擬.
- simulation.restart - 重新啟動模擬的定時器.
- simulation.stop - 停止模擬的定時器.
- simulation.tick - 進行一步模擬模擬.
- simulation.nodes - 設定模擬的節點.
- simulation.alpha - 設定當前的 alpha 值.
- simulation.alphaMin - 設定最小 alpha 閾值.
- simulation.alphaDecay - 設定 alpha 衰減率.
- simulation.alphaTarget - 設定目標 alpha 值.
- simulation.velocityDecay - 設定速度衰減率. 1 對應於無摩擦環境, 0 對應凍結所有粒子.
- simulation.force - 新增或移除一個力模型.
- simulation.find - 根據指定的位置找出最近的節點.
- 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>
複製程式碼
效果如下:
我們在每次點選時都根據點選的位置生成個數隨機的圓點和線條,根據圓點資料生成的線條資料是首尾相連的(線條資料中的資料和節點資料是相同引用地址的), 隨後我們給模擬力新增 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>
複製程式碼
效果如下:
改變的程式碼如下:
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>
複製程式碼
效果如下:
我們使用 hierarchy.descendants()
hierarchy.links()
來獲取我們要渲染的節點資料和連結資料,然後就是正常的按照資料進行渲染。對於節點,我們將同父節點下的節點設定為同顏色。