debugging d3.attrTween

I am animating an element as this.

Now, for my use case, I can use d3.transition.attr which I don’t want to.
My preference is to use d3.attrTween as I can dynamically query the value, within it rather than hardcoding required in d3.transition.attr.

I don’t know how can I debug attr.Tween to give me the exact same effect as d3.transition.attr.

Thank you in advance.

If you want to fetch the y2 value directly in the attrTween factory, I fear you’ll have to incorporate the delay into your interpolator, because (as far as I’m aware) the tween does not even start until the delay has passed, and therefore does not set y2 to 0.

The other option is to store the original y2 somewhere (e.g. in a custom attribute or d3.local), set it to 0 before your transition, then fetch it from the custom attribute / local var in your attrTween factory.

1 Like

It worked. Thanks for the suggestion.

    d3.selectAll(".xAxis>.xAxisBottom>.tick>line")
        .each(function(d, i) {
            const selection = d3.select(this);
            selection.attr('data-t', `${this.getAttribute("y2")}`);
        })
        .attr('y2', '0')
        .transition()
        .duration(3000)
        .delay(function() {
            const child = this;
            const parent = this.parentNode;
            const p = parent
                .getAttribute("transform")
                .match(/(\d+\.\d+)(?=\,)|(\d+)(?=\,)/gm);
            const b = axisX.node().getPointAtLength(axisXLen * 0).x;
            const c = axisXLen;
            const elapsedTimePct = Math.sqrt((p - b) / c);
            return `${duration}` * elapsedTimePct;
        })
        .attrTween("y2", function() {
            const end = this.getAttribute('data-t');
            return d3.interpolate(0, end);
        });

Edit: Looks like your edit overlapped with my reply. :slight_smile:

There are a few things here that are bound to cause problems:

  1. Your code produces side effects. Every time you run your cell, you select from the already modified SVG DOM. Instead, you should
    1. label your SVG cell (e.g. chart)
    2. clone the DOM:
    const svg = d3.select(chart.cloneNode(true));
    
    1. select from the cloned DOM, e.g.:
    const axisX = svg.select(".xAxis>.xAxisBottom>.domain");
    
    1. return the cloned and animated DOM at the end of your cell:
    return svg.node();
    
  2. You’re doing math with strings, which may cause unexpected behavior. I recommend to always cast to Number, so that you’d at least get NaN in the right place. Example (note the “+”):
    const end = +child.getAttribute("y2");
    
  3. In this line you always fetch the same value:
    const b = axisX.node().getPointAtLength(axisXLen * 0).x;
    

There are other problems here like your interpolator function not returning any value. In general I highly recommend that you keep an eye out for errors in your dev tools console and use methods like console.assert() to verify that your code is doing what you think it does (e.g. to check with isNaN()).

I won’t go into more detail for the updated version that you shared, because I feel that in it you’re making things more difficult for yourself than they need to be. :slight_smile:

Instead I’ll pick up from the second option that I mentioned and use d3.local (we could also use a JS Map instead). The full streamlined implementation then looks like this:

{
  const duration = 5000;
  const svg = d3.select(chart.cloneNode(true));

  const axisX = svg.select(".xAxis>.xAxisBottom>.domain");
  const axisXLen = axisX.node().getTotalLength();
  const x0 = axisX.node().getPointAtLength(0).x;

  ////////////////////////////////////////////////////////////
  //////////////////////// Axis transition////////////////////
  ////////////////////////////////////////////////////////////
  axisX
    .attr("stroke-dasharray", `0 ${axisXLen}`)
    .transition()
    .duration(duration)
    .ease(d3.easeQuadIn)
    .attr("stroke-dasharray", `${axisXLen} ${axisXLen}`);

  ////////////////////////////////////////////////////////////
  //////////Axis gridline transition with tranition.attr//////
  ////////////////////////////////////////////////////////////

  const y2Values = d3.local();
  svg.selectAll(".xAxis>.xAxisBottom>.tick>line")
    .each(function() {
      y2Values.set(this, +this.getAttribute("y2"));
    })
    .attr("y2", 0)
    .transition()
    .duration(duration)
    .delay(function() {
      const p = +this.parentNode
        .getAttribute("transform")
        .match(/(\d+\.\d+)(?=\,)|(\d+)(?=\,)/gm);
      const elapsedTimePct = Math.sqrt((p - x0) / axisXLen);
      return duration * elapsedTimePct;
    })
    .attrTween("y2", function () {
      const end = y2Values.get(this);
      return d3.interpolate(0, end);
    });

  return svg.node();
}
1 Like

@mootari

I am guessing you are advising this in the context of observable. My data-viz will run in the browser not in observable. So it will probably not affect the data-viz on the browser. But I use observable, cause it is easier to ask questions in this forum using that. But that is not really an excuse for me not to incorporate your suggestions for future notebooks. I am finding observable extremely hard compared to what I can write in VS code. I wish there was a stackoverflow-like code snippet. However, I will try to improve my future notebooks.

I am extremely thankful for your suggestions and I learned something new. In fact, both of us solved it differently - so it currently gives me two arsenals in my armoury. I had trouble translating your suggestion into a workable code but I got it done, finally.

Thanks for your time and help.

1 Like

For what it’s worth: While many notebooks fully embrace Observable’s REPL-like nature, most of the time I try to make sure that my notebook code is properly wrapped in functions, and thus reusable. When you follow this paradigm combined e.g. with dependency injection (pass everything that’s needed into the function as argument) you may find that you get both a high level of flexibility and portability.

Building on this topic’s example, you could .e.g. pass in the target element and even make selectors configurable. Applied to the code I posted earlier it could look like this:

function applyTransition(element, options = {}) {
  const {
    duration = 5000,
    axisSelector = ".xAxis>.xAxisBottom>.domain",
    lineSelector = ".xAxis>.xAxisBottom>.tick>line"),
  } = options;

  const svg = element;

  const axisX = svg.select(axisSelector);
  const axisXLen = axisX.node().getTotalLength();
  const x0 = axisX.node().getPointAtLength(0).x;

  ////////////////////////////////////////////////////////////
  //////////////////////// Axis transition////////////////////
  ////////////////////////////////////////////////////////////
  axisX
    .attr("stroke-dasharray", `0 ${axisXLen}`)
    .transition()
    .duration(duration)
    .ease(d3.easeQuadIn)
    .attr("stroke-dasharray", `${axisXLen} ${axisXLen}`);

  ////////////////////////////////////////////////////////////
  //////////Axis gridline transition with tranition.attr//////
  ////////////////////////////////////////////////////////////

  const y2Values = d3.local();
  svg.selectAll(lineSelector)
    .each(function() {
      y2Values.set(this, +this.getAttribute("y2"));
    })
    .attr("y2", 0)
    .transition()
    .duration(duration)
    .delay(function() {
      const p = +this.parentNode
        .getAttribute("transform")
        .match(/(\d+\.\d+)(?=\,)|(\d+)(?=\,)/gm);
      const elapsedTimePct = Math.sqrt((p - x0) / axisXLen);
      return duration * elapsedTimePct;
    })
    .attrTween("y2", function () {
      const end = y2Values.get(this);
      return d3.interpolate(0, end);
    });
  // Not strictly necessary, but nice for convenience / chaining.
  return svg.node();
}