Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to remove data cleanly before updating d3.js chart?

I'm quite new to D3 and been working through trying to figure everything out. I'm trying to configure this example here to update with new data and transition appropriately.

Here is the code pen I have configured (click the submit to update) http://codepen.io/anon/pen/pbjLRW?editors=1010

From what I can gather, using some variation of .exit() is required for a clean data transition, but after reading some tutorials I'm still finding it difficult to know how it works. I have seen examples where simply removing the containers before calling the draw function works, but in my limited experience it can cause a flicker when changing data so I'm not sure if it's best practice?

Now, I'm not sure why the data is not updating correctly in my codepen, but my main concern is trying to get the transition right. Ideally I would like to know how I could just move the needle when changing data, so It would go from 90 > 40 for example, instead of 90 > 0 > 40.

However, I will definitely settle for figuring out why it doesn't redraw itself in the same location once clicking submit in the linked codepen.

Here is my update function;

function updateGuage() {
  d3.selectAll("text").remove()
  d3.selectAll('.needle').remove()
  chart.remove()
  name = "qwerty";
  value = "25";
  drawGuage();
}

initial draw;

function drawGuage() {
  percToDeg = function(perc) {
    return perc * 360;
  };

  percToRad = function(perc) {
    return degToRad(percToDeg(perc));
  };

  degToRad = function(deg) {
    return deg * Math.PI / 180;
  };

  // Create SVG element
  svg = el.append('svg').attr('width', width + margin.left + margin.right).attr('height', height + margin.top + margin.bottom);

  // Add layer for the panel
  chart = svg.append('g').attr('transform', "translate(" + ((width + margin.left) / 2) + ", " + ((height + margin.top) / 2) + ")");

  chart.append('path').attr('class', "arc chart-first");
  chart.append('path').attr('class', "arc chart-second");
  chart.append('path').attr('class', "arc chart-third");

  arc3 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)
  arc2 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)
  arc1 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)

  repaintGauge = function() {
      perc = 0.5;
      var next_start = totalPercent;
      arcStartRad = percToRad(next_start);
      arcEndRad = arcStartRad + percToRad(perc / 3);
      next_start += perc / 3;

      arc1.startAngle(arcStartRad).endAngle(arcEndRad);

      arcStartRad = percToRad(next_start);
      arcEndRad = arcStartRad + percToRad(perc / 3);
      next_start += perc / 3;

      arc2.startAngle(arcStartRad + padRad).endAngle(arcEndRad);

      arcStartRad = percToRad(next_start);
      arcEndRad = arcStartRad + percToRad(perc / 3);

      arc3.startAngle(arcStartRad + padRad).endAngle(arcEndRad);

      chart.select(".chart-first").attr('d', arc1);
      chart.select(".chart-second").attr('d', arc2);
      chart.select(".chart-third").attr('d', arc3);

    }
    /////////

  var texts = svg.selectAll("text")
    .data(dataset)
    .enter();

  texts.append("text")
    .text(function() {
      return dataset[0].metric;
    })
    .attr('id', "Name")
    .attr('transform', "translate(" + ((width + margin.left) / 6) + ", " + ((height + margin.top) / 1.5) + ")")
    .attr("font-size", 25)
    .style("fill", "#000000");

  var trX = 180 - 210 * Math.cos(percToRad(percent / 2));
  var trY = 195 - 210 * Math.sin(percToRad(percent / 2));
  // (180, 195) are the coordinates of the center of the gauge.

  displayValue = function() {
    texts.append("text")
      .text(function() {
        return dataset[0].value;
      })
      .attr('id', "Value")
      .attr('transform', "translate(" + trX + ", " + trY + ")")
      .attr("font-size", 18)
      .style("fill", '#000000');
  }

  texts.append("text")
    .text(function() {
      return 0;
    })
    .attr('id', 'scale0')
    .attr('transform', "translate(" + ((width + margin.left) / 100) + ", " + ((height + margin.top) / 2) + ")")
    .attr("font-size", 15)
    .style("fill", "#000000");

  texts.append("text")
    .text(function() {
      return gaugeMaxValue / 2;
    })
    .attr('id', 'scale10')
    .attr('transform', "translate(" + ((width + margin.left) / 2.15) + ", " + ((height + margin.top) / 30) + ")")
    .attr("font-size", 15)
    .style("fill", "#000000");

  texts.append("text")
    .text(function() {
      return gaugeMaxValue;
    })
    .attr('id', 'scale20')
    .attr('transform', "translate(" + ((width + margin.left) / 1.03) + ", " + ((height + margin.top) / 2) + ")")
    .attr("font-size", 15)
    .style("fill", "#000000");

  var Needle = (function() {

    //Helper function that returns the `d` value for moving the needle
    var recalcPointerPos = function(perc) {
      var centerX, centerY, leftX, leftY, rightX, rightY, thetaRad, topX, topY;
      thetaRad = percToRad(perc / 2);
      centerX = 0;
      centerY = 0;
      topX = centerX - this.len * Math.cos(thetaRad);
      topY = centerY - this.len * Math.sin(thetaRad);
      leftX = centerX - this.radius * Math.cos(thetaRad - Math.PI / 2);
      leftY = centerY - this.radius * Math.sin(thetaRad - Math.PI / 2);
      rightX = centerX - this.radius * Math.cos(thetaRad + Math.PI / 2);
      rightY = centerY - this.radius * Math.sin(thetaRad + Math.PI / 2);

      return "M " + leftX + " " + leftY + " L " + topX + " " + topY + " L " + rightX + " " + rightY;

    };

    function Needle(el) {
      this.el = el;
      this.len = width / 2.5;
      this.radius = this.len / 8;
    }

    Needle.prototype.render = function() {
      this.el.append('circle').attr('class', 'needle-center').attr('cx', 0).attr('cy', 0).attr('r', this.radius);

      return this.el.append('path').attr('class', 'needle').attr('id', 'client-needle').attr('d', recalcPointerPos.call(this, 0));

    };

    Needle.prototype.moveTo = function(perc) {
      var self,
        oldValue = this.perc || 0;

      this.perc = perc;
      self = this;

      // Reset pointer position
      this.el.transition().delay(100).ease('quad').duration(200).select('.needle').tween('reset-progress', function() {
        return function(percentOfPercent) {
          var progress = (1 - percentOfPercent) * oldValue;

          repaintGauge(progress);
          return d3.select(this).attr('d', recalcPointerPos.call(self, progress));
        };
      });

      this.el.transition().delay(300).ease('bounce').duration(1500).select('.needle').tween('progress', function() {
        return function(percentOfPercent) {
          var progress = percentOfPercent * perc;

          repaintGauge(progress);
          return d3.select(this).attr('d', recalcPointerPos.call(self, progress));
        };
      });

    };

    return Needle;

  })();

  needle = new Needle(chart);
  needle.render();
  needle.moveTo(percent);

  setTimeout(displayValue, 1350);

}

Any help/advice is much appreciated,

Thanks

like image 781
alexc Avatar asked Jun 07 '16 19:06

alexc


2 Answers

What you wanna check out is How selections work written by Mike Bostock. After reading this article, everything around enter, update and exit selections will become clearer.

In a nutshell:

  1. You create a selection of elements with selectAll('li')
  2. You join the selection with a data array through calling data([...])
  3. Now D3 compares what's already in the DOM with the joined data. Each DOM element processed this way has a __data__ property that allows D3 to bind a data item to an element.
  4. After you've joined data, you receive the enter selection by calling enter(). This is every data element that has not yet been bound to the selected DOM elements. Typically you use the enter selection to create new elements, e.g. through append()
  5. By calling exit() you receive the exit selection. These are all already existing DOM elements which no longer have an associated data item after the join. Typically you use the exit selection to remove DOM elements with remove()
  6. The so called update selection is the one thing that's been returned after joining the selection with data(). You will want to store the update selection in a variable, so you have access to it even after calling enter() or exit().

Note the difference between d3v3 and d3v4:

In d3v3, when you've already added elements via the enter selection, the update selection includes those newly created DOM elements as well. It's crucial to know that the update selection changes after you've created new elements.

However, this is no longer true when using d3v4. The change log says

"In addition, selection.append no longer merges entering nodes into the update selection; use selection.merge to combine enter and update after a data join."

like image 69
rmoestl Avatar answered Nov 16 '22 15:11

rmoestl


It is important to know that there are three different operations you can perform after binding data. Handle additions, deletions and modify things that did not change (or have been added just before).

Here is an example creating and manipulating a simple list: http://jsbin.com/sekuhamico/edit?html,css,js,output

var update = () => {

  // bind data to list elements
  // think of listWithData as a virtual representation of 
  // the array of list items you will later see in the
  // DOM. d3.js does not handle the mapping from this 
  // virtual structure to the DOM for you. It is your task
  // to define what is to happen with elements that are
  // added, removed or updated.
  var listWithData = ul.selectAll('li').data(listItems);
  
  // handle additions
  // by calling enter() on our virtual list, you get the
  // subset of entries which need to be added to the DOM
  // as their are not yet present there.
  listWithData.enter().append('li').text(i => i.text).on('click', i => toggle(i));

  // handle removal
  // by calling exit() on our virtual list, you get the
  // subset of entries which need to be removed from the
  // DOM as they are not longer present in the virtual list.
  listWithData.exit().remove();

  // update existing
  // acting directly on the virtual list will update any
  // elements currently present in the DOM. If you would
  // execute this line before calling exit(), you would 
  // also manipulate those items to be removed. If you
  // would even call it before calling enter() you would
  // miss on updating the newly added element.
  listWithData.attr('class', i => i.active ? 'active' : '');
  
};

Be aware that in reality you probably need to add some sort of id to your items. To ensure the right items are removed and you do not get ordering issues.

Explanation

The update function knows nothing about what, or even if anything has changed. It does not know, nor care, if there are new data elements or if old ones have been removed. But both things could happen. Therefore we handle both cases by calling enter() and exit() respectively. The d3 functions enter() and exit() provides us with the subsets of list elements that should be added or removed. Finally we need to take care of changes in the existing data.

var listItems = [{ text: 1, active: false}, { text: 2, active: true}];

var ul = d3.select('#id').append('ul');


var update = () => {

    var listWithData = ul.selectAll('li').data(listItems);
  
    // add new 
    listWithData.enter().append('li').text(i => i.text).on('click', i => toggle(i));

    // remove old
    listWithData.exit().remove();

    // update existing
    listWithData.attr('class', i => i.active ? 'active' : '');
  
};

update();

$('#add').click(() => {
  listItems.push({
    text: listItems.length+1,
    active: false
  });
  update();
});

var toggle = (i) => {
  i.active = !i.active;
  update();
};
li.active {
  background-color:lightblue;
}

li {
  padding: 5px;
}
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
  <div id="id"></div>
  <button id="add">Add</button>
</body>
</html>
like image 40
pintxo Avatar answered Nov 16 '22 13:11

pintxo