javascript - 更改数据后强制图表不更新

标签 javascript d3.js

我得到了一个带有 3 个主节点的小型 D3 强制图。这些节点包含一个属性shoes,它保存一个整数。图表顶部有两个按钮,用于添加删除鞋子。一旦单击其中一个按钮,我就想更新 D3 强制图形数据。基本上蓝色节点中的整数值应该增加或减少。

我搜索并找到了几篇 stackoverflow 文章,其中解释了实现我的需求的步骤。不幸的是,我还无法成功地将这些文章映射到我的原型(prototype)上。

问题是:它向最后一个数据节点添加了一个元素,但在视觉上并没有改变蓝色圆圈中的数量。相反,控制台输出显示正确的值并正确增加或减少鞋子的数量。

我错过了什么?

        var width = window.innerWidth,
            height = window.innerHeight;

        var buttons = d3.select("body").selectAll("button")
            .data(["add Shoes", "remove Shoes"])
            .enter()
            .append("button")
            .text(function(d) {
                return d;
            })

        var svg = d3.select("body").append("svg")
            .attr("width", width)
            .attr("height", height)
            .call(d3.zoom().on("zoom", function(event) {
                svg.attr("transform", event.transform)
            }))
            .append("g")

        ////////////////////////
        // outer force layout

        var data = {
            "nodes":[
                { "id": "A", "shoes": 1}, 
                { "id": "B", "shoes": 1},
                { "id": "C", "shoes": 0},
            ],
            "links": [
                { "source": "A", "target": "B"},
                { "source": "B", "target": "C"},
                { "source": "C", "target": "A"}
            ]
        };

        var simulation = d3.forceSimulation()
            .force("size", d3.forceCenter(width / 2, height / 2))
            .force("charge", d3.forceManyBody().strength(-1000))
            .force("link", d3.forceLink().id(function (d) { return d.id }).distance(250))
       
        linksContainer = svg.append("g").attr("class", "linkscontainer")
        nodesContainer = svg.append("g").attr("class", "nodesContainer")
       
        var links = linksContainer.selectAll("g")
            .data(data.links)
            .join("g")
            .attr("fill", "transparent")

        var linkLine = linksContainer.selectAll(".linkPath")
            .data(data.links)
            .join("path")
            .attr("stroke", "red")
            .attr("fill", "transparent")
            .attr("stroke-width", 3)
        
        nodes = nodesContainer.selectAll(".nodes")
            .data(data.nodes, function (d) { return d.id; })
            .join("g")
            .attr("class", "nodes")
            .attr("id", function (d) { return d.id; })
            .call(d3.drag()
                .on("start", dragStarted)
                .on("drag", dragged)
                .on("end", dragEnded)
            )

        nodes.selectAll("circle")
            .data(d => [d])
            .join("circle")
            .style("fill", "lightgrey")
            .style("stroke", "blue")
            .attr("r", 40)

        var smallCircle = nodes.selectAll("g")
            //.data(d => d.shoes)
            .data(d => [d])
            .enter()
            .filter(function(d) { 
                return d.shoes !== 0; 
            })
            .append("g")
            .attr("cursor", "pointer")
            .attr("transform", function(d, i) {
                const factor = (i / 40) * (15 / 2) * 5;
                return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
            });
                        
        smallCircle.append('circle')
            .attr("class", "circle-small")
            .attr('r', 15)
            .attr("fill", "blue")

        smallCircle.append("text")
            .attr("font-size", 15)
            .attr("fill", "white")
            .attr("dominant-baseline", "central")
            .style("text-anchor", "middle")
            .attr("pointer-events", "cursor")
            .text(function(d) {
                return d.shoes;
            })

        simulation
            .nodes(data.nodes)
            .on("tick", tick)

        simulation
            .force("link")
            .links(data.links)

        function tick() {
            linkLine.attr("d", function(d) {
                var dx = (d.target.x - d.source.x),
                    dy = (d.target.y - d.source.y),
                    dr = Math.sqrt(dx * dx + dy * dy)

                return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
            })
                
            nodes
                .attr("transform", d => `translate(${d.x}, ${d.y})`);
        }

        function dragStarted(event, d) {
            if (!event.active) simulation.alphaTarget(0.3).restart();
            d.fx = d.x;
            d.fy = d.y;
        }

        function dragged(event, d) {
            d.fx = event.x;
            d.fy = event.y;
        }

        function dragEnded(event, d) {
            if (!event.active) simulation.alphaTarget(0);
            d.fx = null;
            d.fy = null;
        }

        buttons.on("click", function(d) {
            if (d.srcElement.__data__ == "add Shoes") {
                
                data.nodes.forEach(function(item) {
                    item.shoes = item.shoes + 1
                })
            } else if (d.srcElement.__data__ == "remove Shoes") {
                data.nodes.forEach(function(item) {
                    if (!item.shoes == 0) {
                        item.shoes = item.shoes - 1
                    }
                })
            }

            restart()
        })

        function restart() {
            // Apply the general update pattern to the nodes.
    
            smallCircle = nodes.selectAll("g")
                .data(d => [d])
                .enter()
                .filter(function(d) { 
                    return d.shoes !== 0; 
                })
                .append("g")
                .attr("cursor", "pointer")
                .attr("transform", function(d, i) {
                    const factor = (i / 40) * (15 / 2) * 5;
                    return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
                });

            smallCircle.append("circle")
                .attr("class", "circle-small")
                .attr('r', 15)
                .attr("fill", "blue")

            smallCircle.append("text")
                .attr("font-size", 15)
                .attr("fill", "white")
                .attr("dominant-baseline", "central")
                .style("text-anchor", "middle")
                .text(function(d) {
                    return d.shoes;
                })

            smallCircle.exit().remove();

            // Update and restart the simulation.
            simulation.nodes(data.nodes);
            simulation.restart()
        }
body {
        background: whitesmoke,´;
        overflow: hidden;
        margin: 0px;
    }
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>D3v7</title>
    <!-- d3.js framework -->
    <script src="https://d3js.org/d3.v7.js"></script>
</head>

<body>
 
</body>

</html>

进一步的 d3 选择行为。

我接受了答案,这是完全正确的。此外,我尝试在同一级别添加另一个 g 元素,似乎 d3 选择器无法按组处理选择。至少 selectAll("gblue") 的行为与 selectAll("g") 不同

       var width = window.innerWidth,
            height = window.innerHeight;

        var buttons = d3.select("body").selectAll("button")
            .data(["add blue", "remove blue", "add red", "remove red"])
            .enter()
            .append("button")
            .text(function (d) {
                return d;
            })

        var svg = d3.select("body").append("svg")
            .attr("width", width)
            .attr("height", height)
            .call(d3.zoom().on("zoom", function (event) {
                svg.attr("transform", event.transform)
            }))
            .append("g")

        ////////////////////////
        // outer force layout

        var data = {
            "nodes": [{
                "id": "A",
                "blue": 1,
                "red": 0,
            },
            {
                "id": "B",
                "blue": 1,
                "red": 1
            },
            {
                "id": "C",
                "blue": 0,
                "red": 1
            },
            ],
            "links": [{
                "source": "A",
                "target": "B"
            },
            {
                "source": "B",
                "target": "C"
            },
            {
                "source": "C",
                "target": "A"
            }
            ]
        };

        var simulation = d3.forceSimulation()
            .force("size", d3.forceCenter(width / 2, height / 2))
            .force("charge", d3.forceManyBody().strength(-1000))
            .force("link", d3.forceLink().id(function (d) {
                return d.id
            }).distance(250))

        linksContainer = svg.append("g").attr("class", "linkscontainer")
        nodesContainer = svg.append("g").attr("class", "nodesContainer")

        var links = linksContainer.selectAll("g")
            .data(data.links)
            .join("g")
            .attr("fill", "transparent")

        var linkLine = linksContainer.selectAll(".linkPath")
            .data(data.links)
            .join("path")
            .attr("stroke", "red")
            .attr("fill", "transparent")
            .attr("stroke-width", 3)

        nodes = nodesContainer.selectAll(".nodes")
            .data(data.nodes, function (d) {
                return d.id;
            })
            .join("g")
            .attr("class", "nodes")
            .call(d3.drag()
                .on("start", dragStarted)
                .on("drag", dragged)
                .on("end", dragEnded)
            )

        nodes.selectAll("circle")
            .data(d => [d])
            .join("circle")
            .style("fill", "lightgrey")
            .style("stroke", "blue")
            .attr("r", 40)

        var blueNode = nodes.selectAll("gblue")
            .data(d => d.blue ? [d] : [])
            .enter()
            .append("g")
            .attr("class", "gblue")
            .attr("cursor", "pointer")
            .attr("transform", function (d, i) {
                const factor = (i / 40) * (15 / 2) * 5;
                return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
            });

        blueNode.append('circle')
            .attr('r', 15)
            .attr("fill", "blue")

        blueNode.append("text")
            .attr("font-size", 15)
            .attr("fill", "white")
            .attr("dominant-baseline", "central")
            .style("text-anchor", "middle")
            .attr("pointer-events", "cursor")
            .text(function (d) {
                return d.blue;
            })

        var redNode = nodes.selectAll("gred")
            .data(d => d.red ? [d] : [])
            .enter()
            .append("g")
            .attr("class", "gred")
            .attr("cursor", "pointer")
            .attr("transform", function (d, i) {
                const factor = (i / 40) * (15 / 2) * 5;
                return `translate(${40 * Math.cos(factor - Math.PI * 0.3)},${40 * Math.sin(factor - Math.PI * 0.3)})`;
            });

        redNode.append('circle')
            .attr('r', 15)
            .attr("fill", "red")

        redNode.append("text")
            .attr("font-size", 15)
            .attr("fill", "white")
            .attr("dominant-baseline", "central")
            .style("text-anchor", "middle")
            .attr("pointer-events", "cursor")
            .text(function (d) {
                return d.red;
            })

        simulation
            .nodes(data.nodes)
            .on("tick", tick)

        simulation
            .force("link")
            .links(data.links)

        function tick() {
            linkLine.attr("d", function (d) {
                var dx = (d.target.x - d.source.x),
                    dy = (d.target.y - d.source.y),
                    dr = Math.sqrt(dx * dx + dy * dy)

                return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
            })

            nodes
                .attr("transform", d => `translate(${d.x}, ${d.y})`);
        }

        function dragStarted(event, d) {
            if (!event.active) simulation.alphaTarget(0.3).restart();
            d.fx = d.x;
            d.fy = d.y;
        }

        function dragged(event, d) {
            d.fx = event.x;
            d.fy = event.y;
        }

        function dragEnded(event, d) {
            if (!event.active) simulation.alphaTarget(0);
            d.fx = null;
            d.fy = null;
        }

        buttons.on("click", function (_, d) {
            if (d === "add blue") {

                data.nodes.forEach(function (item) {
                    item.blue = item.blue + 1
                })
            } else if (d === "remove blue") {
                data.nodes.forEach(function (item) {
                    if (!item.blue == 0) {
                        item.blue = item.blue - 1
                    }
                })
            } else if (d === "add red") {
                data.nodes.forEach(function (item) {
                    item.red = item.red + 1
                })
            } else if (d === "remove red") {
                data.nodes.forEach(function (item) {
                    if (!item.red == 0) {
                        item.red = item.red - 1
                    }
                })
            }

            restart()
        })

        function restart() {
            // Apply the general update pattern to the nodes.

            let blueNode = nodes.selectAll("g")
                .data(d => d.blue ? [d] : []);

            blueNode.exit().remove();

            const blueNodeEnter = blueNode.enter()
                .append("g")
                .attr("class", "blue")
                .attr("cursor", "pointer")
                .attr("transform", function (d, i) {
                    const factor = (i / 40) * (15 / 2) * 5;
                    return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
                });

            blueNodeEnter.append("circle")
                .attr('r', 15)
                .attr("fill", "blue")

            blueNodeEnter.append("text")
                .attr("font-size", 15)
                .attr("fill", "white")
                .attr("dominant-baseline", "central")
                .style("text-anchor", "middle")
                .text(function (d) {
                    return d.blue;
                });

            blueNode = blueNodeEnter.merge(blueNode);

            blueNode.select("text")
                .text(function (d) {
                    return d.blue;
                });


            let redNode = nodes.selectAll("g")
                .data(d => d.red ? [d] : []);

            redNode.exit().remove();

            const redNodeEnter = redNode.enter()
                .append("g")
                .attr("class", "red")
                .attr("cursor", "pointer")
                .attr("transform", function (d, i) {
                    const factor = (i / 40) * (15 / 2) * 5;
                    return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
                });

            redNodeEnter.append("circle")
                .attr('r', 15)
                .attr("fill", "blue")

            redNodeEnter.append("text")
                .attr("font-size", 15)
                .attr("fill", "white")
                .attr("dominant-baseline", "central")
                .style("text-anchor", "middle")
                .text(function (d) {
                    return d.red;
                });

            redNode = redNodeEnter.merge(redNode);

            redNode.select("text")
                .text(function (d) {
                    return d.red;
                });


            // Update and restart the simulation.
            simulation.nodes(data.nodes);
            simulation.restart()
        }
    .link {
        stroke: #000;
        stroke-width: 1.5px;
    }

    .nodes {
        fill: whitesmoke;
    }
<!DOCTYPE html>
<html lang="de">

<head>
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
    <meta charset="utf-8">

    <!-- jQuery -->
    <script src="https://code.jquery.com/jquery-3.6.3.js"></script>
    <!-- D3 -->
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <!-- fontawesome stylesheet https://fontawesome.com/ -->
    <script src="https://kit.fontawesome.com/98a5e27706.js" crossorigin="anonymous"></script>

</head>

<body>
 
</body>

</html>

最佳答案

首先,不要弄乱私有(private)变量,通常分配为__foo__。所以,而不是...

buttons.on("click", function(d) {
    if (d.srcElement.__data__ == "add Shoes") { etc...

...只需执行:

buttons.on("click", function(_, d) {
    if (d == "add Shoes") {

回到问题:这里的问题是不正确的进入-更新-退出模式。应该是这样:

//the update selection:
let smallCircle = nodes.selectAll("g")
    .data(d => d.shoes ? [d] : []);

//the exit selection:
smallCircle.exit().remove();

//the enter selection:
const smallCircleEnter = smallCircle.enter()
    .append("g")
    //etc...

//appending elements in the enter selection only:
smallCircleEnter.append("circle")
    //etc...

smallCircleEnter.append("text")
    //etc...

//merging the enter and the update selections:
smallCircle = smallCircleEnter.merge(smallCircle);

//modifying the update selection
smallCircle.select("text")
    .text(function(d) {
      return d.shoes;
    });

另外,如果鞋子的数量为零,我将删除该过滤器并传递一个空数组。

这是经过这些更改的代码:

var width = window.innerWidth,
  height = window.innerHeight;

var buttons = d3.select("body").selectAll("button")
  .data(["add Shoes", "remove Shoes"])
  .enter()
  .append("button")
  .text(function(d) {
    return d;
  })

var svg = d3.select("body").append("svg")
  .attr("width", width)
  .attr("height", height)
  .call(d3.zoom().on("zoom", function(event) {
    svg.attr("transform", event.transform)
  }))
  .append("g")

////////////////////////
// outer force layout

var data = {
  "nodes": [{
      "id": "A",
      "shoes": 1
    },
    {
      "id": "B",
      "shoes": 1
    },
    {
      "id": "C",
      "shoes": 0
    },
  ],
  "links": [{
      "source": "A",
      "target": "B"
    },
    {
      "source": "B",
      "target": "C"
    },
    {
      "source": "C",
      "target": "A"
    }
  ]
};

var simulation = d3.forceSimulation()
  .force("size", d3.forceCenter(width / 2, height / 2))
  .force("charge", d3.forceManyBody().strength(-1000))
  .force("link", d3.forceLink().id(function(d) {
    return d.id
  }).distance(250))

linksContainer = svg.append("g").attr("class", "linkscontainer")
nodesContainer = svg.append("g").attr("class", "nodesContainer")

var links = linksContainer.selectAll("g")
  .data(data.links)
  .join("g")
  .attr("fill", "transparent")

var linkLine = linksContainer.selectAll(".linkPath")
  .data(data.links)
  .join("path")
  .attr("stroke", "red")
  .attr("fill", "transparent")
  .attr("stroke-width", 3)

nodes = nodesContainer.selectAll(".nodes")
  .data(data.nodes, function(d) {
    return d.id;
  })
  .join("g")
  .attr("class", "nodes")
  .attr("id", function(d) {
    return d.id;
  })
  .call(d3.drag()
    .on("start", dragStarted)
    .on("drag", dragged)
    .on("end", dragEnded)
  )

nodes.selectAll("circle")
  .data(d => [d])
  .join("circle")
  .style("fill", "lightgrey")
  .style("stroke", "blue")
  .attr("r", 40)

var smallCircle = nodes.selectAll("g")
  //.data(d => d.shoes)
  .data(d => [d])
  .enter()
  .filter(function(d) {
    return d.shoes !== 0;
  })
  .append("g")
  .attr("cursor", "pointer")
  .attr("transform", function(d, i) {
    const factor = (i / 40) * (15 / 2) * 5;
    return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
  });

smallCircle.append('circle')
  .attr("class", "circle-small")
  .attr('r', 15)
  .attr("fill", "blue")

smallCircle.append("text")
  .attr("font-size", 15)
  .attr("fill", "white")
  .attr("dominant-baseline", "central")
  .style("text-anchor", "middle")
  .attr("pointer-events", "cursor")
  .text(function(d) {
    return d.shoes;
  })

simulation
  .nodes(data.nodes)
  .on("tick", tick)

simulation
  .force("link")
  .links(data.links)

function tick() {
  linkLine.attr("d", function(d) {
    var dx = (d.target.x - d.source.x),
      dy = (d.target.y - d.source.y),
      dr = Math.sqrt(dx * dx + dy * dy)

    return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
  })

  nodes
    .attr("transform", d => `translate(${d.x}, ${d.y})`);
}

function dragStarted(event, d) {
  if (!event.active) simulation.alphaTarget(0.3).restart();
  d.fx = d.x;
  d.fy = d.y;
}

function dragged(event, d) {
  d.fx = event.x;
  d.fy = event.y;
}

function dragEnded(event, d) {
  if (!event.active) simulation.alphaTarget(0);
  d.fx = null;
  d.fy = null;
}

buttons.on("click", function(_, d) {
  if (d === "add Shoes") {

    data.nodes.forEach(function(item) {
      item.shoes = item.shoes + 1
    })
  } else if (d === "remove Shoes") {
    data.nodes.forEach(function(item) {
      if (!item.shoes == 0) {
        item.shoes = item.shoes - 1
      }
    })
  }

  restart()
})

function restart() {
  // Apply the general update pattern to the nodes.

  let smallCircle = nodes.selectAll("g")
    .data(d => d.shoes ? [d] : []);

  smallCircle.exit().remove();

  const smallCircleEnter = smallCircle.enter()
    .append("g")
    .attr("cursor", "pointer")
    .attr("transform", function(d, i) {
      const factor = (i / 40) * (15 / 2) * 5;
      return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
    });

  smallCircleEnter.append("circle")
    .attr("class", "circle-small")
    .attr('r', 15)
    .attr("fill", "blue")

  smallCircleEnter.append("text")
    .attr("font-size", 15)
    .attr("fill", "white")
    .attr("dominant-baseline", "central")
    .style("text-anchor", "middle")
    .text(function(d) {
      return d.shoes;
    });

  smallCircle = smallCircleEnter.merge(smallCircle);

  smallCircle.select("text")
    .text(function(d) {
      return d.shoes;
    });


  // Update and restart the simulation.
  simulation.nodes(data.nodes);
  simulation.restart()
}
body {
  background: whitesmoke, ´;
  overflow: hidden;
  margin: 0px;
}
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <title>D3v7</title>
  <!-- d3.js framework -->
  <script src="https://d3js.org/d3.v7.js"></script>
</head>

<body>

</body>

</html>

关于javascript - 更改数据后强制图表不更新,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/75028754/

相关文章:

javascript - JavaScript 中集合的所有可能唯一组合

javascript - Intl.NumberFormat 中的多个区域设置?

javascript - 使用layout.pack将标签放置在圆圈外而不重叠

javascript - 使用 ReactJS 的 D3Plus 可视化

javascript - d3.mouse 在 Firefox 中的像素偏移

javascript - 将 html 文件转换为 pdf

javascript - 获取两个 Moment 日期/时间之间的日期/时间

jquery - 谷歌图表格式

javascript - 当每个单独的段都是一个对象时,如何在 d3 中创建堆积条形图?

javascript - 如何将 angular2 组件附加到 d3 选择?