I'm trying to display a network graph with clustered nodes but I'm having trouble with the nested nodes in D3. The first "layer" contains clusters and each node of the first layer can contain multiple nodes. Links in the network would probably only occur between clusters (meaning between nodes of the first layer).
Here is the code I have so far. I'm able to display the first level of nodes. I cannot figure out how to display the nested nodes (see code in const data in each node children).
const node_radius = 100;
const width = 800;
const height = 400;
const links = [
{ "source": 1, "target": 6}
] ;
const data = [
{
"id":1,
"level": "cluster",
"name": "analytics1",
"children": [
{
"id":2,
"name": "animate1",
"level": "leaf",
"size": 15,
"parent": 1
},
{
"id":3,
"name": "animate2",
"level": "leaf",
"size": 15,
"parent": 1
},
{
"id":4,
"name": "animate3",
"level": "leaf",
"size": 15,
"parent": 1
}
]
},
{
"id":6,
"name": "analytics2",
"level": "cluster",
"children": [
{
"id":7,
"name": "animate4",
"level": "leaf",
"size": 10,
"parent": 6
}
]
}
]
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var simulation = d3.forceSimulation()
// pull nodes together based on the links between them
.force("link", d3.forceLink().id(function(d) { return d.id; }).strength(0.0001))
// push nodes apart to space them out
.force("charge", d3.forceManyBody().strength(-10))
// add some collision detection so they don't overlap
.force("collide", d3.forceCollide().radius(node_radius))
// and draw them around the centre of the space
.force("center", d3.forceCenter(width / 2, height / 2));
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(links).enter().append("line")
.attr("stroke-width", 5)
.attr("stroke","#000");
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("id", function(d) {return "circle"+d.id;})
.attr("class", "node")
.attr("r", node_radius)
.style("opacity", 0.2)
.attr("dx", 12)
.attr("dy", ".35em");
var text = svg.append("g")
.attr("class", "label")
.selectAll("text")
.data(data)
.enter().append("text")
.text(function(d) { return d.name });
// Update and restart the simulation.
simulation.nodes(data).on("tick", ticked);
simulation.force("link").links(links);
simulation.alpha(1).restart();
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", positionNode);
text
.attr("dx", function(d) { return d.x - 30; })
.attr("dy", function(d) { return d.y + 15; });
}
// move the node based on forces calculations
function positionNode(d) {
// keep the node within the boundaries of the svg
if (d.x < node_radius) {
d.x = 2*node_radius
};
if (d.y < node_radius) {
d.y = 2*node_radius
};
if (d.x > width-node_radius) {
d.x = width-(2*node_radius)
};
if (d.y > height-node_radius) {
d.y = height-(2*node_radius)
};
return "translate(" + d.x + "," + d.y + ")";
}
<script src="https://d3js.org/d3.v4.min.js"></script>
I would like to have something like the following image. The two clusters are displayed and in each group, all children (leaf nodes) are represented by a smaller node. Node size should be customizable for both "layers" from data input.
Example I'm trying to follow on Fiddle.
I have also tried to use d3.pack() to pack circles inside of other circles. Here is an example. The problem I have currently with this approach is that I did not succeed in adding space and links between nodes of the first "layer" (between clusters). The high level clusters are also packed together and it would be impossible to add comprehensible links betwen them.
I finally succeeded in merging the d3.pack() example with the clustering example. Here is my solution.
var data = [
{
"id":1,
"level": "cluster",
"name": "analytics1",
"children": [
{
"id":2,
"name": "animate1",
"level": "leaf",
"size": 8,
"parent": 1,
"icon":"https://image.freepik.com/free-icon/apple-logo_318-40184.jpg"
},
{
"id":3,
"name": "animate2",
"level": "leaf",
"size": 10,
"parent": 1,
"icon": "https://www.freelogodesign.org/img/logo-ex-7.png"
},
{
"id":4,
"name": "animate3",
"level": "leaf",
"size": 5,
"parent": 1,
"icon": "http://brandmark.io/logo-rank/random/pepsi.png"
}
]
},
{
"id":6,
"name": "analytics2",
"level": "cluster",
"children": [
{
"id":7,
"name": "animate4",
"level": "leaf",
"size": 10,
"parent": 6,
"icon":"https://www.seoclerk.com/pics/558390-11FO8A1505384509.png"
}
]
}
]
var links = [
{ "source": 1, "target": 6}
] ;
var w = 1200, h = 500;
var cluster_padding = 5;
var node_padding = 2;
var size_ratio =100;
var color = d3.scaleOrdinal(d3.schemeCategory20c);
let sumSizes = 0;
data.forEach(function(cluster){
cluster.children.forEach(function(node){
sumSizes += node.size;
});
});
// Compute sum of sizes for cluster size.
data.forEach(function(cluster){
cluster.size = (
cluster.children.map(function(d){return d.size;})
.reduce(function(acc, val){ return acc+val+node_padding; })/sumSizes
)*size_ratio + cluster_padding;
cluster.children = cluster.children.sort(function(a,b){
return (a.size < b.size) ? 1 : ((b.size < a.size) ? -1 : 0);
})
cluster.children.forEach(function(node){
node.parentSize = cluster.size;
node.size = node.size*size_ratio/sumSizes;
});
});
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h);
////////////////////////
// outer force layout
var outerSimulation = d3.forceSimulation()
// pull nodes together based on the links between them
.force("link", d3.forceLink().id(function(d) { return d.id; }).strength(0.001))
// push nodes apart to space them out
.force("charge", d3.forceManyBody().strength(-5))
// add some collision detection so they don't overlap
.force("collide", d3.forceCollide().radius(function(d){return d.size+cluster_padding;}))
// and draw them around the centre of the space
.force("center", d3.forceCenter(w / 2, h / 2));
var outerLinks = svg.selectAll("line")
.data(links)
.enter().append("line")
.attr("class", "links")
.attr("stroke-width", 5);
var outerNodes = svg.selectAll("g.outer")
.data(data, function (d) {return d.id;})
.enter()
.append("g")
.attr("class", "outer")
.attr("id", function (d) {return "cluster"+d.id;})
.attr("x", w/2)
.attr("y", w/2)
.call(d3.drag());
outerNodes.append("circle")
.style("fill", function(d,i){return color(i);})
.style("stroke", "blue")
.attr("r", function(d){return d.size});
// Update and restart the simulation.
outerSimulation.nodes(data).on("tick", outerTick);
outerSimulation.force("link").links(links);
outerSimulation.alpha(1).restart();
////////////////////////
// inner force layouts
var innerNodes = [];
var innerTexts = [];
var packs = [];
var margin = 20;
data.forEach(function(n){
// Pack hierarchy definition
var pack = d3.pack()
.size([2*n.size, 2*n.size])
.padding(cluster_padding);
var root = d3.hierarchy(n)
.sum(function(d) { return d.size; })
.sort(function(a, b) { return b.value - a.value; });
var nodes = pack(root).descendants();
// Round images
var defs = svg.append("defs").attr("id", "imgdefs")
var pattern = defs
.selectAll("pattern")
.data(nodes.filter(function(d) { return d.parent }))
.enter().append("pattern")
.attr("id", function(d){return "photo"+d.data.name})
.attr("height", 1)
.attr("width", 1)
.attr("x", "0")
.attr("y", "0");
var image = pattern.append('image')
.attr("class","roundImg")
.attr("id", function(d){return "photo"+d.data.name;})
.attr("xlink:href", function(d){return d.data.icon ? d.data.icon : "";})
.attr("height", function(d){return 3.2*d.r ;})
;
// Nodes
var circle = svg.select("g.outer#cluster"+n.id).selectAll("g.inner")
.data(nodes.filter(function(d) { return d.parent }))
.enter().append("circle")
.attr("class", "node node--leaf ")
.attr("id", function(d) {return d.data.name})
.style("fill", function(d) { return "url(#photo"+d.data.name+")";})
.attr("r", function(d) { return d.r; })
.attr("transform", function(d) { return "translate("+(d.x-n.size) +","+ (d.y-n.size)+")"; })
;
});
////////////////////////
// functions
function outerTick (e) {
outerNodes.attr("transform", positionNode);
outerLinks
.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; });
}
function positionNode(d) {
// keep the node within the boundaries of the svg
if (d.x - d.size < 0) {
d.x = d.size + 2
};
if (d.y - d.size < 0) {
d.y = d.size + 2
};
if (d.x + d.size > w) {
d.x = w - d.size - 2
};
if (d.y + d.size > h) {
d.y = h - d.size - 2
};
return "translate(" + d.x + "," + d.y + ")";
}
<!DOCTYPE html>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<link rel="stylesheet" type="text/css" href="css/pack.css">
<body>
<div class="packed" id="packed"></div>
</body>
Related
Currently, I am building a system, and I am having some trouble with the update function.
Essentially, I am trying to add new nodes to a D3 tree. A new child node can be added when the user clicks the "add button" of a node. Each add button can be found on the left side of each node.
I have followed Mike Bostock's general update pattern. Once I click on the button, the only "new" data element should be the newly created child node, but it looks like the entire data is being treat as "new". I came to this conclusion when I looked at the class names for each node and the obvious fact that there is a transition of all the nodes coming to the central node and disappearing. The other original data should be "updated", but it's not. Can anyone please gently point out as to why this is happening?
A working sample of my code can be found in this jfiddle link.
EDIT 06/09
Given Gordon's suggestion, I have found a unique field for both of my nodes and link. So to uniquely identify the data I have made the following change:
NODE
.data(d, d => d.data.name)
LINK
.data(d, d => d.source.data.name)
This change works (mostly), but I see that some weird behaviors are still occurring: (1) Branch 7.2.1 is still being recognized a new node and disappear; (2) links are not being properly aligned with their respective node after the second "add" or so. I think my two small edits are affecting this because when I went back to the original code, the lines are being properly drawn, despite the fact they're transitioning away. Thoughts? Advice?
HTML
<div id="div-mindMap">
CSS
.linkMindMap {
fill: none;
stroke: #555;
stroke-opacity: 0.4;
}
rect {
fill: white;
stroke: #3182bd;
stroke-width: 1.5px;
}
JS
const widthMindMap = 700;
const heightMindMap = 700;
let parsedData;
let parsedList = {
"name": " Stapler",
"children": [{
"name": " Bind",
"children": []
},
{
"name": " Nail",
"children": []
},
{
"name": " String",
"children": []
},
{
"name": " Glue",
"children": [{
"name": "Gum",
"children": []
},
{
"name": "Sticky Gum",
"children": []
}
]
},
{
"name": " Branch 3",
"children": []
},
{
"name": " Branch 4",
"children": [{
"name": " Branch 4.1",
"children": []
},
{
"name": " Branch 4.2",
"children": []
},
{
"name": " Branch 4.1",
"children": []
}
]
},
{
"name": " Branch 5",
"children": []
},
{
"name": " Branch 6",
"children": []
},
{
"name": " Branch 7",
"children": []
},
{
"name": " Branch 7.1",
"children": []
},
{
"name": " Branch 7.2",
"children": [{
"name": " Branch 7.2.1",
"children": []
},
{
"name": " Branch 7.2.1",
"children": []
}
]
}
]
}
let svgMindMap = d3.select('#div-mindMap')
.append("svg")
.attr("id", "svg-mindMap")
.attr("width", widthMindMap)
.attr("height", heightMindMap);
let backgroundLayer = svgMindMap.append('g')
.attr("width", widthMindMap)
.attr("height", heightMindMap)
.attr("class", "background")
let gLeft = backgroundLayer.append("g")
.attr("transform", "translate(" + widthMindMap / 2 + ",0)")
.attr("class", "g-left");
let gLeftLink = gLeft.append('g')
.attr('class', 'g-left-link');
let gLeftNode = gLeft.append('g')
.attr('class', 'g-left-node');
function loadMindMap(parsed) {
var data = parsed;
var split_index = Math.round(data.children.length / 2);
parsedData = {
"name": data.name,
"children": JSON.parse(JSON.stringify(data.children.slice(split_index)))
};
var left = d3.hierarchy(parsedData, d => d.children);
drawLeft(left, "left");
}
// draw single tree
function drawLeft(root, pos) {
var SWITCH_CONST = 1;
if (pos === "left") SWITCH_CONST = -1;
update(root, SWITCH_CONST);
}
function update(source, SWITCH_CONST) {
var tree = d3.tree()
.size([heightMindMap, SWITCH_CONST * (widthMindMap - 150) / 2]);
var root = tree(source);
console.log(root)
var nodes = root.descendants();
var links = root.links();
console.log(nodes)
console.log(links)
// Set both root nodes to be dead center vertically
nodes[0].x = heightMindMap / 2
//JOIN new data with old elements
var link = gLeftLink.selectAll(".link-left")
.data(links, d => d)
.style('stroke-width', 1.5);
var linkEnter = link.enter().append("path")
.attr("class", "linkMindMap link-left")
.attr("d", d3.linkHorizontal()
.x(d => d.y)
.y(d => d.x));
var linkUpdate = linkEnter.merge(link);
linkUpdate.transition()
.duration(750)
var linkExit = link.exit()
.transition()
.duration(750)
.attr('x1', function(d) {
return root.x;
})
.attr('y1', function(d) {
return root.y;
})
.attr('x2', function(d) {
return root.x;
})
.attr('y2', function(d) {
return root.y;
})
.remove();
//JOIN new data with old elements
var node = gLeftNode.selectAll(".nodeMindMap-left")
.data(nodes, d => d);
console.log(nodes);
//ENTER new elements present in new data
var nodeEnter = node.enter().append("g").merge(node)
.attr("class", function(d) {
return "nodeMindMap-left " + "nodeMindMap" + (d.children ? " node--internal" : " node--leaf");
})
.classed("enter", true)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
})
.attr("id", function(d) {
let str = d.data.name;
str = str.replace(/\s/g, '');
return str;
});
nodeEnter.append("circle")
.attr("r", function(d, i) {
return 2.5
});
var addLeftChild = nodeEnter.append("g")
.attr("class", "addHandler")
.attr("id", d => {
let str = d.data.name;
str = "addHandler-" + str.replace(/\s/g, '');
return str;
})
.style("opacity", "1")
.on("click", (d, i, nodes) => addNewLeftChild(d, i, nodes));
addLeftChild.append("line")
.attr("x1", -74)
.attr("y1", 1)
.attr("x2", -50)
.attr("y2", 1)
.attr("stroke", "#85e0e0")
.style("stroke-width", "2");
addLeftChild.append("rect")
.attr("x", "-77")
.attr("y", "-7")
.attr("height", 15)
.attr("width", 15)
.attr("rx", 5)
.attr("ry", 5)
.style("stroke", "#444")
.style("stroke-width", "1")
.style("fill", "#ccc");
addLeftChild.append("line")
.attr("x1", -74)
.attr("y1", 1)
.attr("x2", -65)
.attr("y2", 1)
.attr("stroke", "#444")
.style("stroke-width", "1.5");
addLeftChild.append("line")
.attr("x1", -69.5)
.attr("y1", -3)
.attr("x2", -69.5)
.attr("y2", 5)
.attr("stroke", "#444")
.style("stroke-width", "1.5");
// .call(d3.drag().on("drag", dragged));;
nodeEnter.append("foreignObject")
.style("fill", "blue")
.attr("x", -50)
.attr("y", -7)
.attr("height", "20px")
.attr("width", "100px")
.append('xhtml:div')
.append('div')
.attr("class", 'clickable-node')
.attr("id", function(d) {
let str = d.data.name;
str = "div-" + str.replace(/\s/g, '');
return str;
})
.attr("ondblclick", "this.contentEditable=true")
.attr("onblur", "this.contentEditable=false")
.attr("contentEditable", "false")
.style("text-align", "center")
.text(d => d.data.name);
//TODO: make it dynamic
nodeEnter.insert("rect", "foreignObject")
.attr("ry", 6)
.attr("rx", 6)
.attr("y", -10)
.attr("height", 20)
.attr("width", 100)
// .filter(function(d) { return d.flipped; })
.attr("x", -50)
.classed("selected", false)
.attr("id", function(d) {
let str = d.data.name;
str = "rect-" + str.replace(/\s/g, '');
return str;
});
var nodeUpdate = nodeEnter.merge(node);
// Transition to the proper position for the node
nodeUpdate.transition()
.duration(750)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Remove any exiting nodes
var nodeExit = node.exit()
.transition()
.duration(750)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
// On exit reduce the node circles size to 0
nodeExit.select('circle').attr('r', 0);
// node = nodeEnter.merge(node)
}
function addNewLeftChild(d, i, nodes) {
console.log("make new child");
event.stopPropagation();
var newNodeObj = {
// name: new Date().getTime(),
name: "New Child",
children: []
};
console.log("this is ", parsedData)
//Creates new Node
var newNode = d3.hierarchy(newNodeObj);
newNode.depth = d.depth + 1;
newNode.height = d.height - 1;
newNode.parent = d;
newNode.id = Date.now();
console.log(newNode);
console.log(d)
if (d.data.children.length == 0) {
console.log("i have no children")
d.children = []
}
d.children.push(newNode)
d.data.children.push(newNode.data)
console.log(d)
let foo = d3.hierarchy(parsedData, d => d.children)
drawLeft(foo, "left");
}
loadMindMap(parsedList);
There are a few things going on:
Using Unique Keys
Using names isn't the best for keys, because each new node has the same name ("New Child"). Instead it's probably better to use some sort of ID system. Here's a quick function to tag the data for each node with an ID.
let currNodeId = 0;
function idData(node) {
node.nodeId = ++currNodeId;
node.children.forEach(idData);
}
idData(parsedList);
And since you're redefining the data in parsedData, you need to use the id property there too:
parsedData = {
"name": data.name,
"nodeId": data.nodeId,
"children": JSON.parse(JSON.stringify(data.children.slice(split_index)))
};
When adding a new node, you can also set it in the nodeData:
var newNodeObj = {
// name: new Date().getTime(),
name: "New Child",
nodeId: ++currNodeId,
children: []
};
Then to actually use .nodeId as the key for nodes, use it as the key function:
.data(nodes, d => d.data.nodeId);
For links, you should use the target instead of the source, since this is a tree and there is only one link per child (instead of multiple links for one parent).
.data(nodes, d => d.target.data.nodeId);
Prevent multiple node elements from being added
There's also an issue where you're merging your new and old nodes before adding new elements. To prevent this, you should change
node.enter().append("g").merge(node)
to:
node.enter().append("g")
Link transitions
Finally the transitions for your links are not transitioning with the nodes. To make them transition, move:
.attr("d", d3.linkHorizontal()
.x(d => d.y)
.y(d => d.x));
to under
linkUpdate.transition()
.duration(750)
All together it looks like this: https://jsfiddle.net/v9wyb6q4/
Or:
const widthMindMap = 700;
const heightMindMap = 700;
let parsedData;
let parsedList = {
"name": " Stapler",
"children": [{
"name": " Bind",
"children": []
},
{
"name": " Nail",
"children": []
},
{
"name": " String",
"children": []
},
{
"name": " Glue",
"children": [{
"name": "Gum",
"children": []
},
{
"name": "Sticky Gum",
"children": []
}
]
},
{
"name": " Branch 3",
"children": []
},
{
"name": " Branch 4",
"children": [{
"name": " Branch 4.1",
"children": []
},
{
"name": " Branch 4.2",
"children": []
},
{
"name": " Branch 4.1",
"children": []
}
]
},
{
"name": " Branch 5",
"children": []
},
{
"name": " Branch 6",
"children": []
},
{
"name": " Branch 7",
"children": []
},
{
"name": " Branch 7.1",
"children": []
},
{
"name": " Branch 7.2",
"children": [{
"name": " Branch 7.2.1",
"children": []
},
{
"name": " Branch 7.2.1",
"children": []
}
]
}
]
}
let currNodeId = 0;
function idData(node) {
node.nodeId = ++currNodeId;
node.children.forEach(idData);
}
idData(parsedList);
let svgMindMap = d3.select('#div-mindMap')
.append("svg")
.attr("id", "svg-mindMap")
.attr("width", widthMindMap)
.attr("height", heightMindMap);
let backgroundLayer = svgMindMap.append('g')
.attr("width", widthMindMap)
.attr("height", heightMindMap)
.attr("class", "background")
let gLeft = backgroundLayer.append("g")
.attr("transform", "translate(" + widthMindMap / 2 + ",0)")
.attr("class", "g-left");
let gLeftLink = gLeft.append('g')
.attr('class', 'g-left-link');
let gLeftNode = gLeft.append('g')
.attr('class', 'g-left-node');
function loadMindMap(parsed) {
var data = parsed;
var split_index = Math.round(data.children.length / 2);
parsedData = {
"name": data.name,
"nodeId": data.nodeId,
"children": JSON.parse(JSON.stringify(data.children.slice(split_index)))
};
var left = d3.hierarchy(parsedData, d => d.children);
drawLeft(left, "left");
}
// draw single tree
function drawLeft(root, pos) {
var SWITCH_CONST = 1;
if (pos === "left") SWITCH_CONST = -1;
update(root, SWITCH_CONST);
}
function update(source, SWITCH_CONST) {
var tree = d3.tree()
.size([heightMindMap, SWITCH_CONST * (widthMindMap - 150) / 2]);
var root = tree(source);
console.log(root)
var nodes = root.descendants();
var links = root.links();
console.log(nodes)
console.log(links)
// Set both root nodes to be dead center vertically
nodes[0].x = heightMindMap / 2
//JOIN new data with old elements
var link = gLeftLink.selectAll(".link-left")
.data(links, d => d.target.data.nodeId)
.style('stroke-width', 1.5);
var linkEnter = link.enter().append("path")
.attr("class", "linkMindMap link-left");
var linkUpdate = linkEnter.merge(link);
linkUpdate.transition()
.duration(750)
.attr("d", d3.linkHorizontal()
.x(d => d.y)
.y(d => d.x));
var linkExit = link.exit()
.transition()
.duration(750)
.attr('x1', function(d) {
return root.x;
})
.attr('y1', function(d) {
return root.y;
})
.attr('x2', function(d) {
return root.x;
})
.attr('y2', function(d) {
return root.y;
})
.remove();
//JOIN new data with old elements
var node = gLeftNode.selectAll(".nodeMindMap-left")
.data(nodes, d => d.data.nodeId);
console.log(nodes);
//ENTER new elements present in new data
var nodeEnter = node.enter().append("g")
.attr("class", function(d) {
return "nodeMindMap-left " + "nodeMindMap" + (d.children ? " node--internal" : " node--leaf");
})
.classed("enter", true)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
})
.attr("id", function(d) {
let str = d.data.name;
str = str.replace(/\s/g, '');
return str;
});
nodeEnter.append("circle")
.attr("r", function(d, i) {
return 2.5
});
var addLeftChild = nodeEnter.append("g")
.attr("class", "addHandler")
.attr("id", d => {
let str = d.data.name;
str = "addHandler-" + str.replace(/\s/g, '');
return str;
})
.style("opacity", "1")
.on("click", (d, i, nodes) => addNewLeftChild(d, i, nodes));
addLeftChild.append("line")
.attr("x1", -74)
.attr("y1", 1)
.attr("x2", -50)
.attr("y2", 1)
.attr("stroke", "#85e0e0")
.style("stroke-width", "2");
addLeftChild.append("rect")
.attr("x", "-77")
.attr("y", "-7")
.attr("height", 15)
.attr("width", 15)
.attr("rx", 5)
.attr("ry", 5)
.style("stroke", "#444")
.style("stroke-width", "1")
.style("fill", "#ccc");
addLeftChild.append("line")
.attr("x1", -74)
.attr("y1", 1)
.attr("x2", -65)
.attr("y2", 1)
.attr("stroke", "#444")
.style("stroke-width", "1.5");
addLeftChild.append("line")
.attr("x1", -69.5)
.attr("y1", -3)
.attr("x2", -69.5)
.attr("y2", 5)
.attr("stroke", "#444")
.style("stroke-width", "1.5");
// .call(d3.drag().on("drag", dragged));;
nodeEnter.append("foreignObject")
.style("fill", "blue")
.attr("x", -50)
.attr("y", -7)
.attr("height", "20px")
.attr("width", "100px")
.append('xhtml:div')
.append('div')
.attr("class", 'clickable-node')
.attr("id", function(d) {
let str = d.data.name;
str = "div-" + str.replace(/\s/g, '');
return str;
})
.attr("ondblclick", "this.contentEditable=true")
.attr("onblur", "this.contentEditable=false")
.attr("contentEditable", "false")
.style("text-align", "center")
.text(d => d.data.name);
//TODO: make it dynamic
nodeEnter.insert("rect", "foreignObject")
.attr("ry", 6)
.attr("rx", 6)
.attr("y", -10)
.attr("height", 20)
.attr("width", 100)
// .filter(function(d) { return d.flipped; })
.attr("x", -50)
.classed("selected", false)
.attr("id", function(d) {
let str = d.data.name;
str = "rect-" + str.replace(/\s/g, '');
return str;
});
var nodeUpdate = nodeEnter.merge(node);
// Transition to the proper position for the node
nodeUpdate.transition()
.duration(750)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Remove any exiting nodes
var nodeExit = node.exit()
.transition()
.duration(750)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
// On exit reduce the node circles size to 0
nodeExit.select('circle').attr('r', 0);
// node = nodeEnter.merge(node)
}
function addNewLeftChild(d, i, nodes) {
console.log("make new child");
event.stopPropagation();
var newNodeObj = {
// name: new Date().getTime(),
name: "New Child",
nodeId: ++currNodeId,
children: []
};
console.log("this is ", parsedData)
//Creates new Node
var newNode = d3.hierarchy(newNodeObj);
newNode.depth = d.depth + 1;
newNode.height = d.height - 1;
newNode.parent = d;
newNode.id = Date.now();
console.log(newNode);
console.log(d)
if (d.data.children.length == 0) {
console.log("i have no children")
d.children = []
}
d.children.push(newNode)
d.data.children.push(newNode.data)
console.log(d)
let foo = d3.hierarchy(parsedData, d => d.children)
drawLeft(foo, "left");
}
loadMindMap(parsedList);
.linkMindMap {
fill: none;
stroke: #555;
stroke-opacity: 0.4;
}
rect {
fill: white;
stroke: #3182bd;
stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<div id="div-mindMap">
These sorts of problems usually arise because the key function which you pass as the second parameter to selection.data() is not unique or "too unique".
It should return a simple value which uniquely identifies each datum. In your case
.data(nodes, d => d);
might be better as
.data(nodes, d => d.name);
and similarly for the links.
However you will need to examine the output of d3.tree to see what fields the nodes and links have and which field contains a unique id.
CODE:
// some colour variables
var tcBlack = "#130C0E";
// rest of vars
var w = 1255,
h = 925,
maxNodeSize = 50,
x_browser = 10,
y_browser = 15,
root;
var vis;
var force = d3.layout.force();
vis = d3.select("#vis").append("svg").attr("width", w).attr("height", h);
d3.json("marvel.json", function(json) {
root = json;
root.fixed = true;
root.x = w / 2;
root.y = h / 4;
// Build the path
var defs = vis.insert("svg:defs")
.data(["end"]);
defs.enter().append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
update();
});
function update() {
var nodes = flatten(root),
links = d3.layout.tree().links(nodes);
// Restart the force layout.
force.nodes(nodes)
.links(links)
.gravity(0.05)
.charge(-1500)
.linkDistance(100)
.friction(0.5)
.linkStrength(function(l, i) {return 1; })
.size([w, h])
.on("tick", tick)
.start();
var path = vis.selectAll("path.link")
.data(links, function(d) { return d.target.id; });
path.enter().insert("svg:path")
.attr("class", "link")
// .attr("marker-end", "url(#end)")
.style("stroke", "#eee");
// Exit any old paths.
path.exit().remove();
// Update the nodes…
var node = vis.selectAll("g.node")
.data(nodes, function(d) { return d.id; });
// Enter any new nodes.
var nodeEnter = node.enter().append("svg:g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.on("click", click)
.call(force.drag);
// Append a circle
nodeEnter.append("svg:circle")
.attr("r", function(d) { return Math.sqrt(40000) / 10 || 30; })
.style("fill", "#ffffff");
// Append images
var images = nodeEnter.append("svg:image")
.attr("xlink:href", function(d) { return d.img;})
.attr("x", function(d) { return -25;})
.attr("y", function(d) { return -25;})
.attr("height", 50)
.attr("width", 50);
// make the image grow a little on mouse over and add the text details on click
var setEvents = images
// Append hero text
.on( 'click', function (d) {
// d3.select("h1").html(d.hero);
d3.select("h3").html(d.name);
// d3.select("h3").html ("Take me to " + "<a href='" + d.link + "' >" + d.hero + " web page ⇢"+ "</a>" );
})
.on( 'mouseenter', function() {
// select element in current context
d3.select( this )
.transition()
.attr("x", function(d) { return -60;})
.attr("y", function(d) { return -60;})
.attr("height", 100)
.attr("width", 100);
})
// set back
.on( 'mouseleave', function() {
d3.select( this )
.transition()
.attr("x", function(d) { return -25;})
.attr("y", function(d) { return -25;})
.attr("height", 50)
.attr("width", 50);
});
// Append hero name on roll over next to the node as well
nodeEnter.append("text")
.attr("class", "nodetext")
.attr("x", x_browser)
.attr("y", y_browser +15)
.attr("fill", "red")
.text(function(d) { return d.name; });
// Exit any old nodes.
node.exit().remove();
// Re-select for update.
path = vis.selectAll("path.link");
node = vis.selectAll("g.node");
function tick() {
path.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + ","
+ d.source.y
+ "A" + dr + ","
+ dr + " 0 0,1 "
+ d.target.x + ","
+ d.target.y;
});
node.attr("transform", nodeTransform);
}
}
function nodeTransform(d) {
d.x = Math.max(maxNodeSize, Math.min(w - (d.imgwidth/2 || 16), d.x));
d.y = Math.max(maxNodeSize, Math.min(h - (d.imgheight/2 || 16), d.y));
return "translate(" + d.x + "," + d.y + ")";
}
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update();
}
/**
* Returns a list of all nodes under the root.
*/
function flatten(root) {
var nodes = [];
var 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;
}
I'm using the above code to display image using d3.js. But the image is not showing as circle. I want the child node images to fill in the circle properly.
So please help to to fix this. Answers will be appreciated. Thanks
This is the example I use: d3 | Force layout with images
var marvelJson={
"name": "marvel",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/marvel.png",
"children": [
{
"name": "Heroes",
"children": [
{
"hero": "Spider-Man",
"name": "Peter Benjamin Parker",
"link": "http://marvel.com/characters/54/spider-man",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/top_spiderman.png",
"size": 40000
},
{
"hero": "CAPTAIN MARVEL",
"name": "Carol Danvers",
"link": "http://marvel.com/characters/9/captain_marvel",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/top_captainmarvel.png",
"size": 40000
},
{
"hero": "HULK",
"name": "Robert Bruce Banner",
"link": "http://marvel.com/characters/25/hulk",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/top_hulk.png",
"size": 40000
},
{
"hero": "Black Widow",
"name": "Natalia 'Natasha' Alianovna Romanova",
"link": "http://marvel.com/characters/6/black_widow",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/top_blackwidow.png",
"size": 40000
},
{
"hero": "Daredevil",
"name": "Matthew Michael Murdock",
"link": "http://marvel.com/characters/11/daredevil",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/top_daredevil.png",
"size": 40000
},
{
"hero": "Wolverine",
"name": "James Howlett",
"link": "http://marvel.com/characters/66/wolverine",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/top_wolverine.png",
"size": 40000
},
{
"hero": "Captain America",
"name": "Steven Rogers",
"link": "http://marvel.com/characters/8/captain_america",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/top_captainamerica.png",
"size": 40000
},
{
"hero": "Iron Man",
"name": "Anthony Edward 'Tony' Stark",
"link": "http://marvel.com/characters/29/iron_man",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/top_ironman.png",
"size": 40000
},
{
"hero": "THOR",
"name": "Thor Odinson",
"link": "http://marvel.com/characters/60/thor",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/top_thor.png",
"size": 40000
}
]
},
{
"name": "Villains",
"children": [
{
"hero": "Dr. Doom",
"name": "Victor von Doom",
"link": "http://marvel.com/characters/13/dr_doom",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/drdoom.png",
"size": 40000
},
{
"hero": "Mystique",
"name": "Unrevealed",
"link": "http://marvel.com/characters/1552/mystique",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/mystique.png",
"size": 40000
},
{
"hero": "Red Skull",
"name": "Johann Shmidt",
"link": "http://marvel.com/characters/1901/red_skull",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/redskull.png",
"size": 40000
},
{
"hero": "Ronan",
"name": "Ronan",
"link": "http://marvel.com/characters/49/ronan",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/ronan.png",
"size": 40000
},
{
"hero": "Magneto",
"name": "Max Eisenhardt",
"link": "http://marvel.com/characters/35/magneto",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/magneto.png",
"size": 40000
},
{
"hero": "Thanos",
"name": "Thanos",
"link": "http://marvel.com/characters/58/thanos",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/thanos.png",
"size": 40000
},
{
"hero": "Black Cat",
"name": "Felicia Hardy",
"link": "http://marvel.com/characters/271/black_cat",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/blackcat.png",
"size": 40000
}
]
},
{
"name": "Teams",
"children": [
{
"hero": "Avengers",
"name": "",
"link": "http://marvel.com/characters/68/avengers",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/avengers.png",
"size": 40000
},
{
"hero": "Guardians of the Galaxy",
"name": "",
"link": "http://marvel.com/characters/70/guardians_of_the_galaxy",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/gofgalaxy.png",
"size": 40000
},
{
"hero": "Defenders",
"name": "",
"link": "http://marvel.com/characters/534/defenders",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/defenders.png",
"size": 40000
},
{
"hero": "X-Men",
"name": "",
"link": "http://marvel.com/characters/71/x-men",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/xmen.png",
"size": 40000
},
{
"hero": "Fantastic Four",
"name": "",
"link": "http://marvel.com/characters/69/fantastic_four",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/fantasticfour.png",
"size": 40000
},
{
"hero": "Inhumans",
"name": "",
"link": "http://marvel.com/characters/1040/inhumans",
"img": "https://dl.dropboxusercontent.com/u/19954023/marvel_force_chart_img/inhumans.png",
"size": 40000
}
]
}
]
}
/*
var imgurl = "http://wallpapers.androlib.com/wallicons/wallpaper.big-pqC.cs.png"
var margin = {top: 20, right: 10, bottom: 20, left: 10};
var width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var defs = svg.append("defs").attr("id", "imgdefs")
var catpattern = defs.append("pattern")
.attr("id", "catpattern")
.attr("height", 1)
.attr("width", 1)
.attr("x", "0")
.attr("y", "0")
catpattern.append("image")
.attr("x", -130)
.attr("y", -220)
.attr("height", 640)
.attr("width", 480)
.attr("xlink:href", imgurl)
svg.append("circle")
.attr("r", 100)
.attr("cy", 80)
.attr("cx", 120)
.attr("fill", "url(#catpattern)")
*/
// some colour variables
var tcBlack = "#130C0E";
// rest of vars
var w = 960,
h = 800,
maxNodeSize = 50,
x_browser = 20,
y_browser = 25,
root;
var vis;
var force = d3.layout.force();
vis = d3.select("#vis").append("svg").attr("width", w).attr("height", h);
//d3.json("marvel.json", function(json) {
var json = marvelJson;
root = json;
root.fixed = true;
root.x = w / 2;
root.y = h / 4;
// Build the path
var defs = vis.insert("svg:defs")
.data(["end"]);
defs.enter().append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
try{
update();
}catch(e){
console.log(e);
}
//});
/**
*
*/
function update() {
var nodes = flatten(root),
links = d3.layout.tree().links(nodes);
// Restart the force layout.
force.nodes(nodes)
.links(links)
.gravity(0.05)
.charge(-1500)
.linkDistance(100)
.friction(0.5)
.linkStrength(function(l, i) {return 1; })
.size([w, h])
.on("tick", tick)
.start();
var path = vis.selectAll("path.link")
.data(links, function(d) { return d.target.id; });
path.enter().insert("svg:path")
.attr("class", "link")
// .attr("marker-end", "url(#end)")
.style("stroke", "#eee");
// Exit any old paths.
path.exit().remove();
// Update the nodes…
var node = vis.selectAll("g.node")
.data(nodes, function(d) { return d.id; });
// Enter any new nodes.
var nodeEnter = node.enter().append("svg:g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.on("click", click)
.call(force.drag);
/*Dinesh Code*/
/*
var defs = svg.append("defs").attr("id", "imgdefs")
var catpattern = defs.append("pattern")
.attr("id", "catpattern")
.attr("height", 1)
.attr("width", 1)
.attr("x", "0")
.attr("y", "0")
catpattern.append("image")
.attr("x", -130)
.attr("y", -220)
.attr("height", 640)
.attr("width", 480)
.attr("xlink:href", imgurl)
svg.append("circle")
.attr("r", 100)
.attr("cy", 80)
.attr("cx", 120)
.attr("fill", "url(#catpattern)")
*/
/******* Dinesh Code End******/
nodeEnter.append("defs").attr("id", "imgdefs")
.append("pattern")
.attr("id", function(d){ if(d.img)return d.img.replace(/[/|.|:]/g, ""); else null; })
.attr("height", 1)
.attr("width", 1)
.attr("x", "0")
.attr("y", "0")
.append("image")
.attr("height", function(d) { return 2* Math.sqrt(d.size) / 10 || 4.5; })
.attr("width", function(d) { return 2* Math.sqrt(d.size) / 10 || 4.5; })
.attr("xlink:href", function(d){if(d.img)return d.img+""; else null; })
// Append a circle
var images = nodeEnter.append("svg:circle")
.attr("r", function(d) { return Math.sqrt(d.size) / 10 || 4.5; })
.attr("fill", function(d){ if(d.img)return "url(#"+d.img.replace(/[/|.|:]/g, "")+")" ; else null; });
// Append images
/* var images = nodeEnter.append("svg:image")
.attr("xlink:href", function(d) { return d.img;})
.attr("x", function(d) { return -25;})
.attr("y", function(d) { return -25;})
.attr("height", 50)
.attr("width", 50);*/
// make the image grow a little on mouse over and add the text details on click
var setEvents = images
// Append hero text
.on( 'click', function (d) {
d3.select("h1").html(d.hero);
d3.select("h2").html(d.name);
d3.select("h3").html ("Take me to " + "<a href='" + d.link + "' >" + d.hero + " web page ⇢"+ "</a>" );
})
.on( 'mouseenter', function(d) {
if(d.img){
//console.log(d3.select(this).parent);
d3.select("pattern#"+d.img.replace(/[/|.|:]/g, "")).select("image").attr("width","100").attr("height","100");
// select element in current context
d3.select( this )
.transition()
.attr("x", function(d) { return -60;})
.attr("y", function(d) { return -60;})
.attr("r", "50")
//.attr("height", 100)
//.attr("width", 100);
}
})
// set back
.on( 'mouseleave', function(d) {
if(d.img){
d3.select("pattern#"+d.img.replace(/[/|.|:]/g, "")).select("image").attr("width","40").attr("height","40");
d3.select( this )
.transition()
.attr("x", function(d) { return -25;})
.attr("y", function(d) { return -25;})
.attr("r", "20")
//.attr("height", 50)
//.attr("width", 50);
}
});
// Append hero name on roll over next to the node as well
nodeEnter.append("text")
.attr("class", "nodetext")
.attr("x", x_browser)
.attr("y", y_browser +15)
.attr("fill", tcBlack)
.text(function(d) { return d.hero; });
// Exit any old nodes.
node.exit().remove();
// Re-select for update.
path = vis.selectAll("path.link");
node = vis.selectAll("g.node");
function tick() {
path.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + ","
+ d.source.y
+ "A" + dr + ","
+ dr + " 0 0,1 "
+ d.target.x + ","
+ d.target.y;
});
node.attr("transform", nodeTransform);
}
}
/**
* Gives the coordinates of the border for keeping the nodes inside a frame
* http://bl.ocks.org/mbostock/1129492
*/
function nodeTransform(d) {
d.x = Math.max(maxNodeSize, Math.min(w - (d.imgwidth/2 || 16), d.x));
d.y = Math.max(maxNodeSize, Math.min(h - (d.imgheight/2 || 16), d.y));
return "translate(" + d.x + "," + d.y + ")";
}
/**
* Toggle children on click.
*/
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update();
}
/**
* Returns a list of all nodes under the root.
*/
function flatten(root) {
var nodes = [];
var 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;
}
#import url(http://fonts.googleapis.com/css?family=Source+Code+Pro:400,600);
body {font-family: "Source Code Pro", Consolas, monaco, monospace; line-height: 160%; font-size: 16px; margin: 0; }
path.link {
fill: none;
stroke-width: 2px;
}
.node:not(:hover) .nodetext {
display: none;
}
h1 { font-size: 36px; margin: 10px 0; text-transform: uppercase; font-weight: normal;}
h2, h3 { font-size: 18px; margin: 5px 0 ; font-weight: normal;}
header {padding: 20px; position: absolute; top: 0; left: 0;}
a:link { color: #EE3124; text-decoration: none;}
a:visited { color: #EE3124; }
a:hover { color: #A4CD39; text-decoration: underline;}
a:active { color: #EE3124; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.0/d3.min.js"></script>
<section id="vis"></section>
After working for few hours I made something out of it, kindly observe the code changes I've made, If you can't figure it out, comment below, I'll edit my answer with full explanation.
:D
Have been struggling for ages to create a horizontal tree layout with rectangles instead of circles, and have the text wrap inside those rectangles. Nothing I do seems to work, I have tried this code but whoever made it has left out a crucial step, in defining the variable d before the line
if (d.name.length > 26) which stops the whole script from running.
I have also been trying to use d3plus.js from http://d3plus.org/ in order to wrap text inside rect tags but it actually doesn't work half the time and seems to need a trigger like a click function in order to work. Also considering using this example as a guide on text wrapping.
In my research I have not found that anybody has done the combination of horizontal, rectangles and wrapped text in one diagram.
Also I am a bit of a d3 noob so all help is appreciated.
Here is a JSFiddle.
Here is the current code I am using which isn't working:
var w = 960,
h = 2000,
i = 0,
duration = 500,
root;
var tree = d3.layout.tree()
.size([h, w - 160]);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
var vis = d3.select("#container").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(40,0)");
root = treeData[0];
root.x0 = h / 2;
root.y0 = 0;
update(root);
function update(source) {
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse();
// Update the nodes…
var node = vis.selectAll("g.node")
.data(nodes, function(d) { return d.id || (d.id = ++i); });
var nodeEnter = node.enter().append("svg:g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; });
// Enter any new nodes at the parent's previous position.
nodeEnter.append("svg:rect")
.attr("width", 150)
.attr("height", function(d) { return (d.name.length > 30) ? 38 : 19;})
.attr("y",-11)
.attr("rx",2)
.attr("ry",2)
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; })
.on("click", click);
if (d.name.length > 26) {
nodeEnter.append("svg:text")
.attr("x", function(d) { return d._children ? -8 : 8; })
.attr("y", 3)
.text(function(d) { return d.name; });
} else {
nodeEnter.append("svg:text")
.attr("x", function(d) { return d._children ? -8 : 8; })
.attr("y", 3)
.append("svg:tspan")
.text(function(d) { return d.name.slice(0,26); })
.append("svg:tspan")
.attr("x", function(d) { return d._children ? -8 : 8; })
.attr("y",15)
.text(function(d) { return d.name.slice(26); });
}
}
// Transition nodes to their new position.
nodeEnter.transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
.style("opacity", 1)
.select("rect")
.style("fill", "lightsteelblue");
node.transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
.style("opacity", 1);
node.exit().transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
.style("opacity", 1e-6)
.remove();
// Update the links…
var link = vis.selectAll("path.link")
.data(tree.links(nodes), function(d) { return d.target.id; });
// Enter any new links at the parent's previous position.
link.enter().insert("svg:path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {x: source.x0, y: source.y0};
return diagonal({source: o, target: o});
})
.transition()
.duration(duration)
.attr("d", diagonal);
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
})
.remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
// Toggle children on click.
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
d3.select(self.frameElement).style("height", "2000px");
As well as my json:
var treeData = [
{
"name": "Do trainees require direction as to what to do or how to do the task (either before they start or while they are completing it?",
"children": [
{
"name": "Can they satisfactorily complete the task assigned to them?",
"children": [
{
"name": "Rating level 4",
"parent": "A",
},
{
"name": "How many problems / queries are there that still need to be addressed / resolved to be able to satisfactorily complete the task?",
"children": [
{
"name": "Are problems / queries fundamental to the completion of the task at hand?",
"children": [
{
"name": "Rating level 4",
},
{
"name": "Can the problems be resolved by the trainee (after receiving guidance)?",
"children": [
{
"name": "Rating level 3",
},
{
"name": "Can the problems be resolved by the trainee (after receiving guidance)?",
"children": [
{
"name": "Rating level 2",
},
{
"name": "Rating level 1",
}
]
}
]
}
]
},
{
"name": "Are problems / queries fundamental to the completion of the task at hand?",
}
]
}
]
},
{
"name": "Can they satisfactorily complete the task assigned to them?",
"children": [
{
"name": "Rating 1",
},
{
"name": "Rating 2",
},
{
"name": "Rating 3",
},
{
"name": "Rating 4",
}
]
}
]
}];
Your code throws an error at this line:
if (d.name.length > 26) {
d isn't defined. When d3 code references d, it's usually in the scope of a data binding. At this place in the code, you are not looping a binding, like:
nodeEnter.append("text")
.attr("x", function(d) {
return d._children ? -8 : 8;
})
.attr("y", 3)
.attr("dy", "0em")
.text(function(d) {
return d.name; // d is defined from the binding
});
That said, I like the wrap function you link to. So add your text like above and then wrap the text:
wrap(d3.selectAll('text'),150);
Here's a quick modification to the wrap that'll resize your rects as well:
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
// find corresponding rect and reszie
d3.select(this.parentNode.children[0]).attr('height', 19 * (lineNumber+1));
});
}
Example here.
I have a graph structure that stored in json format that looks like this:
{
"links": [
{
"source": 1,
"target": 0,
"value": 1
},
{
"source": 2,
"target": 0,
"value": 1
},
{
"source": 3,
"target": 0,
"value": 1
}
],
"nodes": [
{
"group": 3,
"name": "justintimberlake"
},
{
"group": 2,
"name": "Anastacia Lyn Newton"
},
{
"group": 2,
"name": "Maria Do Carmo"
}
],
"time": [
{
"source": 1,
"time": 6.854456018518518
},
{
"source": 2,
"time": 6.320115740740741
},
{
"source": 3,
"time": 5.962986111111111
}
]
}
And I have D3 code that draws this network:
<!DOCTYPE html xmlns:xlink="http://www.w3.org/1999/xlink">
<meta charset="utf-8">
<style>
// style here
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<div id="animviz"></div>
<script>
d3.json("post000.json", function(error, graph) {
var vv = window,
w = vv.innerWidth,
h = vv.innerHeight;
var svg = d3.select("#animviz")
.append("svg")
.attr("width", w)
.attr("height", h)
.append("g")
.call(d3.behavior.zoom().scaleExtent([0, 8]).on("zoom", zoom))
.append("g");
var color = d3.scale.category10();
var force = d3.layout.force()
.charge(-200)
.linkDistance(50)
.size([w, h]);
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.attr("transform", function(d) { return "translate(" + d + ")"; });
function zoom() {
svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
var myMouseoverFunction = function() {
var circle = d3.select(this);
circle.transition().duration(100)
.attr("r", 20 )
node.append("title")
.text(function(d) { return d.name});
}
var myMouseoutFunction = function() {
var circle = d3.select(this);
circle.transition().duration(500)
.attr("r", 10 );
}
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 10)
.style("fill", function(d) { return color(d.group); })
.call(force.drag)
.on("mouseover", myMouseoverFunction)
.on("mouseout", myMouseoutFunction);
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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
});
</script>
</body>
What I want is to draw this graph node by node according to time parameter (i.e. source: 1 should be drawn after 6.854456018518518 sec after node = 0 was drawn).
If it's not possible to draw them after special number of seconds, I'd like at least to draw them in order, so that I can see how nodes appear one after the other.
I checked similar questions (here, here, and here) and this tutorial but wasn't able to solve my problem. Ideally I would love to have similar to this but for my data from json file and not in infinite loop.
How can I draw a graph stored in json node by node?
one way to achieve this is to create nodes with radius = 0, and then use delay for showing each node (giving it radius = 12):
node.attr("r", 0);
var totalDelay = 0;
node
.transition()
.duration(0)
.delay(function(d, i) {
totalDelay += graph.time[i].time * 1000;
return totalDelay
})
.attr("r", 12);
See this jsFiddle
The problem with this solution is that all the links appear immediately, without waiting for its nodes to appear.
Added:
to deal with links problem, you may want to redraw graph after each interval, every time adding one node, and calculating the array of links for the nodes, displayed in each iteration:
var i = 0;
function redraw() {
if (i === graph.time.length) return;
setTimeout(function() {
var nodes = graph.nodes.slice(0, i + 1);
var links = graph.links.filter(function(link) {
return (link.source <= i && link.target <= i)
});
draw(nodes, links);
i += 1;
redraw();
}, graph.time[i].time * 1000);
}
See improved jsFiddle
For big datasets might be more efficient to keep the same nodes array and do nodes.push(graph.nodes[i]), instead of creating a new array in each iteration.
When I try to layout my force-directed graph, the following is the error I receive. I read about this issue in Mike Bostock's github page and found that it might be due to NaN values of coordinates or all the points drawn at the same point. I checked into the console and I found that all of my points are getting drawn at the same X and Y values.
On an example data, the code worked perfectly well but no more now. My data goes like 45-50 levels away from the center node. I have successfully made a tree layout with this data. Wanted to try the force directed layout but it did not work.
Any help regarding how to draw the nodes on separate coordinates would be much appreciated.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
stroke-opacity: .6;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var margin = {top: 10, right: 10, bottom: 20, left: 40},
width = 1300 - margin.left - margin.right,
height = 800 - margin.top - margin.bottom;
var color = d3.scale.category20();
var force = d3.layout.force().charge(-120)
.linkDistance(30)
.size([width, height]);
var svg = d3.select("body").append("svg:svg")
.attr("width", width)
.attr("height", height)
//.attr("pointer-events","all")
.append('svg:g')
//.call(d3.behavior.zoom().translate([100,50]).scale(.5).on("zoom",redraw))
.append('svg:g')
.attr("transform","translate(100,50)scale(.5,.5)");
svg.append('svg:rect')
.attr('width', width)
.attr('height', height)
.attr('fill','white')
.attr('opacity',0);
function redraw() {
var trans = d3.event.translate;
var scale = d3.event.scale;
svg.attr("transform",
"translate(" + trans + ")"
+ " scale(" + scale + ")");
};
d3.json("test_data.json", function(error, graph) {
var nodeMap = {};
graph.nodes.forEach(function(x) { nodeMap[x.name] = x; });
graph.links = graph.links.map(
function(x)
{
return {
source: nodeMap[x.source],
target: nodeMap[x.target],
value: x.value
};
});
console.log(graph.nodes);
console.log(graph.links);
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke", function(d) { return color(d.value); })
.style("stroke-width", function(d) {
//console.log(d.value);
return Math.sqrt(d.value); });
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 5)
.style("fill", function(d) { return color(d.value); })
.call(force.drag)
.on("mousedown",
function(d) {
d.fixed = true;
d3.select(this).classed("sticky", true);
}
)
.on("mouseover",fade(0.1))
.on("mouseout",fade(1));
var linkedByIndex = {};
graph.links.forEach(function(d) {
linkedByIndex[d.source.index + "," + d.target.index] = 1;
});
function isConnected(a, b) {
return linkedByIndex[a.index + "," + b.index] || linkedByIndex[b.index + "," + a.index] || a.index == b.index;
}
node.append("title")
.text(function(d) {
return "Name : "+d.name+"\n"+"Parent: "+d.parent +"\n"+"Relationship: "+ d.relationship +"\n"+ "Creation Date: "+ d.cod +"\n"; });
function fade(opacity)
{
return function(d) {
node.style("stroke-opacity", function(o) {
thisOpacity = isConnected(d, o) ? 1 : opacity;
this.setAttribute('fill-opacity', thisOpacity);
return thisOpacity;
});
link.style("stroke-opacity", opacity).style("stroke-opacity", function(o) {
return o.source === d || o.target === d ? 1 : opacity;
});
};
}
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 + ")"; });
/*node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });*/
});
});
</script>
The JSON looks like:
{
"links": [
{
"source": "Me",
"target": "Adam",
"value": 10
},
{
"source": "Me",
"target": "You",
"value": 10
}
], "nodes": [
{
"ancestor": "Adam",
"cod": 19061964,
"distance": 0,
"name": "Adam",
"parent": null,
"relationship": null,
"value": 10
},
{
"ancestor": "Adam",
"cod": 13032003,
"distance": 1,
"name": "Me",
"parent": "You",
"relationship": "Father",
"value": 10
}
]
}
EDIT: I get the error in the following statement:
force
.nodes(graph.nodes)
.links(graph.links)
.start();
highlighting:
"start();"
In your data your links are to names as opposed to indices. Check out the example data from http://bl.ocks.org/mbostock/4062045.
Links have the form: {"source":1,"target":0,"value":1}, which points to the index of the node.