Skip to content Skip to sidebar Skip to footer

D3: Adding And Removing Force.nodes Based On Slider Values

I had some difficulty a while back adding and removing nodes based on user input which was solved by updating the entire set of objects pushed into force.nodes() each time the user

Solution 1:

There are a few problems with your current code. The fundamental problem is that you're giving data to force.nodes(), which means that the two data structures are actually the same. That is, when you're removing elements from force.nodes(), you're modifying the underlying data as well. Hence you can't add them back -- they're gone.

This is easily fixed by passing a copy of data to force.nodes():

var force = d3.layout.force()
    .nodes(JSON.parse(JSON.stringify(data)))

Then you're removing the wrong nodes from force.nodes() -- the index you're using is for data, not force.nodes(). You can compute the index of the data element in force.nodes() and use it like this:

data.forEach(function(d, i) {
        var idx = -1;
        force.nodes().forEach(function(node, j) {
            if(node.mIndex == d.mIndex) {
                idx = j;
            }
        });
        //if data point in range (between extent 0 and 1)if (d.date >= brush.extent()[0] && d.date <= brush.extent()[1]) {
            if (idx == -1) {
                force.nodes().push(d)
            }
        }
        elseif(idx > -1) {
            force.nodes().splice(idx, 1)
        }

Finally, you need to call force.start() at the end of brushed for the changes to become visible after the layout has settled down.

Complete example here.

Solution 2:

Building on the answer from the mighty @Lars Kotthoff, who fixed your technical problem, I will focus on the architecture. Here is an architecture that is simpler and more in keeping with d3 idiom.

The main principles are:

  1. Use Array.prototype.filter() to manage the data
  2. Use standard, data driven patterns. Drive the visualisation by managing the data and then use the standard, general update pattern to drive the changes into the vis.
  3. Seperate data events and animation events. Respond to the data changes only when need... do it in the brushed event, not on every tick. The brushed event is a data event and the tick routine is an animation event. It's not optimal to be managing data on animation events on the off chance that it is required.
  4. Make the entry and exit of the nodes a bit smoother.

The benefit of point 1 is that the filtered array is actually an array of reference to the original data elements, so when the extra state is added on the copied array, it is actually added on the original data array. Thus, the previous state is available when it is filtered back in, hence the smooth exit and entry behaviour. Meanwhile, no elements are deleted in the original data when the brush filters down: only the references to them are deleted in the cloned array. I have to admit I had not expected that but it is a nice discovery, even if by accident! Of course this only works because the array elements are objects.

working example here...

var width = 700,
    height = 600,
    padding = 20;

    var start = newDate(2013, 0, 1),
      end = newDate(2013, 11, 31)

    var data = []

    for (i = 0; i < 100; i++) {
      var point = {}

      var year = 2013;
      var month = Math.floor(Math.random() * 12)
      var day = Math.floor(Math.random() * 28)

      point.date = newDate(year, month, day)
      point.mIndex = i
      data.push(point)
    }

    var force = d3.layout.force()
          .size([width - padding, height - 100])
          .on("tick", tick)
          .start()

    var svg = d3.select("body").append("svg")
      .attr({
        "width": width,
        "height": height
      })

    //build stuffvar x = d3.time.scale()
      .domain([start, end])
      .range([padding, width - 6 * padding])
      .clamp(true)

    var xAxis = d3.svg.axis()
      .scale(x)
      .tickSize(0)
      .tickPadding(20)
    //.tickFormat(d3.time.format("%x"))var brush = d3.svg.brush()
      .x(x)
      .extent([start, end])
      .on("brush", brushed1)

    //append stuffvar slidercontainer = svg.append("g")
      .attr("transform", "translate(100, 500)")

    var axis = slidercontainer.append("g")
      .call(xAxis)

    var slider = slidercontainer.append("g")
      .call(brush)
      .classed("slider", true)

    //manipulate stuff
    d3.selectAll(".resize").append("circle")
      .attr("cx", 0)
      .attr("cy", 0)
      .attr("r", 10)
      .attr("fill", "Red")
      .classed("handle", true)

    d3.select(".domain")
      .select(function () { returnthis.parentNode.appendChild(this.cloneNode(true)) })
      .classed("halo", true)

    functionbrushed1(e) {

      var nodes = includedNodes(data, brush);

        nodes.enter().append("circle")
            .attr("r", 10)
            .attr("fill", "red")
            .call(force.drag)
            .attr("class", "node")
            .attr("cx", function (d) { return d.x })
            .attr("cy", function (d) { return d.y })

      nodes
        .exit()
        .remove()

      force
        .nodes(includedData(data, brush))
        .start()

    }

    functionincludedData(data, brush) {

      return data.filter(function (d, i, a) {
        return d.date >= brush.extent()[0] && d.date <= brush.extent()[1]
      })

    }
    functionincludedNodes(data, brush) {
      return svg.selectAll(".node")
              .data(includedData(data, brush), function (d, i) {
                return d.mIndex
              })
    }

    functiontick() {

      includedNodes(data, brush)
        .attr("cx", function (d) { return d.x })
        .attr("cy", function (d) { return d.y })

    }
    brushed1()
.domain {
      fill: none;
      stroke: #000;
      stroke-opacity: .3;
      stroke-width: 10px;
      stroke-linecap: round;
    }

    .halo {
      fill: none;
      stroke: #ddd;
      stroke-width: 8px;
      stroke-linecap: round;
    }

    .tick {
      font-size: 10px;
    }

    .selecting circle {
      fill-opacity: .2;
    }

      .selecting circle.selected {
        stroke: #f00;
      }

    .handle {
      fill: #fff;
      stroke: #000;
      stroke-opacity: .5;
      stroke-width: 1.25px;
      cursor: crosshair;
    }
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script><pid="nodeCount"></p>

Post a Comment for "D3: Adding And Removing Force.nodes Based On Slider Values"