简体   繁体   English

D3 条形图 + React Hooks - exit().remove() 不工作

[英]D3 Bar Chart + React Hooks - exit().remove() not working

I'm using react and d3, trying to create a simple bar chart that updates the chart when data is refreshed.我正在使用 react 和 d3,试图创建一个简单的条形图,在数据刷新时更新图表。 The chart is updating when the data changes, but it seems to be layering on top of the old chart.当数据更改时,图表正在更新,但它似乎在旧图表之上分层。 I think the issue is with the d3 exit().remove() function.我认为问题在于 d3 exit().remove() function。

As I understand it d3's exit method should return an array of items to be removed, however when I console log it I see an array of "undefined"s.据我了解,d3 的退出方法应该返回一个要删除的项目数组,但是当我控制台记录它时,我看到一个“未定义”数组。 I'm super grateful for any help!我非常感谢任何帮助!

Here is the codesandbox: https://codesandbox.io/s/gifted-field-n66hw?file=/src/Barchart.js这是代码框: https://codesandbox.io/s/gifted-field-n66hw?file=/src/Barchart.js

Here is the code snippet:这是代码片段:

 import React, { useEffect, useRef } from "react"; import * as d3 from "d3"; const BarChart = props => { const { randomData, width, height, padding } = props; const ref = useRef(null); function colorGradient(v) { return "rgb(0, " + v * 5 + ", 0"; } //insert & remove elements using D3 useEffect(() => { if (randomData.length > 0 && ref.current) { const group = d3.select(ref.current); // the data operator binds data items with DOM elements // the resulting selection contains the enter and exit subselections const update = group.append("g").selectAll("rect").data(randomData); let bars = update.enter() // create new dom elements for added data items.append("rect").merge(update).attr("x", (d, i) => i * (width / randomData.length)).attr("y", d => height - d * 5).attr("width", width / randomData.length - padding).attr("height", d => d * 5).attr("fill", d => colorGradient(d)); let labels = update.enter().append("text").text(d => d).attr("text-anchor", "middle").attr( "x", (d, i) => i * (width / randomData.length) + (width / randomData.length - padding) / 2 ).attr("y", d => height - d * 5 + 12).style("font-family", "sans-serif").style("font-size", 12).style("fill", "#ffffff"); update.exit().remove(); } }, [randomData, height, padding, width]); return ( <svg width={width} height={height}> <g ref={ref} /> </svg> ); }; export default BarChart;

Everytime you update the chart you run this:每次更新图表时,您都会运行以下命令:

 const update = group
    .append("g")             // create a new g
    .selectAll("rect")       // select all the rectangles in that g (which are none)
    .data(randomData);      

update is now an empty selection, there are no rect s to select in the newly created g . update现在是一个空选择,新创建的g中没有 select 的rect So when we use update.enter() , a DOM element is created for every item in the data array.因此,当我们使用update.enter()时,会为数据数组中的每个项目创建一个 DOM 元素。 Using enter will create an element for every item in the data array that doesn't have a corresponding element already.使用 enter 将为数据数组中还没有对应元素的每个项目创建一个元素。

update.exit() will be empty, because there are no elements selected in update , so nothing will be removed. update.exit()将为空,因为update中没有选择任何元素,因此不会删除任何内容。 Previously created bars are not touched, you aren't selecting them.以前创建的条没有被触及,你没有选择它们。

If we change your code just to remove the .append("g") , it gets us closer to working ( eg ).如果我们更改您的代码只是为了删除.append("g") ,它会让我们更接近工作(例如)。 The bars were colored white, so they were not visible, I've changed the fill color so the update selection is visible条形图为白色,因此不可见,我更改了填充颜色,因此更新选择可见


If we remove .append("g") we have some other problems on update now:如果我们删除.append("g")我们现在更新还有一些其他问题:

  • you are not exiting text (as you are not selecting text with .selectAll() , only rect elements), and您没有退出文本(因为您没有使用.selectAll()选择text ,只有rect元素),并且
  • you're merging text and rectangles into one selection, which is a bit problematic if you want to position and color them differently on update.您正在将文本和矩形合并到一个选择中,如果您想要 position 并在更新时对它们进行不同的着色,这会有点问题。

The second problem could be explained a bit more:第二个问题可以解释得更多:

update.enter().append("text") // returns a selection of newly created text elements
   .merge(update)             // merges the selection of newly created text with existing rectangles
   .attr("fill", ....         // affects both text and rects.

These two issues can be resolved by using the enter/update/exit cycle correctly.这两个问题可以通过正确使用进入/更新/退出循环来解决。

One thing to note is that D3's enter update exit pattern isn't designed to enter elements more than once with the same statement, you're entering text and rects with the same enter statement, see here .需要注意的一点是,D3 的输入更新退出模式并非旨在使用相同的语句多次输入元素,您使用相同的输入语句输入文本和矩形,请参见此处

Therefore, one option is to use two selections, one for text and one for rects:因此,一种选择是使用两种选择,一种用于文本,一种用于矩形:

  const updateRect = group
    .selectAll("rect")
    .data(randomData);

  let bars = updateRect
    .enter() // create new dom elements for added data items
    .append("rect")
    .merge(updateRect)
    .attr("x", (d, i) => i * (width / randomData.length))
    .attr("y", d => height - d * 5)
    .attr("width", width / randomData.length - padding)
    .attr("height", d => d * 5)
    .attr("fill", d => colorGradient(d));

  const updateText = group
    .selectAll("text")
    .data(randomData);

  let labels = updateText
    .enter()
    .append("text")
    .merge(updateText)
    .text(d => d)
    .attr("text-anchor", "middle")
    .attr(
      "x",
      (d, i) =>
        i * (width / randomData.length) +
        (width / randomData.length - padding) / 2
    )
    .attr("y", d => height - d * 5 + 12)
    .style("font-family", "sans-serif")
    .style("font-size", 12)
    .style("fill", "#fff");

  updateRect.exit().remove();
  updateText.exit().remove();

Here in sandbox form.这里是沙盒形式。

The other option is to use a parent g to hold both rect and text, this could be done many ways, but if you don't need a transition between values or the number of bars, would probably be most simple like:另一种选择是使用父g来保存 rect 和 text,这可以通过多种方式完成,但如果您不需要在值或条数之间进行转换,可能最简单,例如:

  const update = group
    .selectAll("g")
    .data(randomData);

  // add a g for every extra datum
  const enter = update.enter().append("g")
    // give them a rect and text element:
    enter.append("rect");
    enter.append("text");

  // merge update and enter:
  const bars = update.merge(enter);

  // modify the rects
  bars.select("rect")
    .attr("x", (d, i) => i * (width / randomData.length))
    .attr("y", d => height - d * 5)
    .attr("width", width / randomData.length - padding)
    .attr("height", d => d * 5)
    .attr("fill", d => { return colorGradient(d)});

  // modify the texts:
  bars.select("text")
    .text(d => d)
    .attr("text-anchor", "middle")
    .attr(
      "x",
      (d, i) =>
        i * (width / randomData.length) +
        (width / randomData.length - padding) / 2
    )
    .attr("y", d => height - d * 5 + 12)
    .style("font-family", "sans-serif")
    .style("font-size", 12)
    .style("fill", "#ffffff");

Here's that in sandox form.这是Sandox形式的。

A bit further explanation: selection.select() selects the first matching element for each element in the selection - so we can select the sole rectangle in each parent g (that we add when entering the parent) with bars.select("rect") above.进一步解释: selection.select()为选择中的每个元素选择第一个匹配元素 - 所以我们可以 select 每个父级 g 中的唯一矩形(我们在输入父级时添加)与bars.select("rect")上面。 D3 passes the parent datum to the child when appending in the above. D3 在上面附加时将父数据传递给子数据。 Note: If we had nested data (multiple bars or texts per data array item) we'd need to have nested enter/exit/update cycles .注意:如果我们有嵌套数据(每个数据数组项有多个条形或文本),我们需要有嵌套的输入/退出/更新周期

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM