0

Disclaimer up front: I mainly work in Python and I'm pretty sure my problem here has to do with my fundamental misunderstanding of the asynchronous nature of Javascript. If so, any explanation is much appreciated.

Either way, my specific problem is with using plotly and d3 to load data from a csv and then plot it. Here is a sample csv (in my code called "fake_data.csv")

x,y,z
0.0,0.0,5.4
0.0,2.1,4.1
3.2,1.5,3.2

I have an index.html like so:

<head>
    <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
</head>
<body>
    <div id="graph" style="width:100%;height:100%"></div>
    <script src="scatter.js"></script>        
</body>

Then I have my scatter.js script, which this calls. It begins with a simple helper function to unpack the csv:

function unpack(rows, key) {
    return rows.map(function(row) 
        { return row[key]; }); 
}

For the plotting function, if I just do this it works fine:

Plotly.d3.csv('fake_data.csv', function(err, rows){
    var x = unpack(rows , 'x');
    var y = unpack(rows , 'y');
    var z = unpack(rows , 'z');
    Plotly.plot('graph', [
      {
              type: 'scatter3d',
              mode: 'lines',
              x: x,
              y: y,
              z: z
    }], {
      height: 640
    });
});

But I need to be able to load several csv's in order to get all of the necessary information into the plot. So I try to wrap this in two functions instead and call them like this:

var x;
var y;
var z;
function getData(filename) {
    Plotly.d3.csv(filename, function(err, rows){
        x = unpack(rows , 'x');
        y = unpack(rows , 'y');
        z = unpack(rows , 'z'); 
    });
}

function makePlot() {
    console.log(x)
    Plotly.plot('graph', [
      {
              type: 'scatter3d',
              mode: 'markers',
              x: x,
              y: y,
              z: z
    }], {
      height: 640
    });
}

getData('fake_data.csv');
makePlot();

This renders nothing. The console.log(x) inside of makePlot() prints undefined.

However when I type makePlot() in the console (I'm using Chrome developer tools) after the page has loaded, it works! So what am I doing wrong? My guess is that makePlot() is being called "before" getData() so there is no value in x yet. But I can't get my head around the right way to tell Javascript to call one function and then call another function after it.

Thanks in advance for any advice. This feels like a very basic concept that I am just banging my head against the wall on.

seth127
  • 1,862
  • 2
  • 18
  • 37
  • 1
    `Plotly.d3.csv(filename, function(err, rows){` looks like there's an asynchronous callback there ... see how do I get a value from an asynchronous call – Jaromanda X Apr 03 '19 at 03:13
  • 1
    By using a promise. See this: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise – Ajay Apr 03 '19 at 03:14

2 Answers2

2

D3 uses old style callback method. So you can do that model, or wrap in Promise.

var x;
var y;
var z;

function getData(filename, resolve, reject) {
  if (!reject) reject = (err) => console.log(err);
  return new Promise((resolve, reject) => {
    Plotly.d3.csv(filename, function(err, rows) {
      x = unpack(rows, 'x');
      y = unpack(rows, 'y');
      z = unpack(rows, 'z');
      // if error
      reject(err);
      // resolve here
      resolve();
    });
  });
}

function makePlot() {
  console.log(x)
  Plotly.plot('graph', [{
    type: 'scatter3d',
    mode: 'markers',
    x: x,
    y: y,
    z: z
  }], {
    height: 640
  });
}

getData('fake_data.csv', makePlot);
Bibberty
  • 4,282
  • 1
  • 6
  • 22
  • These are all very helpful. I guess I should've been more clear that my real issue is getting data from _more than one_ csv, and passing that data to `makePlot()`. For instance, imagine that the `z` column is in a different file. (In reality, it's info for rendering colors, etc. but the principle is the same.) Thank you! – seth127 Apr 03 '19 at 04:00
1

Just to complement answer from Bibberty: if you know exactly which csv files to download for executing makePlot(), you can simply use Promise.all to make sure all files are presented before ploting.

var x;
var y;
var z;

function getData(filename, resolve, reject) {
  if (!reject) reject = (err) => console.log(err);
  return new Promise((resolve, reject) => {
    Plotly.d3.csv(filename, function(err, rows) {
      x = unpack(rows, 'x');
      y = unpack(rows, 'y');
      z = unpack(rows, 'z');
      // if error
      if (err instanceof Error) {
        reject(err);
      } else {
        // resolve here
        resolve(true); // return true to check resolve status
      }
    });
  });
}

function makePlot() {
  console.log(x)
  Plotly.plot('graph', [{
    type: 'scatter3d',
    mode: 'markers',
    x: x,
    y: y,
    z: z
  }], {
    height: 640
  });
}

var promise1 = getData('fake_data.csv');
var promise2 = getData('fake_data2.csv');
var promise3 = getData('fake_data3.csv');

Promise.all([
  promise1,
  promise2,
  promise3
]).then(function(values) {
  // check if all promises are resolved
  if (values.every(function(value) { return value == true })) {
    makePlot();
  }
})
blaz
  • 3,415
  • 20
  • 30
  • This worked perfectly for what I wanted! Thank you very much. Also thank you @Bibberty for laying the groundwork. One minor issue: the `reject(err);` line always seems to trigger and it fails. But when I just comment that line out, everything works. Is that a typo or is there something I'm doing that's triggering that error? Thanks again. Great stuff. – seth127 Apr 04 '19 at 13:17
  • @seth127 Ah that part - you have to check if the error is valid. Since I don't know if plotly returns error object or not, you might need to modify the code in if condition. See my update – blaz Apr 04 '19 at 15:08
  • Ah, that makes sense. I felt like there must be some kind of if/else control flow in there, but I wasn’t sure if somehow that was inherently part of ‘reject()’. Thanks again. – seth127 Apr 04 '19 at 16:23