How to bind d3 generated HTML elements to scope? - javascript

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();
}

Related

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.

Append elements into different containers depending on conditions with D3

I have an array, looking as simple as
var labels = ['label 1', 'label2', ..., 'label n']
Now if I want to put them as visual elements in the chart, I can do like this:
var legendItem = legendArea
.selectAll('g')
.data(labels)
.enter()
.append('g')
.attr({class: 'legend-element'});
legendArea becomes a parent for all the labels now. But, I have a more complex scenario, where I need to put labels not in legendArea directly but create a wrapper g element first inside legendArea, which will then contain a set of labels, depending on some criteria that I get from each label.
As a result, I will have a number of g elements with a set of labels inside of them, one can have 5, another can have 8, any number, as they are not spread evenly.
What I see now, is I need to run a loop through all labels array elements, check if current labels conforms to criteria, create a new wrapper element if needed and then append. But this solution seems to be not D3-style, as in most cases it's possible to do functional style code with D3, not for..loop.
I suspect I can do something more custom here, something like:
var legendItem = legendArea
.selectAll('g')
.data(labels)
.enter()
// Do some unknown D3 magic here to create a new wrapper element and append the label to it.
.attr({class: 'legend-element'});
Please advise on how to do it in D3 fashion.
I would get a list of categories, use that as data for the g elements, and then add the rest with filters:
var cats = svg.selectAll("g").data(categories)
.enter().append("g")
.attr("transform", function(d, i) { return "translate(10," + ((i+1) * 30) + ")"; });
cats.selectAll("text")
.data(function(d) { return data.filter(function(e) { return e.category == d; }); })
.enter().append("text")
.text(function(d) { return d.label; });
This works similar to nested selections, except that the nesting isn't in the data itself (although you can modify the data structures for that as well), but by referencing different data structures.
The first block of code simply adds a g element for each category. The second block of code does a nested selection (and is hence able to reference the data bound to the g element just before). In the .data() function, we take all the data and filter out the items that match the current category. The rest is bog-standard D3 again.
Complete demo here.

Adding on-exit transition to D3 circle pack-based reusable module

I have a reusable module based on the d3.layout.pack graph example.
I added a transition on exit on the node elements but it seems like the transition works only for one data set and it's not working for the other.
Basically, to simulate the data update I am calling a function with setInterval this way:
function test(){
d3.select('#vis')
.datum(data2)
.call(cluster);
}
setInterval(test, 1500);
...and I added the transition this way:
c.exit().transition()
.duration(700)
.attr("r", function(d){ return 0; })
.remove();
You can find the data update section in the bottom of the file and find the exit transition handling on line 431.
Could you please check what's wrong?
The behaviour you're seeing is caused by the way data is matched in D3. For one of the data sets, all the data elements are matched by existing DOM elements and hence the exit selection is empty -- you're simply updating position, dimensions, etc. of the existing elements.
The way to "fix" this is to tell D3 explicitly how you want data to be matched -- for example by changing line 424 to
.data(nodes, function(d) { return d.name; });
which will compare the name to determine whether a data element is represented by a DOM element. Modified jsfiddle here.

Adding and Removing Nodes in D3js Force Graph

I am loading json from database and creating a json file which loads fine. Now I don't know which steps to take for making the nodes responsive in a Force-Directed Graph. I need to remove and add new nodes and their links.
force.nodes(json.nodes)
.links(json.links)
.start();
initNodes(json);
How can I make this more dynamic or update it without resetting the whole visualization?
I have seen this question a couple of times not being answered so I hope someone can post and give a guide.
Adding nodes/links to my D3 force graph was very confusing until I better understood the way I was adding the initial set of nodes.
Assuming a <g> is what you'd like to use for your nodes:
// Select the element where you'd like to create the force layout
var el = d3.select("#svg");
// This should not select anything
el.selectAll("g")
// Because it's being compared to the data in force.nodes()
.data(force.nodes())
// Calling `.enter()` below returns the difference in nodes between
// the current selection and force.nodes(). At this point, there are
// no nodes in the selection, so `.enter()` should return
// all of the nodes in force.nodes()
.enter()
// Create the nodes
.append("g")
.attr("id", d.name)
.classed("manipulateYourNewNode", true);
Now let's make that function that will add a node to the layout once the graph has been initialized!
newNodeData is an object with the data you'd like to use for your new node.
connectToMe is a string containing the unique id of a node you'd like to connect your new node to.
function createNode (newNodeData, connectToMe) {
force.nodes().push(newNodeData);
el.selectAll("g")
.data(force.nodes(), function(datum, index) { return index })
The function given as the optional second argument in .data() is run once for each node in the selection and again for each node in force.nodes(), matching them up based on the returned value. If no function is supplied, a fallback function is invoked, which returns the index (as above).
However, there's most likely going to be a dispute between the index of your new selection (I believe the order is random) and the order in force.nodes(). Instead you'll most likely need the function to return a property that is unique to each node.
This time, .enter() will only return the node you're trying to add as newData because no key was found for it by the second argument of .data().
.enter()
.insert("g", "#svg")
.attr("id", d.name)
.classed("manipulatYourNewNode", true);
createLink(connectToMe, newNodeData.name);
force.start();
}
The function createLink (defined below) creates a link between your new node and your node of choice.
Additionally, the d3js API states that force.start() should be called after updating the layout.
Note: Calling force.stop() at the very beginning of my function was a huge help for me when I was first trying to figure out how to add nodes and links to my graph.
function createLink (from, to) {
var source = d3.select( "g#" + from ).datum(),
target = d3.select( "g#" + to ).datum(),
newLink = {
source: source,
target: target,
value: 1
};
force.links().push(newLink);
The code below works under the assumptions that:
#links is the wrapper element that contains all of your link elements
Your links are represented as <line> elements:
d3.select("#links")
.selectAll("line")
.data(force.links())
.enter()
.append("line");
You can see an example of how to append new nodes and relationships here:
http://bl.ocks.org/2432083
Getting rid of nodes and relationships is slightly trickier, but you can see the process here:
http://bl.ocks.org/1095795

Categories

Resources