D3 - 如何在数据值更改时更新力模拟

Zuh*_*med 6 javascript d3.js force-layout

我在 D3 中遇到了一个关于力模拟的小问题。

我有代表每个国家从 1998 年到 2008 年的贫困率的数据。这是一个气泡图,分为三个集群,分别代表贫困国家、非贫困国家和没有信息的国家。

最初加载应用程序时,它会加载 1998 年的数据。但是,我在顶部有一些按钮,点击后会改变年份,随后气泡应该重新排列。我所能做的就是,当单击按钮时,我更改了一个变量year。但是,在year整个代码中都使用了一些函数和变量。当year发生变化时,我想重新计算所有依赖的节点属性和力参数year

这是我的代码。如果您想尝试一下,我已经包含了所有内容。数据文件在这篇文章的末尾。

 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;
            })

        }
    });
}

Run Code Online (Sandbox Code Playgroud)

year发生变化时,数据值将每个国家改变。我希望更新我的代码的以下部分。

节点上的 x 力:国家可以在一年内从贫困变为不贫困,因此它们的集群会发生变化

圆圈的半径半径代表贫困水平。这些每年都会发生变化,因此单击按钮时圆圈的大小会发生变化

国家标签的坐标:这些标签也附在数据上。因此,当圆圈上的 x 力导致圆圈移动时,标签也应该移动。

我将不胜感激。

数据文件可以在这里找到。我不小心将其命名为贫困CSV,但在代码中,它被引用为“wd3.csv”

And*_*eid 8

如果我正确理解问题:

重新初始化力

用于设置 d3 力的参数(例如 forceX 或 forceCollision)的函数在模拟初始化时每个节点执行一次(当节点最初分配给布局时)。一旦模拟开始,这将节省大量时间:我们不会每次都重新计算力参数。

但是,如果您有一个现有的力布局,并且想要forceX使用新的 x 值或新强度或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);
Run Code Online (Sandbox Code Playgroud)

这意味着如果我们有一个力,例如:

simulation.force("x",d3.forceX().x(function(d) { return fn(d["year"]); }))
Run Code Online (Sandbox Code Playgroud)

我们更新变量year,我们需要做的就是:

year = "newValue";

simulation.force("x").initialize(nodes);
Run Code Online (Sandbox Code Playgroud)

定位

如果重新初始化(或重新分配)力,则无需触摸刻度函数:它会根据需要更新节点。标签和圆圈将继续正确更新。

此外,还需要在重新初始化力的事件处理程序中更新诸如颜色之类的非位置性事物。除了半径之外,大多数东西应该通过力或通过直接修改元素来更新,而不是两者。

半径是一个特例:

  • 使用 d3.forceCollide,半径影响定位
  • 然而,Radius 不需要每次更新。

所以在更新半径的时候,我们需要更新碰撞力,修改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]);
    })
})
Run Code Online (Sandbox Code Playgroud)

以下代码段使用任意数据,并且由于其大小可能不允许节点每次都完美地重新组织。为简单起见,位置、颜色和半径都基于同一个变量。最终,它应该解决问题的关键部分:当year更改时,我想更新year用于设置节点和强制属性的所有内容。

 // assign a force to the force diagram:
 simulation.force("someForce", d3.forceSomeForce().someProperty(function(d) { ... }) )

// re-initialize the force
simulation.force("someForce").initialize(nodes);
Run Code Online (Sandbox Code Playgroud)
simulation.force("x",d3.forceX().x(function(d) { return fn(d["year"]); }))
Run Code Online (Sandbox Code Playgroud)