简体   繁体   中英

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. 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.

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. I'm super grateful for any help!

Here is the codesandbox: 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 . So when we use update.enter() , a DOM element is created for every item in the data array. Using enter will create an element for every item in the data array that doesn't have a corresponding element already.

update.exit() will be empty, because there are no elements selected in update , so nothing will be removed. 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 ). 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:

  • you are not exiting text (as you are not selecting text with .selectAll() , only rect elements), and
  • 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.

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 .

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:

  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.

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. D3 passes the parent datum to the child when appending in the above. Note: If we had nested data (multiple bars or texts per data array item) we'd need to have nested enter/exit/update cycles .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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