Objective: center a node in the viewing area following search
I created a force directed graph that enables a user to search for a node by name. When a node is searched, the selected node remains visible while all other nodes temporarily reduce opacity to highlight the searched node. Now, I would also like to center the searched node in the viewing area. I have attempted numerous methods, but have been unsuccessful in translating the entire graph including node labels. Any help would be greatly appreciated.
jsfiddle (note: autocomplete search works perfectly when run from my script but seems to fail in jsfiddle): https://jsfiddle.net/dereksmith5822/73j63nn0/
<script>
var width = 900,
height = 590;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.behavior.zoom().scaleExtent([0.1,5]).on("zoom", redraw)).on("dblclick.zoom", null)
.append('g');
//INPUT DATA
var links = [
{source: 'N1', target: 'N2'},
{source: 'N1', target: 'N3'},
{source: 'N2', target: 'N3'},
{source: 'N3', target: 'N4'},
];
var nodes = [
{id: 'N1', name: 'A'},
{id: 'N2', name: 'B'},
{id: 'N3', name: 'C'},
{id: 'N4', name: 'D'},
];
//CONNECTIONS
var hash_lookup = [];
nodes.forEach(function(d, i) {
hash_lookup[d.id] = d;
});
links.forEach(function(d, i) {
d.source = hash_lookup[d.source];
d.target = hash_lookup[d.target];
});
//FORCE LAYOUT
var force = d3.layout.force()
.size([width, height])
.nodes(d3.values(nodes))
.links(links)
.on('tick', tick)
.linkDistance(100)
.gravity(.15)
.friction(.8)
.linkStrength(1)
.charge(-425)
.chargeDistance(600)
.start();
//LINKS
var link = svg.selectAll('.link')
.data(links)
.enter().append('line')
.attr('class', 'link');
//NODES
var node = svg.selectAll('.node')
.data(force.nodes())
.enter().append('circle')
.attr('class', 'node')
.attr('r', width * 0.01)
//LABELS
var text_center = false;
var nominal_text_size = 12;
var max_text_size = 22;
var nominal_base_node_size = 8;
var max_base_node_size = 36;
var size = d3.scale.pow().exponent(1)
.domain([1,100])
.range([8,24]);
var text = svg.selectAll(".text")
.data(nodes)
.enter().append("text")
.attr("dy", ".35em")
.style("font-size", nominal_text_size + "px")
if (text_center)
text.text(function(d) { return d.name; })
.style("text-anchor", "middle");
else
text.attr("dx", function(d) {return (size(d.size)|| nominal_base_node_size);})
.text(function(d) { return '\u2002'+d.name; });
//ZOOM AND PAN
function redraw() {
svg.attr("transform",
"translate(" + d3.event.translate + ")"
+ " scale(" + d3.event.scale + ")");
}
var drag = force.drag()
.on("dragstart", function(d) {
d3.event.sourceEvent.stopPropagation();
});
//NODES IN SPACE
function tick(e) {
text.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
node.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.call(force.drag);
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; });
};
//AUTOCOMPLETE SEARCH
var optArray = [];
for (var i = 0; i < nodes.length - 1; i++) {
optArray.push(nodes[i].name);
}
optArray = optArray.sort();
$(function () {
$("#search").autocomplete({
source: optArray
});
});
function searchNode() {
var selectedVal = document.getElementById('search').value;
if (selectedVal == 'none') {}
else {
var selected = node.filter(function (d, i) {
return d.name != selectedVal;
});
var selectedText = text.filter(function (d, i) {
return d.name != selectedVal;
});
selected.style("opacity", "0");
selectedText.style("opacity", "0");
var link = svg.selectAll(".link")
link.style("opacity", "0");
d3.selectAll(".node, .link, .text").transition()
.duration(3000)
.style("opacity", '1');
}
}
</script>
Here is an idea how you can do it:
Save your zoom behaviour:
var zoom = d3.behavior.zoom().scaleExtent([0.1,5]).on("zoom", redraw);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom).on("dblclick.zoom", null)
.append('g');
In searchNode function:
var selectedNode = node
.filter(function (d, i) { return d.name == selectedVal; })
.datum();
var desiredPosition = { x: 100, y: 100 }; // constants, set to svg center point
zoom.translate([desiredPosition.x - selectedNode.x, desiredPosition.y - selectedNode.y]);
zoom.event(svg);
This code does not respect zoom's scale. I think you can just get zoom.scale() and multiply selectedNode coordinates by it.
Related
I am new to D3 and working on a project, trying to add new nodes to my graph on mousedown event but the problem is that I am getting X and Y as NaN for these new nodes. The data I am getting is from visaul basic program and not possible to load json directly.
The data is provided to me in .js format and use require library to load. I have a very short scenario of my problem.
<script src="firstloaddata.js"></script>
<script src="resources/d3.v3.min.js"></script>
<script src="resources/Require/require.js"></script>
<script>
var nodes = {};
var nodes_current = {};
var width = screen.width - 50;
var height = 900;
var links = links_current;
var nominal_text_size = 12;
var link_distance = 100;
processnodes();
var force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(link_distance)
.charge(-1700)
.friction(0.9);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var nodes = force.nodes(),
links = force.links();
force.on("tick", function() {
svg.selectAll(" line.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;
})
.attr("stroke-width", function(d) {
return (d.relCol != "grey") ? 3 : 1.5;
})
.attr("stroke", function(d) {
return d.relCol;
});
svg.selectAll("g.node")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
});
function processnodes(){
// Compute the distinct nodes from the links.
links.forEach(function(link) {
link.source = nodes[link.source] || (nodes[link.source] = {
id: link.source,
caption: link.sCaption,
});
link.target = nodes[link.target] || (nodes[link.target] = {
id: link.target,
caption: link.tCaption,
});
});
}
function restart() {
var link = svg.selectAll("line.link");
link = link.data(links);
link.enter().append("line")
.style("stroke-width", 2.5)
.attr("stroke", "red")
.attr("class", "link")
.attr("xID", links.source);
link.exit()
.remove();
var node = svg.selectAll("g.node");
node = node.data(nodes);
node.enter().append("g")
.attr("class", "node")
.call(force.drag)
.on("mousedown", mousedownNode);
node.append("svg:circle")
.attr("class", "circle")
.attr("r", 8);
node.append("text")
.style("font-size", nominal_text_size)
.attr("dy", ".35em")
.attr("x", "1.5em")
.text(function(d) {
return d.caption;
});
node.exit()
.remove();
force.start();
}
function mousedownNode(){
requirejs(['newdata.js?_r=' + Math.random()], function() {
// Adding links to our already list
links_current.forEach(function(link) {
link.source = nodes_current[link.source] || (nodes_current[link.source] = {
id: link.source,
caption: link.sCaption
});
link.target = nodes_current[link.target] || (nodes_current[link.target] = {
id: link.target,
caption: link.tCaption
});
});
for (i in links_current){
links.push(links_current[i]);
}
// Added nodes to our list
nodes_id = [];
for (i in nodes){
nodes_id.push(nodes[i].id);
}
for (j in nodes_current){
if(!nodes_id.includes(nodes_current[j].id)){
nodes.push(nodes_current[j]);
}
}
restart();
});
}
restart();
</script>
Here are the two JS files I am trying to load -
firstloaddata.js
var links_current = [
{
source: "1",
sCaption: "A",
target: "2",
tCaption: "B"
},
{
source: "2",
sCaption: "B",
target: "3",
tCaption: "C"
}
];
newdata.js
var links_current = [
{
source: "2",
sCaption: "B",
target: "3",
tCaption: "C"
},
{
source: "2",
sCaption: "B",
target: "4",
tCaption: "D"
}
];
Here is the screenshot of the error from chrome -
Error_NaN
I have already tried solution from these sources -
http://bl.ocks.org/emeeks/2432083
D3 force-directed graph adding new nodes causes x & y to be NaN
NaN x and y values in Pack Layout nodes using d3.js
and many other sources.
Thanks a lot for your time and help.
The value of X and Y are resolved by emptying the nodes and pushing the new nodes. Then restarting the force layout, it automatically assigns X and Y values to the nodes.
nodes = [];
let count = 0;
for (j in nodes_current) {
if(count < nodes_current.length ){
nodes[count] = nodes_current[j];
count = count + 1;
}
restart();
I've been wrestling very hard with D3 to try to make a simple bubble-chart using force collide that live-updates the bubble size.
I can get the chart to show on the first data update with force collide. However subsequent data calls freeze the chart and the sizes are never updated:
https://jsfiddle.net/d2zcfjfa/1/
node = svg.selectAll('g.node')
.data(root.children)
.enter()
.append('g')
.attr('class', 'node')
.append('circle')
.attr('r', function(d) { return d.r * 1.4; })
.attr('fill', function(d) { return color(d.data.name); })
.call(d3.drag()
.on("start", dragStart)
.on("drag", dragged)
.on("end", dragEnd));
var circleUpdate = node.select('circle')
.attr('r', function(d)
{
return d.r;
});
simulation.nodes(root.children);
I can get updates to work but only without using the collide simulation as seen here:
https://jsfiddle.net/rgdox7g7/1/
node = svg.selectAll('g.node')
.data(root.children)
.enter()
.append('g')
.attr('id', function(d) { return d.id; })
.attr('class', 'node')
.attr('transform', function(d)
{
return "translate(" + d.x + "," + d.y + ")";
});
var nodeUpdate = svg.selectAll('g.node')
.transition()
.duration(2000)
.ease(d3.easeLinear);
var circleUpdate = nodeUpdate.select('circle')
.attr('r', function(d)
{
return d.r;
});
node.append("circle")
.attr("r", function(d) { return d.r; })
.style('fill', function(d) { return color(d.data.name); });
Everything I have tried to mix these two solutions together simply does not work. I have scoured the internet for other examples and nothing I can find is helping. Can someone please help me understand what to do? I never thought D3 would be such a frustration!
stackoverflow: the place where you have to answer your own questions.
here is my working solution:
https://jsfiddle.net/zc0fgh6y/
var subscription = null;
var width = 600;
var height = 300;
var maxSpeed = 1000000;
var pack = d3.pack().size([width, height]).padding(0);
var svg = d3.select('svg');
var node = svg.selectAll("g.node");
var root;
var nodes = [];
var first = true;
var scaleFactor = 1.4;
var color = d3.interpolateHcl("#0faac3", "#dd2323");
var forceCollide = d3.forceCollide()
.strength(.8)
.radius(function(d)
{
return d.r;
}).iterations(10);
var simulationStart = d3.forceSimulation()
.force("forceX", d3.forceX(width/2).strength(.04))
.force("forceY", d3.forceY(height/2).strength(.2))
.force('collide', forceCollide)
.on('tick', ticked);
var simulation = d3.forceSimulation()
.force("forceX", d3.forceX(width/2).strength(.0005))
.force("forceY", d3.forceY(height/2).strength(.0025))
.force('collide', forceCollide)
.on('tick', ticked);
function ticked()
{
if (node)
{
node.attr('transform', function(d)
{
return "translate(" + d.x + "," + d.y + ")";
}).select('circle').attr('r', function(d)
{
return d.r;
});
}
}
function rand(min, max)
{
return Math.random() * (max - min) + min;
};
setInterval(function()
{
var hosts = [];
for (var i = 0; i < 100; i++)
{
hosts.push({name: i, cpu: rand(10,100), speed: rand(0,maxSpeed)});
}
root = d3.hierarchy({children: hosts})
.sum(function(d)
{
return d.cpu ? d.cpu : 0;
});
var leaves = pack(root).leaves().map(function(item)
{
return {
id: 'node-'+item.data.name,
name: item.data.name,
r: item.r * scaleFactor,
x: width/2,
y: height/2,
cpu: item.data.cpu,
speed: item.data.speed
};
});
for (var i = 0; i < leaves.length; i++)
{
if (nodes[i] && nodes[i].id == leaves[i].id)
{
var oldR = nodes[i].newR;
nodes[i].oldR = oldR;
nodes[i].newR = leaves[i].r;
nodes[i].cpu = leaves[i].cpu;
nodes[i].speed = leaves[i].speed;
}
else
{
nodes[i] = leaves[i];
//nodes[i].r = 1;
nodes[i].oldR = 1;//nodes[i].r;
nodes[i].newR = leaves[i].r;
}
}
if (first)
{
first = false;
node = node.data(nodes, function(d) { return d.id; });
node = node.enter()
.append('g')
.attr('class', 'node');
node.append("circle")
.style("fill", 'transparent');
node.append("text")
.attr("dy", "0.3em")
.style('fill', 'transparent')
.style("text-anchor", "middle")
.text(function(d)
{
return d.name;//.substring(0, d.r / 4);
});
// transition in size
node.transition()
.ease(d3.easePolyInOut)
.duration(950)
.tween('radius', function(d)
{
var that = d3.select(this);
var i = d3.interpolate(1, d.newR);
return function(t)
{
d.r = i(t);
that.attr('r', function(d)
{
return d.r;
});
simulationStart.nodes(nodes).alpha(1);
}
});
// fade in text color
node.select('text')
.transition()
.ease(d3.easePolyInOut)
.duration(950)
.style('fill', 'white');
// fade in circle size
node.select('circle')
.transition()
.ease(d3.easePolyInOut)
.duration(950)
.style('fill', function(d)
{
return color(d.speed / maxSpeed);
});
}
else
{
// transition to new size
node.transition()
.ease(d3.easeLinear)
.duration(950)
.tween('radius', function(d)
{
var that = d3.select(this);
var i = d3.interpolate(d.oldR, d.newR);
return function(t)
{
d.r = i(t);
that.attr('r', function(d)
{
return d.r;
});
simulation.nodes(nodes).alpha(1);
}
});
// transition to new color
node.select('circle')
.transition()
.ease(d3.easeLinear)
.duration(950)
.style('fill', function(d)
{
return color(d.speed / maxSpeed);
});
}
}, 1000);
Using D3.js's force layout I'm trying to make the links automatically generate based on the node data. The nodes already appear exactly as expected.
The database json strings use the following format:
{
"id": 1,
"title": "name"
},
{
"id": 2,
"title": "other name",
"primaryDependants": 1
}
and I'm basically trying to have it say:
'for each node, check if it has a primaryDependant property, if it does then make a link between that node and the one identified as the primary dependant.'
however I haven't dealt with force graphs before and tutorials are few and far between so I'm really struggling with how to make any change to my code without breaking it. Currently it is based on the answer here and I use the Meteor framework if that's of any relevance.
Template.tree.rendered = function () {
var graph = new myGraph("#svgdiv");
Lessons.find().observe({
added: function (doc) {
graph.addNode(doc._id, doc.title);
},
removed: function (doc) {
graph.removeNode(doc._id);
}
});
function myGraph(el) {
w = 1500,
h = 1000;
var svg = d3.select(el)
.append("svg")
.attr("width", w)
.attr("height", h)
.attr("pointer-events", "all")
svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.attr("fill", "lightgrey");
var vis = svg.append('g');
var force2 = d3.layout.force();
var links = force2.links();
var nodes = force2.nodes();
var update = function () {
var link = vis.selectAll("line")
.data(links, function(d) {return d.source.id + "-" + d.target.id;});
link.enter().append("line")
.attr("id",function(d){return d.source.id + "-" + d.target.id;})
.attr("class","link");
link.append("title")
.text(function(d){
return d.value;
});
link.exit().remove();
var node = vis.selectAll("g")
.data(nodes, function(d) { return d.id;});
var nodeEnter = node.enter()
.append("g")
.call(force2.drag)
.append("circle")
.attr("r", 8)
.attr("fill", "#585858")
.attr("stroke", "#008db7")
.attr("stroke-width", 3)
.attr("id", function(e) { return "Node;"+e.id;})
.attr("class", ( function(f){return f.id;}));
node.exit().remove();
force2.on("tick", function() {
node.attr("transform", function(g) { return "translate(" + g.x + "," + g.y + ")"; });
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; });
});
force2
.gravity(.02)
.linkDistance( 200 )
.size([w, h])
.start();
};
update();
var findNode = function(id) {
for (var i in nodes) {
if (nodes[i]["id"] === id) return nodes[i];};
};
var findNodeIndex = function(id) {
for (var i=0;i<nodes.length;i++) {
if (nodes[i].id==id){
return i;
}
};
};
this.addNode = function (id, title) {
nodes.push({"id":id,"title":title});
update();
};
this.addLink = function (source, target, value) {
links.push({"source":findNode(source),"target":findNode(target),"value":value});
update();
};
this.removeNode = function (id) {
var i = 0;
var n = findNode(id);
nodes.splice(findNodeIndex(id),1);
update();
};
}
To create the links array based on your description:
var dataset = [{
"id": 1,
"title": "name"
},
{
"id": 2,
"title": "other name",
"primaryDependants": 1
}];
var links = [];
dataset.forEach(function(d){
if(d.primaryDependants){
links.push({source: d.id,
target: d.primaryDependants});
}});
console.log(links)
I have been trying to project a heat map with data loaded from csv onto a orthogonal projection on D3. While rotating the earth (i.e. D3, orthogonal projection), the points/circles remain static. I have tried many combinations but failed to figure out what is missing.
Basically, i need the small circles move along the path of countries.
Here is the complete code :
<script>
var width = 600,
height = 500,
sens = 0.25,
focused;
//Setting projection
var projection = d3.geo.orthographic()
.scale(245)
.rotate([0,0])
.translate([width / 2, height / 2])
.clipAngle(90);
var path = d3.geo.path()
.projection(projection);
//SVG container
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// Define the gradient
var gradient = svg.append("svg:defs")
.append("svg:linearGradient")
.attr("id", "gradient")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "100%")
.attr("y2", "100%")
.attr("spreadMethod", "pad");
// Define the gradient colors
gradient.append("svg:stop")
.attr("offset", "0%")
.attr("stop-color", "#FFFF00")
.attr("stop-opacity", 0);
gradient.append("svg:stop")
.attr("offset", "100%")
.attr("stop-color", "#FF0000")
.attr("stop-opacity", 1);
//Adding water
svg.append("path")
.datum({type: "Sphere"})
.attr("class", "water")
.attr("d", path)
var countryTooltip = d3.select("body").append("div").attr("class", "countryTooltip"),
countryList = d3.select("body").append("select").attr("name", "countries");
queue()
.defer(d3.json, "world-110m.json")
.defer(d3.tsv, "world-110m-country-names.tsv")
.await(ready);
//Main function
function ready(error, world, countryData) {
var countryById = {},
countries = topojson.feature(world, world.objects.countries).features;
//Adding countries to select
countryData.forEach(function(d) {
countryById[d.id] = d.name;
option = countryList.append("option");
option.text(d.name);
option.property("value", d.id);
});
//circles for heatmap are coming from the csv below
d3.csv("cities.csv", function(error, data) {
svg.selectAll("circle")
.data(data)
.enter()
.append("a")
.attr("xlink:href", function(d) {
return "https://www.google.com/search?q="+d.city;}
)
.append("circle")
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];
})
.attr("r", 5.5)
.attr('fill', 'url(#gradient)');
var world = svg.selectAll("path.circle")
.data(countries) //countries from the tsc file is used to populate the names
.enter().append("path")
.attr("class", "land")
.attr("d", path)
//.attr('fill', 'url(#gradient)')
//Drag event
.call(d3.behavior.drag()
.origin(function() { var r = projection.rotate(); return {x: r[0] / sens, y: -r[1] / sens}; })
.on("drag", function() {
var rotate = projection.rotate();
projection.rotate([d3.event.x * sens, -d3.event.y * sens, rotate[2]]);
svg.selectAll("path.land").attr("d", path);
svg.selectAll(".focused").classed("focused", focused = false);
}))
//Mouse events
.on("mouseover", function(d) {
countryTooltip.text(countryById[d.id])
.style("left", (d3.event.pageX + 7) + "px")
.style("top", (d3.event.pageY - 15) + "px")
.style("display", "block")
.style("opacity", 1);
})
.on("mouseout", function(d) {
countryTooltip.style("opacity", 0)
.style("display", "none");
})
.on("mousemove", function(d) {
countryTooltip.style("left", (d3.event.pageX + 7) + "px")
.style("top", (d3.event.pageY - 15) + "px");
});
});//closing d3.csv here
//Country focus on option select
d3.select("select").on("change", function() {
var rotate = projection.rotate(),
focusedCountry = country(countries, this),
p = d3.geo.centroid(focusedCountry);
svg.selectAll(".focused").classed("focused", focused = false);
//Globe rotating
(function transition() {
d3.transition()
.duration(2500)
.tween("rotate", function() {
var r = d3.interpolate(projection.rotate(), [-p[0], -p[1]]);
return function(t) {
projection.rotate(r(t));
svg.selectAll("path").attr("d", path)
.classed("focused", function(d, i) { return d.id == focusedCountry.id ? focused = d : false; });
//svg.selectAll("circle").attr("d", data)
//.classed("focused", function(d, i) { return d.id == focusedCountry.id ? focused = d : false; });
};
})
})();
});
function country(cnt, sel) {
for(var i = 0, l = cnt.length; i < l; i++) {
if(cnt[i].id == sel.value) {return cnt[i];}
}
};
};
</script>
Please help.
Thank you in advance
Here is an option, using point geometries:
.enter().append('path')
.attr('class', 'circle_el')
.attr('fill', function(d) {return d.fill; })
.datum(function(d) {
return {type: 'Point', coordinates: [d.lon, d.lat], radius: some_radius};
})
.attr('d', path);
This is cool because you will update the circles simultaneously with a path redraw. And in addition it will account for the projection as a sphere, not showing circles that should be on the non-visible side of the sphere. I got the idea from this post by Jason Davies.
I have a working D3 example. I use the force and everything works fine. But I have a small issue. Let's say I have 4 Nodes. Just see this picture: http://i.imgur.com/J0P4I0n.png and now when I click on the Node "AKZO NV" then I want to get: http://i.imgur.com/fGXVGMd.png with the old nodes and links.
So at the end I want to have 7 Nodes. And the "AKZO NV" shall be focused. All Nodes still have their links and the "AKZO NV" shall have two. I think you now know what I want.
So I already have this code. Working like a charm but it's not properly adding the new nodes and links. I think there is a small issue with the order of the commands.
Any ideas are welcome:
var alreadyThere = false;
var nodeCircles = {};
var svg, link, node;
var force = d3.layout.force();
var nodes, links;
var width = 700, height = 400;
var boxIDName = "#main-rightinfo";
var JSONFORMERGING;
function createRealGraph(jsonData){
//console.log(jsonData);
if (alreadyThere == false){
JSONFORMERGING=jsonData;
initializeGraph(jsonData);
}else{
update(JSONFORMERGING.concat(jsonData));
}
alreadyThere = true;
}
function update(jsonData) {
console.log(jsonData);
jsonData.forEach(function(link) {
link.source = nodeCircles[link.source] || (nodeCircles[link.source] = {name: link.sourceName, ID: link.source, class: link.sourceClass});
link.target = nodeCircles[link.target] || (nodeCircles[link.target] = {name: link.targetName, ID: link.target, class: link.targetClass});
});
link = link.data(links);
link.enter().insert("line")
.attr("class", "link");
node = node.data(nodes);
node.enter().append("g")
.attr("class", "node")
.attr("r", 5)
.call(force.drag);
force
.nodes(d3.values(nodeCircles))
.links(jsonData)
.start();
nodes = force.nodes();
links = force.links();
}
function initializeGraph(jsonData){
jsonData.forEach(function(link) {
link.source = nodeCircles[link.source] || (nodeCircles[link.source] = {name: link.sourceName, ID: link.source, class: link.sourceClass});
link.target = nodeCircles[link.target] || (nodeCircles[link.target] = {name: link.targetName, ID: link.target, class: link.targetClass});
});
force
.nodes(d3.values(nodeCircles))
.links(jsonData)
.size([width, height])
.linkDistance(60)
.charge(-200)
.on("tick", tick)
.start();
nodes = force.nodes();
links = force.links();
svg = d3.select("#main-right")
.append("svg")
.attr("width", width)
.attr("height", height);
svg
.append("svg:defs").selectAll("marker")
.data(["end"])
.enter().append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 27)
.attr("refY", -0.5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5")
.attr('fill', '#00b');
link = svg.selectAll(".link")
.data(links)
.enter().append("line")
.attr("class", "link")
.attr("marker-end", "url(#end)");
node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", function(d) {click(d);})
.on("dblclick", function(d) {dblclick(d);})
.call(force.drag);
node
.append("image")
.attr("xlink:href", function(d) {if (d.class == "Person") {return "pics/node_person.png";} else {return "pics/node_appln.png";} })
.attr("x", -20)
.attr("y", -20)
.attr("width", 40)
.attr("height", 40);
node
.append("text")
.attr("x", 19)
.attr("dy", ".25em")
.text( function(d) {return d.name; });
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 + ")"; });
}
}
You just need to test if it's NOT an object. That works like a charm:
jsonData.forEach(function(link) {
if (typeof(link.source) != "object"){
link.source = nodeCircles[link.source] || (nodeCircles[link.source] = {name: link.sourceName, ID: link.source, class: link.sourceClass});
}
if (typeof(link.target) != "object"){
link.target = nodeCircles[link.target] || (nodeCircles[link.target] = {name: link.targetName, ID: link.target, class: link.targetClass});
}
});
I'm working on the same problem and got a "solution". It is still a prototype, but maybe it helps you.
Every mouse click on a node calls a function and detects new nodes. They are all saved in nodes and all active, it means all nodes on the display are in activeNodes. All nodes belonging to the path from the root node to the clicked one are stored in pathNodes.The result is, that only the active path is displayed inclusivly the children of the clicked one.
Hopefully it is clearly explained. For abetter understanding please look up the source code at: http://github.com/nextlevelshit/d3_nested_nodes
For something to play with check out: http://dailysh.it/github/d3_nested_nodes/
Here the snippet of my code:
/**
* Triggering mouse click start
*/
function mousedown() {
if (mousedown_node !== null) {
var pathNodes = findPathNodesTo(mousedown_node);
var point = d3.mouse(this),
node = {
id: nodes.length,
parent: mousedown_node.id
};
node.x = point[0];
node.y = point[1];
var newNodes = findNodesbyParentId(mousedown_node.id),
startingPoint = {
x: mousedown_node.x,
y: mousedown_node.y
};
for (var i = 0; i < pathNodes.length; i++) {
newNodes.push(pathNodes[i]);
pathNodes[i].path = true;
}
var removeNodes = activeNodes.diff(newNodes);
var addNodes = newNodes.diff(pathNodes).diff(activeNodes);
for (var i = 0; i < removeNodes.length; i++) {
removeNode(removeNodes[i].id);
}
for (var i = 0; i < addNodes.length; i++) {
addNodes[i].x = startingPoint.x;
addNodes[i].y = startingPoint.y;
activeNodes.push(addNodes[i]);
activeLinks.push({source: findNode(addNodes[i].parent), target: findNode(addNodes[i].id)});
}
// TODO: Find a smoother way do delay popping out new nodes
});
}
restart();
}
The last problem is to find a smooth way for popping out new nodes...