Draw text on svg path element with d3.js - javascript

I am trying to modify this force directed graph, and in particular what I'd like to do is to draw text on each graph edge. I tried the following:
// add new links
path.enter().append('svg:path')
.attr('class', 'link')
.classed('selected', function(d) { return d === selected_link; })
.style('marker-start', function(d) { return d.left ? 'url(#start-arrow)' : ''; })
.style('marker-end', function(d) { return d.right ? 'url(#end-arrow)' : ''; })
.on('mousedown', function(d) {
if(d3.event.ctrlKey) return;
// select link
mousedown_link = d;
if(mousedown_link === selected_link) selected_link = null;
else selected_link = mousedown_link;
selected_node = null;
restart();
})
.append('svg:text').attr('x', 0).attr('y', 4)
.attr('class', 'id').text(function(d) { console.log(d.id); return d.id; })
Only the last line is my modification. I also added an id property to the link objects so the code I'm working with is not exactly what I've linked to. The log statement is printing as expected but no text or error messages appear. How can I append text right in the middle of each link?

You will need to use the svg group <g>, that will let you append elements inside.
var new_paths = path.enter().append('g'),
links = new_paths.append('svg:path').attr('class', 'link');
Be free to add whatever style or behaviour to links as you wish.
Then, for the labels, you will append again to the group:
var labels = new_paths.append('svg:text').attr('class', 'label');
And again you can add whatever you want to labels.
When you define the force, you will need to select the links and labels, wich will be something like this:
force.on("tick", function() {
links.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
labels.attr("transform",function(d){
return
"translate("+
(0.5*(d.source.x+d.target.x))+
","+
(0.5*(d.source.y+d.target.y))+
")";
});
});
Here is a demo fiddle http://jsfiddle.net/k2oyef30/
Update
Sorry, I didn't follow your example before. Here is your particular solution.
Change path to be defined as:
// handles to link and node element groups
var path = svg.append('svg:g').selectAll('.path'),
circle = svg.append('svg:g').selectAll('g');
Inside function tick() { you have to select the .links or the .id
path.select('.link').attr('d', function(d) {
and manage the new element:
path.select('.id').attr("transform",function(d){
return "translate("+
(0.5*(d.source.x+d.target.x))+
","+
(0.5*(d.source.y+d.target.y))+
")";
});
Inside function restart() { you have to create a <g> element, for example with class path, and append the elements into it.
// add new links
var newPaths = path.enter().append('svg:g').attr('class','path');
newPaths.append('svg:path')
.attr('class', 'link')
.classed('selected', function(d) { return d === selected_link; })
.style('marker-start', function(d) { return d.left ? 'url(#start-arrow)' : ''; })
.style('marker-end', function(d) { return d.right ? 'url(#end-arrow)' : ''; })
.on('mousedown', function(d) {
if(d3.event.ctrlKey) return;
// select link
mousedown_link = d;
if(mousedown_link === selected_link) selected_link = null;
else selected_link = mousedown_link;
selected_node = null;
restart();
});
newPaths.append('svg:text').attr('x', 0).attr('y', 4)
.attr('class', 'id').text(function(d) { return d.id; });
The problem with the example definition was that it already selected the <path> element making you unable to select the new <g class="path">

Related

D3.js v5 - Update nodes from another array of nodes

Each node I got is in g html tag which contains 2 more tags:
Text tag- representing the name of the node.
Circle tag - which is the node shape.
When I shifting from 1 set of nodes to another set of nodes in the HTML I see both text tags and circle tags of data from the old set of nodes and from the new set of node.
I tried to play with the exit().remove() method and with merge() method but nothing seems to fix my problem
Here is part of my update function which relevant to the nodes.
var node = d3.select('.nodes')
.selectAll('g.node');
//here is my selection between two array of nodes
node = node.data(show_bad_connections ? bad_nodes : nodes)
node.exit().remove();
node_enter = node.enter().append("g")
.attr("class", "node").merge(node);
node_enter.append("text")
.attr("class", "nodetext")
.attr("x", "0em")
.attr("y", 15)
.text(function (d) { return d.name });
node_enter.append("circle")
.style("fill", function (d) {
if (d.id == 0) { return "#0099ff" }
if (d.CF.includes(0)) { return "#00cc00" }
if (d.TSP > 0.5) { return "#ff9900" } else { return "#ff0000" }
})
.attr("r", r);
node_enter.on("mouseover", function (d) {
var g = d3.select(this); // The node
// The class is used to remove the additional text later
var info = g.append('text')
.classed('info', true)
.attr('dx', "0em")
.attr('dy', -10)
.text(function (d) {
if (d.id == 0) { return "id=0" }
else { return "id=" + d.id.toString() + ",TF=" + d.TF.toString() + ",AUA=" + d.AUA.toString() }
})
.style("font-size", "12px");
d3.selectAll('line.link')
.filter(function (l) {
return (d.id != 0 && (d.id == l.source.id || d.id == l.target.id));
})
.style("opacity", 1)
}).on("mouseout", function () {
d3.selectAll('line.link').style("opacity", 0.1)
// Remove the info text on mouse out.
d3.select(this).select('text.info').remove();
});
I expect to get g html tag with only the circle and text of new node.

Removing two nodes from a Force Layout fails while one succeeds

I'm trying to remove some nodes from a Force Layout.
The first part of the process is selecting one or more nodes. That's done with the click handler:
var nodeg = node.enter().append("g")
.attr("class", "node")
.on('click', function (n) {
if (n.dragging === true) {
return;
}
// select the clicked node
n.selected = !n.selected;
d3.select(this)
.transition()
.duration(500)
.ease('bounce')
.attr('fill', getNodeBackground(n))
.attr('transform', getNodeTransform(n));
})
.call(drag);
Note that I'm setting selected to true.
Next, if the user presses the delete key I remove the selected nodes:
function removeSelectedNodes() {
if (!confirm('Are you sure you want to delete the selected relationships?')) {
return;
}
var firstIndex = -1;
d3.selectAll('.node')
.each(function (n, i) {
if (n.selected !== true) {
return;
}
n.data.remove = true;
n.data.index = i;
if (firstIndex === -1) {
firstIndex = i;
}
});
var offset = 0;
_.each(_.where(scope.nodes, {data: {remove: true}}), function (n) {
var removeAt = n.index;
if (n.index > firstIndex) {
removeAt = n.index - 1 - offset;
offset++;
}
scope.nodes.splice(removeAt, 1);
scope.links.splice(removeAt - 1, 1);
});
renderGraph();
}
The entire renderGraph function looks like this:
function renderGraph() {
force
.nodes(scope.nodes)
.links(scope.links)
.start();
var link = svg.selectAll(".link")
.data(scope.links);
link.exit().remove();
link.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(scope.nodes);
node.exit().remove();
var nodeg = node.enter().append("g")
.attr("class", "node")
.on('click', function (n) {
if (n.dragging === true) {
return;
}
// select the clicked node
n.selected = !n.selected;
d3.select(this)
.transition()
.duration(500)
.ease('bounce')
.attr('fill', getNodeBackground(n))
.attr('transform', getNodeTransform(n));
})
.call(drag);
nodeg.append("image")
.attr("xlink:href", function (d) {
return d.avatar || 'https://github.com/favicon.ico'
})
.attr("x", -56)
.attr("y", -8)
.attr("width", 64)
.attr("height", 64);
nodeg.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.attr('class', 'name')
.text(function (d) {
return d.displayName;
});
nodeg.append("text")
.attr("dx", 12)
.attr("dy", "1.35em")
.text(function (d) {
return d.relationship;
});
force.on("tick", function () {
link.attr("x1", function (d) {
return d.source.x;
})
.attr("y1", function (d) {
return d.source.y;
})
.attr("x2", function (d) {
return d.target.x;
})
.attr("y2", function (d) {
return d.target.y;
});
node.attr("transform", function (d) {
return getNodeTransform(d);
});
});
}
This is where it starts going south. If I select one node, the graph is rendered correctly after splicing the node. However, if I remove two nodes the graph ends up persisting the first removed node (by index).
Let's say the first node (by index) was 'Bob' and the second node was 'Bill'. The second node will be removed, but the first one will persist. Interestingly, another one of the nodes, the current last node (by index), will be gone instead.
NOTE: the array looks good. The nodes I wanted removed are gone, and the remaining ones are correct.
What did I do wrong here?
UPDATE: I've tried not setting nodes and links after removing nodes, and just calling start:
force.start()
This didn't work.
The answer, thanks to Lars again, was to add a key function to the links and nodes:
var link = svg.selectAll(".link")
.data(scope.links, function (d) {
return d.source.data._id + '|' + d.target.data._id;
});
var node = svg.selectAll(".node")
.data(scope.nodes, function (d) {
return d.data._id;
});
Thanks Lars!

How to read D3 node label text into a div/paragraph element?

I'm working with a force labelled graph layout in D3 and one of my project requirements is that the text associated with each node should show up in a div/p element that occupies a fixed position on the web page instead of showing up directly adjacent to the concerned node (currently it activates on mouse hover). So every time a user hovers on any node, the corresponding text should show up in the div tag and not right next to the node in the graph.
The following code snippets show how I'm currently working with my nodes and labels:
var node = vis.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.call(force.drag);
node.append("text")
.text("");
force.on("tick", function() {
nodes[0].x = w / 2;
nodes[0].y = h / 2;
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
node.on("mouseover", function(d){
node.classed("node-active", function(o) {
thisOpacity = isConnected(d, o) ? true : false;
this.setAttribute('fill-opacity', thisOpacity);
return thisOpacity;
});
link.classed("link-active", function(o) {
return o.source === d || o.target === d ? true : false;
});
d3.select(this).classed("node-active", true);
d3.select(this).select("circle").transition()
.duration(500)
.attr("r", 20);
d3.select(this).select("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.label; });
d3.select(this).select("circle")
.on("click", function(d){
var win = window.open(d.label, '_blank');
win.focus();
});
})
.on("mouseout", function(d){
node.classed("node-active", false);
link.classed("link-active", false);
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 10);
d3.select(this).select("text")
.text("");
});
I have a div tag with id = 'nodeLabel' that will be used to print the node's label. I hope the attached code helps. I tried using document.getElementById('nodeLabel') to set it's value but I'm not sure if this is the right approach or where would this line of code go?
How do I achieve the desired effect?
In your mouseover handler, simply add something like this:
$("#nodeLabel").text(d.label);
Building on your code, this would result in:
node.on("mouseover", function(d){
node.classed("node-active", function(o) {
thisOpacity = isConnected(d, o) ? true : false;
this.setAttribute('fill-opacity', thisOpacity);
return thisOpacity;
});
link.classed("link-active", function(o) {
return o.source === d || o.target === d ? true : false;
});
d3.select(this).classed("node-active", true);
d3.select(this).select("circle").transition()
.duration(500)
.attr("r", 20);
d3.select(this).select("circle")
.on("click", function(d){
var win = window.open(d.label, '_blank');
win.focus();
});
$("#nodeLabel").text(d.label);
})
I would go about it probably by
At the beginning, when you create the nodes, you call .each() and pass it a function that sets a member variable of each of the nodes to the value you want to display.
In the onClick handler, you can get the element, on which the event occured, by d3.select(this) so I would take from it the value you stored in step 1 and set the div's text accordingly. Which would be something like d3.select("#nodeLabel").text(nodeText)
Ok, I found a way to do it.
I stored the value of d.label into a variable and then simply set the innerHTML of the 'p' tag in the mouseOver handle.
Here's the updated code snippet:
node.on("mouseover", function(d){
node.classed("node-active", function(o) {
thisOpacity = isConnected(d, o) ? true : false;
this.setAttribute('fill-opacity', thisOpacity);
return thisOpacity;
});
link.classed("link-active", function(o) {
return o.source === d || o.target === d ? true : false;
});
var nodeText;
d3.select(this).classed("node-active", true);
d3.select(this).select("circle").transition()
.duration(500)
.attr("r", 20);
d3.select(this).select("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { nodeText = d.label; return d.label; });
d3.select("#nodeLabel").text(nodeText)
d3.select(this).select("circle")
.on("click", function(d){
var win = window.open(d.label, '_blank');
win.focus();
});
console.log(nodeText);
document.getElementById('nodeLabel').innerHTML = "Node selected: "+nodeText;
})
If there's a simpler/better solution, I would definitely still like to hear it and learn!

Hierarchical JSON into flare.json format for use in Bilevel Partition

I have a CSV that I want to convert to hierarchical JSON for use with a Bilevel Partition.
The Bilevel Partition wants its JSON data in format similar to this flare.json file. Basically the leaf nodes have a name and size properties and everything in between has a name and children properties.
Here is my code for attempting to convert the CSV file into Hierarchical JSON.
CODE
var root = { "key": "Leeds CCGs", "values": d3.nest()
.key(function(d) { return d.ccgname; })
.key(function(d) { return d.practicename; })
.key(function(d) { return d.diagnosisname; })
.rollup(function(leaves) { return d3.sum(leaves, function(d) { return d.numpatientswithdiagnosis; }) })
.entries(data)
}
The above code works as far as giving the data the required hierarchical structure, but the labels are wrong. Instead of name, children and size it gives me key and values only, all the way to the leaf nodes, similar to this file.
So I read around, and found this question on SO, it isn't to do with a Bilevel Partition, but I thought the same principle would apply, since both layouts need hierarchical JSON data.
So I set about doing the same, but I just can't get it to work. First of all in my code I do not have a size() function as mentioned in the SO question. Here is my whole code which is pretty much copied from the official example:
CODE
d3.csv('data/partition.csv', function (data) {
var root = { "key": "Leeds CCGs", "values": d3.nest()
.key(function(d) { return d.ccgname; })
.key(function(d) { return d.practicename; })
.key(function(d) { return d.diagnosisname; })
.rollup(function(leaves) { return d3.sum(leaves, function(d) { return d.numpatientswithdiagnosis; }) })
.entries(data)
}
// Compute the initial layout on the entire tree to sum sizes.
// Also compute the full name and fill color for each node,
// and stash the children so they can be restored as we descend.
partition
.value(function(d) { return d.values; })
.nodes(root)
.forEach(function(d) {
d._children = d.values;
d.sum = d.value;
d.key = key(d);
d.fill = fill(d);
});
// Now redefine the value function to use the previously-computed sum.
partition.children(function(d, depth) {
return depth < 2 ? d._children : null;
}).value(function(d) {
return d.sum;
});
var center = svg.append("circle")
.attr("r", radius / 4)
.style('fill-opacity', '.2')
.style('cursor', 'pointer')
.on("click", zoomOut);
center.append("title")
.text("zoom out");
var path = svg.selectAll("path")
.data(partition.nodes(root).slice(1))
.enter().append("path")
.attr("d", arc)
.style("fill", function(d) { return d.fill; })
.style('cursor', 'help')
.each(function(d) { this._current = updateArc(d); })
.on("mouseover", update_legend)
.on("mouseout", remove_legend)
.on("click", zoomIn);
function zoomIn(p) {
if (p.depth > 1) p = p.parent;
if (!p.children) return;
zoom(p, p);
}
function zoomOut(p) {
if (!p.parent) return;
zoom(p.parent, p);
}
// Zoom to the specified new root.
function zoom(root, p) {
if (document.documentElement.__transition__) return;
// Rescale outside angles to match the new layout.
var enterArc,
exitArc,
outsideAngle = d3.scale.linear().domain([0, 2 * Math.PI]);
function insideArc(d) {
return p.key > d.key
? {depth: d.depth - 1, x: 0, dx: 0} : p.key < d.key
? {depth: d.depth - 1, x: 2 * Math.PI, dx: 0}
: {depth: 0, x: 0, dx: 2 * Math.PI};
}
function outsideArc(d) {
return {depth: d.depth + 1, x: outsideAngle(d.x), dx: outsideAngle(d.x + d.dx) - outsideAngle(d.x)};
}
center.datum(root);
// When zooming in, arcs enter from the outside and exit to the inside.
// Entering outside arcs start from the old layout.
if (root === p) enterArc = outsideArc, exitArc = insideArc, outsideAngle.range([p.x, p.x + p.dx]);
path = path.data(partition.nodes(root).slice(1), function(d) { return d.key; });
// When zooming out, arcs enter from the inside and exit to the outside.
// Exiting outside arcs transition to the new layout.
if (root !== p) enterArc = insideArc, exitArc = outsideArc, outsideAngle.range([p.x, p.x + p.dx]);
d3.transition().duration(d3.event.altKey ? 7500 : 750).each(function() {
path.exit().transition()
.style("fill-opacity", function(d) { return d.depth === 1 + (root === p) ? 1 : 0; })
.attrTween("d", function(d) { return arcTween.call(this, exitArc(d)); })
.remove();
path.enter().append("path")
.style("fill-opacity", function(d) { return d.depth === 2 - (root === p) ? 1 : 0; })
.style("fill", function(d) { return d.fill; })
.style('cursor', 'help')
.on("mouseover",update_legend)
.on("mouseout",remove_legend)
.on("click", zoomIn)
.each(function(d) { this._current = enterArc(d); });
path.transition()
.style("fill-opacity", 1)
.attrTween("d", function(d) { return arcTween.call(this, updateArc(d)); });
});
}
});
function key(d) {
var k = [];
var p = d;
while (p.depth) k.push(p.key), p = p.parent;
return k.reverse().join(".");
}
function fill(d) {
var p = d;
while (p.depth > 1) p = p.parent;
var c = d3.lab(hue(p.key));
c.l = luminance(d.sum);
return c;
}
function arcTween(b) {
var i = d3.interpolate(this._current, b);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
function updateArc(d) {
return {depth: d.depth, x: d.x, dx: d.dx};
}
All the above gives me this in my browser:
You should be able to exactly re-use the rest of the code by simply transforming the hierarchical output of d3.nest() into the same format as the flare.json dataset, something like this:
(should be run immediately after the definition of root)
//rename object keys generated from d3.nest() to match flare.json format
renameStuff(root);
function renameStuff(d) {
d.name = d.key; delete d.key;
if (typeof d.values === "number") d.size = d.values;
else d.values.forEach(renameStuff), d.children = d.values;
delete d.values;
}
You could also change the accessor functions to the d3.layout.partition() object to match the new object, but you will at a minimum need to change the structure of the original object so that the leaf nodes do not store values in the same field name as the children. The solution above is probably the simplest way to get things working quickly.

D3 treemap - How to insert div into first level nodes?

I am new to D3.js and want to make a treemap like this: http://bl.ocks.org/mbostock/4063582#index.html
What I am going to do is to insert new div into parent nodes.
...
d3.json("flare.json", function(error, root) {
var node = div.datum(root).selectAll(".node")
.data(treemap.nodes)
.enter().append("div")
// parent nodes have class name "first"
.attr("class", function(d) { return d.children ? "node first" : "node"; })
.call(position)
.style("background", function(d) { return d.children ? color(d.name) : null; })
.text(function(d) { return d.children ? null : d.name; });
// my code
var parents = d3.selectAll(".first")
.data(treemap.value(function(d) { return d.size; }).nodes) // ISSUE
.enter().append("div")
.text(function(d) { return d.name; });
d3.selectAll("input").on("change", function change() {
var value = this.value === "count"
? function() { return 1; }
: function(d) { return d.size; };
node
.data(treemap.value(value).nodes)
.transition()
.duration(1500)
.call(position);
});
});
...
But this doesn't work correctly.
HOW CAN I UPDATE ONLY THE PAREENT NODES BY ADDING NEW DIV TO THEM?
Could anybody help me please. Thanks a lot...

Categories