我在D3中有一个气泡图,并用它来显示每组有多少气泡。这个版本的开始大约有500个气泡,而我的完整版本大约有3,000个气泡。
我在二维上挣扎。我正在尝试使气泡在状态之间不过渡时保持原状,并且还试图使气泡创建矩形。
这是气泡图的demo。我将添加代码,然后再进行尝试。
这是我的泡泡的代码。
// Initial time and quarter
let time_so_far = 0;
let quarter = 0;
const tick_time = 100
// Forces
const radius = 1.5
const padding1 = 10;
const padding2 = 2;
const strength = 50
const veloc_decay = .99
const alpha = .05
const alpha_decay = 0
const alpha_min = 0.001
const alpha_Collision = .08;
const charge_strength = -.5
const charge_theta = .9
// Load data
Promise.all([
d3.tsv("stages.tsv", d3.autoType),
d3.tsv("customers.tsv", d3.autoType),
])
// Once data is loaded...
.then(function(files){
// Prepare the data...
const stage_data = files[0]
const customer_data = files[1]
// Consolidate stages by id.
stage_data.forEach(d => {
if (d3.keys(stakeholders).includes(d.id+"")) {
stakeholders[d.id+""].push(d);
} else {
stakeholders[d.id+""] = [d];
}
});
// Consolidate customers by week.
customer_data.forEach(d => {
if (d3.keys(customers).includes(d.week+"")) {
customers[d.week+""].push(d);
} else {
customers[d.week+""] = [d];
}
});
// Create node data.
var nodes = d3.keys(stakeholders).map(function(d) {
// Initialize count for each group.
groups[stakeholders[d][0].stage].cnt += 1;
return {
id: "node"+d,
x: groups[stakeholders[d][0].stage].x + Math.random(),
y: groups[stakeholders[d][0].stage].y + Math.random(),
r: radius,
color: groups[stakeholders[d][0].stage].color,
group: stakeholders[d][0].stage,
timeleft: stakeholders[d][0].weeks,
istage: 0,
stages: stakeholders[d]
}
});
// Circle for each node.
const circle = svg.append("g")
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("fill", d => d.color)
.attr("r", d => d.r);
// Forces
const simulation = d3.forceSimulation(nodes)
// .force("bounds", boxingForce)
.force("x", d => d3.forceX(d.x))
.force("y", d => d3.forceY(d.y))
.force("cluster", forceCluster())
.force("collide", forceCollide())
.force("charge", d3.forceManyBody().strength(charge_strength).theta(charge_theta))
// .force('center', d3.forceCenter(center_x, center_y))
.alpha(alpha)
.alphaDecay(alpha_decay)
.alphaMin(alpha_min)
.velocityDecay(veloc_decay)
// Adjust position of circles.
simulation.on("tick", () => {
circle
.attr("cx", d => Math.max(r, Math.min(500 - r, d.x)))
.attr("cy", d => Math.max(r, Math.min(500 - r, d.y)))
.attr("fill", d => groups[d.group].color);
});
// Force to increment nodes to groups.
function forceCluster() {
let nodes;
function force(alpha) {
const l = alpha * strength;
for (const d of nodes) {
d.vx -= (d.x - groups[d.group].x) * l;
d.vy -= (d.y - groups[d.group].y) * l;
}
}
force.initialize = _ => nodes = _;
return force;
}
// Force for collision detection.
function forceCollide() {
let nodes;
let maxRadius;
function force() {
const quadtree = d3.quadtree(nodes, d => d.x, d => d.y);
for (const d of nodes) {
const r = d.r + maxRadius;
const nx1 = d.x - r, ny1 = d.y - r;
const nx2 = d.x + r, ny2 = d.y + r;
quadtree.visit((q, x1, y1, x2, y2) => {
if (!q.length) do {
if (q.data !== d) {
const r = d.r + q.data.r + (d.group === q.data.group ? padding1 : padding2);
let x = d.x - q.data.x, y = d.y - q.data.y, l = Math.hypot(x, y);
if (l < r) {
l = (l - r) / l * alpha_Collision;
d.x -= x *= l, d.y -= y *= l;
q.data.x += x, q.data.y += y;
}
}
} while (q = q.next);
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
}
}
force.initialize = _ => maxRadius = d3.max(nodes = _, d => d.r) + Math.max(padding1, padding2);
return force;
}
// Make time pass. Adjust node stage as necessary.
function timer() {
// Ticker...
nodes.forEach(function(o,i) {
o.timeleft -= 1;
if (o.timeleft == 0 && o.istage < o.stages.length-1) {
// Decrease counter for previous group.
groups[o.group].cnt -= 1;
// Update current node to new group.
o.istage += 1;
o.group = o.stages[o.istage].stage;
o.timeleft = o.stages[o.istage].weeks;
// Increment counter for new group.
groups[o.group].cnt += 1;
}
});
// Previous quarter
quarter = Math.floor(time_so_far / 12)
// Increment time.
time_so_far += 1;
// goes by week, timer updates every quarter
var current_quarter = Math.floor(time_so_far / 13) + 1
// stop on the last quarter
if(time_so_far == d3.keys(customers).length) { return }
d3.select("#timecount .cnt").text(quarters[current_quarter]);
// update counter
d3.selectAll(".counter")
.text(d => d.cnt)
// Define length of a tick
d3.timeout(timer, tick_time);
} // @end timer()
timer()
}); // end TSV
现在,我的泡沫在不断地移动。即使我将气泡的空间变大而填充物的空间变小,它们也会继续运动。
我尝试将.alphaDecay()
的值设置为大于0
的值,气泡会停止移动,而且看上去还不错,但是它们没有能量在状态之间转换
我想进行设置,以便气泡在页面加载时找到它们的位置,然后它们不动,除了从no interactions
变为portfolio
到partner
类似于气泡图here。
另一个问题是气泡聚集成圆形。我想让他们为每个州填写整个矩形背景。
根据Mike Bostock的comments,我在simulation.on
函数中添加了边界。它可以在整个空间上设置边界,但不会将边界单独应用于每个状态,因此它们最终仍会聚成圆形。
我也尝试过John Guerra's d3.forceBoundary
,但遇到了同样的问题。
如何强制气泡停留在一个位置并仅在状态转换发生时移动?如何使气泡在每个状态下聚集为矩形?
编辑:我尝试将alphaDecay设置为0,以便气泡会初始化并停止移动,然后在.on("tick",
函数中添加了一个新的alpha值,但这只是让它们保持能量。
问题的核心是,我不知道如何施加力使他们从一个状态跨越视线移动到另一状态,但不会导致他们四处乱动。
我的下一个尝试是为改变状态创造不同于创建状态的力量。
Edit2:我有解决能源问题的解决方案。这有点hacky。
我在o.statechange = 3
内的if loop
内添加了nodes.forEach(function(o,i) {
,并在if循环的上方添加了o.statechange -= 1
。然后,在forceCluster
中添加
for (var i = 0, n = nodes.length, node, k = alpha * strength; i < n; ++i) {
node = nodes[i];
if(node.statechange <= 0) { continue }
node.vx -= (node.x - groups[node.group].x) * k;
node.vy -= (node.y - groups[node.group].y) * k;
}
如果他们需要移动,它将为圆圈提供三个滴答的能量。否则,他们什么也得不到。 (最后编辑,此变通方法仅适用于少量节点,但随着节点数量变大而失败)