I am having an annoying issue with my d3 force directed map I'm building where I initially render the page with the nodes and links I need, then periodically check for new information via ajax. When I need a new node and link I draw them, which is fine
However because of the way SVG layers elements, new links draw over older nodes, so where I have nodes as circles and draw lines between them, any new nodes added draw over the top of the circles on older nodes. See image below:
(http://i40.tinypic.com/4fx25j.gif)
I know it is not technically a d3 issue but there must be a way of fixing this. I did try deleting all the circles and redrawing them, but the issue is that the svg:g node it is attached to is too low in the layers so it is still drawn over.
Demo at jsfiddle - look at the following section
draw() {
...
}
as that is where the magic happens.
http://jsfiddle.net/zuzzy/uwAhy/
I have simulated the ajax using a 5 second timer, it was easier for the demo.
Any ideas?
As far as I am aware, you can only control the depth of an SVG element by it's position in the DOM.
So what might work for you is to create two groups <g id='lines'> and <g id='circles'>.
When you append your elements, all of the lines should be added to the first group, and all of the circles to the second.
You might have to alter the way that you add the elements, but so long as you make sure that the lines group appears before the circles group then you should be golden.
I apologise if this totally does not fit your implementation. I ran into a very similar problem and found the only resolution for me was to draw the 'lower' elements first.
Worked first time! I had already grouped all my elements under one so I just replaced:
var vis = d3.select("body")
.append("svg:svg")
.attr("pointer-events", "all");
.append('svg:g')
where i used vis.xxxx to render both links and circles, with
var vis = d3.select("body")
.append("svg:svg")
.attr("pointer-events", "all");
var linkvis = vis.append('svg:g')
.attr("id","link_elements");
vis = vis.append('svg:g')
.attr("id","node_elements");
and referred to linkvis when drawing the links and vis drawing the circles.
(NB I know this should be a comment but I couldn't fit it in and I thought it might be helpful for someone. #Paul's answer has been marked as the answer)
Another way of resolving this issue would be to use insert method as shown in following code.
link.enter().insert("line",".node"); //Inserts link element before the first DOM element with class node.
Posting just because this may be helpful for other users who search solution for this question.
//Settings:
//width, height and the default radius of the circles
var w = 1024,
h = 768,
r = 10;
//test data - usually this is recieved via ajax
//Initial Data:
var hosts = eval({
"ITEM003": {
"name": "ITEM003",
"parents": [],
"status": 0,
"hostgroup": "Secure"
},
"ITEM004": {
"name": "ITEM004",
"parents": [],
"status": 0,
"hostgroup": "Secure"
},
"CORE": {
"name": "CORE",
"parents": ["ITEM004", "ITEM003"],
"status": 0,
"hostgroup": "DMZ"
}
});
var mylinks = eval({
"0": ["CORE", "ITEM004"],
"1": ["CORE", "ITEM003"]
});
//Data after update
var updated_hosts = eval({
"ITEM003": {
"name": "ITEM003",
"parents": [],
"status": 0,
"hostgroup": "Secure"
},
"ITEM004": {
"name": "ITEM004",
"parents": [],
"status": 0,
"hostgroup": "Secure"
},
"CORE": {
"name": "CORE",
"parents": ["ITEM004", "ITEM003"],
"status": 0,
"hostgroup": "DMZ"
},
"REMOTE": {
"name": "REMOTE",
"parents": [],
"status": 0,
"hostgroup": ""
}
});
var updated_mylinks = eval({
"0": ["CORE", "ITEM004"],
"1": ["CORE", "ITEM003"],
"2": ["CORE", "REMOTE"]
});
//I define these here so they carry between functions - not really necessary in this jsfiddle probably
window.link = undefined;
window.node = undefined;
//make up my node object
window.nodeArray = [];
window.node_hash = [];
for (var key in hosts) {
var a = {
id: "node_" + hosts[key].name,
labelText: hosts[key].name,
status: hosts[key].status,
hostgroup: hosts[key].hostgroup,
class: "node realnode",
iconimage: hosts[key].iconimage,
added: true
};
nodeArray.push(a);
node_hash[key] = a;
}
//make up my link object
window.linkArray = [];
for (var key in mylinks) {
var linkcolor = "#47CC60";
var a = {
source: node_hash[mylinks[key][0]],
target: node_hash[mylinks[key][1]],
color: linkcolor,
class: "link reallink"
};
linkArray.push(a);
}
//make up my node text objects
//these are just more nodes with a different class
//we will append text to them later
//we also add the links to the linkArray now to bind them to their real nodes
window.text_hash = [];
for (var key in hosts) {
var a = {
id: "label_" + hosts[key].name,
text: hosts[key].name,
color: "#ffffff",
size: "6",
class: "node label",
added: true
};
nodeArray.push(a);
text_hash[key] = a;
}
//because the text labels are in the same order as the
//original nodes we know that node_hash[0] has label text_hash[0]
//it doesn't matter which we iterate through here
for (var key in text_hash) {
var a = {
source: node_hash[key],
target: text_hash[key],
class: "link label"
};
linkArray.push(a);
}
//set up the environment in a div called graph using the settings baove
window.vis = d3.select("body")
.append("svg:svg")
.attr("height", 500)
.attr("width", 500)
.attr("pointer-events", "all")
.append('svg:g')
//object to interact with the force libraries in d3
//the settings here set how the nodes interact
//seems a bit overcomplicated but it stops the diagram going nuts!
window.force = d3.layout.force()
.friction("0.7")
.gravity(function(d, i) {
if (d.class == "link reallink") {
return "0.95";
} else {
return "0.1";
}
})
.charge(function(d, i) {
if (d.class == "link reallink") {
return "-1500";
} else {
return "-300";
}
})
.linkDistance(function(d) {
if (d.class == "link reallink") {
return "120";
} else {
return "35";
}
})
.linkStrength(function(d) {
if (d.class == "link reallink") {
return "8";
} else {
return "6";
}
})
.nodes(nodeArray)
.links(linkArray)
.on("tick", tick)
node = vis.selectAll(".node");
link = vis.selectAll(".link");
//create the objects and run it
draw();
for (key in nodeArray) {
nodeArray[key].added = false;
}
//wait 5 seconds then update the diagram TO ADD A NODE
setTimeout(function() {
//update the objects
//vis.selectAll("g.node").data(nodeArray).exit().transition().ease("elastic").remove();
//vis.selectAll("line").data(linkArray).exit().transition().ease("elastic").remove();
var a = {
id: "node_REMOTE",
labelText: "REMOTE",
status: "0",
hostgroup: "",
class: "node realnode",
iconimage: "",
added: true
};
nodeArray.push(a);
node_hash["REMOTE"] = a;
var linkcolor = "#47CC60";
var a = {
source: node_hash["CORE"],
target: node_hash["REMOTE"],
color: linkcolor,
class: "link reallink"
};
linkArray.push(a);
//make up my node text objects
var a = {
id: "label_REMOTE",
text: "REMOTE",
color: "#000000",
size: "6",
class: "node label",
added: true
};
nodeArray.push(a);
text_hash["REMOTE"] = a;
var a = {
source: node_hash["REMOTE"],
target: text_hash["REMOTE"],
class: "link label"
};
linkArray.push(a);
//redraw it
draw();
}, 5000);
//----- functions for drawing and tick below
function draw() {
link = link.data(force.links(), function(d) {
return d.source.id + "-" + d.target.id;
});
node = node.data(force.nodes(), function(d) {
return d.id;
});
//create the link object using the links object in the json
//link = vis.selectAll("line").data(linkArray);
link.enter().insert("line", ".node")
.attr("stroke-width", '0')
.transition()
.duration(1000)
.ease("bounce")
.attr("stroke-width", function(d, i) {
if (d.class == 'link reallink') {
return '3';
} else {
return '0';
};
})
.style("stroke", function(d, i) {
return d.color;
})
.attr("class", function(d, i) {
return d.class;
});
//node = vis.selectAll("g").data(nodeArray);
node.enter().append("svg:g")
.attr("class", function(d) {
return d.class
})
.attr("id", function(d) {
return d.id
})
.call(force.drag);
//append to each node an svg circle element
vis.selectAll(".realnode").filter(function(d) {
return d.added;
})
.append("svg:circle")
.attr("r", "0")
.transition()
.duration(1000)
.ease("bounce")
.attr("r", "6")
.style("fill", "#000000")
.style("stroke", function(d) {
return d.color;
})
.style("stroke-width", "4");
//append to each node the attached text desc
vis.selectAll(".label").filter(function(d) {
return d.added;
})
.append("svg:text")
.attr("text-anchor", "middle")
.attr("fill", "black")
.style("pointer-events", "none")
.attr("font-size", "9px")
.attr("font-weight", "100")
.text(function(d) {
return d.text;
})
.attr("transform", "rotate(180)")
.transition()
.duration(1000)
.ease("bounce")
.attr("transform", "rotate(0)");
node.exit().transition().ease("elastic").remove();
link.exit().transition().ease("elastic").remove();
//activate it all - initiate the nodes and links
force.start();
}
function tick() {
node.attr("cx", function(d) {
return d.x = Math.max(r + 15, Math.min(w - r - 15, d.x));
})
.attr("cy", function(d) {
return d.y = Math.max(r + 15, Math.min(h - r - 15, d.y));
})
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
link.data(linkArray).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;
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Related
I am now to d3 and web development in general.
I am creating a graph using the d3 library. I am trying to ensure that whenever the user hovers upon a node, the opacity of its immediate parents and children should remain the same but the opacity of the rest of the nodes should decrease.
I am partly achieving my goal by letting the text written below fade for all the ones other than the one I hover on.
Here is my javascript code:
// setting up the canvas size :)
var width = 960,
height = 500;
// initialization
var svg = d3.select("div").append("svg")
.attr("width", width)
.attr("height", height)
.attr("id", "blueLine"); // the graph invisible thing :)
var force = d3.layout.force()
.gravity(0) // atom's cohesiveness / elasticity of imgs :)
.distance(150) // how far the lines ---> arrows :)
.charge(-50) // meta state transition excitement
.linkDistance(140)
//.friction(0.55) // similar to charge for quick reset :)
.size([width, height]); // degree of freedom to the canvas
// exception handling
d3.json("graph.json", function(error, json) {
if (error) throw error;
// Restart the force layout
force
.nodes(json.nodes)
.links(json.links)
.start();
// Build the link
var link = svg.selectAll(".links")
.data(json.links)
.enter().append("line")
.attr("class", "lol")
.style("stroke-width", "2")
.attr("stroke", function(d){
return linkColor(d.colorCode);})
.each(function(d) {
var color = linkColor(d.colorCode);
d3.select(this).attr("marker-end", marker(color));
});
function marker(color) {
svg.append("svg:marker")
.attr("id", color.replace("#", ""))
.attr("viewBox", "0 -5 10 10")
.attr("refX", 10)
.attr("refY", 0)
.attr("markerWidth", 15)
.attr("markerHeight", 15)
.attr("orient", "auto")
.attr("markerUnits", "userSpaceOnUse")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5")
.style("fill", color);
return "url(" + color + ")";
};
// this link : https://stackoverflow.com/questions/32964457/match-arrowhead-color-to-line-color-in-d3
// create a node
var node = svg.selectAll(".nodes")
.data(json.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag)
.on("mouseover", fade(.2))
.on("mouseout", fade(1));;
// Define the div for the tooltip
var div = d3.select("body").append("pre")
.attr("class", "tooltip")
.style("opacity", 0);
// Append custom images
node.append("svg:image")
.attr("xlink:href", function(d) { return d.img;}) // update the node with the image
.attr("x", function(d) { return -5;}) // how far is the image from the link??
.attr("y", function(d) { return -25;}) // --- same ---
.attr("height", 55) // size
.attr("width", 55);
node.append("text")
.attr("class", "labelText")
.attr("x", function(d) { return -5;})
.attr("y", function(d) { return 48;})
.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 + ")"; });
force.stop();
});
function linkColor(linkCode) {
switch (linkCode)
{
case 'ctoc':
return '#0000FF';//blue
break;
case 'ctof':
return '#00afaa';//green
break;
case 'ftoc':
return '#fab800';//yellow
break;
case 'ftof':
return '#7F007F';//purple
break;
default:
return '#0950D0';//generic blue
break;
}
}
// build a dictionary of nodes that are linked
var linkedByIndex = {};
links.forEach(function(d) {
linkedByIndex[d.source.id + "," + d.target.id] = 1;
});
// check the dictionary to see if nodes are linked
function isConnected(a, b) {
return linkedByIndex[a.index + "," + b.index] || linkedByIndex[b.index + "," + a.index] || a.index == b.index;
}
// fade nodes on hover
function fade(opacity) {
return function(d) {
// check all other nodes to see if they're connected
// to this one. if so, keep the opacity at 1, otherwise
// fade
node.style("stroke-opacity", function(o) {
thisOpacity = isConnected(d, o) ? 1 : opacity;
return thisOpacity;
});
node.style("fill-opacity", function(o) {
thisOpacity = isConnected(d, o) ? 1 : opacity;
return thisOpacity;
});
// also style link accordingly
link.style("stroke-opacity", function(o) {
return o[0] === d || o[2] === d ? 1 : opacity;
});
};
}
});
The css:
.node text {
font-size: 1rem;
text-decoration: underline;
fill: #aeb4bf;
font-weight: 700;
text-anchor: end;
alignment-baseline: central;
pointer-events: none;
}
.node:not(:hover) .nodetext {
display: none;
}
pre.tooltip {
position: absolute;
text-align: left;
width: auto;
height: auto;
padding: 5px;
font: 14px "Helvetica","Arial",sans-serif bold;
background: #273142;
border: 0;
border-radius: 8px;
cursor: pointer!important;
pointer-events: none;
color: #aeb4bf;
}
and my json file:
{
"nodes": [
{"x": 100, "y": 100, "name": "A", "img": "https://cdn0.iconfinder.com/data/icons/flat-round-system/512/android-128.png", "id" : 0},
{"x": 250, "y": 100, "name": "B", "img":"https://cdn0.iconfinder.com/data/icons/flat-round-system/512/android-128.png", "id" : 1},
{"x": 400, "y": 100, "name": "C", "img": "https://cdn0.iconfinder.com/data/icons/flat-round-system/512/android-128.png", "id": 2},
{"x": 550, "y": 200, "name": "D", "img":"https://cdn0.iconfinder.com/data/icons/flat-round-system/512/android-128.png", "id" : 3},
{"x": 700, "y": 200, "name": "E", "img": "https://cdn0.iconfinder.com/data/icons/flat-round-system/512/android-128.png", "id" : 4},
{"x": 100, "y": 300, "name": "F", "img": "https://cdn0.iconfinder.com/data/icons/flat-round-system/512/android-128.png", "id" : 5},
{"x": 250, "y": 300, "name": "G", "img": "https://cdn0.iconfinder.com/data/icons/flat-round-system/512/android-128.png", "id" : 6},
{"x": 400, "y": 300, "name": "H", "img": "https://cdn0.iconfinder.com/data/icons/flat-round-system/512/android-128.png", "id": 7}
],
"links": [
{"source": 0, "target": 1, "colorCode" : "ctof"},
{"source": 1, "target": 2, "colorCode" : "ftoc"},
{"source": 2, "target": 3, "colorCode" : "ctof"},
{"source": 3, "target": 4, "colorCode" : "ftoc"},
{"source": 5, "target": 6, "colorCode" : "ctof"},
{"source": 6, "target": 7, "colorCode" : "ftoc"},
{"source": 7, "target": 3, "colorCode" : "ctof"}
]
}
I dont know where I am going wrong. I need to achieve two things: 1. The immediate parents and children of X should stay unfaded if I hover over X and 2. The other nodes which aren't directly related to X should fade just the way the other links do. Currently none of the node fades.
I researched over my code and realized that it says that all the nodes are connected to each other so my isConnected() is the culprit. I still have no clue about the links though.
Please help me.
two issues to resolve
For your nodes, as they are image files, you need set their 'opacity', and not the stroke/fill opacity.
node.style("opacity", function(o) {
thisOpacity = isConnected(d, o) ? 1 : opacity;
return thisOpacity;
});
For your links, assuming the name attributes are unique, you should match the link's source and target to the chosen node's name.
link.style("stroke-opacity", function(o) {
return o.source.name === d.name || o.target.name === d.name ? 1 : opacity;
});
Addition to #TomShanley answer
Why are you using d3v3 if you are new to d3? Currently we are at d3v5 and the API has much been improved.
The program does not run out of the box because in determining linkedByIndex it complains that links does not exist. It should be json.links.
There is no need to put break after a return in linkColor.
You search for elements of class svg.selectAll(".nodes") but you create elements with .attr("class", "node"). This will not work if you want to use the enter-exit-update properly. The same with the links: search for class links but add elements with class lol.
Your markers are not unique and no need to use each to add the marker-end.
Maybe best to create a set of markers based on color and just reference them.
In the original code you have multiple tags with the same id. And an id in HTML should be unique.
// Build the link
var link = svg.selectAll(".lol")
.data(json.links)
.enter().append("line")
.attr("class", "lol")
.style("stroke-width", "2")
.attr("stroke", function(d){
return linkColor(d.colorCode);})
.attr("marker-end", function(d, i){
return marker(i, linkColor(d.colorCode));} );
function marker(i, color) {
var markId = "#marker"+i;
svg.append("svg:marker")
.attr("id", markId.replace("#", ""))
.attr("viewBox", "0 -5 10 10")
.attr("refX", 10)
.attr("refY", 0)
.attr("markerWidth", 15)
.attr("markerHeight", 15)
.attr("orient", "auto")
.attr("markerUnits", "userSpaceOnUse")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5")
.style("fill", color);
return "url(" + markId + ")";
};
Edit Unique markers, link is path from edge to edge
I have modified the code to have:
unique markers for each color put in the defs tag of the svg. Create a new marker if not already done for this color using an object to keep track.
links are now paths to apply a marker trick described by Gerardo
images are now centered on the node position, this only works for circular images.
Here is the full code
var width = 960,
height = 500;
// initialization
var svg = d3.select("div").append("svg")
.attr("width", width)
.attr("height", height)
.attr("id", "blueLine"); // the graph invisible thing :)
var svgDefs = svg.append("defs");
var force = d3.layout.force()
.gravity(0) // atom's cohesiveness / elasticity of imgs :)
.distance(150) // how far the lines ---> arrows :)
.charge(-50) // meta state transition excitement
.linkDistance(140)
//.friction(0.55) // similar to charge for quick reset :)
.size([width, height]); // degree of freedom to the canvas
// exception handling
d3.json("/fade-links.json", function(error, json) {
if (error) throw error;
var imageSize = { width:55, height:55 };
// Restart the force layout
force
.nodes(json.nodes)
.links(json.links)
.start();
var markersDone = {};
// Build the link
var link = svg.selectAll(".lol")
.data(json.links)
.enter().append("path")
.attr("class", "lol")
.style("stroke-width", "2")
.attr("stroke", function(d){
return linkColor(d.colorCode);})
.attr("marker-end", function(d){
return marker(linkColor(d.colorCode));} );
function marker(color) {
var markerId = markersDone[color];
if (!markerId) {
markerId = color;
markersDone[color] = markerId;
svgDefs.append("svg:marker")
.attr("id", color.replace("#", ""))
.attr("viewBox", "0 -5 10 10")
.attr("refX", 10)
.attr("refY", 0)
.attr("markerWidth", 15)
.attr("markerHeight", 15)
.attr("orient", "auto")
.attr("markerUnits", "userSpaceOnUse")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5")
.style("fill", color);
}
return "url(" + markerId + ")";
};
// this link : https://stackoverflow.com/questions/32964457/match-arrowhead-color-to-line-color-in-d3
// create a node
var node = svg.selectAll(".node")
.data(json.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag)
.on("mouseover", fade(.2))
.on("mouseout", fade(1));
// Define the div for the tooltip
var div = d3.select("body").append("pre")
.attr("class", "tooltip")
.style("opacity", 0);
// Append custom images
node.append("svg:image")
.attr("xlink:href", function(d) { return d.img;}) // update the node with the image
.attr("x", function(d) { return -imageSize.width*0.5;}) // how far is the image from the link??
.attr("y", function(d) { return -imageSize.height*0.5;}) // --- same ---
.attr("height", imageSize.width)
.attr("width", imageSize.height);
node.append("text")
.attr("class", "labelText")
.attr("x", function(d) { return 0;})
.attr("y", function(d) { return imageSize.height*0.75;})
.text(function(d) { return d.name });
force.on("tick", function() {
// use trick described by Gerardo to only draw link from image border to border: https://stackoverflow.com/q/51399062/9938317
link.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y;
var angle = Math.atan2(dy, dx);
var radius = imageSize.width*0.5;
var offsetX = radius * Math.cos(angle);
var offsetY = radius * Math.sin(angle);
return ( `M${d.source.x + offsetX},${d.source.y + offsetY}L${d.target.x - offsetX},${d.target.y - offsetY}`);
});
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
force.stop();
});
function linkColor(linkCode) {
switch (linkCode)
{
case 'ctoc': return '#0000FF';//blue
case 'ctof': return '#00afaa';//green
case 'ftoc': return '#fab800';//yellow
case 'ftof': return '#7F007F';//purple
}
return '#0950D0';//generic blue
}
// build a dictionary of nodes that are linked
var linkedByIndex = {};
json.links.forEach(function(d) {
linkedByIndex[d.source.id + "," + d.target.id] = 1;
});
// check the dictionary to see if nodes are linked
function isConnected(a, b) {
return linkedByIndex[a.index + "," + b.index] || linkedByIndex[b.index + "," + a.index] || a.index == b.index;
}
// fade nodes on hover
function fade(opacity) {
return function(d) {
// check all other nodes to see if they're connected
// to this one. if so, keep the opacity at 1, otherwise
// fade
node.style("opacity", function(o) {
return isConnected(d, o) ? 1 : opacity;
});
// also style link accordingly
link.style("opacity", function(o) {
return o.source.name === d.name || o.target.name === d.name ? 1 : opacity;
});
};
}
});
I have the following d3 chart. It's simply a handful of nodes and links between them. The chart does 2 things:
You can click and drag individual nodes around the graph.
You can click and drag around the svg in order to pan (and zoom with mousewheel) using d3.behavior.zoom.
My problem I'm seeing is that if I drag a node around (for example, move a node 20px to the right), and then try to pan the chart, the entire graph immediately jumps 20px to the right (per the example). d3.behavior.zoom seems to initially use the d3.event of dragging the node around or something like that. Not sure. Here is the code:
var data = {
nodes: [{
name: "A",
x: 200,
y: 150,
size: 30
}, {
name: "B",
x: 140,
y: 300,
size: 15
}, {
name: "C",
x: 300,
y: 300,
size: 15
}, {
name: "D",
x: 300,
y: 180,
size: 45
}],
links: [{
source: 0,
target: 1
}, {
source: 1,
target: 2
}, {
source: 2,
target: 3
}, ]
};
var dragging = false;
var svg = d3.select("body")
.append("svg")
.attr("width", "100%")
.attr("height", "100%")
.call(d3.behavior.zoom().on("zoom", function() {
if (dragging === false) {
svg.attr("transform", "translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")")
}
})).append("g");
var links = svg.selectAll("link")
.data(data.links)
.enter()
.append("line")
.attr("class", "link")
.attr("x1", function(l) {
var sourceNode = data.nodes.filter(function(d, i) {
return i == l.source
})[0];
d3.select(this).attr("y1", sourceNode.y);
return sourceNode.x
})
.attr("x2", function(l) {
var targetNode = data.nodes.filter(function(d, i) {
return i == l.target
})[0];
d3.select(this).attr("y2", targetNode.y);
return targetNode.x
})
.attr("fill", "none")
.attr("stroke", "white");
var nodes = svg.selectAll("node")
.data(data.nodes)
.enter()
.append("circle")
.each(function(d, i) {
d.object = d3.select(this);
console.log(d);
})
.attr("class", "node")
.attr("cx", function(d) {
return d.x
})
.attr("cy", function(d) {
return d.y
})
.attr("r", function(d) {
return d.size;
})
.on('click', function(d) {
})
.call(d3.behavior.drag()
.on('dragstart', function() {
dragging = true;
})
.on('dragend', function() {
dragging = false;
})
.on("drag", function(d, i) {
d.x += d3.event.dx
d.y += d3.event.dy
d3.select(this).attr("cx", d.x).attr("cy", d.y);
links.each(function(l, li) {
if (l.source == i) {
d3.select(this).attr("x1", d.x).attr("y1", d.y);
} else if (l.target == i) {
d3.select(this).attr("x2", d.x).attr("y2", d.y);
}
});
})
);
Here is a demo:
https://jsfiddle.net/25q1Lu3x/2/
How can I prevent that initially jump when trying to pan around? I have read a few things that suggest making an extra nested svg g element and move that around, but that doesn't seem to make a difference.
According to the D3 v3 API:
When combining drag behaviors with other event listeners for interaction events (such as having drag take precedence over zoom), you may also consider stopping propagation on the source event to prevent multiple actions
So, in your dragstart, just add d3.event.sourceEvent.stopPropagation();:
.on('dragstart', function() {
d3.event.sourceEvent.stopPropagation();
dragging = true;
});
Here is your fiddle: https://jsfiddle.net/hj7krohd/
Sorry for the silly question, I'm just a poor d3 newbie...
I have the following demo bubble diagram on JSFiddle: what I am trying to achieve is to increase the radius of the circles whenever I click over them and, after that, to adjust the pack layout accordingly.
This is the code in JSFiddle for your convenience:
text {
font: 20px sans-serif;
}
<!DOCTYPE html>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script>
var root = {
"name": "Root",
"children": [
{
"name": "Leaf One",
"children": null,
"size": 1
},
{
"name": "Leaf Two",
"children": null,
"size": 1
},
{
"name": "Leaf Three",
"children": null,
"size": 1
}
],
"size": 1
};
var diameter = 400,
format = d3.format(",d"),
color = d3.scale.category20c();
var bubble = d3.layout.pack()
.sort(null)
.size([diameter, diameter])
.padding(1.5);
var svg = d3.select("body").append("svg")
.attr("width", diameter)
.attr("height", diameter)
.attr("class", "bubble");
// not needed in JSfiddle, data is hard-coded:
// d3.json("data.json", function(error, root) {
// if (error) throw error;
var node = svg.selectAll(".node")
.data(bubble.nodes(classes(root))
.filter(function(d) { return !d.children; }))
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
node.append("title")
.text(function(d) { return d.className + ": " + format(d.value); });
node.append("circle")
.attr("r", function(d) { return d.r; })
.style("fill", function(d) { return color(d.packageName); });
node.append("text")
.attr("dy", ".3em")
.style("text-anchor", "middle")
.text(function(d) { return d.className.substring(0, d.r / 3); });
node.on("click", function(e, i){
var circle = svg.select("circle");
circle.attr("value", function(d) {
d.value += 1;
return d.value;
});
circle.attr("r", function(d) {
d.r += 1.0;
return d.r;
});
});
// });
// Returns a flattened hierarchy containing all leaf nodes under the root.
function classes(root) {
var classes = [];
function recurse(name, node) {
if (node.children) node.children.forEach(function(child) { recurse(node.name, child); });
else classes.push({packageName: name, className: node.name, value: node.size});
}
recurse(null, root);
return {children: classes};
}
d3.select(self.frameElement).style("height", diameter + "px");
</script>
I tried to do that via the node.on("click", ...) method but I got somehow stuck, as the modified circle is always the first one: what is the best way to select the circle I clicked?
Besides, how can I force the d3 pack layout to refresh after I modify the circle radius?
first, you have to remember that in event handlers the this is bound to the current DOM element (in out case - the <g>). so you can select clicked <g> by d3.select(this), and then select the circle within:
var circle = d3.select(this).select("circle");
(by just doing svg.select("circle") you select the first circle in the DOM, which always happens to be the same one)
in order to refresh the layout you have to update the underlying data, recalculate layout, recompute data join, and update values:
// store the underlying data in a value
var classedRoot = classes(root);
// ...your node creating code here
node.on("click", function(d) {
// update the data in classedRoot
d.value += 1;
// recalculate layout
var data = bubble.nodes(classedRoot);
// recompute data join
node.data(data, function(d) { return d.className; }) // use key function
// update values
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.select("circle")
.attr("r", function(d) { return d.r });
});
note the use of key function here to properly bind updated data with the circles already present. the value you use must be unique for all circles. i used className here, but if it's not unique, you'll have to use some kind of Id.
here's updated jsFiddle
I'm very new to d3 and js
I'm using a d3.js collapsible force layout with images based exactly off this example: http://bl.ocks.org/eesur/be2abfb3155a38be4de4
However I wish to start with all the nodes collapsed. I've already tried all the solutions from this thread but they aren't working correctly.
The second solution has no effect at all.
When I use the first solution and change children to _children the functionality of the nodes changes and instead of expanding they start to split apart. This is a screenshot: https://dl.dropboxusercontent.com/u/38140937/Memory%20site/screenshot/solution2.png
Any help would be much appreciated
You should update the json data before the inital call of the update function.
$(function () {
---------------------
---------------------
---------------------
var nodes = flatten(g.data);
nodes.forEach(function(d) {
d._children = d.children;
d.children = null;
});
update();
});
Here is the working code snippet.
//Notes:
// Src: http://bl.ocks.org/mbostock/1093130
//Notes:
// * Each dom element is using
// children to store refs to expanded children
// _children to store refs to collapsed children
//* It's using both a tree and a graph layout.
//root
var g = {
data: null,
force: null
};
$(function() {
//use a global var for the data:
g.data = data;
var width = 960,
height = 500;
//Create a sized SVG surface within viz:
var svg = d3.select("#viz")
.append("svg")
.attr("width", width)
.attr("height", height);
g.link = svg.selectAll(".link"),
g.node = svg.selectAll(".node");
//Create a graph layout engine:
g.force = d3.layout.force()
.linkDistance(80)
.charge(-120)
.gravity(0.05)
.size([width, height])
//that invokes the tick method to draw the elements in their new location:
.on("tick", tick);
var nodes = flatten(g.data);
nodes.forEach(function(d) {
d._children = d.children;
d.children = null;
});
//Draw the graph:
//Note that this method is invoked again
//when clicking nodes:
update();
});
//invoked once at the start,
//and again when from 'click' method
//which expands and collapses a node.
function update() {
//iterate through original nested data, and get one dimension array of nodes.
var nodes = flatten(g.data);
//Each node extracted above has a children attribute.
//from them, we can use a tree() layout function in order
//to build a links selection.
var links = d3.layout.tree().links(nodes);
// pass both of those sets to the graph layout engine, and restart it
g.force.nodes(nodes)
.links(links)
.start();
//-------------------
// create a subselection, wiring up data, using a function to define
//how it's suppossed to know what is appended/updated/exited
g.link = g.link.data(links, function(d) {
return d.target.id;
});
//Get rid of old links:
g.link.exit().remove();
//Build new links by adding new svg lines:
g.link
.enter()
.insert("line", ".node")
.attr("class", "link");
// create a subselection, wiring up data, using a function to define
//how it's suppossed to know what is appended/updated/exited
g.node = g.node.data(nodes, function(d) {
return d.id;
});
//Get rid of old nodes:
g.node.exit().remove();
//-------------------
//create new nodes by making groupd elements, that contain circls and text:
var nodeEnter = g.node.enter()
.append("g")
.attr("class", "node")
.on("click", click)
.call(g.force.drag);
//circle within the single node group:
nodeEnter.append("circle")
.attr("r", function(d) {
return Math.sqrt(d.size) / 10 || 4.5;
});
//text within the single node group:
nodeEnter.append("text")
.attr("dy", ".35em")
.text(function(d) {
return d.name;
});
//All nodes, do the following:
g.node.select("circle")
.style("fill", color); //calls delegate
//-------------------
}
// Invoked from 'update'.
// The original source data is not the usual nodes + edge list,
// but that's what's needed for the force layout engine.
// So returns a list of all nodes under the root.
function flatten(data) {
var nodes = [],
i = 0;
//count only children (not _children)
//note that it doesn't count any descendents of collapsed _children
//rather elegant?
function recurse(node) {
if (node.children) node.children.forEach(recurse);
if (!node.id) node.id = ++i;
nodes.push(node);
}
recurse(data);
//Done:
return nodes;
}
//Invoked from 'update'
//Return the color of the node
//based on the children value of the
//source data item: {name=..., children: {...}}
function color(d) {
return d._children ? "#3182bd" // collapsed package
:
d.children ? "#c6dbef" // expanded package
:
"#fd8d3c"; // leaf node
}
// Toggle children on click by switching around values on _children and children.
function click(d) {
if (d3.event.defaultPrevented) return; // ignore drag
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
//
update();
}
//event handler for every time the force layout engine
//says to redraw everthing:
function tick() {
//redraw position of every link within the link set:
g.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;
});
//same for the nodes, using a functor:
g.node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
var data =
{
name: "flare",
children: [{
name: "analytics",
children: [{
name: "cluster",
children: [{
name: "AgglomerativeCluster",
size: 3938
}, {
name: "CommunityStructure",
size: 3812
}, {
name: "HierarchicalCluster",
size: 6714
}, {
name: "MergeEdge",
size: 743
}]
}, {
name: "graph",
children: [{
name: "BetweennessCentrality",
size: 3534
}, {
name: "LinkDistance",
size: 5731
}, {
name: "MaxFlowMinCut",
size: 7840
}, {
name: "ShortestPaths",
size: 5914
}, {
name: "SpanningTree",
size: 3416
}]
}, {
name: "optimization",
children: [{
name: "AspectRatioBanker",
size: 7074
}]
}]
}]
};
.node circle {
cursor: pointer;
stroke: #3182bd;
stroke-width: 1.5px;
}
.node text {
font: 10px sans-serif;
pointer-events: none;
text-anchor: middle;
}
line.link {
fill: none;
stroke: #9ecae1;
stroke-width: 1.5px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="viz" />
I am new to d3.js and coding in general. This is my question:
I am trying to find a way to break long displayed names of force layout objects in lines.
I would like to be able to determine where to break these lines, and I am guessing this is something that might be possible to be done from the json file.
I am aware that there have been similar questions asked already, but I just can't find where to put the code or why my previous attempts haven't been successful. This is the code that I have:
var width = 960,
height = 800,
root;
var force = d3.layout.force()
.linkDistance(120)
.charge(-600)
.gravity(.06)
.size([width, height])
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
d3.json("graph.json", function(error, json) {
root = json;
update();
});
function update() {
var nodes = flatten(root),
links = d3.layout.tree().links(nodes);
// Restart the force layout.
force
.nodes(nodes)
.links(links)
.start();
// Update links.
link = link.data(links, function(d) { return d.target.id; });
link.exit().remove();
link.enter().insert("line", ".node")
.attr("class", "link");
// Update nodes.
node = node.data(nodes, function(d) { return d.id; });
node.exit().remove();
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.on("click", click)
.call(force.drag);
nodeEnter.append("circle")
.attr("r", function(d) { return Math.sqrt(d.size) / 3 || 10; });
nodeEnter.append("text")
.attr("dy", "0.3em")
.text(function(d) { return d.name; });
node.select("circle")
.style("fill", color);
}
// Divide text
text.append("text")
.each(function (d) {
var arr = d.name.split(" ");
if (arr != undefined) {
for (i = 0; i < arr.length; i++) {
d3.select(this).append("tspan")
.text(arr[i])
.attr("dy", i ? "1.2em" : 0)
.attr("x", 0)
.attr("text-anchor", "middle")
.attr("class", "tspan" + i);
}
}
});
// Divide text
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 + ")"; });
}
function color(d) {
return d._children ? "#9ecae1" // collapsed package
: d.children ? "#ffffff" // expanded package
: "#ffcc50"; // leaf node
}
// Toggle children on click.
function click(d) {
if (d3.event.defaultPrevented) return; // ignore drag
if (d.children) {
d._children = d.children;
d.children = null;
} else if (d._children) {
d.children = d._children;
d._children = null;
} else {
// This was a leaf node, so redirect.
window.open(d.url, 'popUpWindow','height=600,width=800,left=10,top=10,resizable=yes,scrollbars=no,toolbar=no,menubar=no,location=no,directories=no,status=yes');
}
update();
}
// Returns a list of all nodes under the root.
function flatten(root) {
var nodes = [], i = 0;
function recurse(node) {
if (node.children) node.children.forEach(recurse);
if (!node.id) node.id = ++i;
nodes.push(node);
}
recurse(root);
return nodes;
}
And this is the json info:
{
"name": "flare",
"children": [
{
"name": "analytics",
"children": [
{
"name": "cluster",
"children": [
{"name": "Agglomerative Cluster", "size": 3938},
{"name": "Community Structure", "size": 3812},
{"name": "Hierarchical Cluster", "size": 6714},
{"name": "Merge Edge", "size": 743}
]
},
{
"name": "graph",
"children": [
{"name": "Betweenness Centrality", "size": 3534},
{"name": "Link Distance", "size": 5731},
{"name": "Max Flow Min Cut", "size": 7840},
{"name": "Shortest Paths", "size": 5914},
{"name": "Spanning Tree", "size": 3416}
]
},
{
"name": "optimization",
"children": [
{"name": "Aspect Ratio Banker", "size": 7074}
]
}
]
}
]
I would like to be able to decide, for example, to break Aspect / Ratio Banker or Aspect Ratio / Banker.
I believe this example on jsfiddle solves your problem.
The code is actually your example, just a little bit modified.
There is a new function wordwrap2() that takes care of proper splitting of the names:
function wordwrap2( str, width, brk, cut ) {
brk = brk || '\n';
width = width || 75;
cut = cut || false;
if (!str) { return str; }
var regex = '.{1,' +width+ '}(\\s|$)' + (cut ? '|.{' +width+ '}|.+$' : '|\\S+?(\\s|$)');
return str.match( RegExp(regex, 'g') ).join( brk );
}
Then, there is a new important part of the code that, instead of just creating one text label per node, creates this:
var maxLength = 20;
var separation = 18;
var textX = 0;
nodeEnter.append("text")
.attr("dy", "0.3em")
.each(function (d) {
var lines = wordwrap2(d.name, maxLength).split('\n');
console.log(d.name);
console.log(lines);
for (var i = 0; i < lines.length; i++) {
d3.select(this)
.append("tspan")
.attr("dy", separation)
.attr("x", textX)
.text(lines[i]);
}
});
(variable maxLength - length used for criterium for splitting names)
(variable separation - visual vertical distance between split lines of a name)
For example this would be the output for maxLength=20:
This would be the output for maxLength=15: (notice that Aspect Ratio Banker became Aspect Ratio/Banker)
This would be the output for maxLength=10: (now, check out Aspect/Ratio/Banker !)
And this would be the output for maxLength=10 and separation=30 (a little more space between individual lines):