2
\$\begingroup\$

In my code below I have restructured the following csv file from something like this:

date,type
2017-01,E
2017-01,E
2017-01,E
2017-01,U

To this:

{date: "2017-01", E: 3, U: 1}

Using d3.nest() with a rollup method.

The only thing is that I'm mapping out the type values somewhat manually like this:

var lineData = nestData.map(function(d) {
  return {date: d.key, E: d.values[0].value, U: d.values[1].value};
}); 

always expecting the types to be in order. I do this by simply sorting the data by type and then re-sort by date again.

What I basically want to know is if there's a better way of achieving this or if there's a step or two that I can omit from the code making it more efficient.

Here's all the code:

var csvData = 
`date,type
2017-01,E
2017-01,E
2017-01,E
2017-01,U
2017-01,U
2017-02,E
2017-02,E
2017-02,U
2017-02,U
2017-02,U
2017-03,U
2017-03,U
2017-03,E
2017-03,E
2017-03,E
2017-03,E`;

var durations = 0;

var parseTime = d3.timeParse("%Y-%m");

var margin = {top: 25, right: 25, bottom: 25, left: 25},
    width = 420 - margin.left - margin.right,
    height = 185 - margin.top - margin.bottom;

var g = d3.select("#chart").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 + ")");

var x = d3.scaleTime().range([0, width]),
    y = d3.scaleLinear().range([height, 0]),
    z = d3.scaleOrdinal(d3.schemeCategory10);

var xAxis = d3.axisBottom(x)
    .tickFormat(d3.timeFormat("%B"))
    .ticks(3);

var yAxis = d3.axisLeft(y).ticks(4);

var line = d3.line()
    .curve(d3.curveCardinal)
    .x(function(d) { return x(d.date); })
    .y(function(d) { return y(d.lines); });

g.append("g")
    .attr("class", "axis axis--x")
    .attr("transform", "translate(0," + height + ")");

g.append("g")
    .attr("class", "axis axis--y");

var data = d3.csvParse(csvData, function(d) {
  d.date = d.date;
  d.type = d.type
  return d;
}).sort((a, b) => a.type - b.type)

  var nestData = d3.nest()
    .key(d => d.date)
    .key(d => d.type)
    .rollup(leaves => leaves.length)
    .entries(data);

  console.log("Nested: ", nestData)
  
  var lineData = nestData.map(function(d) {
    return {date: d.key, E: d.values[0].value, U: d.values[1].value};
  });

  lineData.forEach(function(d) {
    d.date = parseTime(d.date);
  })

  lineData.sort((a, b) => a.date - b.date)

  console.log("Mapped: ", lineData)

  parsed(lineData);

  function parsed(dataTest) {

    var keys = ["E","U"]

    var newData = keys.map(function(id) {
      return {
        id: id,
        values: dataTest.map(function(d) {
          return {date: d.date, lines: d[id]};
        })
      };
    });

    console.log("Mapped w/key: ", newData)

    z.domain(newData.map(function(c) { return c.id; }));

    var max = d3.max(dataTest, d => d3.max([d.E, d.U]));

    y.domain([0, max]).nice();

    x.domain(d3.extent(dataTest, d => d.date));

    g.selectAll(".axis.axis--y").transition()
      .duration(durations)
      .call(yAxis);

    g.selectAll(".axis.axis--x").transition()
        .duration(durations)
        .call(xAxis);

    var lineValues = g.selectAll(".lineValues")
      .data(newData);

    lineValues = lineValues
      .enter()
    .append("path")
      .attr("class", "line lineValues")
      .style("stroke", function(d) { return z(d.id); })
      .merge(lineValues);
      
    lineValues.transition()
      .duration(durations)
      .attr("d", function(d) { return line(d.values) })

  }
.line {
  fill: none;
  stroke: steelblue;
  stroke-width: 1.5px;
}
<html>
<head>
<meta charset ="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>

<style type="text/css">
  
.line {
  fill: none;
  stroke: steelblue;
  stroke-width: 1.5px;
}

</style>
</head>

<body>

<div id="chart"></div>

<script>
</script>

</body>
</html> 

\$\endgroup\$
1
\$\begingroup\$

First of all: drop the row function, which is the anonymous function as the second argument of csvParse...

var data = d3.csvParse(csvData, function(d) {
    d.date = d.date;
    d.type = d.type
    return d;
})

... because you are not changing anything!

So, it can be just:

var data = d3.csvParse(csvData);

Back to your question:

Indeed, you don't need to reference the array by the indices, which forces you to sort the data array and then to sort the resulting array... that's not only confusing and error-prone, but also very computer-intensive for the browser: sorting big arrays of objects can take quite some time. It's a good idea avoiding sorting, specially because, as we'll see, you don't need it.

There is a way to omit that step, making it automatic. Better than that, you can make the whole thing automatic!

In my proposed solution, you don't even need to set the keys, as you do right now...

var keys = ["E","U"];

You can easily get all the unique type values:

var keys = [...new Set(data.map(function(d) {
    return d.type
}))];

And then, creating your lineData:

var lineData = nestData.map(function(d) {
    var obj = {};
    obj.date = d.key;
    keys.forEach(function(e) {
        obj[e] = d.values.find(function(f) {
            return f.key === e
        }).value
    });
    return obj;
});

The above code will work for any number of different types you have, and since you already have the dates in the chronological order, you don't need any sort.

This, in short, is what the function does:

  1. For each map interaction we create a new object:

    var obj = {};
    
  2. We set the key/value pair for the dates, since there is always a date:

    obj.date = d.key;
    
  3. Then, for each value in the keys array, we find the respective value in the values array:

    keys.forEach(function(e) {
        obj[e] = d.values.find(function(f) {
            return f.key === e
        }).value
    });
    
  4. Finally, we return the object:

    return obj;
    

Here is a demo, using only the relevant parts in your code:

var csvData = `date,type
2017-01,E
2017-01,E
2017-01,E
2017-01,U
2017-01,U
2017-02,E
2017-02,E
2017-02,U
2017-02,U
2017-02,U
2017-03,U
2017-03,U
2017-03,E
2017-03,E
2017-03,E
2017-03,E`;

var parseTime = d3.timeParse("%Y-%m");

var data = d3.csvParse(csvData);

var keys = [...new Set(data.map(function(d) {
  return d.type
}))];

var nestData = d3.nest()
  .key(d => d.date)
  .key(d => d.type)
  .rollup(leaves => leaves.length)
  .entries(data);

var lineData = nestData.map(function(d) {
  var obj = {};
  obj.date = d.key;
  keys.forEach(function(e) {
    obj[e] = d.values.find(function(f) {
      return f.key === e
    }).value
  });
  return obj;
});

lineData.forEach(function(d) {
  d.date = parseTime(d.date);
})

console.log("Nested: ", nestData)

console.log("Mapped: ", lineData)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>

Now, using this code, you can automatically create the adequate lineData array regardless the number and the order of the type properties in your data array.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.