Search code examples
javascriptd3.jsstring-interpolation

D3 transition works unexpectedly


For some reason, D3 is not transitioning the div's width smoothing on exit.

In the GIF below, I switch between 2 datasets, from [20, 40] to [20]

The expected behavior is that the blue div should shrink smoothly, but instead, it gets stuck.

what should I do differently?

demo

Live example: http://jsbin.com/vikodufugo/1/edit?js,output

const render = (data) => {
  const colors = ['red', 'blue', 'green', 'gold'];
  const width = (data) => (d) => (d / d3.max(data)) * 100 + '%';

  const selection = d3
    .select('.chart')
    .selectAll('div')
    .data(data);

  selection
    .transition()
    .duration(750)
    .style('width', width(data))

  selection
    .enter()
    .append('div')
    .style('width', '0px')
    .style('height', '100px')
    .style('background-color', (d, i) => colors[i % colors.length])
    .transition()
      .duration(750)
      .style('width', width(data))

  selection
    .exit()
    .transition()
    .duration(750)
    // shouldn't it transition smoothly?
    .style('width', '0px')
    .remove()
}

...

onClick('#button-1', () => render([20, 40]))
onClick('#button-2', () => render([20]))

Solution

  • The problem here is just the interpolation used by the transition.

    D3 can interpolate two strings without any problem, for instance 300px to 20px. However, in your case, the start string has a "%" while the final string has "px". D3 cannot interpolate that, which is quite excusable: what's the transition from "%" to "px"?

    Let's show it in the following snippets. Here, I'm using d3.interpolateString(a,b), which:

    Returns an interpolator between the two strings a and b. The string interpolator finds numbers embedded in a and b, where each number is of the form understood by JavaScript.

    In the first demo, both strings have "px":

    var interpolator = d3.interpolateString("300px", "0px");
    d3.range(0,1.1,.1).forEach(function(d){
    	console.log(interpolator(d))
    });
    <script src="https://d3js.org/d3.v4.min.js"></script>

    As you can see, nice results.

    Now look what happens if the first string has "%" while the second one has "px":

    var interpolator = d3.interpolateString("100%", "0px");
    d3.range(0,1.1,.1).forEach(function(d){
    	console.log(interpolator(d))
    });
    <script src="https://d3js.org/d3.v4.min.js"></script>

    As you can see, at the very first interpolation, the "%" is converted to "px". That explains the behaviour you're seeing: the div goes from "100%" to "100px" suddenly, and from there to "0px" during the transition time.

    Solution:

    Change your string to use % in the exit selection:

    selection
        .exit()
        .transition()
        .duration(750) 
        .style('width', '0%')
        //use % here -----^
        .remove()
    

    Here is your code with that change:

    const render = (data) => {
      const colors = ['red', 'blue', 'green', 'gold'];
      const width = (data) => (d) => (d / d3.max(data)) * 100 + '%';
    
      const selection = d3
        .select('.chart')
        .selectAll('div')
        .data(data);
    
      selection
        .transition()
        .duration(750)
        .style('width', width(data))
    
      selection
        .enter()
        .append('div')
        .style('width', '0px')
        .style('height', '100px')
        .style('background-color', (d, i) => colors[i % colors.length])
        .transition()
          .duration(750)
          .style('width', width(data))
    
      selection
        .exit()
        .transition()
        .duration(750)
        // shouldn't it transition smoothly?
        .style('width', '0%')
        .remove()
    }
    
    
    const onClick = (selector, callback) => (
      document.querySelector(selector).addEventListener('click', callback)
    );
    
    onClick('#button-1', () => render([20, 40]))
    onClick('#button-2', () => render([20]))
    
    render([20, 40]);
    <script src="https://d3js.org/d3.v4.js"></script> 
    <div class="buttons">
        <button id="button-1" value="0">Option 1</button>
        <button id="button-2" value="1">Option 2</button>
    </div>
    <div class="chart"></div>