D3 - 如何在数据值变化时更新力模拟
D3 - How to update force simulation when data values change
我在 D3 中遇到了一个关于力模拟的小问题。
我有代表每个国家/地区从 1998 年到 2008 年的贫困率的数据。它是一个气泡图,分为三组,代表贫穷国家、不贫穷国家和没有信息的国家。
最初加载应用程序时,它加载了 1998 年的数据。但是,我在顶部有一些按钮,单击它们会更改年份,随后气泡会自行重新排列。我所能做的就是在单击按钮时更改变量 year
。但是,在整个代码中有使用 year
的函数和变量。当 year
发生变化时,我想重新计算所有依赖于 year
的节点属性和强制参数
这是我的代码。如果您想尝试一下,我已经包含了所有内容。数据文件在这个post.
的最后
async function init() {
// Set up the canvas
var height = 1000, width = 2000;
var svg = d3.select("#panel1").append("svg")
.attr("height", height)
.attr("width", width)
.attr("class", "bubblePanel");
var canvas = svg.append("g")
.attr("transform", "translate(0,0)");
// Choose what year to look at, based on button clicks.
var year = "X1998"
d3.select("#b1998").on("click", function() {
year = "X1998"
console.log(year)
// NOTIFY SIMULATION OF CHANGE //
})
d3.select("#b1999").on("click", function() {
year = "X1999"
console.log(year)
// NOTIFY SIMULATION OF CHANGE //
})
d3.select("#b2000").on("click", function() {
year = "X2000"
console.log(year)
// NOTIFY SIMULATION OF CHANGE //
})
// Implement the physics of the elements. Three forces act according to the poverty level (poor, not poor, and no info)
var simulation = d3.forceSimulation()
.force("x", d3.forceX(function(d) {
if (parseFloat(d[year]) >= 10) {
return 1700
} else if (parseFloat(d[year]) === 0) {
return 1000
} else {
return 300
}
}).strength(0.05))
.force("y", d3.forceY(300).strength(0.05))
.force("collide", d3.forceCollide(function(d) {
return radiusScale(d[year])
}));
// Function to pick colour of circles according to region
function pickColor(d) {
if (d === "East Asia & Pacific") {
return "red"
} else if (d === "Europe & Central Asia") {
return "orange"
} else if (d === "Latin America & Caribbean") {
return "yellow"
} else if (d === "Middle East & North Africa") {
return "green"
} else if (d === "North America") {
return "blue"
} else if (d === "South Asia") {
return "indigo"
} else {
return "violet"
}
}
// Set the scales for bubble radius, and text size.
var radiusScale = d3.scaleSqrt().domain([0, 50]).range([20,80]);
var labelScale = d3.scaleSqrt().domain([0,50]).range([10,40]);
// Read the data
await d3.csv("wd3.csv").then(function(data) {
// Assign each data point to a circle that is colored according to region and has radius according to its poverty level
var bubbles = svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", 100)
.attr("cy", 100)
.attr("fill", function(d) {
return pickColor(d.Region)
})
.attr("r", function(d) {
return radiusScale(d[year])
});
// Assign each ddata point to a text element that shows the counry code of the data point. The text is scaled according to the poverty level
var labels = svg.selectAll("text")
.data(data)
.enter().append("text")
.attr("x", 100)
.attr("y", 100)
.attr("dominant-baseline", "central")
.text(function(d) { return d.XCountryCode })
.style("stroke", "black")
.style("text-anchor", "middle")
.style("font-size", function(d) { return labelScale(d[year]); });
// Code to handle the physics of the bubble and the text
simulation.nodes(data)
.on("tick", ticked)
function ticked() {
bubbles.attr("transform", function(d) {
var k = "translate(" + d.x + "," + d.y + ")";
return k;
})
labels.attr("transform", function(d) {
var k = "translate(" + d.x + "," + d.y + ")";
return k;
})
}
});
}
当 year
发生变化时,每个国家/地区的数据值都会发生变化。我希望我的代码的以下部分得到更新。
节点上的x力:国家可以在一年内从贫困变为另一年不贫困,因此他们的集群将发生变化
圆圈的半径: 半径代表贫困程度。这些每年都会发生变化,因此单击按钮时圆圈的大小会发生变化
国家标签的坐标:这些标签也附加到数据中。因此,当圆圈上的 x 力导致圆圈移动时,标签也应该移动。
非常感谢您的帮助。
可以找到数据文件here。我不小心将它命名为 povertyCSV,但在代码中,它被引用为 "wd3.csv"
如果我理解正确:
重新初始化部队
用于设置 d3 力(例如 forceX 或 forceCollision)参数的函数在模拟初始化时(当节点最初分配给布局时)每个节点执行一次。一旦模拟开始,这会节省很多时间:我们不会在每个刻度都重新计算力参数。
但是,如果您有一个现有的力布局,并且想用新的 x 值或新的强度修改 forceX
,或者用新的半径修改 forceCollision
,例如,我们可以重新初始化强制执行重新计算:
// assign a force to the force diagram:
simulation.force("someForce", d3.forceSomeForce().someProperty(function(d) { ... }) )
// re-initialize the force
simulation.force("someForce").initialize(nodes);
这意味着如果我们有这样的力:
simulation.force("x",d3.forceX().x(function(d) { return fn(d["year"]); }))
然后我们更新变量year
,我们需要做的就是:
year = "newValue";
simulation.force("x").initialize(nodes);
定位
如果力量被重新初始化(或重新分配),则无需触摸刻度功能:它会根据需要更新节点。标签和圈子将继续正确更新。
此外,还需要在事件处理程序中更新颜色等非位置性内容,该事件处理程序也会重新初始化力。除了半径之外,大多数东西应该通过强制或直接修改元素来更新,而不是两者都更新。
半径是一个特例:
- 使用d3.forceCollide,半径影响定位
- 然而,半径不需要每次更新都更新。
因此,在更新半径的时候,我们需要更新碰撞力,修改每个圆的r
属性。
如果寻找以图形和碰撞力反映的半径的平滑过渡,这应该是一个单独的问题。
实施
我借鉴了您的代码来制作一个相当通用的示例。下面的代码包含一些按钮的以下事件侦听器,其中每个按钮的数据是一年:
buttons.on("click", function(d) {
// d is the year:
year = d;
// reheat the simulation:
simulation
.alpha(0.5)
.alphaTarget(0.3)
.restart();
// (re)initialize the forces
simulation.force("x").initialize(data);
simulation.force("collide").initialize(data);
// update altered visual properties:
bubbles.attr("r", function(d) {
return radiusScale(d[year]);
}).attr("fill", function(d) {
return colorScale(d[year]);
})
})
以下代码段使用任意数据,由于其大小,可能不允许节点每次都完美地重新组织。为简单起见,位置、颜色和半径都基于同一个变量。最终,它应该解决问题的关键部分:当 year
更改时,我想更新使用 year
设置节点和强制属性的所有内容。
var data = [
{year1:2,year2:1,year3:3,label:"a"},
{year1:3,year2:4,year3:5,label:"b"},
{year1:5,year2:9,year3:7,label:"c"},
{year1:8,year2:16,year3:11,label:"d"},
{year1:13,year2:25,year3:13,label:"e"},
{year1:21,year2:36,year3:17,label:"f"},
{year1:34,year2:1,year3:19,label:"g"},
{year1:2,year2:4,year3:23,label:"h"},
{year1:3,year2:9,year3:29,label:"i"},
{year1:5,year2:16,year3:31,label:"j"},
{year1:8,year2:25,year3:37,label:"k"},
{year1:13,year2:36,year3:3,label:"l"},
{year1:21,year2:1,year3:5,label:"m"}
];
// Create some buttons:
var buttons = d3.select("body").selectAll("button")
.data(["year1","year2","year3"])
.enter()
.append("button")
.text(function(d) { return d; })
// Go about setting the force layout:
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 300);
var radiusScale = d3.scaleSqrt()
.domain([0, 40])
.range([5,30]);
var colorScale = d3.scaleLinear()
.domain([0,10,37])
.range(["#c7e9b4","#41b6c4","#253494"]);
var year = "year1";
var simulation = d3.forceSimulation()
.force("x", d3.forceX(function(d) {
if (parseFloat(d[year]) >= 15) {
return 100
} else if (parseFloat(d[year]) > 5) {
return 250
} else {
return 400
}
}).strength(0.05))
.force("y", d3.forceY(150).strength(0.05))
.force("collide", d3.forceCollide()
.radius(function(d) {
return radiusScale(d[year])
}));
var bubbles = svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", function(d) {
return radiusScale(d[year])
})
.attr("fill", function(d) {
return colorScale(d[year]);
});
var labels = svg.selectAll("text")
.data(data)
.enter()
.append("text")
.text(function(d) {
return d.label;
})
.style("text-anchor","middle");
simulation.nodes(data)
.on("tick", ticked)
function ticked() {
bubbles.attr("cx", function(d) {
return d.x;
}).attr("cy", function(d) {
return d.y;
})
labels.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y +5;
})
}
buttons.on("click", function(d) {
// d is the year:
year = d;
simulation
.alpha(0.5)
.alphaTarget(0.3)
.restart();
simulation.force("x").initialize(data);
simulation.force("collide").initialize(data);
bubbles.attr("r", function(d) {
return radiusScale(d[year]);
}).attr("fill", function(d) {
return colorScale(d[year]);
})
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
我在 D3 中遇到了一个关于力模拟的小问题。
我有代表每个国家/地区从 1998 年到 2008 年的贫困率的数据。它是一个气泡图,分为三组,代表贫穷国家、不贫穷国家和没有信息的国家。
最初加载应用程序时,它加载了 1998 年的数据。但是,我在顶部有一些按钮,单击它们会更改年份,随后气泡会自行重新排列。我所能做的就是在单击按钮时更改变量 year
。但是,在整个代码中有使用 year
的函数和变量。当 year
发生变化时,我想重新计算所有依赖于 year
这是我的代码。如果您想尝试一下,我已经包含了所有内容。数据文件在这个post.
的最后 async function init() {
// Set up the canvas
var height = 1000, width = 2000;
var svg = d3.select("#panel1").append("svg")
.attr("height", height)
.attr("width", width)
.attr("class", "bubblePanel");
var canvas = svg.append("g")
.attr("transform", "translate(0,0)");
// Choose what year to look at, based on button clicks.
var year = "X1998"
d3.select("#b1998").on("click", function() {
year = "X1998"
console.log(year)
// NOTIFY SIMULATION OF CHANGE //
})
d3.select("#b1999").on("click", function() {
year = "X1999"
console.log(year)
// NOTIFY SIMULATION OF CHANGE //
})
d3.select("#b2000").on("click", function() {
year = "X2000"
console.log(year)
// NOTIFY SIMULATION OF CHANGE //
})
// Implement the physics of the elements. Three forces act according to the poverty level (poor, not poor, and no info)
var simulation = d3.forceSimulation()
.force("x", d3.forceX(function(d) {
if (parseFloat(d[year]) >= 10) {
return 1700
} else if (parseFloat(d[year]) === 0) {
return 1000
} else {
return 300
}
}).strength(0.05))
.force("y", d3.forceY(300).strength(0.05))
.force("collide", d3.forceCollide(function(d) {
return radiusScale(d[year])
}));
// Function to pick colour of circles according to region
function pickColor(d) {
if (d === "East Asia & Pacific") {
return "red"
} else if (d === "Europe & Central Asia") {
return "orange"
} else if (d === "Latin America & Caribbean") {
return "yellow"
} else if (d === "Middle East & North Africa") {
return "green"
} else if (d === "North America") {
return "blue"
} else if (d === "South Asia") {
return "indigo"
} else {
return "violet"
}
}
// Set the scales for bubble radius, and text size.
var radiusScale = d3.scaleSqrt().domain([0, 50]).range([20,80]);
var labelScale = d3.scaleSqrt().domain([0,50]).range([10,40]);
// Read the data
await d3.csv("wd3.csv").then(function(data) {
// Assign each data point to a circle that is colored according to region and has radius according to its poverty level
var bubbles = svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", 100)
.attr("cy", 100)
.attr("fill", function(d) {
return pickColor(d.Region)
})
.attr("r", function(d) {
return radiusScale(d[year])
});
// Assign each ddata point to a text element that shows the counry code of the data point. The text is scaled according to the poverty level
var labels = svg.selectAll("text")
.data(data)
.enter().append("text")
.attr("x", 100)
.attr("y", 100)
.attr("dominant-baseline", "central")
.text(function(d) { return d.XCountryCode })
.style("stroke", "black")
.style("text-anchor", "middle")
.style("font-size", function(d) { return labelScale(d[year]); });
// Code to handle the physics of the bubble and the text
simulation.nodes(data)
.on("tick", ticked)
function ticked() {
bubbles.attr("transform", function(d) {
var k = "translate(" + d.x + "," + d.y + ")";
return k;
})
labels.attr("transform", function(d) {
var k = "translate(" + d.x + "," + d.y + ")";
return k;
})
}
});
}
当 year
发生变化时,每个国家/地区的数据值都会发生变化。我希望我的代码的以下部分得到更新。
节点上的x力:国家可以在一年内从贫困变为另一年不贫困,因此他们的集群将发生变化
圆圈的半径: 半径代表贫困程度。这些每年都会发生变化,因此单击按钮时圆圈的大小会发生变化
国家标签的坐标:这些标签也附加到数据中。因此,当圆圈上的 x 力导致圆圈移动时,标签也应该移动。
非常感谢您的帮助。
可以找到数据文件here。我不小心将它命名为 povertyCSV,但在代码中,它被引用为 "wd3.csv"
如果我理解正确:
重新初始化部队
用于设置 d3 力(例如 forceX 或 forceCollision)参数的函数在模拟初始化时(当节点最初分配给布局时)每个节点执行一次。一旦模拟开始,这会节省很多时间:我们不会在每个刻度都重新计算力参数。
但是,如果您有一个现有的力布局,并且想用新的 x 值或新的强度修改 forceX
,或者用新的半径修改 forceCollision
,例如,我们可以重新初始化强制执行重新计算:
// assign a force to the force diagram:
simulation.force("someForce", d3.forceSomeForce().someProperty(function(d) { ... }) )
// re-initialize the force
simulation.force("someForce").initialize(nodes);
这意味着如果我们有这样的力:
simulation.force("x",d3.forceX().x(function(d) { return fn(d["year"]); }))
然后我们更新变量year
,我们需要做的就是:
year = "newValue";
simulation.force("x").initialize(nodes);
定位
如果力量被重新初始化(或重新分配),则无需触摸刻度功能:它会根据需要更新节点。标签和圈子将继续正确更新。
此外,还需要在事件处理程序中更新颜色等非位置性内容,该事件处理程序也会重新初始化力。除了半径之外,大多数东西应该通过强制或直接修改元素来更新,而不是两者都更新。
半径是一个特例:
- 使用d3.forceCollide,半径影响定位
- 然而,半径不需要每次更新都更新。
因此,在更新半径的时候,我们需要更新碰撞力,修改每个圆的r
属性。
如果寻找以图形和碰撞力反映的半径的平滑过渡,这应该是一个单独的问题。
实施
我借鉴了您的代码来制作一个相当通用的示例。下面的代码包含一些按钮的以下事件侦听器,其中每个按钮的数据是一年:
buttons.on("click", function(d) {
// d is the year:
year = d;
// reheat the simulation:
simulation
.alpha(0.5)
.alphaTarget(0.3)
.restart();
// (re)initialize the forces
simulation.force("x").initialize(data);
simulation.force("collide").initialize(data);
// update altered visual properties:
bubbles.attr("r", function(d) {
return radiusScale(d[year]);
}).attr("fill", function(d) {
return colorScale(d[year]);
})
})
以下代码段使用任意数据,由于其大小,可能不允许节点每次都完美地重新组织。为简单起见,位置、颜色和半径都基于同一个变量。最终,它应该解决问题的关键部分:当 year
更改时,我想更新使用 year
设置节点和强制属性的所有内容。
var data = [
{year1:2,year2:1,year3:3,label:"a"},
{year1:3,year2:4,year3:5,label:"b"},
{year1:5,year2:9,year3:7,label:"c"},
{year1:8,year2:16,year3:11,label:"d"},
{year1:13,year2:25,year3:13,label:"e"},
{year1:21,year2:36,year3:17,label:"f"},
{year1:34,year2:1,year3:19,label:"g"},
{year1:2,year2:4,year3:23,label:"h"},
{year1:3,year2:9,year3:29,label:"i"},
{year1:5,year2:16,year3:31,label:"j"},
{year1:8,year2:25,year3:37,label:"k"},
{year1:13,year2:36,year3:3,label:"l"},
{year1:21,year2:1,year3:5,label:"m"}
];
// Create some buttons:
var buttons = d3.select("body").selectAll("button")
.data(["year1","year2","year3"])
.enter()
.append("button")
.text(function(d) { return d; })
// Go about setting the force layout:
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 300);
var radiusScale = d3.scaleSqrt()
.domain([0, 40])
.range([5,30]);
var colorScale = d3.scaleLinear()
.domain([0,10,37])
.range(["#c7e9b4","#41b6c4","#253494"]);
var year = "year1";
var simulation = d3.forceSimulation()
.force("x", d3.forceX(function(d) {
if (parseFloat(d[year]) >= 15) {
return 100
} else if (parseFloat(d[year]) > 5) {
return 250
} else {
return 400
}
}).strength(0.05))
.force("y", d3.forceY(150).strength(0.05))
.force("collide", d3.forceCollide()
.radius(function(d) {
return radiusScale(d[year])
}));
var bubbles = svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", function(d) {
return radiusScale(d[year])
})
.attr("fill", function(d) {
return colorScale(d[year]);
});
var labels = svg.selectAll("text")
.data(data)
.enter()
.append("text")
.text(function(d) {
return d.label;
})
.style("text-anchor","middle");
simulation.nodes(data)
.on("tick", ticked)
function ticked() {
bubbles.attr("cx", function(d) {
return d.x;
}).attr("cy", function(d) {
return d.y;
})
labels.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y +5;
})
}
buttons.on("click", function(d) {
// d is the year:
year = d;
simulation
.alpha(0.5)
.alphaTarget(0.3)
.restart();
simulation.force("x").initialize(data);
simulation.force("collide").initialize(data);
bubbles.attr("r", function(d) {
return radiusScale(d[year]);
}).attr("fill", function(d) {
return colorScale(d[year]);
})
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>