1

I have finally decided to saddle up and adopt d3 v5 syntax after years of using v3. After looking at some tutorials and examples, v5 syntax really struck me as sublime. The readability is far improved and it seems easier to integrate multiple data sources.

To my dismay, and despite my reverence of it, I couldn't quite build a visual from scratch with the new Promise syntax. Here is my simple graph: (note I'm using hard coded data for the sake of this post, and I have commented out the .csv() call that I'd actually use. It should still be functionally the same)

var margins = {top:50, right:50, bottom:50, left:50};

var width = window.innerWidth - margins.left - margins.right;
var height = window.innerHeight - margins.top - margins.bottom;

var sampleData = [
  {'y':32, 'x':1},
  {'y':20, 'x':2},
  {'y':19, 'x':3},
  {'y':12, 'x':4},
  {'y':15, 'x':5},
  {'y':19, 'x':6},
  {'y':22, 'x':7},
  {'y':26, 'x':8},
  {'y':31, 'x':9},
  {'y':36, 'x':10}
];


//var dataset = d3.csv("my-data.csv").then(function(data)
//  {return data;
//  });

var dataset = sampleData.then(function(data)
  {return data;
  });

var svg = d3.select('body').append('svg').attr('id','svg').attr('height','100%').attr('width','100%');

var myLine = dataset.then(function(data) {
  Promise.all(data.map(function(d) {return {X:+d.x, Y:+d.y}}))//ensure numeric parsing

  var xScale = d3.scaleLinear()
      .domain(d3.extent(data, function(d) { return d.X; }))
      .range([0,width]);

  var yScale = d3.scaleLinear()
      .domain(d3.extent(data, function(d) {return d.Y; }))
      .range([height,0]);

  var xAxis = d3.axisBottom(xScale);

  var yAxis = d3.axisLeft(yScale);

  var line = d3.line()
      .x(function(d) {return xScale(d.x); })
      .y(function(d) {return yScale(d.y); });

  var svg = d3.select('body').append('svg').attr('id','svg').attr('height','100%').attr('width','100%');

  var graphGroup = svg.append('g')
      .attr('transform',"translate("+margins.left+","+margins.top+")");

  graphGroup.append('path')
      .attr('d', function(d) {return line(data); });

  graphGroup.append('g')
      .attr('class', 'axis x')
      .attr('transform', "translate(0,"+height+")")
      .call(xAxis);

  graphgroup.append('g')
      .attr('class', 'axis y')
      .call(yAxis);


    });

I get this error in the console:

Uncaught TypeError: sampleData.then is not a function

Question

I take the point that Promise.all() and .then() are not always favorable for really simple data visuals, but I'd still like to know why I can't make the above script output a minimal line graph. From then, hopefully, I can slowly take the training wheels off and find my stride with v5.

I'm particularly confused with how to cast to numbers using the unary + with Promise.

Arash Howaida
  • 2,575
  • 2
  • 19
  • 50

1 Answers1

1

Although there are many twists and turns when it comes to using Promises, it turns out that the actual changes required to port code to make use of the d3-fetch module in favor of the deprecated d3-request module are strikingly minimal. Loosely speaking, to adapt your—or any pre-v5—code to use the new d3-fetch module you just move the callback from one method to another. Thus, the former

d3.dsv(url, callback);

now becomes

d3.dsv(url).then(callback);

The only thing to be aware of is to check if the callback's signature matches the one expected for .then. This only becomes relevant, though, if your callback used two parameters to handle errors:

function callback(error, data) {
  // Handle error
  if (error) throw error;

  // Manipulate data
}

With Promises this is split into two separated methods:

function onFullfilled(data) {
  // Manipulate data
}

function onRejected(error) {
  // Handle error
}

These callback can be used in two ways:

// 1.
d3.dsv(url).then(onFullfilled, onRejected);

// 2.
d3.dsv(url).then(onFullfilled).catch(onRejected);

Another important point is that you cannot return data from your callback (beware of the infamous "How do I return the response from an asynchronous call?"!). d3.dsv now returns a Promise, not your data; you have to handle the data inside your callback. If you become more skilled using Promises you might have a look into the await operator, though, which allows you to wait for a Promise and its fulfilled value. Although this is ECMAScript 2017 (ES8) syntax it has already seen wide-spread browser support.


That being the general case, now for your code: sampleData is an Array object which, of course, does not have a .then() method and, hence, the error. To make the code work there is not much to do apart from uncommenting the lines featuring d3.dsv and putting the relevant code handling data inside the callback.

If you really want to do an offline simulation with hardcoded data you can use Promise.resolve() which will return a Promise already resolved with the given value. In your case instead of

d3.csv("my-data.csv")
  .then(function(data) { });

you can use

Promise.resolve(sampleDate)
  .then(function(data) { });   // Same handler as above
altocumulus
  • 21,179
  • 13
  • 61
  • 84
  • With your help I got it working! The graph displays nicely, I'm very pumped! I updated with my functional code so you can check to make sure I have the implementation you had in mind. The only thing I didn't get to was the error handling. So for that I just make another function that is totally outside the scope of my `Promise.all()`, right? – Arash Howaida Dec 10 '18 at 15:35
  • @ArashHowaida Please rollback your edit as it renders this answer at least partially obsolete. Keep it to one question and post follow-up issues as new questions. – altocumulus Dec 10 '18 at 15:45
  • @ArashHowaida Just post the new question and put a link in this and the new post connecting both of them. Since I closely follow the d3 tag it is very likely I will have a look into that; or some other smart person will ;-) – altocumulus Dec 10 '18 at 15:50