Trigger d3 update when node data changes - javascript

I have data (can be several thousand nodes) which looks something like this which is bound in d3:
{
"key1": 1,
"key2": 2
...
}
There are times where key1 may be modified while others where key2 changes. How can I tell d3 to redraw the modified elements when I call data with this new content?
In the following example, if one of the key fields change in the inputData then I would like to call this code in order to trigger redraw of the affected items:
svg.selectAll('.node')
.data(inputData, function (d) { return d; });
.append('g')
.attr('class', function(d) {
if(d.key1 === 1) return 'node-class-A'
else if(d.key2 === 2) return 'node-class-B'
// etc
});

See https://bl.ocks.org/mbostock/1095795 for the General Update pattern for D3 implemented for a Force-layout. As long as you are drawing your graph in this way, you can just call the update function to redraw the graph with the updated data.
Just call update in some sort of listener off of whatever is changing the data, be it a text input, button, select box, etc...

Related

D3 exit selecting wrong data

I'm trying to build kind of real time graph using D3.js. Code is available at https://plnkr.co/edit/hrawv8CTBIsJf2QWTBMb?p=preview.
The source data represent user authentication results from different organizations. For each organization there is a name, ok count and fail count. The graph should be dynamically (getting the data in loop) updated based on data.
The code is based on https://bl.ocks.org/mbostock/3808234.
There are few problems and few things i'm not sure about.
exit function only selects the red bars based on data update:
// JOIN new data with old elements
// specify function for data matching - correct?
var boxes = svg.selectAll(".box").data(data, function(d) {
return d.inst_name;
});
// EXIT old elements not present in new data
// this works somehow strange
// it does select all red boxes
boxes.exit().transition(t).remove();
Why does exit() select only red bars and not all? From my point of understanding the d3 documentation exit() should only select such elements that do not have any new data. Shouldn't that be all bars in case of infinite loop and constant data file?
This obviously breaks the graph quite a lot (see plunker). I need the exit to select only bars, which are not available in data file anymore. See example below.
initial state of data file:
inst_name,ok,fail
inst1,24,-1
inst2,23,-3
...
updated state of data file:
inst_name,ok,fail
inst1,26,-1
inst14,22,-4
...
The bars (both blue and red) for inst2 from intial state should be removed (and replaced by data of inst14) when the data is updated. Why is this not working?
I've read, that new data are matched against older using index. I've specified that inst_name should be used:
var boxes = svg.selectAll(".box").data(data, function(d) {
return d.inst_name;
});
Is this necessary (I've used it everywhere when inserting data)?
Also the transition for removing the elements does not work. What is the problem?
I'm also not sure if specifying data is necessary when adding new bars:
var boxes = svg.selectAll(".box").data(data, function(d) {
return d.inst_name;
});
.....
// add new element in new data
svg.selectAll(".blue")
.data(data, function(d) { // is this necessary ?
return d.inst_name;
}) // use function for new data matching against inst_name, necessary?
.enter().append("rect")
.transition(t)
.attr("class", function(d) {
return "blue box "
})
.attr("x", function(d) {
return x(d.inst_name);
})
.attr("width", x.bandwidth())
.attr("y", function(d) {
return y(d.ok);
})
.attr("height", function(d) {
return height - y(d.ok + min);
})
Thanks for help.
EDIT
The underlying data get changed by script (this was not written clearly in the original post), so it can change independently of the graph state. The data should be only growing.
You've asked a lot of questions.
Why does exit() select only red bars and not all? From my point of understanding the d3 documentation exit() should only select such elements that do not have any new data. Shouldn't that be all bars in case of infinite loop and constant data file?
First, you build two sets of bars (blue [ok] and red [fail]). When you data bind these you give them the same key function, which identifies them by inst_name. You then do your data update, which now selects all the bars at once with:
svg.selectAll(".box")
You again data-bind with the same key function. Your data has 10 values in the array but you just selected 20 bars. The second 10 bars exit (the red ones) because to d3 they are not in your 10 data-points
The bars (both blue and red) for inst2 from intial state should be removed (and replaced by data of inst14) when the data is updated. Why is this not working?
I don't see that in your plunker, you are giving it the same data over and over.
Also the transition for removing the elements does not work. What is the problem?
You haven't given the transition anything to do. It'll run it, then at the end remove the rects. What you need is something for it to transition, like "height":
boxes.exit().transition(t).attr('height', 0).remove();
This will shrink them to 0 height.
So how do we clean up your code?
First, I would operate on g elements each one paired to an item in your data array. You then place both bars in the g that belong to that data point. Take a look here, I've started to clean-up your code (incomplete, though, hopefully it gets you going).

D3: Separating data exit/remove/merge from drawing of elements

I am drawing some complex interactive SVGs with D3 v4 and running into some problems. My goals are:
Each data element corresponds to a group with multiple SVG shape elements (e.g. <g><circle></circle><circle></circle></g>)
The multiple SVG shape elements have to be drawn in a certain order (because they overlap)
Certain shape elements are updated without data elements being added or removed (e.g. when clicking on a shape, change the shape color)
I am running into trouble because the .data() -> .exit().remove() -> .enter() -> .merge() process requires a specific order and that order conflicts with the necessary draw order as well as the ability to update styles on the fly. This is what I started with, which does not work because of draw order:
function updateGraph() {
let eachNodeG = allNodesG
.selectAll('.eachNodeG')
.data(graphData._nodes, function (d) {
return d._id;
})
eachNodeG.exit().remove();
let eachNodeGEnter = eachNodeG.enter()
.append('g')
.attr("class", "eachNodeG")
eachNodeGEnter
.append('circle')
.classed('interactivecircle', true)
.on('click', function (d) {...})
let eachNodeG = eachNodeGEnter
.merge(eachNodeG)
.style('fill', function (d) {...}) //this is here b/c it needs to change
// when data change (without them being added/removed)
// this must be separate because the background circle needs to change even
// when nodes are not added and removed; but this doesn't work here because
// the circle needs to be in the background
eachNodeG
.append('circle')
.classed('bgcircle', true)
}
I thought maybe I could separate the data update process from the data drawing process entirely, by doing enter() exit() merge() just on the groups containing the data and then drawing everything afterward. But here I run into a different problem: either I remove and re-add all of the shapes on every update (which makes double-clicking difficult and seems like a waste of processing power), or I have to figure out some way to update only the shapes that have changed. Does it using the remove and re-add method looks like this:
// add/remove individual groups based on updated data
let eachNodeG = allNodesG
.selectAll('.eachNodeG')
.data(graphData._nodes)
eachNodeG.exit().remove();
let eachNodeGEnter = eachNodeG.enter()
.append('g')
.attr("class", "eachNodeG")
eachNodeG = eachNodeGEnter
.merge(eachNodeG)
// draw (or remove and re-draw) elements within individual groups
d3.selectAll('.bgcircle').remove()
eachNodeG.append('circle')
.classed('bgcircle', true)
d3.selectAll('.interactivecircle').remove()
eachNodeG.append('circle')
.classed('interactivecircle', true)
.style('fill', function (d) {...})
.on('click',function(d){...})
})
Is there a better way to draw the shapes in order while keeping them updateable?
You could use selection.raise or selection.lower to move circles after they have been created.

D3: change data based on svg element manipulation

I'm new to [d3.js][1], so my question may be stupid.
I divided my svg into regions, and I appended circles for the user to drag around. Every region has an id, and every circle has the id of the region in which it was created.
What I need is to update the DATUM linked to the circle in the drop event. The other way around is quite easy, since when you change the data, the update() event does all the work. But is there a way for the svg element to change the data?
EDIT:
Some of the code. I edited so it is cleaner and more direct. The circles call the drag object and everything works, but the TODO section needs to be... hum... done:
var drag;
function configDrag () {
drag = d3.behavior.drag();
drag.on("dragstart", function() {
d3.event.sourceEvent.stopPropagation();
})
.origin(function(d){
return d;
})
.on("dragstart", draggrab)
.on("drag", dragmove)
.on("dragend", dragdrop);
}
function dragdrop(d){
reg = regionOf(d.x, d.y); // null if in undefined region
if(reg){
d3.select(this)
.attr("cy", parseInt(reg.y1 + reg.y2) / 2);
var regionId = getRegionId(d.x, d.y);
// TODO update data.tsv with regionId
}
}
}
When you bind data to a selection using selection.data(values), values is always an array and normally an array of object elements. Those elements, or references to them, are bound to the DOM elements and can be retrieved using selection.datum(). They are also the d argument that is normally the first argument passed callbacks by the various d3 operator methods. Because they are objects, when you change the value of their members, you are changing the values of the members of the associated element, in the original values array that was bound.

How to bind d3 generated HTML elements to scope?

I am generating nodes in d3 in an angular directive, and I would like the classes of the nodes to be bound dynamically to an element in my scope. Below is a general outline of what I would like to do:
app.directive('myDirective',function(){
return {
restrict: 'EA',
link: function(scope,element,attrs){
var node = d3.selectAll('.node')
.data(nodes)
.enter().append('circle')
.classed('selected',function(d){return d.id=scope.selected.id})
}
}
})
This is pseudocode, but essentially d3 is generating these nodes/circles, and I want the class of these circles to depend on the value of an element within the scope. So, if at any point in time I modify the scope.selected.id, it should affect the node's class. Using the approach shown above, however, does not work. I have tried modifying the scope.selected.id, but the classes of the nodes are unaffected.
How can I dynamically bind a d3 generated element to scope? I don't want to redraw nodes whenever the scope is modified. I simply want their classes to be binded to the scope.
Add a watch around scope.selected in your link function then wrap your d3 drawing code in a function and call that method when the watch fires.
(BTW a fiddle is always easier to deal with when asking questions)
You might want to read up on the General update pattern for D3 too http://bl.ocks.org/mbostock/3808218
It will both make life easier and your updates faster if you get to grips with the various stages a D3 documents lifecycle has.
function update(data) {
// DATA JOIN
// Join new data with old elements, if any.
var text = svg.selectAll("text")
.data(data);
// UPDATE
// Update old elements as needed.
text.attr("class", "update");
// ENTER
// Create new elements as needed.
text.enter().append("text")
.attr("class", "enter")
.attr("x", function(d, i) { return i * 32; })
.attr("dy", ".35em");
// ENTER + UPDATE
// Appending to the enter selection expands the update selection to include
// entering elements; so, operations on the update selection after appending to
// the enter selection will apply to both entering and updating nodes.
text.text(function(d) { return d; });
// EXIT
// Remove old elements as needed.
text.exit().remove();
}

Circles aren't updating in a multiline graph

I have a D3.js multiline graph with circles added on every path peak. When I update my graph, the paths update just fine with the new data but the circles don't seem to get updated at all. Here's my code: http://jsbin.com/eMuQOHoV/3/edit
Does anyone know what I'm doing wrong?
You need to update the data point circles in the same way that you've created them. In particular, you're using a nested selection when creating them, but not when updating. This means that the data won't get matched correctly on update and nothing happens.
The code for the update should look like this.
var sel = svg.selectAll('.series')
.data(sources);
sel.select('path')
.transition()
// etc
// update circles
sel.selectAll('.datapoint')
.data(function (d) {
return d.values;
})
// etc
Complete jsbin here.

Categories

Resources