d3 forced layout graph label and text on vertex and edges - javascript

I am trying to create forced directed graph using d3 v4 .
I am able to create vertex and edges ,but not able to add name/text inside vertex shape .
For Edges text should be above line in its half length.
The below code is adding text but not in correct position
node.append("text").
attr("dx", 6).attr("dy", ".35em")
.style("font-size",10).text(function(d) { return d.name });
JSFIDDLE

now you will be able
explain
first you create a group of node and link
node is group to hold the circle
link is group to hold the line connection
you create variable node append the data by
node = node.data(nodes, function(d) { return d.id;});
//var node pointing to group cos still node = g.append("g")
than you update variable node to append circle
node = node.enter().append("circle")
//var node pointing to circle on group
if you make text by
node.append("text").
attr("dx", 6).attr("dy", ".35em")
.style("font-size",10).text(function(d) { return d.name });
inspect the HTML
it will append on node>circle>text and this is wrong
the right one should be node>text
so create text on node group not in circle
before node variable pointing to circle create variable text to append on group
uu = node.enter().append("text")
than update the x and y position like you update cx and cy of circle on
function ticked() {//update x and y of uu variable}
var createD3 = function(){
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
color = d3.scaleOrdinal(d3.schemeCategory10);
var a = {id: "a",name:"y"},
b = {id: "b",name:"x"},
c = {id: "c",name:"z"},
nodes = [a, b, c],
links = [{source: a, target: b},{source: b, target: c},{source: c, target: a}];
var simulation = d3.forceSimulation(nodes)
.force("charge", d3.forceManyBody().strength(-1000))
.force("link", d3.forceLink(links).distance(200))
.force("x", d3.forceX())
.force("y", d3.forceY())
.alphaTarget(1)
.on("tick", ticked);
var g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"),
link = g.append("g").attr("stroke", "#000").attr("stroke-width", 1.5).selectAll(".link"),
node = g.append("g").attr("stroke-width", 1.5).selectAll(".node");
restart();
function restart() {
// Apply the general update pattern to the nodes.
node = node.data(nodes, function(d) { return d.id;});
node.exit().remove();
uu = node.enter().append("text").text(function(d) { return d.name }).attr("fill",'blue').attr('text-anchor','middle').style('font-size',24)
node = node.enter().append("circle").attr("fill", function(d) { return color(d.id); }).attr("r", 8).merge(node);
//var labels = node
// Apply the general update pattern to the links.
link = link.data(links, function(d) { return d.source.id + "-" + d.target.id; });
link.exit().remove();
link = link.enter().append("line").merge(link);
// Update and restart the simulation.
simulation.nodes(nodes);
simulation.force("link").links(links);
simulation.alpha(1).restart();
}
function ticked() {
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
uu.attr("x", function(d,i) {
if(i==0){return d.x-80;}
if(i==1){return d.x+10;}
if(i==2){return d.x+80;}
})
.attr("y", function(d,i) {
if(i==0){return d.y+40;}
if(i==1){return d.y-80;}
if(i==2){return d.y+80;}
})
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; });
}
};
createD3();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.3/d3.min.js"></script>
<svg width="500" height="500"></svg>

Related

D3.js update graph when pushing new nodes

I am trying to add new nodes to a D3 graph dynamically by nodes.push({"id": item, "group": 1, "size": 30}); but when I do this there is a visual bug where there are duplicates. Anytime I update() I get a double of whatever was already there. Anyone have any advice? Would be appreciated.
var node;
var link;
var circles
var lables;
function update(){
node = svg.append("g")
.attr("class", "nodes")
.selectAll("g")
.data(nodes)
.enter().append("g")
link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(links)
.enter().append("line")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); });
circles = node.append("circle")
.attr("r", function(d) { return (d.size / 10) + 1})
.attr("fill", function(d) { return color(3); })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("click", clicked);
lables = node.append("text")
.text(function(d) {
return d.id;
})
.attr('x', 6)
.attr('y', 3)
.style("font-size", "20px");
node.append("title")
.text(function(d) { return d.id; });
simulation
.nodes(nodes)
.on("tick", ticked);
simulation.force("link")
.links(links);
}
function ticked() {
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 + ")";
})
}
Looking just at the nodes (the links are essentially the same issue), every time you update your data you:
Create a new parent g (svg.append("g"))
Select all the child g elements of that new parent g (.selectAll("g")). Since this new parent g has no children - you just made it, nothing is selected.
Bind data to the selection (.data(nodes))
Using the enter selection, append a g for each item in the data array (as there are no elements in the selection, everything is entered (the enter selection creates an element in the DOM for every item in the data array for which no corresponding element exists in the selection.)
Append a circle to each newly appended g. (.enter().append("g"))
Nowhere do you select the already existing nodes - these are just cast aside. They are ignored by the tick function because link and node refer to selections of newly created nodes and links. Neither do you remove the old links and nodes - so they just sit there for all eternity or until you close the browser.
The canonical solution is to:
Append structural elements once. I say structural in reference to the parent g elements: they aren't data dependent, they're organizational. They should be appended once outside of the update function.
Use the update function to manage (create, update, remove) elements that are dependent on the data: the nodes and links themselves. Anything that depends on the data needs to be modified in the update function, nothing else.
So we could append the parent g elements outside of the update function:
var nodeG = svg.append("g").attr("class", "nodes");
var linkG = svg.append("g").attr("class", "links");
Then in the update function we can use those selections to conduct the enter/update/exit cycle. This is slightly complicated in your case, and many others, because we have nodes represented by a g with child elements. Something like the following:
function update() {
var node = nodeG.selectAll("g")
.data(nodes)
// remove excess nodes.
node.exit().remove();
// enter new nodes as required:
var nodeEnter = node.enter().append("g")
.attr(...
// append circles to new nodes:
nodeEnter.append("circle")
.attr(...
// merge update and enter.
node = nodeEnter.merge(node);
// do enter/update/exit with lines.
var link = linkG.selectAll("line")
.attr(...
link.exit().remove();
var linkEnter = link.enter().append("line")
.attr(...
link = linkEnter.merge(link);
...
Which in your case may look like:
// Random data:
let graph = { nodes: [], links : [] }
function randomizeData(graph) {
// generate nodes:
let n = Math.floor(Math.random() * 10) + 6;
let newNodes = [];
for(let i = 0; i < n; i++) {
if (graph.nodes[i]) newNodes.push(graph.nodes[i]);
else newNodes.push({ id: i,
color: Math.floor(Math.random()*10),
size: Math.floor(Math.random() * 10 + 2),
x: (Math.random() * width),
y: (Math.random() * height)
})
}
// generate links
let newLinks = [];
let m = Math.floor(Math.random() * n) + 1;
for(let i = 0; i < m; i++) {
a = 0; b = 0;
while (a == b) {
a = Math.floor(Math.random() * n);
b = Math.floor(Math.random() * n);
}
newLinks.push({source: a, target: b})
if(i < newNodes.length - 2) newLinks.push({source: i, target: i+1})
}
return { nodes: newNodes, links: newLinks }
}
// On with main code:
// Set up the structure:
const svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
const color = d3.scaleOrdinal(d3.schemeCategory10);
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).strength(0.004))
.force("charge", d3.forceManyBody())
// to attract nodes to center, use forceX and forceY:
.force("x", d3.forceX().x(width/2).strength(0.01))
.force("y", d3.forceY().y(height/2).strength(0.01));
const nodeG = svg.append("g").attr("class","nodes")
const linkG = svg.append("g").attr("class", "links")
graph = randomizeData(graph);
update();
// Two variables to hold our links and nodes - declared outside the update function so that the tick function can access them.
var links;
var nodes;
// Update based on data:
function update() {
// Select all nodes and bind data:
nodes = nodeG.selectAll("g")
.data(graph.nodes);
// Remove excess nodes:
nodes.exit()
.transition()
.attr("opacity",0)
.remove();
// Enter new nodes:
var newnodes = nodes.enter().append("g")
.attr("opacity", 0)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
// for effect:
newnodes.transition()
.attr("opacity",1)
.attr("class", "nodes")
newnodes.append("circle")
.attr("r", function(d) { return (d.size * 2) + 1})
.attr("fill", function(d) { return color(d.color); })
newnodes.append("text")
.text(function(d) { return d.id; })
.attr('x', 6)
.attr('y', 3)
.style("font-size", "20px");
newnodes.append("title")
.text(function(d) { return d.id; });
// Combine new nodes with old nodes:
nodes = newnodes.merge(nodes);
// Repeat but with links:
links = linkG.selectAll("line")
.data(graph.links)
// Remove excess links:
links.exit()
.transition()
.attr("opacity",0)
.remove();
// Add new links:
var newlinks = links.enter()
.append("line")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); });
// for effect:
newlinks
.attr("opacity", 0)
.transition()
.attr("opacity",1)
// Combine new links with old:
links = newlinks.merge(links);
// Update the simualtion:
simulation
.nodes(graph.nodes) // the data array, not the selection of nodes.
.on("tick", ticked)
.force("link").links(graph.links)
simulation.alpha(1).restart();
}
function ticked() {
links // the selection of all 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; });
nodes
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
d3.select("button")
.on("click", function() {
graph = randomizeData(graph);
update();
})
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
<button> Update</button>
<svg width="500" height="300"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
Note
I've updated the force paramaters a bit to use forceX and forceY: forces which attract the nodes to the center. The centering force only ensures the center of gravity is a specific value, not how close the nodes must be.
Alternative approaches:
Of course, you could just remove everything and append it each time: but this limits ability to transition from one dataset to the next and is generally not canonical.
If you only need to enter elements once (no elements need to be added or removed during updates) then you could avoid using the full enter/update/exit cycle and append once outside the update function, updating node/link attributes with new data on update rather than using the more involved enter/update/exit cycle in the snippet above.

d3.js Molecule Diagram only working on the last element of the object

so I'm trying to create a visual representations of a couple of vlans and the connections of switches in each of them. I tried implementing it with this example I found online https://bl.ocks.org/mbostock/3037015 , the problem is that when i created a loop to go through all of the vlans, only the last vlan is drawn, there's really no reason I can see of why this is happening since all elements are calling the function.
If I remove the last element from the array with delete data['80'] then the one before the last starts working, so the only one working it the last one of the dictionary object, don't why though
code:
var data = {{ graph_vlans | safe }};
console.log(data);
$(document).ready(() => {
//-----------------------------------------------------------------
// TREE DISPLAY ---------------------------------------------------
//-----------------------------------------------------------------
var toggler = document.getElementsByClassName("caret");
for (var i = 0; i < toggler.length; i++) {
toggler[i].addEventListener("click", function () {
this.parentElement.querySelector(".nested").classList.toggle("active");
this.classList.toggle("caret-down");
});
}
//-----------------------------------------------------------------
// NETWORK DIAGRAM ------------------------------------------------
//-----------------------------------------------------------------
var width = 960, height = 500;
var color = d3.scale.category20();
var radius = d3.scale.sqrt().range([0, 6]);
var i = 0;
for (var key in data) {
console.log(key);
console.log(key["4"]);
var svg = d3.select("#graph_" + key).append("svg").attr("width", width).attr("height", height);
var force = d3.layout.force()
.size([width, height])
.charge(-400)
.linkDistance(function (d) {
return radius(d.source.size) + radius(d.target.size) + 20;
});
var graph = data[key];
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("g")
.attr("class", "link");
link.append("line")
.style("stroke-width", function (d) {
return (d.bond * 2 - 1) * 2 + "px";
});
link.filter(function (d) {
return d.bond > 1;
}).append("line")
.attr("class", "separator");
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", function (d) {
return radius(d.size);
})
.style("fill", function (d) {
return color(d.atom);
});
node.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function (d) {
return d.atom;
});
force.nodes(graph.nodes)
.links(graph.links)
.on("tick", tick)
.start();
i++;
}
function tick() {
link.selectAll("line")
.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 + ")";
});
}
});
Problem
I made some fake data for your plot and got this:
Your other force layouts are drawing, they're just not positioned. They're at [0,0] - barely visible here, in the top left corner of the SVG. So why is this?
Each for loop iteration you redefine any existing link and node variables - their scope extends beyond the for statement so you overwrite the previous defintion. var restricts a variables scope by function, the for statement doesn't limit scope if using var.
Because of this, when you call the tick function for each force layout, only the last layout is updated because node and link refer to the last layouts nodes and links.
So only your last force layout does anything.
Solution
There are a few solutions, I'm proposing one that adds two simple changes from your current code.
We need to get each force layout's nodes and links to the tick function. Currently we have all the force layout tick functions using the same node and link references. Ultimately, this is a variable scoping issue.
We can start by placing the tick function into the for loop. But, this still runs into the same problem by itself: node and link have a scope that isn't limited to the for loop (or the current iteration of the for loop) - each tick function will still use the same node and link references.
To fix this, we also need to use let when defining link and node (instead of var), now these variables have a block level scope, meaning each iteration's definitions of link and node won't overwrite the previous iterations.
By moving the tick function into the for loop and using let to define node and link, each time we call the tick function it will use the appropriate nodes and links.
Here's an example using a slightly modified example of the above code (removing some of the styling that relies on data properties and re-sizing the layouts for snippet view, but with the changes proposed above):
var data = {
"a":{
nodes:[{name:1},{name:2},{name:3}],
links:[
{source:1, target:2},
{source:2, target:0},
{source:0, target:1}
]
},
"b":{
nodes:[{name:"a"},{name:"b"},{name:"c"}],
links:[
{source:1, target:2},
{source:2, target:0},
{source:0, target:1}
]
}
}
// TREE DISPLAY
var width = 500, height = 100;
var color = d3.scale.category20();
var radius = d3.scale.sqrt().range([0, 6]);
var i = 0;
for (var key in data) {
var svg = d3.select("body").append("svg").attr("width", width).attr("height", height);
var force = d3.layout.force()
.size([width, height])
.charge(-400)
.linkDistance(20);
var graph = data[key];
let link = svg.selectAll(".link")
.data(graph.links)
.enter().append("g")
.attr("class", "link");
link.append("line")
.style("stroke-width", 1)
.style("stroke","#ccc")
let node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node");
node.append("circle")
.attr("r", 5)
.attr("fill","#eee");
node.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function (d) {
return d.name;
});
force.nodes(graph.nodes)
.links(graph.links)
.on("tick", tick)
.start();
i++;
function tick() {
link.selectAll("line")
.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 + ")";
});
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>

Return simulation and radius value for Bubble Graph after applying nest() in D3 v4

I refered to this video in YouTube to make a bubble graph. However, the author didn't use a nest function to group his data. After I pre-processed my data using nest() function, I don't know how to pass the value to a function called radiusScale() in my code. I was thinking maybe I should pass the value of
d3.entries(groupByAgeAndtime)[i]["value"]
to radiusScale().
Here is my code snippet for my problem.
var radiusScale = d3.scaleSqrt()
.domain([d3.min(Object.values(groupByAgeAndtime), function(d){
return d.mean_time_in_hospital;
}),d3.max(Object.values(groupByAgeAndtime), function(d){
return d.mean_time_in_hospital;
})])
.range([50,150]);
for (i = 0; i < 10; i++)
{
console.log(d3.entries(groupByAgeAndtime)[i]["value"]);
}
var simulation = d3.forceSimulation()
.force("x",d3.forceX(width/2).strength(0.05))
.force("y",d3.forceY(height/2).strength(0.05))
.force("collide", d3.forceCollide(function(d){
return radiusScale(d.mean_time_in_hospital) + 2;
}))
var circles = svg.selectAll(".artist")
.data(groupByAgeAndtime)
.enter()
.append("circle")
.attr("class","artist")// the "artist" will transform into class name in HTML
.attr("r", function(d){
return radiusScale(Object.values(groupByAgeAndtime))
})
.attr("fill","lightblue")
.on("click",function(d){
console.log(d)
})
This is the screenshot: for the thing I want to pass to the function radiusScale. I think after passing the correct value, the circle will appear immediately. If not, can anyone tell me what is the value I should pass to get a circle?
Here is my JSFiddle for my js, html and .csv file. I would really appreciate anyone who can tell me what value should I pass to the function.
The grouped data groupByAgeAndtime using d3.nest() has to be used on your simulation and circle drawing.
Note that your radiusScale now gets the correct value to be mapped to chosen range range([50, 150]);
var simulation = d3.forceSimulation()
.force("x", d3.forceX(width / 2).strength(0.05))
.force("y", d3.forceY(height / 2).strength(0.05))
.force("collide", d3.forceCollide(function(d) {
return radiusScale(d.mean_time_in_hospital);
}))
simulation.nodes(Object.values(groupByAgeAndtime))
.on('tick', ticked)
The same for the circles, and the circles radius now matches the simulation radius
var circles = svg.selectAll(".artist")
.data(Object.values(groupByAgeAndtime))
.enter()
.append("circle")
.attr("class", "artist")
.attr("r", function(d) {
return radiusScale(d.mean_time_in_hospital)
})
.attr("fill", "lightblue")
.on("click", function(d) {
console.log(d)
})
Here is the functional example, your text still needs to be implemented.
I've pasted your csv data here https://hastebin.com/raw/pasacimala
(function() {
var width = 800,
height = 350;
var svg = d3.select("#chart")
.append("svg")
.attr("height", height)
.attr("width", width)
.attr("viewBox", `0 0 ${width} ${height}`)
.attr("preserveAspectRatio","xMidYMid meet")
.append("g")
.attr("transform", "translate(0,0)");
// import csv file
d3.csv("https://cors-anywhere.herokuapp.com/https://hastebin.com/raw/pasacimala")
.then(function(d) {
//data preprocessing
d.forEach(e => {
e.age = e.age.replace("[", "").replace(")", "");
e.time_in_hospital = + e.time_in_hospital;
});
return d; //must return something
})
.then((data, err) => ready(err, data))
function ready(error, datapoints) {
var groupByAgeAndtime = d3.nest()
.key(function(d) {
return d.age;
})
//.key(function(d) { return d.time_in_hospital; })
.rollup(function(v) {
return {
mean_time_in_hospital: d3.mean(v, function(d) {
return d.time_in_hospital;
})
}
})
.object(datapoints); //specify the dataset used
/**************************************** SCALING PART **************************************************/
var radiusScale = d3.scaleSqrt()
.domain([d3.min(Object.values(groupByAgeAndtime), function(d) {
return d.mean_time_in_hospital;
}), d3.max(Object.values(groupByAgeAndtime), function(d) {
return d.mean_time_in_hospital;
})])
.range([50, 150]);
/* for (i = 0; i < 10; i++) {
//console.log(d3.entries(groupByAgeAndtime)[i]["key"]);
console.log(d3.entries(groupByAgeAndtime)[i]["value"]);
} */
console.log(Object.values(groupByAgeAndtime))
// STUCK HERE
var simulation = d3.forceSimulation()
.force("x", d3.forceX(width / 2).strength(0.05))
.force("y", d3.forceY(height / 2).strength(0.05))
.force("collide", d3.forceCollide(function(d) {
return radiusScale(d.mean_time_in_hospital);
}))
// END OF STUCK HERE
var circles = svg.selectAll(".artist")
.data(Object.values(groupByAgeAndtime))
.enter()
.append("circle")
.attr("class", "artist")
.attr("r", function(d) {
return radiusScale(d.mean_time_in_hospital)
})
.attr("fill", "lightblue")
.on("click", function(d) {
console.log(d)
})
// append = add something
// text
var texts = svg.selectAll('.text')
.data(Object.keys(groupByAgeAndtime))
.enter()
.append('text')
.text(e => e)
.attr("text-anchor", "middle")
.attr('color', 'black')
.attr('font-size', '13')
simulation.nodes(Object.values(groupByAgeAndtime))
.on('tick', ticked)
function ticked() {
texts
.attr("x", function(d) {
return d.x
})
.attr("y", function(d) {
return d.y
})
circles
.attr("cx", function(d) {
return d.x
})
.attr("cy", function(d) {
return d.y
})
}
}
})();
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="chart"></div>

JS- how to remove duplicate JSON nodes and add one link for all nodes that get merged

enter code here I have a JSON File from which I want to create a d3 directed
graph with arrows in the direction of higher influence score
{"nodes":[{"Name":"GJA","influenceScore":81.0,"type":10.0},
{"Name":"JJZ","influenceScore":82.6,"type":30.0},
{"Name":"SAG","influenceScore":89.0,"type":30.0},
{"Name":"JJZ","influenceScore":82.6,"type":30.0}],"links":
[{"source":0,"target":0,"type":"SA","value":1},
{"source":0,"target":1,"type":"SA","value":1},
{"source":0,"target":2,"type":"SA","value":1},
{"source":0,"target":3,"type":"SA","value":1}]}
I am a d3novice, so would like some help from experts here
My d3 code is here:
.link {
stroke: #ccc;
}
.node text {
pointer-events: none;
font: 12px sans-serif;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
var width = 1200,
height = 900;
var color = d3.scale.category10();
var fill = d3.scale.category10();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.gravity(0.052)
.distance(350)
.charge(-20)
.size([width, height]);
d3.json("\\abc.json", function(error, json) {
if (error) throw error;
force
.nodes(json.nodes)
.links(json.links)
.start();
var link = svg.selectAll(".link")
.data(json.links)
.enter().append("line")
.attr("class", "link").style("stroke-width", function(d) { return
Math.sqrt(d.value); }).style("stroke", function(d) {return
fill(d.value);});
var node = svg.selectAll(".node")
.data(json.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("class", "node")
.attr("r", function(d) { return (d.influenceScore/10) + 10;
}).style("fill", function(d) { return color(d.type); });
node.append("text")
.attr("dx", -35)
.attr("dy", "4.5em").text(function(d) { return d.Name });
node.append("title").text(function(d) { return d.Name ;});
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 "translate(" + d.x + "," + d.y +
")"; });
});
});
I am getting the following image
I would like the target node e.g JJZ here just to occur once ( currently it's occurring as many number of times as it is repeated in the JSON i.e 2 times in the given example) however the line joining the two nodes should increase in thickness depending on the number of times the nodes repeat. so the blue line linking JJZ with GJA should be thicker than GJA and SAG and if another node occurs 5 times that should be thicker than JJZ and GJA. Also how do I insert directed arrows in the direction of a higher influence score
Your question here has little to do with D3: you can manipulate your array with plain JavaScript.
This function looks for the objects on json.nodes based on the property Name. If it doesn't exist, it pushes the object into an array that I named filtered. If it already exists, it increases the value of count in that object:
var filtered = []
json.nodes.forEach(function(d) {
if (!this[d.Name]) {
d.count = 0;
this[d.Name] = d;
filtered.push(this[d.Name])
}
this[d.Name].count += 1
}, Object.create(null))
Here is the demo:
var json = {"nodes":[{"Name":"GJA","influenceScore":81.0,"type":10.0},
{"Name":"JJZ","influenceScore":82.6,"type":30.0},
{"Name":"SAG","influenceScore":89.0,"type":30.0},
{"Name":"JJZ","influenceScore":82.6,"type":30.0}],"links":
[{"source":0,"target":0,"type":"SA","value":1},
{"source":0,"target":1,"type":"SA","value":1},
{"source":0,"target":2,"type":"SA","value":1},
{"source":0,"target":3,"type":"SA","value":1}]};
var filtered = []
json.nodes.forEach(function(d){
if(!this[d.Name]){
d.count = 0;
this[d.Name] = d;
filtered.push(this[d.Name])
}
this[d.Name].count += 1
}, Object.create(null))
console.log(filtered)
Then, you just need to use the property count to set the stroke-width of your links.

Reload d3.js graph on node click

I've had a d3 graph with a bunch of nodes based off items. When I click on one of those nodes, the graph is reloaded with data based off the clicked node.
I use a URL structure like so:
http://siteurl.com/index.html?item=
When a node is clicked, I have a function that runs the d3.json( function again with the new URL and then executes the update function again.
I've recently changed my code so that the node word appears below the node. Now I get an 'undefined is not a function' error on the line of code with node.exit().remove();
EDIT: Issue fixed from #Elijah's answer, but does not resolve my issue.
So when I click on a node, links get removed, then regenerated, but the nodes from the previous graph remain.
JSFiddle
Here's some of my JS
$wordToSearch = "bitter";
var w = 960,
h = 960,
node,
link,
root,
title;
var jsonURL = 'http://desolate-taiga-6759.herokuapp.com/word/' + $wordToSearch;
d3.json(jsonURL, function(json) {
root = json.words[0]; //set root node
root.fixed = true;
root.x = w / 2;
root.y = h / 2 - 80;
update();
});
var force = d3.layout.force()
.on("tick", tick)
.charge(-700)
.gravity(0.1)
.friction(0.9)
.linkDistance(50)
.size([w, h]);
var svg = d3.select(".graph").append("svg")
.attr("width", w)
.attr("height", h);
//Update the graph
function update() {
var nodes = flatten(root),
links = d3.layout.tree().links(nodes);
// Restart the force layout.
force
.nodes(nodes)
.links(links)
.start();
// Update the links…
link = svg.selectAll("line.link")
.data(links, function(d) { return d.target.id; });
// Enter any new links.
link.enter().insert("svg:line", ".node")
.attr("class", "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; });
// Exit any old links.
link.exit().remove();
// Update the nodes…
node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", 10)
.on("click", click)
.style("fill", "red");
node.append("text")
.attr("dy", 10 + 15)
.attr("text-anchor", "middle")
.text(function(d) { return d.word });
svg.selectAll(".node").data(nodes).exit().remove();
}
function tick() {
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 + ")"; });
}
/***********************
*** CUSTOM FUNCTIONS ***
***********************/
//Request extended JSON objects when clicking a clickable node
function click(d) {
$wordClicked = d.word;
var jsonURL = 'http://desolate-taiga-6759.herokuapp.com/word/' + $wordClicked;
console.log(jsonURL);
updateGraph(jsonURL);
}
// Returns a list of all nodes under the root.
function flatten(root) {
var nodes = [], i = 0;
function recurse(node) {
if (node.children) node.size = node.children.reduce(function(p, v) { return p + recurse(v); }, 0);
if (!node.id) node.id = ++i;
nodes.push(node);
return node.size;
}
root.size = recurse(root);
return nodes;
}
//Update graph with new extended JSON objects
function updateGraph(newURL) {
d3.json(newURL, function(json) {
root = json.words[0]; //set root node
root.fixed = true;
root.x = w / 2;
root.y = h / 2 - 80;
update();
});
}
function getUrlParameter(sParam)
{
var sPageURL = window.location.search.substring(1);
var sURLVariables = sPageURL.split('&');
for (var i = 0; i < sURLVariables.length; i++)
{
var sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] == sParam) {
return sParameterName[1];
}
}
}
Does anyone have any ideas why thats not working please?
EDIT: Updated my JS based from #Elijah's answer.
Handle the 3 states enter, exit and update, separate from each other:
node = svg.selectAll(".node")
.data(nodes); // base data selection, this is the update
var nodeE = node
.enter(); // handle the enter case
var nodeG = nodeE.append("g")
.attr("class", "node")
.call(force.drag); // add group ON ENTER
nodeG.append("circle")
.attr("r", 10)
.on("click", click)
.style("fill", "red"); // append circle to group ON ENTER
nodeG.append("text")
.attr("dy", 10 + 15)
.attr("text-anchor", "middle")
.text(function(d) { return d.word }); // append text to group ON ENTER
node.exit().remove(); // handle exit
Update fiddle here.
Your problem is that here:
node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
You're defining node as svg.selectAll(".node").enter() which means your variable now refers to the selection enter behavior and not the selection itself. So when you try to change exit behavior on it with: node.exit().remove();
..you're trying to access the .exit() behavior not of the selection but of the selection's .enter() behavior. Replace that with:
svg.selectAll(".node").data(nodes).exit().remove();
And that should fix your problem. There may be something else going on, but that's definitely going to cause issues.
Edited to add:
You should also update your tick function so that it doesn't reference node which is now assigned to the #selection.enter() and not the selection and instead reference the selection:
svg.selectAll("g.node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });

Categories