Xiao H
Xiao H

Reputation: 165

d3 selection.exit().remove() not working, _exit is always array(0)

I have gone through quite a few tutorial but still couldn't work out how to correctly update data, any help would be highly appreciated, thank you!

For some reason, the code doesn't run properly in the snippet, I am quite new to use it, what I want to achieve is by clicking the button, the data got updated, I tried to use selection.exit().remove, but the exit array is always empty, I don't really understand how that works

const arr = [{
    name: "A",
    dataset_1: 5,
    dataset_2: 6,
  },
  {
    name: "B",
    dataset_1: 7,
    dataset_2: 22,
  },
  {
    name: "C",
    dataset_1: 33,
    dataset_2: 23,
  },
  {
    name: "D",
    dataset_1: 20,
    dataset_2: 12,
  },
  {
    name: "E",
    dataset_1: 21,
    dataset_2: 15,
  },
  {
    name: "F",
    dataset_1: 15,
    dataset_2: 18,
  },
];
//function for adding dataset
let counter = 2;
const add_set = (arr) => {
  const ran = () => Math.floor(Math.random() * 20 + 1);
  const add = (arr) => {
    counter++;
    arr.map((i) => (i[`dataset_${counter}`] = ran()));
  };
  add(arr);
};
//function for removing dataset
const remove_set = (arr) => {
  arr.map((i) => delete i[`dataset_${counter}`]);
  counter >= 1 ? counter-- : counter;
};

//draw area chart
const draw = () => {
  //No.1 define place to draw
  let svg = d3.select("svg"),
    margin = {
      left: 50,
      right: 50,
      top: 15,
      bottom: 30
    },
    width = +svg.attr("width") - margin.left - margin.right,
    height = +svg.attr("height") - margin.top - margin.bottom,
    g = svg
    .append("g")
    .attr(
      "transform",
      "translate(" + margin.left + "," + margin.top + ")"
    );
  //No.2 set axes
  let categoriesNames = arr.map((d) => d.name);
  let xScale = d3.scalePoint().domain(categoriesNames).range([0, width]); // scalepoint make the axis starts with value compared with scaleBand
  let copy = JSON.parse(JSON.stringify(arr));
  copy.map((i) => delete i.name);
  var yScale = d3
    .scaleLinear()
    .range([height, 0])
    .domain([0, d3.max(copy, (i) => Math.max(...Object.values(i)))]);
  let colorScale = d3.scaleOrdinal().range(d3.schemeSet3);
  let dataSets = Object.keys(arr[0]).filter((i) => i !== "name");
  colorScale.domain(dataSets);
  g.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(d3.axisBottom(xScale));
  g.append("g").attr("class", "y axis").call(d3.axisLeft(yScale));
  g.exit().remove();
  //No.3 draw chart
  let areaData = dataSets.map((i) => {
    return {
      id: i,
      values: arr.map((d) => {
        return {
          name: d.name,
          value: d[i]
        };
      }),
    };
  });

  let generator = d3
    .area()
    .curve(d3.curveMonotoneX)
    .y0(yScale(0))
    .x((d) => xScale(d.name))
    .y1((d) => yScale(d.value));
  let area = g
    .selectAll(".area")
    .data(areaData)
    .enter()
    .append("g")
    .attr("class", (d) => `area${d.id}`);
  area.exit().remove();
  area
    .append("path")
    .attr("d", (d) => {
      return generator(d.values);
    })
    .attr("opacity", 0.6)
    .style("fill", (d) => colorScale(d.id));
};

//buttons
draw();
const update = () => {
  add_set(arr);
  draw();
};
const remove = () => {
  remove_set(arr);
  draw();
};
d3.select("body")
  .append("button")
  .text("Remove dataset")
  .on("click", remove);
d3.select("body")
  .append("button")
  .text("Add dataset")
  .on("click", update);
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
<script src="https://d3js.org/d3.v6.min.js"></script>
<html>



<body>
  <svg width="700" height="500"></svg>
</body>

</html>

Upvotes: 0

Views: 900

Answers (2)

Eli
Eli

Reputation: 1846

In my case the selectAll and append were using different types:

groups.selectAll('rect.bar-item')
///...
enter.append('path')

Making them the same rect or only path solved it for me.

Hope this helps someone.

Upvotes: 0

Ruben Helsloot
Ruben Helsloot

Reputation: 13129

selection.exit() is empty, because selection is a subset of nodes from g, and g is newly added every time you call draw. You need to separate one-time logic (draw axes, append container elements, set height/width/etc) from logic you want to run each time.

You should also see in your code that the colour of one of the areas changed. That was because another area was drawn in front of it. If something was really wrong, it would have repurposed the existing areas and failed to remove the one you didn't need. This is a pointer that you append something the second time that you shouldn't have appended!

But there was more. You use g.selectAll(".area"), append a g element, but don't give it class area. Instead, you give it class areadataset_1 and areadataset_2. This way, on subsequent calls, you can't find the area elements!

You also need to learn about .merge(). I recommend looking up some tutorials, like this one. You only updated the new paths, never the old ones. Since you didn't need g, I removed it for now, making the rest of the logic easier.

const arr = [{
    name: "A",
    dataset_1: 5,
    dataset_2: 6,
  },
  {
    name: "B",
    dataset_1: 7,
    dataset_2: 22,
  },
  {
    name: "C",
    dataset_1: 33,
    dataset_2: 23,
  },
  {
    name: "D",
    dataset_1: 20,
    dataset_2: 12,
  },
  {
    name: "E",
    dataset_1: 21,
    dataset_2: 15,
  },
  {
    name: "F",
    dataset_1: 15,
    dataset_2: 18,
  },
];
//function for adding dataset
let counter = 2;
const add_set = (arr) => {
  const ran = () => Math.floor(Math.random() * 20 + 1);
  const add = (arr) => {
    counter++;
    arr.map((i) => (i[`dataset_${counter}`] = ran()));
  };
  add(arr);
};
//function for removing dataset
const remove_set = (arr) => {
  arr.map((i) => delete i[`dataset_${counter}`]);
  counter >= 1 ? counter-- : counter;
};

const margin = {
  left: 50,
  right: 50,
  top: 15,
  bottom: 30
};

let svg,
  width,
  height,
  g;

const setup = () => {
  //No.1 define place to draw
  svg = d3.select("svg");
  width = +svg.attr("width") - margin.left - margin.right;
  height = +svg.attr("height") - margin.top - margin.bottom;
  g = svg
    .append("g")
    .attr(
      "transform",
      "translate(" + margin.left + "," + margin.top + ")"
    );
}

//draw area chart
const draw = () => {
  //No.2 set axes
  let categoriesNames = arr.map((d) => d.name);
  let xScale = d3.scalePoint().domain(categoriesNames).range([0, width]); // scalepoint make the axis starts with value compared with scaleBand
  let copy = JSON.parse(JSON.stringify(arr));
  copy.map((i) => delete i.name);
  var yScale = d3
    .scaleLinear()
    .range([height, 0])
    .domain([0, d3.max(copy, (i) => Math.max(...Object.values(i)))]);
  let colorScale = d3.scaleOrdinal().range(d3.schemeSet3);
  let dataSets = Object.keys(arr[0]).filter((i) => i !== "name");
  colorScale.domain(dataSets);
  g.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(d3.axisBottom(xScale));
  g.append("g").attr("class", "y axis").call(d3.axisLeft(yScale));
  g.exit().remove();
  //No.3 draw chart
  let areaData = dataSets.map((i) => {
    return {
      id: i,
      values: arr.map((d) => {
        return {
          name: d.name,
          value: d[i]
        };
      }),
    };
  });

  let generator = d3
    .area()
    .curve(d3.curveMonotoneX)
    .y0(yScale(0))
    .x((d) => xScale(d.name))
    .y1((d) => yScale(d.value));
  let area = g
    .selectAll(".area")
    .data(areaData);
  area.exit().remove();

  area
    .enter()
    .append("path")
    .attr("class", (d) => `area area${d.id}`)
    .attr("opacity", 0.6)
    .merge(area)
    .attr("d", (d) => {
      return generator(d.values);
    })
    .style("fill", (d) => colorScale(d.id));
};

//buttons
setup();
draw();
const update = () => {
  add_set(arr);
  draw();
};
const remove = () => {
  remove_set(arr);
  draw();
};
d3.select("body")
  .append("button")
  .text("Remove dataset")
  .on("click", remove);
d3.select("body")
  .append("button")
  .text("Add dataset")
  .on("click", update);
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
<script src="https://d3js.org/d3.v6.min.js"></script>
<html>



<body>
  <svg width="700" height="500"></svg>
</body>

</html>

Upvotes: 1

Related Questions