Search code examples
javascriptsvgd3.jsplot

How to fix the 'Expected number' error in parallel plot's path attribute in D3.js with CSV input?


[d3.js parallel plot] Error: attribute d: Expected number, "M0,NaNL122.5,NaNL24…".

I tried to visualize a parallel plot with d3.js, exploiting this example with the iris dataset:

https://d3-graph-gallery.com/graph/parallel_custom.html

My csv file is structured as follows:

City,GreenAreaDensity,LowEmission,AutobusStopDensity,CirculatingVehicles,ExposedNoisePollution
Torino,7.46,17.3,24.4,277.0,14.9
Vercelli,1.28,11.2,2.1,77.0,8.8
Novara,1.22,11.9,3.8,246.0,14.7
Biella,33.56,10.1,4.1,189.0,9.3
Cuneo,6.82,10.4,6.3,85.0,3.6
Asti,17.14,11.0,1.9,139.0,36.5
Aosta,0.42,8.3,19.0,42.0,6.0
Imperia,1.11,5.4,8.2,191.0,16.7
Savona,7.82,7.7,6.1,183.0,5.1
[...]

I get this error in the console for every row in the dataset:

Error: <path> attribute d: Expected number, "M0,NaNL122.5,NaNL24…".

Which made me think there is a problem in the y coordinates of the path function, so I added various print function to see why there is NaN as the second coordinate.

here the parallelplot.js file:

// set the dimensions and margins of the graph
var margin = {top: 30, right: 30, bottom: 10, left: 40},
  width = 560 - margin.left - margin.right,
  height = 400 - margin.top - margin.bottom;

// append the svg object to the body of the page
var svgParallel = d3.select("#parallelPlot")
.append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
.append("g")
  .attr("transform",
        "translate(" + margin.left + "," + margin.top + ")");

// Parse the Data
d3.csv("../../data/processed/ParallelPlotData.csv", function(data) {

  // Here I set the list of dimension manually to control the order of axis:
  dimensions = ["GreenAreaDensity","LowEmission","AutobusStopDensity","CirculatingVehicles","ExposedNoisePollution"]

  // For each dimension, I build a linear scale. I store all in a y object
  var y = {}
  var name;
  for (i in dimensions) {
    name = dimensions[i]
    //console.log(name);
    y[name] = d3.scaleLinear()
    // .domain( [0,150] ) // --> Same axis range for each group
     .domain( [d3.extent(data, function(d) { 
        //console.log(+d[name] + " is of type: " + typeof(+d[name])) // print the value and the type
           return +d[name];   // --> Different axis range for each group
      })] )
      .range([height, 0])
  }

  // Build the X scale -> it find the best position for each Y axis
  x = d3.scalePoint()
    .range([0, width])
    .domain(dimensions);

  // The path function take a row of the csv as input, and return x and y coordinates of the line to draw for this raw.
  function path(d) {
    return d3.line()(dimensions.map(function(column) {
      console.log(y[column](d[column]));
       return [x(column), y[column](d[column])]; 
      })); 
  }

  // Draw the lines
  svgParallel
    .selectAll("myPath")
    .data(data)
    .enter()
    .append("path")
      .attr("d",  path)
      .style("fill", "none" )
      .style("stroke", function(){ return("#AAA")} )
      .style("opacity", 0.5)
 
  // Draw the axis:
  svgParallel.selectAll("myAxis")
    // For each dimension of the dataset I add a 'g' element:
    .data(dimensions).enter()
    .append("g")
    .attr("class", "axis")
    // I translate this element to its right position on the x axis
    .attr("transform", function(d) { return "translate(" + x(d) + ")"; })
    // And I build the axis with the call function
    .each(function(d) { d3.select(this).call(d3.axisLeft().ticks(5).scale(y[d])); })
    // Add axis title
    .append("text")
      .style("text-anchor", "middle")
      .attr("y", -9)
      .text(function(d) { return d; })
      .style("fill", "black")

})

In fact the value ycolumn returns NaN, but here if I print d[column] returns the exact value I want on that axis.

This problem is irrelevant if I choose fixed axis for every parameter, but I need different range for the axis:

// For each dimension, I build a linear scale. I store all in a y object
  var y = {}
  var name;
  for (i in dimensions) {
    name = dimensions[i]
    //console.log(name);
    y[name] = d3.scaleLinear()
    // .domain( [0,150] ) // --> Same axis range for each group
      .domain( [d3.extent(data, function(d) { 
        //console.log(+d[name] + " is of type: " + typeof(+d[name])) // print the value and the type
           return +d[name];   // --> Different axis range for each group
      })] )
      .range([height, 0])
  }

The problem may also lay here since apparently it doesn't read the correct coordinate value so there is no path in visualization.

The parallel plot is like this: enter image description here

It is possible to see that the range value for each feature must be different since there are various metrics.

Someone can help me figure out how to solve this?


Solution

  • Your bug lies on the d3.extent() call. This function itself returns an array, so you should pass that directly to d3.scaleLinear().domain(...). Here is the fixed code:

      var y = {}
      var name;
      for (i in dimensions) {
        name = dimensions[i]
        y[name] = d3.scaleLinear()
          .domain(
            d3.extent(data, function (d) {  // Note d3.extent result is used as is
              return +d[name];
            })
          )
          .range([height, 0])
      }
    

    Working StackBlitz