我在 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
to portfolio
to partner
类似于气泡图here.
另一个问题是气泡聚集成圆圈。我想让它们填充每个州的整个矩形背景。
根据迈克·博斯托克的comments,我添加了边界simulation.on
功能。它可以在整个空间上设置边界,但不会单独将边界应用于每个状态,因此它们最终仍然聚集为圆圈。
我也尝试过约翰·格拉的 d3.forceBoundary
但我遇到了同样的问题。
如何强制气泡保持在一个位置,并且仅在发生状态转换时才移动?如何让气泡在每个状态上聚集在矩形中?
编辑:我尝试设置 alphaDecay > 0 这样气泡就会初始化并停止移动,然后我在.on("tick",
功能,但这只是让他们保持能量。
问题的核心是,我不知道如何施加力,让它们在可视化中从一种状态移动到另一种状态,但又不会导致它们混乱。
我的下一个尝试是创造一种不同的力量来改变状态而不是被创造。
Edit2:我有一个解决能源问题的解决方案。这有点老套。
I added o.statechange = 3
内if loop
inside nodes.forEach(function(o,i) {
我添加了o.statechange -= 1
就在 if 循环之上。然后,在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;
}
如果圆圈需要移动,这将为它们提供三个刻度的能量。否则,他们什么也得不到。 (上次编辑,此解决方法适用于少量节点,但随着节点数量变大而失败)