score:1

Accepted answer

[UPDATE: According to the comments the code was updated to change with a new line starting from the bottom when the set of keys in the new data are different]

here is a contribution for a better understanding of the problem, and a possible answer.

There is some misuse of the key element. When you define the key of the line, it's for d3 to know that one line is binded to that key. In this case, your key is binded to the path. When you add

this.line = this.line
        .data([data], d => d.key)

d3 binds the selection to [data] and will generate exactly one element ([data].length = 1) for this elements, d = data, hence d.key = null. This is the reason why you are not adding multiple lines, because your paths always got the key = null.

So, on the first time everything works as planned, you started a path as zero and then moves it to the final position with the transition.

This path has d attribute generate by the d3.line with a format like M x1 y1 L x2 y2 L x3 y3 ... L x12 y 12. Exactly 12 points for the first time.

When you swap the data, d3 will check the key (null again) and will consider this as an update.

So, it will interpolate the current path to a new one with the new data. The issue here is that there are no keys to bind the values. As you have now 31 points, it will interpolate the first 12 points (which is the part that you see moving) and add the remaining points (13 to 31). Of course, these last points don't have transition, because they didn't exist.

A possible solution for your case is to use a custom interpolator (that you can build) and use an attrTween to do the interpolation.

Fortunately, someone built one already: https://unpkg.com/d3-interpolate-path/build/d3-interpolate-path.min.js

SO here is a working solution

new Vue({
  el: "#app",
  data() {
    return {
      index: 0,
      data: [
        [{
            key: "Jan",
            value: 5787
          },
          {
            key: "Feb",
            value: 6387
          },
          {
            key: "Mrt",
            value: 7375
          },
          {
            key: "Apr",
            value: 6220
          },
          {
            key: "Mei",
            value: 6214
          },
          {
            key: "Jun",
            value: 5205
          },
          {
            key: "Jul",
            value: 5025
          },
          {
            key: "Aug",
            value: 4267
          },
          {
            key: "Sep",
            value: 6901
          },
          {
            key: "Okt",
            value: 5800
          },
          {
            key: "Nov",
            value: 7414
          },
          {
            key: "Dec",
            value: 6547
          }
        ],
        [{
            "key": 1,
            "value": 4431
          },
          {
            "key": 2,
            "value": 5027
          },
          {
            "key": 3,
            "value": 4586
          },
          {
            "key": 4,
            "value": 7342
          },
          {
            "key": 5,
            "value": 6724
          },
          {
            "key": 6,
            "value": 6070
          },
          {
            "key": 7,
            "value": 5137
          },
          {
            "key": 8,
            "value": 5871
          },
          {
            "key": 9,
            "value": 6997
          },
          {
            "key": 10,
            "value": 6481
          },
          {
            "key": 11,
            "value": 5194
          },
          {
            "key": 12,
            "value": 4428
          },
          {
            "key": 13,
            "value": 4790
          },
          {
            "key": 14,
            "value": 5825
          },
          {
            "key": 15,
            "value": 4709
          },
          {
            "key": 16,
            "value": 6867
          },
          {
            "key": 17,
            "value": 5555
          },
          {
            "key": 18,
            "value": 4451
          },
          {
            "key": 19,
            "value": 7137
          },
          {
            "key": 20,
            "value": 5353
          },
          {
            "key": 21,
            "value": 5048
          },
          {
            "key": 22,
            "value": 5169
          },
          {
            "key": 23,
            "value": 6650
          },
          {
            "key": 24,
            "value": 5918
          },
          {
            "key": 25,
            "value": 5679
          },
          {
            "key": 26,
            "value": 5546
          },
          {
            "key": 27,
            "value": 6899
          },
          {
            "key": 28,
            "value": 5541
          },
          {
            "key": 29,
            "value": 7193
          },
          {
            "key": 30,
            "value": 5006
          },
          {
            "key": 31,
            "value": 6580
          }
        ]
      ]
    }
  },
  mounted() {
    // set the dimensions and margins of the graph
    var margin = {
        top: 20,
        right: 20,
        bottom: 30,
        left: 30
      },
      width = 500 - margin.left - margin.right;

    this.height = 200 - margin.top - margin.bottom;

    // append the svg obgect to the body of the page
    // appends a 'group' element to 'svg'
    // moves the 'group' element to the top left margin
    this.svg = d3
      .select("#my_dataviz")
      .append("svg")
      .attr(
        "viewBox",
        `0 0 ${width + margin.left + margin.right} ${this.height +
          margin.top +
          margin.bottom}`
      )
      .attr("preserveAspectRatio", "xMinYMin")
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    // set the ranges
    this.xScale = d3
      .scalePoint()
      .range([0, width])
      .domain(
        this.data.map(function(d) {
          return d.key;
        })
      )
      .padding(0.5);

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

    this.yScale.domain([0, 7000]);

    // Draw Axis
    this.xAxis = d3.axisBottom(this.xScale);

    this.xAxisDraw = this.svg
      .append("g")
      .attr("class", "x axis")
      .attr("transform", `translate(0, ${this.height})`);

    this.yAxis = d3
      .axisLeft(this.yScale)
      .tickValues([0, 7000])
      .tickFormat(d => {
        if (d > 1000) {
          d = Math.round(d / 1000);
          d = d + "K";
        }
        return d;
      });

    this.yAxisDraw = this.svg.append("g").attr("class", "y axis");

    this.update(this.data[this.index]);
  },
  methods: {
    swapData() {
      if (this.index === 0) this.index = 1;
      else this.index = 0;
      this.update(this.data[this.index]);
    },
    update(data) {
      // Update scales.
      this.xScale.domain(data.map(d => d.key));
      this.yScale.domain([0, 7000]);

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

      const line = d3
                .line()
                .x(d => {
                  return this.xScale(d.key);
                })
                .y((d) => {
                  return this.yScale(d.value);
                });

      // Update line.
      this.line = this.svg.selectAll(".line")
      this.line = this.line
        .data([data], d => d.reduce((key, elem) => key + '_' + elem.key, ''))
        .join(
          enter => {
            enter
              .append("path")
              .attr("class", "line")
              .attr("fill", "none")
              .attr("stroke", "#206BF3")
              .attr("stroke-width", 4)
              .attr(
                "d",
                d3
                .line()
                .x(d => {
                  return this.xScale(d.key);
                })
                .y(() => {
                  return this.yScale(0);
                })
              )
              .transition(t)
              .attr(
                "d", (d) => line(d)
              );
          },

          update => {
            update
            .transition(t)
            .attrTween('d', function(d) { 
                var previous = d3.select(this).attr('d');
                var current = line(d);
                return d3.interpolatePath(previous, current); 
            });
          },

          exit => exit.remove()
        );

      // Update Axes.
      this.yAxis.tickValues([0, 7000]);
      if (data.length > 12) {
        this.xAxis.tickValues(
          data.map((d, i) => {
            if (i % 3 === 0) return d.key;
            else return 0;
          })
        );
      } else {
        this.xAxis.tickValues(
          data.map(d => {
            return d.key;
          })
        );
      }
      this.yAxis.tickValues([0, 7000]);
      this.xAxisDraw.transition(t).call(this.xAxis.scale(this.xScale));
      this.yAxisDraw.transition(t).call(this.yAxis.scale(this.yScale));
    }
  }
})
<div id="app">
  <button @click="swapData">Swap</button>
  <div id="my_dataviz" class="flex justify-center"></div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://d3js.org/d3.v6.js"></script>
<script src="https://unpkg.com/d3-interpolate-path/build/d3-interpolate-path.min.js"></script>

score:1

I'm not directly answering your question yet (sorry!) because this might be a better solution. It's possible to interpolate between lines with a different number of points, which may provide a better experience?

There's a d3-interpolate-path plugin that can handle a different number of points being present in the path, but still create a reasonably smooth animation by inserting placeholder points into the line.

There's a really good explanation of how this works, as well as some examples of it working https://bocoup.com/blog/improving-d3-path-animation .

Answer

If you really do want to animate from zero each time, then you need to check the keys match the last set of keys.

  1. Create a d3 local store

    const keyStore = d3.local();

  2. Get the keys from last render (element wants to be your line)

    const oldKeys = keyStore.get(element);

  3. Determine if the keys match:

    const newKeys = data.map(d => d.key);
    // arraysEqual - https://stackoverflow.com/a/16436975/21061
    const keysMatch = arraysEqual(oldKeys, newKeys);
    
  4. Change your interpolation on keysMatch (see previous ternary):

    update.transition(t)
          .attrTween('d', function(d) { 
              var previous = keysMatch ? d3.select(this).attr('d') : 0;
              var current = line(d);
              return d3.interpolatePath(previous, current); 
          });
    
    

Related Query