Home > OS >  D3 Line Chart: Extend existing path with new data
D3 Line Chart: Extend existing path with new data

Time:09-11

I am creating a line chart with D3.js. The line should appear over time while new data points are calculated 'on the fly' (i.e. the data array constantly grows). I have the function that does the data calculation, as well as the updateLine() function you see below, in a setInterval(). My problem is, that this function creates a new svg path for every newly added data point, resulting in a huge number of <path> elements.

function updateLine() {
  canvas
    .append('path')
      .datum(data)
      .attr('fill', 'none')
      .attr('stroke', 'steelblue')
      .attr('stroke-width', 1.5)
      .attr('d', d3.line()
        .x(function(d, i) {return xSc(d.x)})
        .y(function(d, i) {return ySc(d.z)})
      )
}

How can I 'extend' the existing path with the new data points?

CodePudding user response:

I found an answer:

Clearly, the code above appends a new <path> every time the function is called. To avoid a <path> 'flood' in the DOM, I found two options:

Option 1

Before appending a new path, select the old path, that lacked the new data points, and remove it by calling remove(). In order to avoid selecting the wrong path, I used an ID selector.

function updateLine() {
  canvas
    .selectAll('#myPath').remove();
  canvas
    .append('path')
      .datum(data)
      .attr('id', '#myPath')
      .attr('fill', 'none')
      .attr('stroke', 'steelblue')
      .attr('stroke-width', 1.5)
      .attr('d', d3.line()
        .x(function(d, i) {return xSc(d.x)})
        .y(function(d, i) {return ySc(d.z)})
      )
}

Option 2 (more elegant, I think)

Use a selectAll() to select the path, bind the data to the selection and call a join() on the selection. In the first call of updateLine(), the path is created (no SVG path exists and we have a nonempty enter selection to which a SVG path is appended and all attributes are set). In the following calls, the path exists in the DOM and the newly updated data array is bound to it. Thus the enter selection is empty and the update selection gets relevant, where we update the path with the new data.

function updateLine() {
  canvas.selectAll('#myPath')
    .data([data])
    .join(
      function(enter) {
        console.log('Enter selection:');
        console.log(enter);
        return enter
          .append('path')
            .attr('id', 'myPath')
            .attr('fill', 'none')
            .attr('stroke', 'steelblue')
            .attr('stroke-width', 1.5)
            .attr('d', d3.line()
              .x(function(d, i) {return xSc(d.x)})
              .y(function(d, i) {return ySc(d.z)})
            );
      },
      function(update) {
        console.log('Update selection:');
        console.log(update);
        return update
          .attr('d', d3.line()
            .x(function(d, i) {return xSc(d.x)})
            .y(function(d, i) {return ySc(d.z)})
          );
      }
    );
}

A couple notes regarding the code of option 2:

  • It is important to use a selectAll() and not just a select() here, since in the first call of the function, no <path> exists. select() would select the first match which remains empty in this case.
  • I call data([data]) and thus perform a join of data points in the data array with SVG elements. datum() would, to my understanding, not perform a join, however, this is important here, as we rely on the update selection.
  • Passing the data array as an array again to data([data]) causes a data bind of all data points to the one path element, which is exactly what we want here.
  • Related