Reputation: 499
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 selected which they wanted to view from a list of checkboxes.
Updating from a slider however I think requires a more finesse touch - I don't want to update the entire set every time the slider is moved. I want to push and pop nodes in and out of force.nodes().
With my current code the nodes are coming out just fine - they're just not going back in. jsfiddle here - https://jsfiddle.net/hiwilson1/ancmtxux/3/
This is the part causing problems;
function brushed() {
var exists;
//var newd = new Date(2013, 05, 01)
data.forEach(function(d, i) {
//if data point in range (between extent 0 and 1)
if (d.date >= brush.extent()[0] && d.date <= brush.extent()[1]) {
exists = force.nodes().some(function(node, i) {
//check if data point already exists in force.nodes()
return (node.mIndex == d.mIndex)
})
console.log(exists)
if (!exists) {
force.nodes().push(d)
}
}
else {
force.nodes().splice(i, 1)
}
})
d3.select("#nodeCount").text(force.nodes().length)
}
For each data point, I'm checking whether or not the point lies between extent()[0] and [1]. If it does, then check force.nodes() to see if it's currently a member there. If it isn't, then push it into force.nodes().
If the data point doesn't lie between the extents then splice it from force.nodes(). This last bit works just fine.
UPDATE: Fixed. I actually also worked out how to filter links attached to nodes as well. jsfiddle here - https://jsfiddle.net/hiwilson1/7oumeat5/2/. Never try and do this with indexes/hard coded indexes, compare the nodes/links as objects.
Incidentally I'm seeing links over the top of nodes. If there's some way to fix this I'd be happy to hear it.
FURTHER UPDATE: To ensure links behind nodes use .insert("line", ":first-child") in place of .append("line")
Upvotes: 1
Views: 1334
Reputation: 6476
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:
Array.prototype.filter()
to manage the databrushed
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.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 = new Date(2013, 0, 1),
end = new Date(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 = new Date(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 stuff
var 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 stuff
var 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 () { return this.parentNode.appendChild(this.cloneNode(true)) })
.classed("halo", true)
function brushed1(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()
}
function includedData(data, brush) {
return data.filter(function (d, i, a) {
return d.date >= brush.extent()[0] && d.date <= brush.extent()[1]
})
}
function includedNodes(data, brush) {
return svg.selectAll(".node")
.data(includedData(data, brush), function (d, i) {
return d.mIndex
})
}
function tick() {
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;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<p id="nodeCount"></p>
Upvotes: 1
Reputation: 109232
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)
}
}
else if(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.
Upvotes: 2