D3: change data based on svg element manipulation - javascript

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.

Related

D3.js with Observable, problem with event handler [duplicate]

The documentation for d3.drag states the DOM element target of the drag event will be available in this to the callback:
When a specified event is dispatched, each listener will be invoked with the same context and arguments as selection.on listeners: the current datum d and index i, with the this context as the current DOM element.
But my call back is an object instance and this points to that object. So I need another way of accessing the current DOM element that is normally passed in this. How can I do it?
Use the second and the third arguments together to get this when this is not available:
d3.drag().on(typename, function(d, i, n) {
//here, 'this' is simply n[i]
})
For a detailed explanation, have a look at the article below that I wrote to deal with this in arrow functions. The issue is different from yours, but the explanation is the same.
Here is a basic demo, try to drag a circle and look at the console:
var data = d3.range(5)
var svg = d3.select("body")
.append("svg")
.attr("width", 400)
.attr("height", 100);
var circle = svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("cy", 50)
.attr("cx", function(d) {
return 50 + 50 * d
})
.attr("r", 10)
.attr("fill", "tan")
.attr("stroke", "black")
.call(d3.drag()
.on("start", function(d, i, n) {
console.log(JSON.stringify(n[i]))
}))
<script src="https://d3js.org/d3.v4.min.js"></script>
PS: I'm using JSON.stringify on the D3 selection because Stack snippets freeze if you try to console.log a D3 selection.
Using "this" with an arrow function
Most of functions in D3.js accept an anonymous function as an argument. The common examples are .attr, .style, .text, .on and .data, but the list is way bigger than that.
In such cases, the anonymous function is evaluated for each selected element, in order, being passed:
The current datum (d)
The current index (i)
The current group (nodes)
this as the current DOM element.
The datum, the index and the current group are passed as arguments, the famous first, second and third argument in D3.js (whose parameters are traditionally named d, i and p in D3 v3.x). For using this, however, one doesn’t need to use any argument:
.on("mouseover", function(){
d3.select(this);
});
The above code will select this when the mouse is over the element. Check it working in this fiddle: https://jsfiddle.net/y5fwgopx/
The arrow function
As a new ES6 syntax, an arrow function has a shorter syntax when compared to function expression. However, for a D3 programmer who uses this constantly, there is a pitfall: an arrow function doesn’t create its own this context. That means that, in an arrow function, this has its original meaning from the enclosing context.
This can be useful in several circumstances, but it is a problem for a coder accustomed to use this in D3. For instance, using the same example in the fiddle above, this will not work:
.on("mouseover", ()=>{
d3.select(this);
});
If you doubt it, here is the fiddle: https://jsfiddle.net/tfxLsv9u/
Well, that’s not a big problem: one can simply use a regular, old fashioned function expression when needed. But what if you want to write all your code using arrow functions? Is it possible to have a code with arrow functions and still properly use this in D3?
The second and third arguments combined
The answer is yes, because this is the same of nodes[i]. The hint is actually present all over the D3 API, when it describes this:
...with this as the current DOM element (nodes[i])
The explanation is simple: since nodes is the current group of elements in the DOM and i is the index of each element, nodes[i] refer to the current DOM element itself. That is, this.
Therefore, one can use:
.on("mouseover", (d, i, nodes) => {
d3.select(nodes[i]);
});
And here is the corresponding fiddle: https://jsfiddle.net/2p2ux38s/

d3.js : Select this element on mouseover [duplicate]

The documentation for d3.drag states the DOM element target of the drag event will be available in this to the callback:
When a specified event is dispatched, each listener will be invoked with the same context and arguments as selection.on listeners: the current datum d and index i, with the this context as the current DOM element.
But my call back is an object instance and this points to that object. So I need another way of accessing the current DOM element that is normally passed in this. How can I do it?
Use the second and the third arguments together to get this when this is not available:
d3.drag().on(typename, function(d, i, n) {
//here, 'this' is simply n[i]
})
For a detailed explanation, have a look at the article below that I wrote to deal with this in arrow functions. The issue is different from yours, but the explanation is the same.
Here is a basic demo, try to drag a circle and look at the console:
var data = d3.range(5)
var svg = d3.select("body")
.append("svg")
.attr("width", 400)
.attr("height", 100);
var circle = svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("cy", 50)
.attr("cx", function(d) {
return 50 + 50 * d
})
.attr("r", 10)
.attr("fill", "tan")
.attr("stroke", "black")
.call(d3.drag()
.on("start", function(d, i, n) {
console.log(JSON.stringify(n[i]))
}))
<script src="https://d3js.org/d3.v4.min.js"></script>
PS: I'm using JSON.stringify on the D3 selection because Stack snippets freeze if you try to console.log a D3 selection.
Using "this" with an arrow function
Most of functions in D3.js accept an anonymous function as an argument. The common examples are .attr, .style, .text, .on and .data, but the list is way bigger than that.
In such cases, the anonymous function is evaluated for each selected element, in order, being passed:
The current datum (d)
The current index (i)
The current group (nodes)
this as the current DOM element.
The datum, the index and the current group are passed as arguments, the famous first, second and third argument in D3.js (whose parameters are traditionally named d, i and p in D3 v3.x). For using this, however, one doesn’t need to use any argument:
.on("mouseover", function(){
d3.select(this);
});
The above code will select this when the mouse is over the element. Check it working in this fiddle: https://jsfiddle.net/y5fwgopx/
The arrow function
As a new ES6 syntax, an arrow function has a shorter syntax when compared to function expression. However, for a D3 programmer who uses this constantly, there is a pitfall: an arrow function doesn’t create its own this context. That means that, in an arrow function, this has its original meaning from the enclosing context.
This can be useful in several circumstances, but it is a problem for a coder accustomed to use this in D3. For instance, using the same example in the fiddle above, this will not work:
.on("mouseover", ()=>{
d3.select(this);
});
If you doubt it, here is the fiddle: https://jsfiddle.net/tfxLsv9u/
Well, that’s not a big problem: one can simply use a regular, old fashioned function expression when needed. But what if you want to write all your code using arrow functions? Is it possible to have a code with arrow functions and still properly use this in D3?
The second and third arguments combined
The answer is yes, because this is the same of nodes[i]. The hint is actually present all over the D3 API, when it describes this:
...with this as the current DOM element (nodes[i])
The explanation is simple: since nodes is the current group of elements in the DOM and i is the index of each element, nodes[i] refer to the current DOM element itself. That is, this.
Therefore, one can use:
.on("mouseover", (d, i, nodes) => {
d3.select(nodes[i]);
});
And here is the corresponding fiddle: https://jsfiddle.net/2p2ux38s/

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.

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

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