Search code examples
d3.jstransitionuser-inputtween

d3.js tween factory return function applied to non-interpolable property values


This question builds on the (correct) answer provided to this. I simply haven't been able to get any further..

With the help of an interpolator function, d3.js's tween allows smooth graphical transition between existing and new (ie to be set) DOM element values. At the simplest level, for a given animation we have a target element, an start state, an end state, a transition, a tween function and an interpolator.

Now, say I want every so often to programmatically update the contents of an input (text field) element. The value to be entered is non-interpolable (either the text is submitted, or it is not. There is no in-between state). In providing a closure (allowing for text retrieval at the scheduled transition time), tween would seem to be a good vehicle for the updates. Either I replace the interpolator with a fixed value, ensure the start and end values are identical, or find some other way of forcing it to fire at t=1. That's the theory..

To this end, in my case each property (not value) is modified in it's own update call, into which are passed transition, element index and parent element selection.

First cut:

  • an outer, 'governing' transition with delay values staggered using a multiple of the current element's index

        playback_transition = d3.transition()
        .delay(function(d, i,  j) {
            return (time_interval * i);
        })
        .duration(function() {
            return 1; // the minimum
        });
    
  • within a call to playback_transition.each() pass the transition as a parameter to a dependent animation by means of an update() interface

  • within this dependent animation, apply the transition and tween to the current element(s):

        input                        // a UI dialog element
        .transition()
        .tween(i.toString(), setChordname( waveplot.chordname ));
    

Where:

        function setChordname(newValue) {
            return function() {
                var i = newValue;            // a string
                return function(t) {
                    this.value =  i;
                    inputChanged.call(this);
                };
            };
        };

and

        function inputChanged() {
          if (!this.value) return;

          try {
            var chord = chordify.chordObjFromChordName(this.value);
            purge();                         // rid display of superceded elements
            plotChord(chord, options);       // calculate & draw chord using new input property
          } catch (e) {
            console.log(e.toString());
          }
        }

PROBLEM

While setChordname always fires (each chord is in turn correctly found and it's value stored), of the scheduled returned functions, only the first fires and results in display of the associated waveform. For all subsequent return function occurrences, it is as if they had never been scheduled.

From the display:

  • direct user update to the input field still works fine
  • only the first of setChordname's return functions fire, but, for this initial chord, carries right through, correctly displaying the cluster of associated chord and note waves.

From this, we can say that the problem has nothing to do with the integrity of the waveplotting functions.

From the console

  • transitions are accumulating correctly.
  • chord supply is all good
  • associated (ie initial) tween fires at t=1. (specifically, tween appears to accept omission of an interpolator function).
  • looking at the output of transition.toSource(), though the associated outer index increases by single figure leaps, tween itself is always paired with an empty pair of curly brackets.

        transition = [[{__transition__:{8:{tween:{}, time:1407355314749, eas..
    

For the moment, apart from this and the initial execution, the tween factory return function behaviour is a mystery.

From Experiment

Neither of the following have any impact:

  • Extending the period before the initial transition takes effect
  • Extending (by a multiple) each staggered transition delay

Furthermore

  • the same transition configuration used in a different scenario works fine.

These seem to eliminate timing issues as a possible cause, leaving the focus more on the integrity of the tween setup, or conditions surrounding waveplot append/remove.

Afraid it might be interfering with input property text submission via the tween, I also tried disabling a parallel event listener (listening for 'change' events, triggering a call to inputChanged()). Apart from no longer being able to enter own chordnames by hand, no impact.

In addition to 'change', I tried out a variety of event.types ('submit', 'input', 'click' etc). No improvement.

The single most important clue is (to my mind) that only the first setChordname() return function is executed. This suggests that some fundamental rule of tween usage is being breached. The most likely candidate seems to be that the return value of tween **must* be an interpolator.

3 related questions, glad of answers to any:

  1. Anything blatently wrong in this approach?
  2. For a shared transition scenario such as this, do you see a better approach to transitioning a non-interpolable (and normally user-supplied) input property than using tween ?
  3. Provided they are staggered in time, multiple transitions may be scheduled on the same element - but what about multiple tweens? Here, as the staggered transition/tween combos are operating on only one element, they seem likely to be passed identical data (d) and index(i) in every call. Impact?

Solution

  • I'm now able to answer my own question. Don't be put off by the initial couple of paragraphs: there are a couple of valuable lessons further down..

    Ok, there were one or two trivial DOM-to-d3 reworking issues in my adoption of the original code. Moreover, an extra returned function construct managed to find it's way into this:

    Was:

        function setChordname(newValue) {
            return function() {              <--- Nasty..
                var i = newValue;
                return function(t) {
                    this.value =  i;
                    inputChanged.call(this);
                };
            };
        };
    

    Should have been:

        function setChordname(newValue) {
            var i = newValue;
            return function(t) {
                this.value =  i;
                inputChanged.call(this);
            };
        };
    

    The fundamental problem, however, was that the transition -passed in as a parameter to an update() function- seems in this case to have been blocked or ignored.

    Originally (as documented in the question) defined as:

    input                        // a UI dialog element
    .transition()
    .tween(i.toString(), setChordname( waveplot.chordname ));
    

    ..but should have been defined as:

    transition
    .select("#chordinput")       // id attribute of the input element
    .tween(i.toString(), setChordname( waveplot.chordname ));
    

    My guess is that the first version tries to create a new transition (with no delay or duration defined), whereas the second uses the transition passed in through the update() interface.

    Strange is that:

    1. what worked for another dependent animation did not for this.
    2. the staggered delays and their associated durations were nevertheless accepted by the original version, allowing me to be misled by console logs..

    Just to round this topic off, I can point out the the following (event-based) approach seems to work just as well as the tween variant with non-interpolable values documented above. I can switch freely between the two with no apparent difference in the resulting animations:

    transition
    .select("#chordinput")       // id attribute of the input element
    .each("start", setChordname( waveplot.chordname ));
    

    Thug