score:1

Accepted answer

There are at least a few things going on here. As pointed out by Michael Rovinsky, you're appending a new g group for every update that houses all of your bars. Since it's a new group, it has no data bound to it, which will make new bars for every call.

Keeping track of groups

Instead, you should make the group only once, and maybe assign it to a variable to make it a little clearer to work with. I'll use the this.svg.append("g").attr("class", "bars"); group since it doesn't seem to be used for anything else:

var bars = this.svg.append("g").attr("class", "bars");

Then in your update function, change:

this.svg
  .append("g")
  .selectAll("g")
  // ...

to

bars
  .selectAll("g")

Returning values for the join selection

Your invocations of join aren't quite standard when you don't return the enter, update, and exit selections. When you have an arrow function with a body like:

enter => {
  enter.append('g') //...
}

it doesn't actually return the enter selection, it should be either

enter => {
  return enter.append('g') //...
}

or

enter => enter.append('g') //...

While you don't technically need to return the enter or update selections, it's the intended way to use them, and does cause problems when you try to use the combined enter and update selections later, which we will later do.

Understanding the update process

The way you implement your nested joining suggests that you're not quite understanding how joining works. When you enter and append new elements, those elements don't have any data bound to them, so when you use a subsequent join, the nested update and exit functions won't ever run.

You'll also notice that you're repeating a lot of the same code in both enter and update.

The more standard way of doing what I think you intend to do, is to add new bars if necessary, and then to use the new bars and old bars (which is returned by join and update them with the bar segments.

It'll look something like this:

bars.selectAll('g').data(data).join(
    enter => enter.append('g'),
    update => update, // don't do anything with only the update selection
    exit => exit.remove()
  ) // now we're selecting all of the enter and update g's
   .selectAll('rect')
   .data(d => d).join(
      enter => enter.append('rect'),
      update => update,
      exit => exit.remove()
   ) // now we're selecting all of the enter and update rects
     .transition(t)
     // ...

Setting Keys

If you look at the format of the data you can see that stackData is always an array of length 3 that has an array for each entry. Those nested arrays vary based on the number of columns and it's at that level that the keys should be defined:

    //... group selection
    .selectAll('rect')
    .data(d => d, d => d.data.key)
    //...

Fixing Bugs

Finally there's bit of code when you're updating the bar location .attr("x", d => this.xScale(d.key)) which should use d.data.key instead.

All together

I consolidated most of the redundant enter and update code, and removed some extraneous code. All together, it looks like:

this.width = 400;
this.height = 200;
var margin = {
  top: 20,
  right: 20,
  bottom: 30,
  left: 40
}

this.index = 0;

this.svg = d3
  .select(".canvas")
  .classed("svg-container", true)
  .append("svg")
  .attr("class", "chart")
  .attr(
    "viewBox",
    `0 0 ${this.width} ${this.height}`
  )
  .attr("preserveAspectRatio", "xMinYMin meet")
  .classed("svg-content-responsive", true)
  .append("g");

const scale = [0, 1200];

// set the scales
this.xScale = d3
  .scaleBand()
  .range([0, width])
  .padding(0.3);

this.yScale = d3.scaleLinear().range([this.height, 0]);

var bars = this.svg.append("g").attr("class", "bars");

const update = data => {
  const scale = [0, 1200];

  // Update scales.
  this.xScale.domain(data.map(d => d.key));
  this.yScale.domain([scale[0], scale[1]]);

  const subgroups = ["home", "work", "public"];

  var color = d3
    .scaleOrdinal()
    .domain(subgroups)
    .range(["#206BF3", "#171D2C", "#8B0000"]);

  var stackData = d3.stack().keys(subgroups)(data);

  // Set up transition.
  const dur = 1000;
  const t = d3.transition().duration(dur);

  bars
    .selectAll("g")
    .data(stackData)
    .join(
      enter => enter
        .append("g")
        .attr("fill", d => color(d.key)),

      null, // no update function

      exit => {
        exit
          .transition()
          .duration(dur / 2)
          .style("fill-opacity", 0)
          .remove();
      }
    ).selectAll("rect")
      .data(d => d, d => d.data.key)
        .join(
            enter => enter
                .append("rect")
                .attr("class", "bar")
                .attr("x", d => {
                  return this.xScale(d.data.key);
                })
                .attr("y", () => {
                  return this.yScale(0);
                })
                .attr("height", () => {
                  return this.height - this.yScale(0);
                })
                .attr("width", this.xScale.bandwidth())
            ,
            null,
            exit => {
              exit
                .transition()
                .duration(dur / 2)
                .style("fill-opacity", 0)
                .remove();
            }
          )
          .transition(t)
          .delay((d, i) => i * 20)
          .attr("x", d => this.xScale(d.data.key))
          .attr("y", d => {
            return this.yScale(d[1]);
          })
          .attr("width", this.xScale.bandwidth())
          .attr("height", d => {
            return this.yScale(d[0]) - this.yScale(d[1]);
          });
};

const data = [
  [{
      key: "Jan",
      home: 371,
      work: 335,
      public: 300
    },
    {
      key: "Feb",
      home: 343,
      work: 437,
      public: 228
    },
    {
      key: "Mrt",
      home: 359,
      work: 261,
      public: 202
    },
    {
      key: "Apr",
      home: 274,
      work: 217,
      public: 482
    },
    {
      key: "Mei",
      home: 442,
      work: 314,
      public: 477
    },
    {
      key: "Jun",
      home: 464,
      work: 261,
      public: 278
    },
    {
      key: "Jul",
      home: 343,
      work: 244,
      public: 396
    },
    {
      key: "Aug",
      home: 231,
      work: 406,
      public: 338
    },
    {
      key: "Sep",
      home: 380,
      work: 382,
      public: 366
    },
    {
      key: "Okt",
      home: 391,
      work: 408,
      public: 455
    },
    {
      key: "Nov",
      home: 419,
      work: 261,
      public: 226
    },
    {
      key: "Dec",
      home: 217,
      work: 453,
      public: 335
    }
  ],
  [{
      key: "Jan",
      home: 282,
      work: 363,
      public: 379
    },
    {
      key: "Feb",
      home: 428,
      work: 355,
      public: 216
    },
    {
      key: "Mrt",
      home: 217,
      work: 493,
      public: 280
    },
    {
      key: "Apr",
      home: 304,
      work: 283,
      public: 454
    },
    {
      key: "Mei",
      home: 397,
      work: 406,
      public: 289
    },
    {
      key: "Jun",
      home: 242,
      work: 239,
      public: 232
    },
    {
      key: "Jul",
      home: 327,
      work: 453,
      public: 264
    },
    {
      key: "Aug",
      home: 242,
      work: 240,
      public: 414
    },
    {
      key: "Sep",
      home: 495,
      work: 382,
      public: 368
    },
    {
      key: "Okt",
      home: 285,
      work: 471,
      public: 364
    },
    {
      key: "Nov",
      home: 315,
      work: 421,
      public: 482
    },
    {
      key: "Dec",
      home: 214,
      work: 284,
      public: 297
    }
  ],
  [{
      key: "1",
      home: 282,
      work: 363,
      public: 379
    },
    {
      key: "2",
      home: 232,
      work: 432,
      public: 324
    },
    {
      key: "3",
      home: 324,
      work: 124,
      public: 432
    },
    {
      key: "4",
      home: 425,
      work: 353,
      public: 532
    }
  ]
];

update(data[this.index]);

const swap = document.querySelector(".swap");
swap.addEventListener("click", () => {
  if (this.index < 2) this.index += 1;
  else this.index = 0;
  update(data[this.index]);
});
<button class="swap">swap</button>
<div class="canvas"></div>
<script src="https://d3js.org/d3.v6.js"></script>

score:0

The stateData you pass to .selectAll('g').data(stackData) is an array of arrays and does not contain keys. You need to enclose every array into an object and specify unique key:

correctStackData = stackData.map((item, index) => ({data: item, key: index}));

score:0

Exit will not work because you add a "g" container on each update. Just add the following line:

this.svg.selectAll("g").transition().duration(dur).style("opacity",0).remove();

before you call

this.svg.append("g").selectAll("g").data(stackData...

You can see the solution working in a fiddle


Related Query