blabbath
blabbath

Reputation: 462

d3 Update stacked bar graph using selection.join

I am creating a stacked bar graph which should update on changing data and I want to use d3v5 and selection.join as explained here https://observablehq.com/@d3/learn-d3-joins?collection=@d3/learn-d3.
When entering the data everything works as expected, however the update function is never called (that's the console.log() for debugging.). So it looks like it is just entering new data all the time.
How can I get this to work as expected?

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <style>
    body {
      margin: 0;
    }
    .y.axis .domain {
      display: none;
    }
    </style>
  </head>
  <body>
    <div id="chart"></div>

    <script src="https://d3js.org/d3.v5.min.js"></script>
    <script>

    let xVar = "year";

    let alphabet = "abcdef".split("");
    let years = [1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003];

    let margin = { left:80, right:20, top:50, bottom:100 };

    let width = 600 - margin.left - margin.right,
        height = 600 - margin.top - margin.bottom;

    let g = d3.select("#chart")
      .append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
      .append("g")
        .attr("transform", "translate(" + margin.left + ", " + margin.top + ")");

    let color = d3.scaleOrdinal(["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854","#ffd92f"])
    let x = d3.scaleBand()
        .rangeRound([0, width])
        .domain(years)
        .padding(.25);

    let y = d3.scaleLinear()
        .rangeRound([height, 0]);

    let xAxis = d3.axisBottom(x);

    let yAxis = d3.axisRight(y)
      .tickSize(width)

    g.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis);

    g.append("g")
        .attr("class", "y axis")
        .call(yAxis);

    let stack = d3.stack()
        .keys(alphabet)
        .order(d3.stackOrderNone)
        .offset(d3.stackOffsetNone);

    redraw(randomData());

    d3.interval(function(){
      redraw(randomData());  
    }, 1000);

    function redraw(data){

      // update the y scale
      y.domain([0, d3.max(data.map(d => d.sum ))])
      g.select(".y")
        .transition().duration(1000)
          .call(yAxis);

      groups = g.append('g')
        .selectAll('g')
        .data(stack(data))
        .join('g')
        .style('fill', (d,i) => color(d.key));
      groups.selectAll('.stack')
        .data(d => d)
        .attr('class', 'stack')
        .join(
          enter => enter.append('rect')
            .data(d => d)
              .attr('x', d => x(d.data.year))
              .attr('y', y(0))
              .attr('width', x.bandwidth())
            .call(enter => enter.transition().duration(1000)
              .attr('y', d => y(d[1]))
              .attr('height', d => y(d[0]) - y(d[1]))
            ),
          update => update
          .attr('x', d => x(d.data.year))
          .attr('y', y(0))
          .attr('width', x.bandwidth())
            .call(update => update.transition().duration(1000)
            .attr('y', d => y(d[1]))
              .attr('height', d => y(d[0]) - y(d[1]))
            .attr(d => console.log('update stack'))
          )
      )

    }

    function randomData(data){
      return years.map(function(d){
        let obj = {};
        obj.year = d;
        let nums = [];
        alphabet.forEach(function(e){
          let num = Math.round(Math.random()*2);
          obj[e] = num;
          nums.push(num);
        });
        obj.sum = nums.reduce((a, b) => a + b, 0);
        return obj;
      });
    }

    </script>
  </body>
</html>

Here is it in a working jsfiddle: https://jsfiddle.net/blabbath/yeq5d1tp/

EDIT: I provided a wrong link first, here is the right one. My example is heavily based on this: https://bl.ocks.org/HarryStevens/7e3ec1a6722a153a5d102b6c42f4501d

Upvotes: 5

Views: 1145

Answers (2)

Guillermo
Guillermo

Reputation: 1054

I had the same issue a few days ago. The way I did it is as follows:

We have two .join, the parent one is for the stack and the child is for the rectangles.

In the enter of the parent join, we call the updateRects in order to draw the rectangles for the first time, this updateRects function will do the child .join, this second join function will draw the rectangles.

For the update we do the same, but instead of doing it in the enter function of the join, we do it in the update.

Also, my SVG is structured in a different way, I have a stacks groups, then I have the stack group, and a bars group, in this bars group I add the rectangles. In the fiddle below, you can see that I added the parent group with the class stack.

The two functions are below:

updateStack:

  function updateStack(data) {
    // update the y scale
    y.domain([0, d3.max(data.map((d) => d.sum))]);
    g.select(".y").transition().duration(1000).call(yAxis);

    const stackData = stack(data);

    stackData.forEach((stackedBar) => {
      stackedBar.forEach((stack) => {
        stack.id = `${stackedBar.key}-${stack.data.year}`;
      });
    });

    let bars = g
      .selectAll("g.stacks")
      .selectAll(".stack")
      .data(stackData, (d) => {
        return d.key;
      });

    bars.join(
      (enter) => {
        const barsEnter = enter.append("g").attr("class", "stack");

        barsEnter
          .append("g")
          .attr("class", "bars")
          .attr("fill", (d) => {
            return color(d.key);
          });

        updateRects(barsEnter.select(".bars"));

        return enter;
      },
      (update) => {
        const barsUpdate = update.select(".bars");
        updateRects(barsUpdate);
      },
      (exit) => {
        return exit.remove();
      }
    );
  }

updateRects:

  function updateRects(childRects) {
    childRects
      .selectAll("rect")
      .data(
        (d) => d,
        (d) => d.id
      )
      .join(
        (enter) =>
          enter
            .append("rect")
            .attr("id", (d) => d.id)
            .attr("class", "bar")
            .attr("x", (d) => x(d.data.year))
            .attr("y", y(0))
            .attr("width", x.bandwidth())
            .call((enter) =>
              enter
                .transition()
                .duration(1000)
                .attr("y", (d) => y(d[1]))
                .attr("height", (d) => y(d[0]) - y(d[1]))
            ),
        (update) =>
          update.call((update) =>
            update
              .transition()
              .duration(1000)
              .attr("y", (d) => y(d[1]))
              .attr("height", (d) => y(d[0]) - y(d[1]))
          ),
        (exit) =>
          exit.call((exit) =>
            exit
              .transition()
              .duration(1000)
              .attr("y", height)
              .attr("height", height)
          )
      );
  }

Here is an update jsfiddle https://jsfiddle.net/5oqwLxdj/1/

I hope it helps.

Upvotes: 4

Maytha8
Maytha8

Reputation: 866

It is getting called, but you can't see it. Try changing .attr(d => console.log('update stack')) to .attr(console.log('update stack')).

Upvotes: 0

Related Questions