Home > OS >  d3.js wrong boundingbox-values after using a transition
d3.js wrong boundingbox-values after using a transition

Time:05-20

I have a problem understanding the order of operations of d3.js/javascript when using transition methods in combinations with events. To illustrate my general confusion I have created a div which will change its height after clicking on it and a paragraph that uses its bounding box to get the its height for its text value:

dataArr = [{data:50, id:'a', val : true}]

function update(){
    divs = d3.select('body').selectAll('div')
        .data(dataArr)
        .join('div')
        .text("click me")
        .style('background-color','red')
        .attr('id', d=> d.id)
        .on('click', function(_,d){
            let heightVal = 20
            if(d.val){
                heightVal = 50
                d.val = false
            }
            else{
                d.val = true
            }
            d3.select(this).transition().duration(200).style('height',heightVal   'px').text(heightVal) 
            update()
        })
        
    d3.select('body').selectAll('p').data([true]).join('p').text(function(d){
        return 'height: '   d3.select('#a').node().getBoundingClientRect().height
    })
}

update()
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<!DOCTYPE html>
<meta charset="UTF-8">
<body>
<script src="https://d3js.org/d3.v7.min.js"></script>
</body>

From my understanding after a simple click the text of the paragraph should have the value after the transition is done, however the text of the value is from before the transition started. Can someone help me understand why this happens and how to fix this?

CodePudding user response:

Your update function initiates a transition, which starts to run simultaneously (asynchronously) with the next code block which determines how high the div is by measuring its bounding box as it is rendered at that time.

Since the transition hasn't finished, it's actually just barely started, you don't get the future size of the element after the transition is finished, but rather the size right at the beginning, which is the original size, or very close to the original value. This is why "the text of the value is from before the transition started," - you measured the size at the beginning of the transition (if you had a very slow computer, you might see values early in the transition rather than at the start).

If you want the to wait until the end of the transition to measure the size, you can use transition.on("end", function() { ... }):

dataArr = [{data:50, id:'a', val : true}]

function update(){
    divs = d3.select('body').selectAll('div')
        .data(dataArr)
        .join('div')
        .text("click me")
        .style('background-color','red')
        .attr('id', d=> d.id)
        .on('click', function(_,d){
            let heightVal = 20
            if(d.val){
                heightVal = 50
                d.val = false
            }
            else{
                d.val = true
            }
            d3.select(this).transition().duration(200).style('height',heightVal   'px').text(heightVal) 
             .on("end", function() {
                d3.select('body').selectAll('p').data([true]).join('p').text(function(d){
                  return 'height: '   d3.select('#a').node().getBoundingClientRect().height
                })
            })
            update()
        })
       
}

update()
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<!DOCTYPE html>
<meta charset="UTF-8">
<body>
<script src="https://d3js.org/d3.v7.min.js"></script>
</body>

Of course this won't update the text until the transition completes. If you want the eventual value prior to the transition completing, the easiest way you could access it is via using the value provided to the transition directly: heightVal and set the text value to this rather than using getBoundingClientRect. As getBoundingClientRect measures size at a given moment in time, it can't be used to measure the size of something at some point in the future.

For comparison, the text in the box updates right away as it uses transition.text(), which will "For each selected element, sets the text content to the specified target value when the transition starts (docs)."

You could get the text in the box to transition with:

dataArr = [{data:50, id:'a', val : true}]

function update(){
    divs = d3.select('body').selectAll('div')
        .data(dataArr)
        .join('div')
        .text("click me")
        .style('background-color','red')
        .attr('id', d=> d.id)
        .on('click', function(_,d){
            let heightVal = 20
            if(d.val){
                heightVal = 50
                d.val = false
            }
            else{
                d.val = true
            }
            d3.select(this).transition().duration(200).style('height',heightVal   'px').textTween(()=>d3.interpolateRound(heightVal == 20 ? 50 : 20, heightVal))
             .on("end", function() {
                d3.select('body').selectAll('p').data([true]).join('p').text(function(d){
                  return 'height: '   d3.select('#a').node().getBoundingClientRect().height
                })
            })
            update()
        })
       
}

update()
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<!DOCTYPE html>
<meta charset="UTF-8">
<body>
<script src="https://d3js.org/d3.v7.min.js"></script>
</body>

You could also get the size during the transition using getBoundingClientRect for the text outside of the box:

dataArr = [{data:50, id:'a', val : true}]

function update(){
    divs = d3.select('body').selectAll('div')
        .data(dataArr)
        .join('div')
        .text("click me")
        .style('background-color','red')
        .attr('id', d=> d.id)
        .on('click', function(_,d){
            let heightVal = 20
            if(d.val){
                heightVal = 50
                d.val = false
            }
            else{
                d.val = true
            }
            
            d3.select(this)
              .transition()
              .duration(200)
              .style('height',heightVal   'px')
              .attrTween("anything", function() { 
                return function() {
                   d3.select('body')
                       .selectAll('p')
                       .data([true])
                       .join('p')
                       .text(function(d){
                       return 'height: '   d3.select('#a').node().getBoundingClientRect().height
                   })
                }
            })
            .duration(2000);
            
            update()
        })
       
}

update()
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<!DOCTYPE html>
<meta charset="UTF-8">
<body>
<script src="https://d3js.org/d3.v7.min.js"></script>
</body>

  • Related