I am attempting to make a tree viewing web application. I am using the d3 tree layout to generate my trees. What I would like to be able to do is scale the size of the tree in either axis. In otherwords being able to scale the x and y axis of the viewer. The would effectively just increase either the vertical distance between nodes or increase the length of the branches.
I have been unable to accomplish this. I have attempted to change the nodeSize() of the tree layout, and then update which had no effect. I have also attempted to modify the x or y coordinate of each node which again had no effect. I have been completely unable to modify the physical layout of the tree once it has been rendered.
I have read and attempted the fixes in the following threads:
Dynamically resize the d3 tree layout based on number of childnodes
D3 Tree Layout Separation Between Nodes using NodeSize
But they did not solve my problem.
I am implementing a TreeGenerator object, which handles all of the rendering of the SVG, using prototyped functions. I am going for a full screen feeling, so the TreeGenerator appends an SVG to a div with width: 100%; height: 100%. I should node that I have implemented both zooming and panning functionality.
Here is the constructor:
TreeGenerator = function(target, treeString) {
// Set up everything here
// colors
this.nodeColor = "#3B6073";
this.highlightColor = "#F22248";
this.searchColor = "#0DFFC2";
this.nodeHeight = 5;
this.nodeLength = 20;
this.zoomX = 1;
this.zoomY = 5;
this._winHeight = window.innerHeight;
this._winWidth = window.innerWidth;
//this.xScale = d3.scale.linear().domain([0,100]).range([0, this._winWidth])
//this.yScale = d3.scale.linear().domain([0,100]).range([0, this._winHeight])
// keep the matching results from last search
this._searchResults = [];
// path lengths
this.maxPathLength = 0;
this._loadSVG(target);
this._tree(treeString);
}
And here are the two functions _loadSVG and _tree which are called from the constructor:
TreeGenerator.prototype._loadSVG = function(target) {
var zoom = d3.behavior.zoom()
//.size([this._winWidth, this._winHeight])
.scaleExtent([this.zoomX,this.zoomY])
//.x(this.xScale)
//.y(this.yScale)
//.center([height/2, width/2])
.on("zoom", zoomed);
var drag = d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
this.svg = d3.select(target, ":first-child").append("svg")
.append("g")
//.attr("transform", "translate("+width / 2+","+height / 2+")")
.call(zoom)
.on("dblclick.zoom", null)
var rect = this.svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.style("fill", "none")
.style("pointer-events", "all");
this.container = this.svg.append("g")
// scope it for d3 funcs
var container = this.container
var self = this;
function dottype(d) {
d.x = +d.x;
d.y = +d.y;
return d;
}
function zoomed() {
container.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("dragging", true);
}
function dragged(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("dragging", false);
}
}
And _tree:
TreeGenerator.prototype._tree = function (treeString) {
// needs an array for d3
this.treeData = this.parseTree(treeString);
this.root = this.treeData[0];
// set up the layout
this.tree = d3.layout.tree()
.nodeSize([this.nodeHeight,this.nodeLength]);
// set path dists
this._setPathDist();
this.yScale = d3.scale.linear().domain([0, this.maxPathLength]).range([0, 20]);
this.xScale = d3.scale.linear().domain([1,100]).range([10, 30]);
var self = this;
update(this.root);
function update(source) {
var i = 0;
// generator for paths from
// node ---> node
var horizontal = d3.svg.line().interpolate('step-before')
.x(function (d) { return d.x; })
.y(function (d) { return d.y; });
// Compute the new tree layout.
var nodes = self.tree.nodes(source).reverse()
nodes.forEach(function (d) {
if(!d.pathLength == 0) {
d.y = d.y * self.yScale(d.pathLength);
}
else if(d.children != undefined) {
d.y += 5;
}
});
links = self.tree.links(nodes);
// Declare the nodes
var node = self.container.selectAll("g.node")
.data(nodes, function(d) { return d.id || (d.id = ++i); })
// Enter the nodes.
doubleclickTimer = false // dont ask
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("id", function(d) { return "node-"+d.id.toString(); })
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")"; })
.on("click", clicked);
nodeEnter.append("circle")
.attr("r", 1)
.style("fill", self.nodeColor)
nodeEnter.append("text")
.attr("x", function(d) {
return d.children || d._children ? -1 : 1; })
.attr("y", function(d) {
return d.children == undefined ? 1 : -1;
})
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.name; })
.style("fill-opacity", 1);
// try and update
//var nodeUpdate = node.update()
// .attr("x", function(d) { return d.x });
// Declare the links
var link = self.container.selectAll("path.link")
.data(links, function(d) { return d.target.id; });
// Enter the links.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
return horizontal([
{y: d.source.x, x: d.source.y},
{y: d.target.x, x: d.target.y}
]);
});
function clicked(d) {
// need the g group for highlight
node = this;
// if double click timer is active, this click is the double click
if ( doubleclickTimer )
{
clearTimeout(doubleclickTimer);
doubleclickTimer = false;
collapse(d);
}
// otherwise, what to do after single click (double click has timed out)
else {
doubleclickTimer = setTimeout( function() {
doubleclickTimer = false;
highlight(node, d);
}, 175);
}
}
function highlight(node,d) {
// we want to bold the text
// and color the node
self._unhighlightNode();
// toggle if clicking again
if(self._highlightedNode == node) {
self._highlightedNode = undefined;
self.selectedNode = undefined;
return;
}
// set the new one
var circle = d3.select(node).select("circle");
var text = d3.select(node).select("text");
circle.style("fill", self.highlightColor);
text.style("font-size","4px");
// update the pointer
self._highlightedNode = node;
self.selectedNode = d;
}
function collapse(d) {
}
}
};
I know the code is a bit messy, but it is just full of commented out things I did in an attempt to fix this issue. I appreciate any help that can be offered. Hopefully I included enough info.
Related
I have looked for answer to this but none of the similar questions help me in my situation. I have a D3 tree that creates new nodes at runtime. I would like to add HTML (so I can format) to a node when I mouseover that particular node. Right now I can add HTML but its unformatted. Please help!
JSFiddle: http://jsfiddle.net/Srx7z/
JS Code:
var width = 960,
height = 500;
var tree = d3.layout.tree()
.size([width - 20, height - 60]);
var root = {},
nodes = tree(root);
root.parent = root;
root.px = root.x;
root.py = root.y;
var diagonal = d3.svg.diagonal();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(-30,40)");
var node = svg.selectAll(".node"),
link = svg.selectAll(".link");
var duration = 750;
$("#submit_button").click(function() {
update();
});
function update() {
if (nodes.length >= 500) return clearInterval(timer);
// Add a new node to a random parent.
var n = {id: nodes.length},
p = nodes[Math.random() * nodes.length | 0];
if (p.children) p.children.push(n); else p.children = [n];
nodes.push(n);
// Recompute the layout and data join.
node = node.data(tree.nodes(root), function (d) {
return d.id;
});
link = link.data(tree.links(nodes), function (d) {
return d.source.id + "-" + d.target.id;
});
// Add entering nodes in the parent’s old position.
var gelement = node.enter().append("g");
gelement.append("circle")
.attr("class", "node")
.attr("r", 20)
.attr("cx", function (d) {
return d.parent.px;
})
.attr("cy", function (d) {
return d.parent.py;
});
// Add entering links in the parent’s old position.
link.enter().insert("path", ".g.node")
.attr("class", "link")
.attr("d", function (d) {
var o = {x: d.source.px, y: d.source.py};
return diagonal({source: o, target: o});
})
.attr('pointer-events', 'none');
node.on("mouseover", function (d) {
var g = d3.select(this);
g.append("text").html('First Line <br> Second Line')
.classed('info', true)
.attr("x", function (d) {
return (d.x+20);
})
.attr("y", function (d) {
return (d.y);
})
.attr('pointer-events', 'none');
});
node.on("mouseout", function (d) {
d3.select(this).select('text.info').remove();
});
// Transition nodes and links to their new positions.
var t = svg.transition()
.duration(duration);
t.selectAll(".link")
.attr("d", diagonal);
t.selectAll(".node")
.attr("cx", function (d) {
return d.px = d.x;
})
.attr("cy", function (d) {
return d.py = d.y;
});
}
Using Lars Kotthoff's excellent direction, I got it working so I decided to post it for others and my own reference:
http://jsfiddle.net/FV4rL/2/
with the following code appended:
node.on("mouseover", function (d) {
var g = d3.select(this); // The node
var div = d3.select("body").append("div")
.attr('pointer-events', 'none')
.attr("class", "tooltip")
.style("opacity", 1)
.html("FIRST LINE <br> SECOND LINE")
.style("left", (d.x + 50 + "px"))
.style("top", (d.y +"px"));
});
I am trying to display d3 collapsable tree with search, here is the reference example am using [Search tree][1]
[1]: https://bl.ocks.org/jjzieve/a743242f46321491a950 I am getting error select2() is not a function. I did not get any solution on v5, how to change the above example based on my code.
Here is my code
function searchTree(obj, search, path) {
if (obj.name === search) { //if search is found return, add the object to the path and return it
path.push(obj);
return path;
}
else if (obj.children || obj._children) { //if children are collapsed d3 object will have them instantiated as _children
var children = (obj.children) ? obj.children : obj._children;
for (var i = 0; i < children.length; i++) {
path.push(obj);// we assume this path is the right one
var found = searchTree(children[i], search, path);
if (found) {// we were right, this should return the bubbled-up path from the first if statement
return found;
}
else {//we were wrong, remove this parent from the path and continue iterating
path.pop();
}
}
}
else {//not the right object, return false so it will continue to iterate in the loop
return false;
}
}
function extract_select2_data(node, leaves, index) {
if (node.children) {
for (var i = 0; i < node.children.length; i++) {
index = extract_select2_data(node.children[i], leaves, index)[0];
}
}
else {
leaves.push({ id: ++index, text: node.name });
}
return [index, leaves];
}
console.log("left side hitting ");
// Set the dimensions and margins of the diagram
var margin = { top: 20, right: 120, bottom: 20, left: 120 },
width = 1280 * 10 - margin.right - margin.left,
height = 1200 *10 - margin.top - margin.bottom;
// append the svg object to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select(".col-md-6").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate("
+ margin.left + "," + margin.top + ")");
var i = 0,
duration = 750,
root, path,select2_data;
// declares a tree layout and assigns the size
var treemap = d3.tree().size([height, width]);
function openPaths(paths) {
for (var i = 0; i < paths.length; i++) {
if (paths[i].id !== "1") {//i.e. not root
paths[i].class = 'found';
if (paths[i]._children) { //if children are hidden: open them, otherwise: don't do anything
paths[i].children = paths[i]._children;
paths[i]._children = null;
}
update(paths[i]);
}
}
}
// Assigns parent, children, height, depth
root = d3.hierarchy(this.parentTreeObj, function (d) { return d.children });
select2_data = extract_select2_data(this.parentTreeObj,[],0)[1];
root.x0 = height / 2;
root.y0 = 0;
// Collapse after the second level
root.children.forEach(collapse);
update(root);
$(".col-md-6").select2({
data: select2_data,
containerCssClass: "search"
});
//attach search box listener
$(".col-md-6").on("select2-selecting", function (e) {
var paths = searchTree(root, e.object.text, []);
if (typeof (paths) !== "undefined") {
openPaths(paths);
}
else {
alert(e.object.text + " not found!");
}
})
d3.select(self.frameElement).style("height", "800px");
// Collapse the node and all it's children
function collapse(d) {
if (d.children) {
d._children = d.children
d._children.forEach(collapse)
d.children = null
}
}
function update(source) {
var duration = d3.event && d3.event.altKey ? 5000 : 500;
// compute the new height
var levelWidth = [1];
var childCount = function (level, n) {
if (n.children && n.children.length > 0) {
if (levelWidth.length <= level + 1) levelWidth.push(0);
levelWidth[level + 1] += n.children.length;
n.children.forEach(function (d) {
childCount(level + 1, d);
});
}
};
childCount(0, root);
var newHeight = d3.max(levelWidth) * 20;
treemap = d3.tree().size([newHeight, width]);
// Assigns the x and y position for the nodes
var treeData = treemap(root);
// Compute the new tree layout.
var nodes = treeData.descendants(),
links = treeData.descendants().slice(1);
// Normalize for fixed-depth.
nodes.forEach(function (d) { d.y = (d.depth * 180); });
// ****************** Nodes section ***************************
// Update the nodes...
var node = svg.selectAll('g.node')
.data(nodes, function (d) {
return d.id || (d.id = ++i);
});
// Enter any new modes at the parent's previous position.
var nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr("transform", function (d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
// Add Circle for the nodes
nodeEnter.append('circle')
.attr('class', 'node')
.attr('r', 1e-6)
.style("fill", function (d) {
return d._children ? "#eb8c00" : "#da5506";
});
// Add labels for the nodes
nodeEnter.append('text')
.attr("dy", ".35em")
.attr("x", function (d) {
return d.children || d._children ? -13 : 13;
})
.attr("text-anchor", function (d) {
return d.children || d._children ? "end" : "start";
})
.text(function (d) {
return d.data.name;
});
// UPDATE
var nodeUpdate = nodeEnter.merge(node);
// Transition to the proper position for the node
nodeUpdate.transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Update the node attributes and style
nodeUpdate.select('circle.node')
.attr('r', 10)
.style("fill", function (d) {
return d._children || d._id ? "#eb8c00" : "#da5506";
})
.attr('cursor', 'pointer');
// Remove any exiting nodes
var nodeExit = node.exit().transition()
.duration(duration)
.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', 1e-6);
// On exit reduce the opacity of text labels
nodeExit.select('text')
.style('fill-opacity', 1e-6);
// ****************** links section ***************************
// Update the links...
var link = svg.selectAll('path.link')
.data(links, function (d) { return d.id; });
// Enter any new links at the parent's previous position.
var linkEnter = link.enter().insert('path', "g")
.attr("class", "link")
.attr('d', function (d) {
var o = { x: source.x0, y: source.y0 }
return diagonal(o, o)
});
// UPDATE
var linkUpdate = linkEnter.merge(link);
// Transition back to the parent element position
linkUpdate.transition()
.duration(duration)
.attr('d', function (d) { return diagonal(d, d.parent) });
// Remove any exiting links
var linkExit = link.exit().transition()
.duration(duration)
.attr('d', function (d) {
var o = { x: source.x, y: source.y }
return diagonal(o, o)
})
.remove();
// Store the old positions for transition.
nodes.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
// Creates a curved (diagonal) path from parent to the child nodes
function diagonal(s, d) {
console.log("left side hitting inside ");
console.log("in Left");
// console.log("idddd",d.data.id);
path = `M ${s.y} ${s.x}
C ${(s.y + d.y) / 2} ${s.x},
${(s.y + d.y) / 2} ${d.x},
${d.y} ${d.x}`
return path
}
// Toggle children on click.
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
// d._id = d.data.id;
// d.data.id = null;
} else {
d.children = d._children;
d._children = null;
// d.data.id = d._id;
// d._id = null;
}
update(d);
}
}
Can anyone please help me out this. Thanks in advance
Have you linked your file to the select2 library? For example, do you have this:
<link rel="stylesheet" type="text/css" href="http://cdnjs.cloudflare.com/ajax/libs/select2/3.5.2/select2.min.css"></link>
<script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/select2/3.5.2/select2.min.js"></script>
If you could share a link through JSFiddle, bl.ocks, or in a repository, that would help out a lot too. Let me know if that helps.
I have looked for answer to this but none of the similar questions help me in my situation. I have a D3 tree that creates new nodes at runtime. I would like to add HTML (so I can format) to a node when I mouseover that particular node. Right now I can add HTML but its unformatted. Please help!
JSFiddle: http://jsfiddle.net/Srx7z/
JS Code:
var width = 960,
height = 500;
var tree = d3.layout.tree()
.size([width - 20, height - 60]);
var root = {},
nodes = tree(root);
root.parent = root;
root.px = root.x;
root.py = root.y;
var diagonal = d3.svg.diagonal();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(-30,40)");
var node = svg.selectAll(".node"),
link = svg.selectAll(".link");
var duration = 750;
$("#submit_button").click(function() {
update();
});
function update() {
if (nodes.length >= 500) return clearInterval(timer);
// Add a new node to a random parent.
var n = {id: nodes.length},
p = nodes[Math.random() * nodes.length | 0];
if (p.children) p.children.push(n); else p.children = [n];
nodes.push(n);
// Recompute the layout and data join.
node = node.data(tree.nodes(root), function (d) {
return d.id;
});
link = link.data(tree.links(nodes), function (d) {
return d.source.id + "-" + d.target.id;
});
// Add entering nodes in the parent’s old position.
var gelement = node.enter().append("g");
gelement.append("circle")
.attr("class", "node")
.attr("r", 20)
.attr("cx", function (d) {
return d.parent.px;
})
.attr("cy", function (d) {
return d.parent.py;
});
// Add entering links in the parent’s old position.
link.enter().insert("path", ".g.node")
.attr("class", "link")
.attr("d", function (d) {
var o = {x: d.source.px, y: d.source.py};
return diagonal({source: o, target: o});
})
.attr('pointer-events', 'none');
node.on("mouseover", function (d) {
var g = d3.select(this);
g.append("text").html('First Line <br> Second Line')
.classed('info', true)
.attr("x", function (d) {
return (d.x+20);
})
.attr("y", function (d) {
return (d.y);
})
.attr('pointer-events', 'none');
});
node.on("mouseout", function (d) {
d3.select(this).select('text.info').remove();
});
// Transition nodes and links to their new positions.
var t = svg.transition()
.duration(duration);
t.selectAll(".link")
.attr("d", diagonal);
t.selectAll(".node")
.attr("cx", function (d) {
return d.px = d.x;
})
.attr("cy", function (d) {
return d.py = d.y;
});
}
Using Lars Kotthoff's excellent direction, I got it working so I decided to post it for others and my own reference:
http://jsfiddle.net/FV4rL/2/
with the following code appended:
node.on("mouseover", function (d) {
var g = d3.select(this); // The node
var div = d3.select("body").append("div")
.attr('pointer-events', 'none')
.attr("class", "tooltip")
.style("opacity", 1)
.html("FIRST LINE <br> SECOND LINE")
.style("left", (d.x + 50 + "px"))
.style("top", (d.y +"px"));
});
Google Chrome console is returning the following error:
Error: Problem parsing d="M296.20961279999995,NaNC462.51521919999993,NaN 311.0200934399999,NaN 477.3256998399999,NaN"
Posted is my code:
var taskflows = [
{
"description" : "Taskflow #1",
"taskExecutions" : [
{"description" : "First","step" : 1},
{"description" : "Second","step" : 2},
{"description" : "Second", "step" : 2},
{"description" : "Third", "step" : 3},
{"description" : "Fourth", "step" : 4},
{"description" : "Fifth","step" : 5},
{"description" : "Sixth", "step" : 6},
{"description" : "Sixth", "step" : 6},
{"description" : "Seventh","step" : 7},
{"description" : "Eighth", "step" : 8},
{"description" : "Ninth", "step" : 9},
{"description" : "Tenth", "step" : 10},
{"description" : "Eleventh", "step" : 11},
{"description" : "Twelve", "step" : 12},
{"description" : "Twelve","step" : 12}
]
}];
var taskflowTree = function (taskflow) {
var tree = taskflow.map(function(a) {
return { "name": a.description, "step": a.step }
});
tree.forEach(function(v, i, arr) {
v.children = arr.filter(function(a) {
return a.step == v.step+1;
});
});
return tree.filter(function(a) {
return a.step == 1;
});
}
var formattedTaskflowTree = taskflowTree(taskflows[0].taskExecutions);
(function(treeData) {
// Calculate total nodes, max label length
var totalNodes = 0;
var maxLabelLength = 0;
// variables for drag/drop
var selectedNode = null;
var draggingNode = null;
// panning variables
var panSpeed = 200;
var panBoundary = 20; // Within 20px from edges will pan when dragging.
// Misc. variables
var i = 0;
var duration = 750;
var root;
// size of the diagram
var viewerWidth = $(document).width();
var viewerHeight = $(document).height();
var tree = d3.layout.tree()
.size([viewerHeight, viewerWidth]);
// define a d3 diagonal projection for use by the node paths later on.
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
// A recursive helper function for performing some setup by walking through all nodes
function visit(parent, visitFn, childrenFn) {
if (!parent) return;
visitFn(parent);
var children = childrenFn(parent);
if (children) {
var count = children.length;
for (var i = 0; i < count; i++) {
visit(children[i], visitFn, childrenFn);
}
}
}
// Call visit function to establish maxLabelLength
visit(treeData, function(d) {
totalNodes++;
maxLabelLength = Math.max(d.name.length, maxLabelLength);
}, function(d) {
return d.children && d.children.length > 0 ? d.children : null;
});
// sort the tree according to the node names
function sortTree() {
tree.sort(function(a, b) {
return b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1;
});
}
// Sort the tree initially incase the JSON isn't in a sorted order.
sortTree();
// TODO: Pan function, can be better implemented.
function pan(domNode, direction) {
var speed = panSpeed;
if (panTimer) {
clearTimeout(panTimer);
translateCoords = d3.transform(svgGroup.attr("transform"));
if (direction == 'left' || direction == 'right') {
translateX = direction == 'left' ? translateCoords.translate[0] + speed : translateCoords.translate[0] - speed;
translateY = translateCoords.translate[1];
} else if (direction == 'up' || direction == 'down') {
translateX = translateCoords.translate[0];
translateY = direction == 'up' ? translateCoords.translate[1] + speed : translateCoords.translate[1] - speed;
}
scaleX = translateCoords.scale[0];
scaleY = translateCoords.scale[1];
scale = zoomListener.scale();
svgGroup.transition().attr("transform", "translate(" + translateX + "," + translateY + ")scale(" + scale + ")");
d3.select(domNode).select('g.node').attr("transform", "translate(" + translateX + "," + translateY + ")");
zoomListener.scale(zoomListener.scale());
zoomListener.translate([translateX, translateY]);
panTimer = setTimeout(function() {
pan(domNode, speed, direction);
}, 50);
}
}
// Define the zoom function for the zoomable tree
function zoom() {
svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
// define the zoomListener which calls the zoom function on the "zoom" event constrained within the scaleExtents
var zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom);
function initiateDrag(d, domNode) {
draggingNode = d;
d3.select(domNode).select('.ghostCircle').attr('pointer-events', 'none');
d3.selectAll('.ghostCircle').attr('class', 'ghostCircle show');
d3.select(domNode).attr('class', 'node activeDrag');
svgGroup.selectAll("g.node").sort(function(a, b) { // select the parent and sort the path's
if (a.id != draggingNode.id) return 1; // a is not the hovered element, send "a" to the back
else return -1; // a is the hovered element, bring "a" to the front
});
// if nodes has children, remove the links and nodes
if (nodes.length > 1) {
// remove link paths
links = tree.links(nodes);
nodePaths = svgGroup.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
}).remove();
// remove child nodes
nodesExit = svgGroup.selectAll("g.node")
.data(nodes, function(d) {
return d.id;
}).filter(function(d, i) {
if (d.id == draggingNode.id) {
return false;
}
return true;
}).remove();
}
// remove parent link
parentLink = tree.links(tree.nodes(draggingNode.parent));
svgGroup.selectAll('path.link').filter(function(d, i) {
if (d.target.id == draggingNode.id) {
return true;
}
return false;
}).remove();
dragStarted = null;
}
// define the baseSvg, attaching a class for styling and the zoomListener
var baseSvg = d3.select("#tree-container").append("svg")
.attr("width", viewerWidth)
.attr("height", viewerHeight)
.attr("class", "overlay")
.call(zoomListener);
// Helper functions for collapsing and expanding nodes.
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
function expand(d) {
if (d._children) {
d.children = d._children;
d.children.forEach(expand);
d._children = null;
}
}
var overCircle = function(d) {
selectedNode = d;
updateTempConnector();
};
var outCircle = function(d) {
selectedNode = null;
updateTempConnector();
};
// Function to update the temporary connector indicating dragging affiliation
var updateTempConnector = function() {
var data = [];
if (draggingNode !== null && selectedNode !== null) {
// have to flip the source coordinates since we did this for the existing connectors on the original tree
data = [{
source: {
x: selectedNode.y0,
y: selectedNode.x0
},
target: {
x: draggingNode.y0,
y: draggingNode.x0
}
}];
}
var link = svgGroup.selectAll(".templink").data(data);
link.enter().append("path")
.attr("class", "templink")
.attr("d", d3.svg.diagonal())
.attr('pointer-events', 'none');
link.attr("d", d3.svg.diagonal());
link.exit().remove();
};
// Function to center node when clicked/dropped so node doesn't get lost when collapsing/moving with large amount of children.
function centerNode(source) {
scale = zoomListener.scale();
x = -source.y0;
y = -source.x0;
x = x * scale + viewerWidth / 2;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
// Toggle children function
function toggleChildren(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else if (d._children) {
d.children = d._children;
d._children = null;
}
return d;
}
// Toggle children on click.
function click(d) {
if (d3.event.defaultPrevented) return; // click suppressed
d = toggleChildren(d);
update(d);
centerNode(d);
}
function update(source) {
// Compute the new height, function counts total children of root node and sets tree height accordingly.
// This prevents the layout looking squashed when new nodes are made visible or looking sparse when nodes are removed
// This makes the layout more consistent.
var levelWidth = [1];
var childCount = function(level, n) {
if (n.children && n.children.length > 0) {
if (levelWidth.length <= level + 1) levelWidth.push(0);
levelWidth[level + 1] += n.children.length;
n.children.forEach(function(d) {
childCount(level + 1, d);
});
}
};
childCount(0, root);
var newHeight = d3.max(levelWidth) * 25; // 25 pixels per line
tree = tree.size([newHeight, viewerWidth]);
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Set widths between levels based on maxLabelLength.
nodes.forEach(function(d) {
d.y = (d.depth * (maxLabelLength * 10)); //maxLabelLength * 10px
// alternatively to keep a fixed scale one can set a fixed depth per level
// Normalize for fixed-depth by commenting out below line
// d.y = (d.depth * 500); //500px per level.
});
// Update the nodes…
node = svgGroup.selectAll("g.node")
.data(nodes, function(d) {
return d.id || (d.id = ++i);
});
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.call(dragListener)
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
nodeEnter.append("circle")
.attr('class', 'nodeCircle')
.attr("r", 0)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
nodeEnter.append("text")
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.attr("dy", ".35em")
.attr('class', 'nodeText')
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.name;
})
.style("fill-opacity", 0);
// phantom node to give us mouseover in a radius around it
nodeEnter.append("circle")
.attr('class', 'ghostCircle')
.attr("r", 30)
.attr("opacity", 0.2) // change this to zero to hide the target area
.style("fill", "red")
.attr('pointer-events', 'mouseover')
.on("mouseover", function(node) {
overCircle(node);
})
.on("mouseout", function(node) {
outCircle(node);
});
// Update the text to reflect whether node has children or not.
node.select('text')
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.name;
});
// Change the circle fill depending on whether it has children and is collapsed
node.select("circle.nodeCircle")
.attr("r", 4.5)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Fade the text in
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
nodeExit.select("circle")
.attr("r", 0);
nodeExit.select("text")
.style("fill-opacity", 0);
// Update the links…
var link = svgGroup.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
});
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
});
// 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;
});
}
// Append a group which holds all nodes and which the zoom Listener can act upon.
var svgGroup = baseSvg.append("g");
// Define the root
root = treeData;
root.x0 = viewerHeight / 2;
root.y0 = 0;
// Layout the tree initially and center on the root node.
update(root);
centerNode(root);
})(formattedTaskflowTree[0]);
Posted is a screenshot of the re-occurring errors that I'm receiving upon loading up the tree.
I've a problem with creating multiple force layout graphs using d3 and reading data from a json file. I use a for loop to iterate over the graphs, create a separate div containing a svg for each. The problem is, that the force layout is only applied to the last one created, so basically the others just show a dot in the upper left corner. I could solve it partly by putting a for loop at the end of each iteration, but I still lose the interaction capabilities of the separate figures.
Find the code below, thanks in advance.
Cheers, Michael
var color = d3.scale.category20();
var force = new Array();
var div = new Array();
var svg = new Array();
var graph = new Array();
var link;
var node;
var width = 360;
var height = 360;
var brush = new Array();
var shiftKey;
var count = 0;
//loop through the different subsystems in the json-file
for(name_subsystem in graphs) {
//add a div for each subsystem
div[count] = document.createElement("div");
div[count].style.width = "360px";
div[count].style.height = "360px";
div[count].style.cssFloat="left";
div[count].id = name_subsystem;
document.body.appendChild(div[count]);
//force is called. all attributes with default values are noted. see API reference on github.
force[count] = d3.layout.force()
.size([width, height])
.linkDistance(20)
.linkStrength(1)
.friction(0.9)
.charge(-30)
.theta(0.8)
.gravity(0.1);
div[count].appendChild(document.createTextNode(name_subsystem));
//create the svg rectangle in which other elements can be visualised
svg[count] = d3.select("#"+name_subsystem)
.on("keydown.brush", keydown)
.on("keyup.brush", keyup)
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("id",name_subsystem);
brush[count] = svg[count].append("g")
.datum(function() { return {selected: false, previouslySelected: false}; })
.attr("class", "brush");
//force is started
force[count]
.nodes(graphs[name_subsystem].nodes)
.links(graphs[name_subsystem].links)
.start();
//link elements are called, joined with the data, and links are created for each link object in links
link = svg[count].selectAll(".link")
.data(graphs[name_subsystem].links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.thickness); })
.style("stroke", function(d){
if (d.linktype === 'reactant'){
return "black";
} else {
return "red";
}
});
//node elements are called, joined with the data, and circles are created for each node object in nodes
node = svg[count].selectAll(".node")
.data(graphs[name_subsystem].nodes)
.enter().append("circle")
.attr("class", "node")
//radius
.attr("r", 5)
//fill
.attr("fill", function(d) {
if (d.type === 'metabolite') {
return "blue";
} else {
return "red";
}
})
.on("mousedown", function(d) {
if (!d.selected) { // Don't deselect on shift-drag.
if (!shiftKey) node.classed("selected", function(p) { return p.selected = d === p; });
else d3.select(this).classed("selected", d.selected = true);
}
})
.on("mouseup", function(d) {
if (d.selected && shiftKey) d3.select(this).classed("selected", d.selected = false);
})
.call(force[count].drag()
.on("dragstart",function dragstart(d){
d.fixed=true;
d3.select(this).classed("fixed",true);
})
);
//gives titles to nodes. i do not know why this is separated from the first node calling.
node.append("title")
.text(function(d) { return d.name; });
//enable brushing of the network
brush[count].call(d3.svg.brush()
.x(d3.scale.identity().domain([0, width]))
.y(d3.scale.identity().domain([0, height]))
.on("brushstart", function(d) {
node.each(function(d) { d.previouslySelected = shiftKey && d.selected; });
})
.on("brush", function() {
var extent = d3.event.target.extent();
node.classed("selected", function(d) {
return d.selected = d.previouslySelected ^
(extent[0][0] <= d.x && d.x < extent[1][0]
&& extent[0][1] <= d.y && d.y < extent[1][1]);
});
})
.on("brushend", function() {
d3.event.target.clear();
d3.select(this).call(d3.event.target);
})
);
//applies force per step or 'tick'.
force[count].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; });
});
//with this it works partly
//for (var i = 0; i < 5000; ++i)force[count].tick();
count++;
};
function keydown() {
if (!d3.event.metaKey) switch (d3.event.keyCode) {
case 38: nudge( 0, -1); break; // UP
case 40: nudge( 0, +1); break; // DOWN
case 37: nudge(-1, 0); break; // LEFT
case 39: nudge(+1, 0); break; // RIGHT
}
shiftKey = d3.event.shiftKey || d3.event.metaKey;
}
function keyup() {
shiftKey = d3.event.shiftKey || d3.event.metaKey;
}
edit: updated the code after the comments, still the same problem.
i am working on force layout only, with many graphs at same time.
1 You don't need to have a count variable for each graph.
2 Don't make these variable(force, svg, graph) as array. There is no need for it. just declare them above as (var svg;) and further on. As you call the function, it automatically makes its different copy and DOM maintain them separately. So every variable you are using in graph, make it declare on top of function.
3 You are drawing all the graphs at same time, so as the new one is called, the previous one stops from being making on svg, that's why only last graph built successfully. So draw them after small time intervals.
<html>
<script>
function draw_graphs(graphs){
var color = d3.scale.category20();
var force;
var div;
var svg;
var graph;
var link;
var node;
var width = 360;
var height = 360;
var brush = new Array();
var shiftKey;
//loop through the different subsystems in the json-file
for(name_subsystem in graphs) {
//add a div for each subsystem
div = document.createElement("div");
div.style.width = "360px";
div.style.height = "360px";
div.style.cssFloat="left";
div.id = name_subsystem;
document.body.appendChild(div);
//force is called. all attributes with default values are noted. see API reference on github.
force = d3.layout.force()
.size([width, height])
.linkDistance(20)
.linkStrength(1)
.friction(0.9)
.charge(-30)
.theta(0.8)
.gravity(0.1);
div.appendChild(document.createTextNode(name_subsystem));
//create the svg rectangle in which other elements can be visualised
svg = d3.select("#"+name_subsystem)
.on("keydown.brush", keydown)
.on("keyup.brush", keyup)
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("id",name_subsystem);
brush = svg.append("g")
.datum(function() { return {selected: false, previouslySelected: false}; })
.attr("class", "brush");
//force is started
force
.nodes(graphs[name_subsystem].nodes)
.links(graphs[name_subsystem].links)
.start();
//link elements are called, joined with the data, and links are created for each link object in links
link = svg.selectAll(".link")
.data(graphs[name_subsystem].links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.thickness); })
.style("stroke", function(d){
if (d.linktype === 'reactant'){
return "black";
} else {
return "red";
}
});
//node elements are called, joined with the data, and circles are created for each node object in nodes
node = svg.selectAll(".node")
.data(graphs[name_subsystem].nodes)
.enter().append("circle")
.attr("class", "node")
//radius
.attr("r", 5)
//fill
.attr("fill", function(d) {
if (d.type === 'metabolite') {
return "blue";
} else {
return "red";
}
})
.on("mousedown", function(d) {
if (!d.selected) { // Don't deselect on shift-drag.
if (!shiftKey) node.classed("selected", function(p) { return p.selected = d === p; });
else d3.select(this).classed("selected", d.selected = true);
}
})
.on("mouseup", function(d) {
if (d.selected && shiftKey) d3.select(this).classed("selected", d.selected = false);
})
.call(force.drag()
.on("dragstart",function dragstart(d){
d.fixed=true;
d3.select(this).classed("fixed",true);
})
);
//gives titles to nodes. i do not know why this is separated from the first node calling.
node.append("title")
.text(function(d) { return d.name; });
//enable brushing of the network
brush.call(d3.svg.brush()
.x(d3.scale.identity().domain([0, width]))
.y(d3.scale.identity().domain([0, height]))
.on("brushstart", function(d) {
node.each(function(d) { d.previouslySelected = shiftKey && d.selected; });
})
.on("brush", function() {
var extent = d3.event.target.extent();
node.classed("selected", function(d) {
return d.selected = d.previouslySelected ^
(extent[0][0] <= d.x && d.x < extent[1][0]
&& extent[0][1] <= d.y && d.y < extent[1][1]);
});
})
.on("brushend", function() {
d3.event.target.clear();
d3.select(this).call(d3.event.target);
})
);
//applies force per step or 'tick'.
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; });
});
//with this it works partly
//for (var i = 0; i < 5000; ++i)force[count].tick();
};
function keydown() {
if (!d3.event.metaKey) switch (d3.event.keyCode) {
case 38: nudge( 0, -1); break; // UP
case 40: nudge( 0, +1); break; // DOWN
case 37: nudge(-1, 0); break; // LEFT
case 39: nudge(+1, 0); break; // RIGHT
}
shiftKey = d3.event.shiftKey || d3.event.metaKey;
}
function keyup() {
shiftKey = d3.event.shiftKey || d3.event.metaKey;
}
}
</script>
<script>
$(document).ready(function() {
draw_graphs("pass here the json file");
// this will drawn 2nd graph after 1 second.
var t = setTimeout(function(){
draw_graphs("pass here json file");
}, 1000)
});
Here the code I finally used with the help of the comments above, maybe helpful for others as well:
<script type="text/javascript" src="d3_splitted_var.json"></script>
<script>
function draw_graphs(name_subsystem){
var force;
var div;
var svg;
var link;
var node;
var width = 360;
var height = 360;
var r=5;
var brush = new Array();
var shiftKey;
//add a div for each subsystem
div = document.createElement("div");
div.style.width = "360px";
div.style.height = "360px";
div.style.cssFloat="left";
div.id = name_subsystem;
document.body.appendChild(div);
force = d3.layout.force()
.size([width, height])
.linkDistance(20)
.linkStrength(1)
.friction(0.9)
.charge(-50)
.theta(0.8)
.gravity(0.1);
div.appendChild(document.createTextNode(name_subsystem));
//create the svg rectangle in which other elements can be visualised
svg = d3.select("#"+name_subsystem)
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("id",name_subsystem);
//force is started
force
.nodes(graphs[name_subsystem].nodes)
.links(graphs[name_subsystem].links)
.start();
//link elements are called, joined with the data, and links are created for each link object in links
link = svg.selectAll(".link")
.data(graphs[name_subsystem].links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.thickness); })
.style("stroke", function(d){
if (d.linktype === 'reactant'){
return "black";
} else {
return "red";
}
});
//node elements are called, joined with the data, and circles are created for each node object in nodes
node = svg.selectAll(".node")
.data(graphs[name_subsystem].nodes)
.enter().append("circle")
.attr("class", "node")
//radius
.attr("r", r)
//fill
.attr("fill", function(d) {
if (d.type === 'metabolite') {
return "blue";
} else {
return "red";
}
})
.call(force.drag()
.on("dragstart",function dragstart(d){
d.fixed=true;
d3.select(this).classed("fixed",true);
})
);
//gives titles to nodes. i do not know why this is separated from the first node calling.
node.append("title")
.text(function(d) { return d.name; });
//applies force per step or 'tick'.
force.on("tick", function() {
node.attr("cx", function(d) { return d.x = Math.max(r, Math.min(width - r, d.x)); })
.attr("cy", function(d) { return d.y = Math.max(r, Math.min(height - r, d.y)); });
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
});
};
for(name_subsystem in graphs) {
draw_graphs(name_subsystem);
}
</script>
Note: graphs is the name of the variable in my json file. You need to include the d3-library.