Search code examples
javascriptcssd3.jscss-calc

Unexpected transition when using CSS calc in d3js styleTween


I want to use CSS calc to compute the start and/or end value for the left property of a div that I would like to move smoothly by using d3js styleTween. But before the transition to the correct end point starts, the div jumps in the opposite direction.

I tested the code with Chromium Version 73.0.3683.75 and Firefox 60.7.0esr (64-bit). Which both produce the same unexpected behavior.

Using this.style.left as first argument in d3.interpolate won't change the result.

I havn't find a note in the d3js styleTween documentation that warns from using calc and it does work for the end of the transition.

I would expect that computing the calc value first and then just pass the result to the interpolation function, would fix the issue. But I'm not sure if this is possible.

<!DOCTYPE html>
<html>
    <head>
        <title>Odd interpolation with CSS calc and d3 styleTween</title>

        <style>
        html {height: 100%;}
        body {height: 100%; margin:0; }

        div.button {
        position: absolute;
        left: 20%;
        width: 3rem;
        height: 100%;
        background-color: #0af;
        }
        </style>
    </head>

    <body>
        <div class="button" id="menu_toggle"></div>
    </body>

    <script src="https://d3js.org/d3.v4.min.js" />
    <script>
        menu = function()
        {
            var div = d3.select('div.button');
            var is_left = true;
            function toggle_menu()
        {
            var left = "20%"
            var right = "calc(80% - 3rem)"
            if (is_left)
            {
                is_left = false;
                div.transition().duration(2000)
                                .styleTween('left', function() { return d3.interpolate(left, right); });
            }
            else
            {
                is_left = true;
                div.transition().duration(2000)
                                .styleTween('left', function() { return d3.interpolate(right, left); });
            }
        }
        return {toggle_menu:toggle_menu}
        }()

        d3.select('#menu_toggle').on("click", menu.toggle_menu)
    </script>
</html>

I expect a smooth transition between the two positions 20% and 80%-3rem.

Instead when the div starts on the left, it first jumps 3rem to the left before moving right. Similarly, if the div is on the right, it first jumps 3rem to the right before moving to the left.


Solution

  • The problem is quite straightforward: you're interpolating strings, not their evaluated values.

    When you do this in the CSS...

    calc(80% - 3rem)
    

    ... it will be evaluated to a pixel value. So far, so good. However, the D3 interpolator is literally (pun intended) interpolating from "20%" to "calc(80% - 3rem)". Of course, the interpolator doesn't know how to handle this, and it's actually interpolating from "calc(20% - 3rem)" to "calc(80% - 3rem)".

    Let's see:

    const interpolator = d3.interpolate("20%", "calc(80% - 3rem)");
    d3.range(0, 1.1, 0.1).forEach(d => {
      console.log(interpolator(d))
    })
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

    The simplest solution is evaluating the result of calc() and using that value in the interpolator. There are several ways to do that, like using this library.

    So, suppose that after the evaluation we have some values like 150px and 430px. Now the interpolation is easy:

    const interpolator = d3.interpolate("150px", "430px");
    d3.range(0, 1.1, 0.1).forEach(d => {
      console.log(interpolator(d))
    })
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>