0

I'm trying to add some elements on a chart, and I'm doing it in the onrendered function passed to billboard.js configuration.

In this function, I have to know if some series are or not on the chart, and this can be done by easily checking their opacities. Now, in the example below, you can see that sometimes the opacity transitions are not done when the code in onrendered fires.

How to make sure that what happens in onrendered is after all the series are shown/hidden from the chart? So after all the transitions ended.

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <script src="https://d3js.org/d3.v4.min.js"></script>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/billboard.js/dist/billboard.min.css" />
  <script src="https://cdn.jsdelivr.net/npm/billboard.js/dist/billboard.pkgd.min.js"></script>
  <title>JS Bin</title>
</head>

<body>
 <div id="chart"></div>
  <script>
  console.log('should be d1=1,d2=1')
  var c = bb.generate({
    bindto: "#chart",
    size: {
      width: 500,
      height: 250
    },
    transition: 0,
    data: {
      x: "x",
      columns: [
        ["x", "2013-01-01", "2013-01-02", "2013-01-03", "2013-01-04", "2013-01-05", "2013-01-06"],
        ["data1", 30, 200, 100, 400, 150, 250],
        ["data2", 130, 340, 200, 500, 250, 350],
      ]
    },
    axis: {
      x: {
        type: "timeseries",
        tick: {
          format: "%Y-%m-%d"
        }
      }
    },
    onrendered: function() {
      var data1op = d3.select(".bb-chart-line.bb-target.bb-target-data1").style("opacity");
      var data2op = d3.select(".bb-chart-line.bb-target.bb-target-data2").style("opacity");
      console.log(`data1 op: ${data1op} --- data2 op: ${data2op}`);
    }
  });
  
  setTimeout(function(){console.log('should be d1=0,d2=1');c.hide('data1')},500);
  setTimeout(function(){console.log('should be d1=0,d2=0');c.hide('data2')},1000);
  setTimeout(function(){console.log('should be d1=1,d2=0');c.show('data1')},1500);
  setTimeout(function(){console.log('should be d1=1,d2=1');c.show('data2')},2000);

  setTimeout(function(){console.log('more waiting between hide/show')},3500);

  setTimeout(function(){console.log('should be d1=0,d2=1');c.hide('data1')},5000);
  setTimeout(function(){console.log('should be d1=0,d2=0');c.hide('data2')},6000);
  setTimeout(function(){console.log('should be d1=1,d2=0');c.show('data1')},7000);
  setTimeout(function(){console.log('should be d1=1,d2=1');c.show('data2')},8000);
  </script>
</body>

</html>
Hyyan Abo Fakher
  • 3,497
  • 3
  • 21
  • 35
knee pain
  • 600
  • 3
  • 19

1 Answers1

0

The problem is that onrendered is called when all the svg manipulations are done. But the transitions still have to be started. You have to wait till the transitions are finished.

The naive way is to get the transition after a call to c.hide or c.show but it is not attached yet. You have to wait till the first idle processing is done and the first transition tick is processed. You can do that by using a timeout of a few milisecond, then use the attached transition and hook a follow-up on to it and wait for the start of it to call a function to be handled when transition ends. This is done with the following function.

function atEndOf(dataN, action) {
  setTimeout(function() {
    var transition = d3.active(d3.select(".bb-chart-line.bb-target.bb-target-"+dataN).node());
    if (!transition) { return; } // no transition running
    transition.transition().on("start", action);
  }, 10);
}

To know when the hide and show transitions are done you can build an atEndOf chain

// wait till main animation is finished
setTimeout(function() {
  c.hide('data1');
  atEndOf('data1', function () {
    console.log('should be d1=0,d2=1');
    showOpacity();
    c.hide('data2');
    atEndOf('data2', function () {
      console.log('should be d1=0,d2=0');
      showOpacity();
    });
  });
}, 6000);

I don't know why but this trick does not work for the main start up draw transition. I get the error "too late: already started"

This uses the following function

function showOpacity() {
  var data1op = d3.select(".bb-chart-line.bb-target.bb-target-data1").style("opacity");
  var data2op = d3.select(".bb-chart-line.bb-target.bb-target-data2").style("opacity");
  console.log(`data1 op: ${data1op} --- data2 op: ${data2op}`);
}

Because the show and hide transitions use the default 1s duration it is hard to see that we wait till the end of the transition. To change the duration of these transitions we need to decorate the ChartInternal.endAll() method. Here I set a duration of 5000ms to all transitions.

var bb_endall = bb.chart.internal.fn.endall;
bb.chart.internal.fn.endall = function (transition, callback) {
  transition.duration(5000);
  bb_endall(transition, callback);
};

I tried to use another decoration that uses the atEndOf technique to call a user defined function but I could not get it to work.

A complete example with all transitions set to a duration of 5000ms

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <script src="https://d3js.org/d3.v4.min.js"></script>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/billboard.js/dist/billboard.min.css" />
  <script src="https://cdn.jsdelivr.net/npm/billboard.js/dist/billboard.pkgd.min.js"></script>
  <title>JS Bin</title>
</head>

<body>
 <div id="chart"></div>
<script>
  var bb_endall = bb.chart.internal.fn.endall;
  bb.chart.internal.fn.endall = function (transition, callback) {
    transition.duration(5000);
    bb_endall(transition, callback);
  };

  //console.log('should be d1=1,d2=1');
  var c = bb.generate({
    bindto: "#chart",
    size: {
      width: 500,
      height: 250
    },
    // transition: 0,
    transition: {duration: 5000},
    data: {
      x: "x",
      columns: [
        ["x", "2013-01-01", "2013-01-02", "2013-01-03", "2013-01-04", "2013-01-05", "2013-01-06"],
        ["data1", 30, 200, 100, 400, 150, 250],
        ["data2", 130, 340, 200, 500, 250, 350],
      ]
    },
    axis: {
      x: {
        type: "timeseries",
        tick: {
          format: "%Y-%m-%d"
        }
      }
    },
    onrendered: function() {
      // not usefull for end of transition
    }
  });
  function showOpacity() {
    var data1op = d3.select(".bb-chart-line.bb-target.bb-target-data1").style("opacity");
    var data2op = d3.select(".bb-chart-line.bb-target.bb-target-data2").style("opacity");
    console.log(`data1 op: ${data1op} --- data2 op: ${data2op}`);
  }
  function atEndOf(dataN, action) {
    setTimeout(function() {
      var transition = d3.active(d3.select(".bb-chart-line.bb-target.bb-target-"+dataN).node());
      if (!transition) { return; } // no transition running
      transition.transition().on("start", action);
    }, 10);
  }

  // wait till main animation is finished
  setTimeout(function() {
    c.hide('data1');
    atEndOf('data1', function () {
      console.log('should be d1=0,d2=1');
      showOpacity();
      c.hide('data2');
      atEndOf('data2', function () {
        console.log('should be d1=0,d2=0');
        showOpacity();
      });
    });
  }, 6000);


//   setTimeout(function(){console.log('should be d1=0,d2=1');c.hide('data1')},500);
//   setTimeout(function(){console.log('should be d1=0,d2=0');c.hide('data2')},1000);
//   setTimeout(function(){console.log('should be d1=1,d2=0');c.show('data1')},1500);
//   setTimeout(function(){console.log('should be d1=1,d2=1');c.show('data2')},2000);

//   setTimeout(function(){console.log('more waiting between hide/show')},3500);

//   setTimeout(function(){console.log('should be d1=0,d2=1');c.hide('data1')},5000);
//   setTimeout(function(){console.log('should be d1=0,d2=0');c.hide('data2')},6000);
//   setTimeout(function(){console.log('should be d1=1,d2=0');c.show('data1')},7000);
//   setTimeout(function(){console.log('should be d1=1,d2=1');c.show('data2')},8000);
  </script>
</body>

</html>

There is some code in billboard.js that calls onrendered when there is a TabVisible. It will be postponed to when the transition has finished. I have no clue yet when that is the case.

rioV8
  • 24,506
  • 3
  • 32
  • 49
  • still getting some weird numbers sometimes on your code snippet. `should be d1=0,d2=1 data1 op: 3.7044e-08 --- data2 op: 1` – knee pain Aug 27 '18 at 09:50
  • @bgiuga: 3.7e-8 is almost 0 (3.7 * 10^-8), for some reason the callback that should run on the end of the transition is not yet run when this callback is called. – rioV8 Aug 27 '18 at 12:03
  • yeah, I saw that the values are ~0, just pointing out that we didn't always get a "perfect" 0. I'm going to try some things out for my particular case, and I'll be back with feedback and hopefully a check mark for your answer. – knee pain Aug 27 '18 at 12:29
  • @bgiuga if you look in the browser the values should be 0/1, due to the `endAll` callback set by `hide` and `show` – rioV8 Aug 27 '18 at 14:07