我希望使用D3.js v5实现一个随时间动态变化的气泡图,如this。
如果您运行以下代码并移动滑动条,您会发现上一步中出现的气泡位置正在发生变化,这使得步骤之间的过渡"跳跃"。
如何尽可能保持上一步中出现的气泡位置并仅更新气泡尺寸?
换句话说,给定一组键/值对的序列,如何更新这些对而不让它们使用forceSimulation
相互冲突,同时保持已经存在的键的x,y位置出现在上一个时间步骤?
var data = [
[{text:"a", size: 500},{text:"b", size: 300},{text:"c", size: 150},{text:"d", size: 100},{text:"e", size: 200}],
[{text:"a", size: 400},{text:"b", size: 200},{text:"c", size: 100},{text:"x", size: 300},{text:"y", size: 300}],
[{text:"a", size: 200},{text:"b", size: 100},{text:"x", size: 300},{text:"y", size: 350},{text:"z", size: 200}],
[{text:"a", size: 500},{text:"b", size: 300},{text:"c", size: 150},{text:"d", size: 100},{text:"e", size: 200}],
[{text:"a", size: 400},{text:"b", size: 200},{text:"c", size: 100},{text:"x", size: 300},{text:"y", size: 300}]
]
var tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "hidden")
.style("color", "white")
.style("padding", "8px")
.style("background-color", "rgba(0, 0, 0, 0.75)")
.style("border-radius", "6px")
.style("font", "14px sans-serif")
.text("tooltip");
var width = 300,
height = 300,
color = d3.scaleOrdinal(d3.schemePastel1)
var svg = d3.select("#chart")
.append("svg")
.attr("height", height)
.attr("width", width)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
var circles = svg.selectAll(".node");
var texts = svg.selectAll(".text");
var radiusScale = d3.scaleSqrt().domain([100, 400]).range([10, 40])
var slider = d3.select("#slider")
.append("input")
.attr("class", "slider")
.attr("type", "range")
.attr("min", 0)
.attr("max", data.length - 1)
.attr("value", 0)
.attr("style", "width:300px")
.on("change", function(d) {
update(data[this.value]);
})
var simulation = d3.forceSimulation()
.force("x", d3.forceX())
.force("y", d3.forceY())
.force("collide", d3.forceCollide(function(d) {
return radiusScale(d.size) + 1;
}))
.on('tick', ticked)
// Initial update
update(data[0])
function update(data) {
var t = d3.transition()
.duration(750);
//For circles
//JOIN
circles = circles.data(data, function(d) {
return d.text
})
//EXIT
circles.exit().remove()
//ENTER
circles = circles
.enter().append("circle")
.merge(circles)
circles
.transition(t)
.attr("class", "node")
.attr("r", function(d) {
return radiusScale(d.size)
})
.style("fill", function(d) {
return color((d.text[0].charCodeAt()))
})
circles
.on("mouseover", function(d) {
tooltip.html("Word: " + d.text + "<br>Frequency: " + d.size)
tooltip.style("visibility", "visible")
})
.on("mousemove", function() {
return tooltip
.style("top", (d3.event.pageY - 10) + "px")
.style("left", (d3.event.pageX + 10) + "px")
})
.on("mouseout", function() {
return tooltip.style("visibility", "hidden");
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
//For texts
//JOIN
texts = texts.data(data, function(d) {
return d.text
})
//EXIT
texts.exit().remove()
//ENTER
texts = texts
.enter().append("text")
.merge(texts)
texts
.transition(t)
.attr("class", "text")
.text(function(d) {
return d.text
})
simulation.nodes(data)
simulation.alpha(1).restart();
}
function ticked() {
circles
.attr("cx", function(d) {
return d.x
})
.attr("cy", function(d) {
return d.y
})
texts
.attr("x", function(d) {
return d.x
})
.attr("y", function(d) {
return d.y
})
}
// make circles dragable
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
tooltip.style("visibility", "hidden");
}
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;
}
&#13;
.node:hover {
stroke: #000;
stroke-width: 2px;
opacity: .7;
}
&#13;
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="//d3js.org/d3-scale-chromatic.v0.3.min.js"></script>
<div id="slider"></div>
<div id="chart"></div>
&#13;