I am programming a collapsibleTree in R, which is using JavaScript. Unfortunately it looks liek this:
I can choose the y axis of the label, but they are always threated the same height. I want to have them alternating a bit on top and bottom of the knot, so that long texts dont overlap. Here is the code, I really have no clue because I never programmed JavaScript.
Thank you
HTMLWidgets.widget({
name: 'collapsibleTree',
type: 'output',
factory: function(el, width, height) {
var i = 0,
duration = 750,
root = {},
options = {},
treemap;
// Optionally enable zooming, and limit to 1/5x or 5x of the original viewport
var zoom = d3.zoom()
.scaleExtent([1/5, 5])
.on('zoom', function () {
svg.attr('transform', d3.event.transform)
})
// create our tree object and bind it to the element
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select(el).append('svg')
.attr('width', width)
.attr('height', height)
.append('g');
// Define the div for the tooltip
var tooltip = d3.select(el).append('div')
.attr('class', 'tooltip')
.style('opacity', 0);
function update(source) {
// 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 * options.linkLength});
// ****************** 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 tooltips, if specified in options
if (options.tooltip) {
nodeEnter = nodeEnter
.on('mouseover', mouseover)
.on('mouseout', mouseout);
}
// Enable zooming, if specified
if (options.zoomable) d3.select(el).select('svg').call(zoom)
// Add Circle for the nodes
nodeEnter.append('circle')
.attr('class', 'node')
.attr('r', 1e-6)
.style('fill', function(d) {
return d.data.fill || (d._children ? options.fill : '#fff');
})
.style('stroke-width', function(d) {
return d._children ? 3 : 1;
});
// Add labels for the nodes
nodeEnter.append('text')
// .attr('dy', '1em')
// .attr('class', 'place-label')
// .attr('text-anchor', 'middle')
// .attr('separation', '2')
.attr('x', function(d) {
// Scale padding for label to the size of node
var padding = (d.data.SizeOfNode || 10) + 3
return d.children || d._children ? -1 * padding : padding;
})
.attr('text-anchor', function(d) {
return d.children || d._children ? 'end' : 'start';
})
.style('font-size', options.fontSize + 'px')
.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', function(d) {
return d.data.SizeOfNode || 10; // default radius is 10
})
.style('fill', function(d) {
return d.data.fill || (d._children ? options.fill : '#fff');
})
.style('stroke-width', function(d) {
return d._children ? 3 : 1;
})
.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')
// Potentially, this may one day be mappable
// .style('stroke-width', function(d) { return d.data.linkWidth || 1 })
.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) {
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;
} else {
d.children = d._children;
d._children = null;
}
update(d);
// Hide the tooltip after clicking
tooltip.transition()
.duration(100)
.style('opacity', 0)
// Update Shiny inputs, if applicable
if (options.input) {
var nest = {},
obj = d;
// Navigate up the list and recursively find parental nodes
for (var n = d.depth; n > 0; n--) {
nest[options.hierarchy[n-1]] = obj.data.name
obj = obj.parent
}
Shiny.onInputChange(options.input, nest)
}
}
// Show tooltip on mouseover
function mouseover(d) {
tooltip.transition()
.duration(200)
.style('opacity', .9);
// Show either a constructed tooltip, or override with one from the data
tooltip.html(
d.data.tooltip || d.data.name + '<br>' +
options.attribute + ': ' + d.data.WeightOfNode
)
// Make the tooltip font size just a little bit bigger
.style('font-size', (options.fontSize + 1) + 'px')
.style('left', (d3.event.layerX) + 'px')
.style('top', (d3.event.layerY - 30) + 'px');
}
// Hide tooltip on mouseout
function mouseout(d) {
tooltip.transition()
.duration(500)
.style('opacity', 0);
}
}
return {
renderValue: function(x) {
// Assigns parent, children, height, depth
root = d3.hierarchy(x.data, function(d) { return d.children; });
root.x0 = height / 2;
root.y0 = 0;
// Attach options as a property of the instance
options = x.options;
// Update the canvas with the new dimensions
svg = svg.attr('transform', 'translate('
+ options.margin.left + ',' + options.margin.top + ')')
// width and height, corrected for margins
var heightMargin = height - options.margin.top - options.margin.bottom,
widthMargin = width - options.margin.left - options.margin.right;
// declares a tree layout and assigns the size
treemap = d3.tree().size([heightMargin, widthMargin])
.separation(separationFun);
// Calculate a reasonable link length, if not otherwise specified
if (!options.linkLength) {
options.linkResponsive = true
options.linkLength = widthMargin / options.hierarchy.length
if (options.linkLength < 10) {
options.linkLength = 10 // Offscreen or too short
}
}
// Optionally collapse after the second level
if (options.collapsed) root.children.forEach(collapse);
update(root);
// 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
}
}
},
resize: function(width, height) {
// Resize the canvas
d3.select(el).select('svg')
.attr('width', width)
.attr('height', height);
// width and height, corrected for margins
var heightMargin = height - options.margin.top - options.margin.bottom,
widthMargin = width - options.margin.left - options.margin.right;
// Calculate a reasonable link length, if not originally specified
if (options.linkResponsive) {
options.linkLength = widthMargin / options.hierarchy.length
if (options.linkLength < 10) {
options.linkLength = 10 // Offscreen or too short
}
}
// Update the treemap to fit the new canvas size
treemap = d3.tree().size([heightMargin, widthMargin])
.separation(separationFun);
update(root)
},
// Make the instance properties available as a property of the widget
svg: svg,
root: root,
options: options
};
}
});
function separationFun(a, b) {
var height = a.data.SizeOfNode + b.data.SizeOfNode,
// Scale distance to SizeOfNode, if defined
distance = (height || 20) / 10;
return (a.parent === b.parent ? 1 : distance);
};
Related
I have a problem where the node always move left. Here is my code:
<div id="tree-container"></div>
<script src="<?php echo base_url('d3js-nettree/js/d3.js');?>" type="text/javascript"></script>
<script>
<?php if(isset($params['user_code'])){ ?>
treeJSON = d3.json("<?php echo base_url('d3js-parse/'.$params['user_code'].'.json');?>", function(error, treeData) {
// Calculate total nodes, max label length
var totalNodes = 0;
var maxLabelLength = 0;
// Misc. variables
var zoomFactor = 1;
var i = 0;
var duration = 750;
var root;
var rectWidth = 160, rectHeight = 125;
// size of the diagram
var viewerWidth = $("#tree-container").width();
var viewerHeight = 500;
var tree = d3.layout.tree()
.size([viewerWidth, viewerHeight]);
// define a d3 diagonal projection for use by the node paths later on.
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.x, d.y];
});
// 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);
}
}
}
// 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);
d3.selectAll('button').on('click', function(){
if(this.id === 'zoom_in') {
zoomFactor = zoomFactor + 0.2;
zoomListener.scale(zoomFactor).event(d3.select("#tree-container"));
}
else if(this.id === 'zoom_out') {
zoomFactor = zoomFactor - 0.2;
zoomListener.scale(zoomFactor).event(d3.select("#tree-container"));
}
else if(this.id === 'up_level') {
updateNewTree("ID04838614");
}
});
// 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);
// 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.x0;
y = -source.y0;
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]);
}
// Call visit function to establish maxLabelLength
visit(treeData, function(d) {
totalNodes++;
maxLabelLength = Math.max(d.distributor_code.length, maxLabelLength);
}, function(d) {
return d.children && d.children.length > 0 ? d.children : null;
});
// define click event
function click(d) {
console.log("clicked");
// if (d3.event.defaultPrevented) return; // click suppressed
//d = toggleChildren(d);
if(d.url !== "") {
window.open(d.url, "_self");
} else {
updateNewTree(d.distributor_code);
}
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 newWidth = d3.max(levelWidth) * 300; // 300 pixels per line
tree = tree.size([newWidth, viewerHeight]);
// 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 * 200); //200px 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")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + source.x0 + "," + source.y0 + ")";
})
.on('click', click);
nodeEnter.append("image")
.attr("href", "<?php echo base_url('assets/images/people.png');?>")
.attr("x", -rectWidth/4)
.attr("y", -rectHeight-75)
.attr("width", 75)
.attr("height", 75);
nodeEnter.append("rect")
.attr('class', 'nodeRect')
.attr("x", -rectWidth/2)
.attr("y", -rectHeight)
.attr("rx", 10)
.attr("ry", 10)
.attr("width", rectWidth)
.attr("height", rectHeight)
.style("fill", function(d) {
//return d._children ? "lightsteelblue" : "#fff";
});
nodeEnter.append("text")
.attr('class', 'txt1')
.attr("x", 0)
.attr("y", -rectHeight+15)
.attr('class', 'textBold')
.attr("text-anchor", "middle")
.text(function(d) {
if(d.distributor_code === "") return "";
else return d.user_code;
});
nodeEnter.append("text")
.attr('class', 'txt2')
.attr("x", 0)
.attr("y", -rectHeight+25)
.attr("text-anchor", "middle")
.text(function(d) {
if(d.distributor_code === "") return "";
else return d.fullname;
});
//IT GOES ON FOR SEVERAL MORE
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
// 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.x + "," + source.y + ")";
})
.remove();
nodeExit.select("rect")
.attr("width", 0)
.attr("height", 0);
nodeExit.select("text")
.style("fill-opacity", 0);
nodeExit.select("image")
.style("display", "none");
// 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;
});
}
function updateNewTree($base_id) {
// Get the data again
d3.json("nettree-alt.json", function(error, treeData) {
// Call visit function to establish maxLabelLength
visit(treeData, function(d) {
totalNodes++;
maxLabelLength = Math.max(d.distributor_code.length, maxLabelLength);
}, function(d) {
return d.children && d.children.length > 0 ? d.children : null;
});
root = treeData;
root.x0 = viewerHeight / 2;
root.y0 = 0;
// Layout the tree initially and center on the root node.
update(root);
centerNode(root);
});
}
// 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 = -100;
// Layout the tree initially and center on the root node.
update(root);
centerNode(root);
});
<?php } ?>
function EnableTreeMode(){
$('.tree').treegrid({
expanderExpandedClass: 'glyphicon glyphicon-minus',
expanderCollapsedClass: 'glyphicon glyphicon-plus'
});
$('.tree').treegrid('collapseAll');
}
EnableTreeMode();
function collapse(){
$('.tree').treegrid('collapseAll');
}
function expand(){
$('.tree').treegrid('expandAll');
}
</script>
The problem is like this, i have a "tree" that consist of users which pulls data from a json file:
USER 1
/\
USER 2 USER 3
What i was expecting when i remove a user, say user 2, the "tree" should be like this:
USER 1
/\
NONE USER 3
But instead the result i got were like this:
USER 1
/\
USER 3 NONE
After i deleted USER 2 the USER 3, which is on the right node, moved on the left node. I have tried debugging my code line by line but still no result.
TIA.
I am a beginner in d3.js and I am trying to do a collapsible horizontal tree with d3.js. I started with this example :
http://blockbuilder.org/d3noob/43a860bc0024792f8803bba8ca0d5ecd
There is one flaw that I try to solve but without success. It is when you click multiple times quickly on the same circle, it launches the same animations many times and it broke the tree.
So I would like to know if there is a possibility to make the click event disabled during the animation and make it available at the end ?
I tried using this method to remove the event and put it back, but it does not work.
Thanks for your help.
Rather than using timeouts or removing and adding event listeners or tracking transitions, you can check to see if a transition is occurring on any individual node with d3.active(node). It will return either null (in the event of no transition) or the transition if one is taking place. If a transition is occurring, ignore the click event (or more accurately, don't carry out any actions on click events if a transition is happening).
First let's check to see if a selection has transitioning elements:
function isTransitioning(selection) {
var transitioning = false;
selection.each(function() { if(d3.active(this)) { transitioning = true; } })
return transitioning;
}
Then let's apply that in the click function in your linked example:
function click(d) {
// Are no transitions happening?
if(!isTransitioning(d3.selectAll("circle"))) {
// If none are continue with updating the graph:
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
}
Giving us:
var treeData =
{
"name": "Top Level",
"children": [
{
"name": "Level 2: A",
"children": [
{ "name": "Son of A" },
{ "name": "Daughter of A" }
]
},
{ "name": "Level 2: B" }
]
};
// Set the dimensions and margins of the diagram
var margin = {top: 20, right: 90, bottom: 30, left: 90},
width = 600 - margin.left - margin.right,
height = 300 - 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("body").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;
// declares a tree layout and assigns the size
var treemap = d3.tree().size([height, width]);
// Assigns parent, children, height, depth
root = d3.hierarchy(treeData, function(d) { return d.children; });
root.x0 = height / 2;
root.y0 = 0;
// Collapse after the second level
root.children.forEach(collapse);
update(root);
// 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) {
// 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 ? "lightsteelblue" : "#fff";
});
// 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 ? "lightsteelblue" : "#fff";
})
.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) {
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(!isTransitioning(d3.selectAll("circle"))) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
}
}
function isTransitioning(selection) {
var transitioning = false;
selection.each(function() { if(d3.active(this)) { transitioning = true; } })
return transitioning;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text {
font: 12px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
I had good results with selecting only circles (you can click twice without error and without stopping the transition from the looks of it), but in some cases you might want to select all items with a transition. In this example if selecting all the gs you have to wait until all transitions are done before updating again by click:
var treeData =
{
"name": "Top Level",
"children": [
{
"name": "Level 2: A",
"children": [
{ "name": "Son of A" },
{ "name": "Daughter of A" }
]
},
{ "name": "Level 2: B" }
]
};
// Set the dimensions and margins of the diagram
var margin = {top: 20, right: 90, bottom: 30, left: 90},
width = 600 - margin.left - margin.right,
height = 300 - 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("body").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;
// declares a tree layout and assigns the size
var treemap = d3.tree().size([height, width]);
// Assigns parent, children, height, depth
root = d3.hierarchy(treeData, function(d) { return d.children; });
root.x0 = height / 2;
root.y0 = 0;
// Collapse after the second level
root.children.forEach(collapse);
update(root);
// 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) {
// 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 ? "lightsteelblue" : "#fff";
});
// 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 ? "lightsteelblue" : "#fff";
})
.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) {
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(!isTransitioning(d3.selectAll("g"))) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
}
}
function isTransitioning(selection) {
var transitioning = false;
selection.each(function() { if(d3.active(this)) { transitioning = true; } })
return transitioning;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text {
font: 12px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
You can maybe grab the onclik event and throttle the click the duration of the animation in this case 750 milisecs
You need to be able to tell if an animation is currently ongoing, and also deal with multiple animations being possible at once. I made a fork here: http://blockbuilder.org/WilliamNHarvey/4a39d034cef90be249b5ab003ecf775d
Make an array that holds current animations
var animations = []
Then on click, check if the current node is already animated. If so, cancel the click event. If not, add it to the animations queue. Set a timeout to remove it after 750 milliseconds
function click(d) {
if (isAnimated(d)) return;
animations.push(d);
setTimeout(function(){ removeAnimation(d) }, duration);
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
The two functions here, isAnimated and removeAnimation, simply loop through the nodes to find the one you're looking for
function isAnimated(d) {
res = false;
animations.forEach(function(e, i) {
if(e.id == d.id) res = true;
});
return res;
}
function removeAnimation(d) {
animations.forEach(function(e, i) {
if(e.id == d.id) animations.splice(i, 1);
});
}
I am using d3.tree to create my tree hierarchy, I am relatively new to this lib, so I need a little help.
var treeData = {{ data | safe }};
// Set the dimensions and margins of the diagram
var margin = {top: 30, right: 90, bottom: 30, left: 150},
width = $('#tree-container').width(),
height = $('#tree-container').height();
// 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("#tree-container").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function () {
svg.attr("transform", d3.event.transform)
}))
.append("g")
.attr("width", width)
.attr("height", height)
.attr("id", "place")
.attr("transform", "translate("
+ (width/2) + "," + margin.top + ")");
var i = 0,
duration = 750,
root;
// declares a tree layout and assigns the size
var treemap = d3.tree()
.nodeSize([70, 10]);
// Assigns parent, children, height, depth
root = d3.hierarchy(treeData, function(d) { return d.children; });
root.x0 = 0;
root.y0 = width / 2;
// Collapse after the second level
root.children.forEach(collapse);
update(root);
// 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
}
}
var nodes;
function update(source) {
// Assigns the x and y position for the nodes
var treeData = treemap(root);
// Compute the new tree layout.
nodes = treeData.descendants();
links = treeData.descendants().slice(1);
// Normalize for fixed-depth.
nodes.forEach(function(d){ //HERE
d.y = d.depth * 120;
});
// ****************** 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.x0 + "," + source.y0 + ")";
});
// Add Circle for the nodes
nodeEnter.append('circle')
.attr('class', 'node')
.attr('id', function(d) { return "circle-"+d.data.name; })
.attr('r', 1e-6)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";// make text color appear read or as data.color
}).on('click', click);
// Add labels for the nodes
nodeEnter.append('text')
.attr("dy", "0.45em")
.attr("y", function(d) {
return d.children || d._children ? -13 : 13;
})
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.attr("cursor", "pointer")
.text(function(d) { return d.data.name; })
.on("click", textClick);
// 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.x + "," + d.y + ")";
});
// Update the node attributes and style
nodeUpdate.select('circle.node')
.attr('r', 10)
.style("fill", function(d) { return d._children ? d.data.ncolor : "#fff"; }) // change color of inner circle
.style("stroke", function (d) { return d.data.ncolor; }) // changing color of outere circle
.attr('cursor', 'pointer');
// Remove any exiting nodes
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.x + "," + source.y + ")";
})
.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")
.style("stroke", function (d) { return d.data.color; }) // Place where color of previous link changes
.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) {
path = "M" + s.x + "," + s.y +
"C" + (s.x + d.x) / 2 + " " + s.y + ","
+ (s.x + d.x) / 2 + " " + d.y + ","
+ d.x + " " + d.y;
return path
}
// Toggle children on click.
function click(d) {
conosle.log();
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
It actually works, but what I am looking for is the way to have everything inside my viewing window. Like if someone click on node it expands and if children nodes overflow(go out of the window), I need to zoom out, so they will be visible. Hope there is d3 jedi-master to answer me :)))
This is very hard to answer without seeing your actual data or a running version of your code, but here is a suggestion. When you do this:
var treemap = d3.tree()
.nodeSize([70, 10]);
You're setting the size of each node, but not the whole layout. Also, according to the API,
When a node size is specified, the root node is always positioned at ⟨0, 0⟩.
Here is an example: I just forked Bostock's tree example, setting the nodeSize, you can see that the nodes are going outside the SVG: http://blockbuilder.org/anonymous/7a84944610c7e6b3b9ebf063977955c9
So, a possible solution is using size instead of nodeSize:
var treemap = d3.tree()
.nodeSize([width, height]);
Here is the original tree example using size, for comparison: http://blockbuilder.org/mbostock/4339083
I have a D3 collapsible tree, the problem is that a single node can have upto 4000 leaves. I would like to make it scrollable or expandable, i.e. the height should work for both 10 leaf nodes or several 1000.
Currently my height parameter is static, can anyone tell me how I can make it dynamic?
The following is the code:
$(function(){
var m = [20, 120, 20, 120],
w = 1280 - m[1] - m[3],
h = 80000 - m[0] - m[2],
i = 0,
root;
var tree = d3.layout.tree()
.size([h, w]);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
var vis = d3.select("#modelPatterns").append("svg:svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
d3.json("./static/data/type1Tree.json", function(json) {
root = json;
root.x0 = h / 2;
root.y0 = 0;
function toggleAll(d) {
if (d.children) {
d.children.forEach(toggleAll);
toggle(d);
}
}
// Initialize the display to show a few nodes.
root.children.forEach(toggleAll);
//toggle(root.children[1]);
//toggle(root.children[1].children[2]);
//toggle(root.children[9]);
//toggle(root.children[9].children[0]);
update(root);
});
In case it helps someone, I used the zoom function which enabled panning and zooming. This may suffice for what you're looking for when trying to make the graph scrollable so to speak.
D3.select('svg')
.call(D3.zoom()
.scaleExtent([-5, 8])
.extent([[0, 0], [300, 300]])
.on('zoom', () => {
D3.selectAll('g')
.attr('transform', D3.event.transform);
if (has('root.children', this) && this.root.children.length > 50) {
this.updateAfterInit(this.root);
} else {
this.update(this.root);
}
// this.centerNode(this.root);
})
.filter(() => {
const foundNode = this.N.findNodeByID(D3.event.srcElement.id.split('_')[1])
if ( !!foundNode && D3.event.type === 'dblclick' && foundNode.data.type === 'SearchRelationspec') {
return false;
} else {
return !D3.event.target.classList.contains('drawarea') && D3.event.type === 'dblclick';
}
})
You can modify your update function like below code as per your need...
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);
newHeight = d3.max(levelWidth) * 60; // 20 pixels per line
tree = tree.size([newHeight, width]);
d3.select("svg").remove();//TO REMOVE THE ALREADY SVG CONTENTS AND RELOAD ON EVERY UPDATE
svg = d3.select("body").append("svg");
svg.attr("width", width + margin.right + margin.left)
.attr("height", newHeight + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Normalize for fixed-depth.
nodes.forEach(function(d) { d.y = d.depth * 180; });
// Update the nodes…
var node = svg.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")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
.on("click", click);
nodeEnter.append("circle")
.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", "-.75em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.name; })
.style("fill-opacity", 1e-2);
nodeEnter.append("text")
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("dy", "1.00em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.info1; })
.style("fill-opacity", 1e-2);
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
nodeUpdate.select("circle")
.attr("r", 6)
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
nodeUpdate.selectAll("text")
.style("fill-opacity", 4);
// 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.selectAll("text")
.style("fill-opacity", 1e-6);
// Update the links…
var link = svg.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;
});
It will give you a dynamic size tree and serve for any number of nodes in the tree.
I hope this will solve your purpose.
Fiddle Example
I can't figure out how to tweak the transform:rotate attribute for the nodes so that the pictures and text in the foreign objects don't rotate/ go upside down. I have tried tweaking with this block of code:
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function (d) {
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
});
In the Chrome Console, I change a node from
<g transform="translate(226.5247584249853,-164.57987064189248)rotate(-36)">
<foreignObject.....></foreignObject></g>
to
transform="translate(226.5247584249853,-164.57987064189248)rotate(0)
and it works. But changing rotate to 0 in the code above would make every children node go to the left side., like this
Full code:
treeData = myJSON;
// 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 width = $(document).width();
var height = $(document).height();
var diameter = 800;
var tree = d3.layout.tree().size([360, diameter / 2 - 120])
.separation(function (a, b) {
return (a.parent == b.parent ? 1 : 10) / a.depth;
});
// define a d3 diagonal projection for use by the node paths later on.
var diagonal = d3.svg.diagonal.radial()
.projection(function (d) {
return [d.y, d.x / 180 * Math.PI];
});
// Define the root
root = treeData;
root.x0 = height / 2;
root.y0 = 0;
// 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([1, 1]).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", width)
.attr("height", height)
.attr("class", "overlay")
.call(zoomListener);
// Define the drag listeners for drag/drop behaviour of nodes.
dragListener = d3.behavior.drag()
.on("dragstart", function (d) {
if (d == root) {
return;
}
dragStarted = true;
nodes = tree.nodes(d);
d3.event.sourceEvent.stopPropagation();
// it's important that we suppress the mouseover event on the node being dragged. Otherwise it will absorb the mouseover event and the underlying node will not detect it d3.select(this).attr('pointer-events', 'none');
})
.on("drag", function (d) {
if (d == root) {
return;
}
if (dragStarted) {
domNode = this;
initiateDrag(d, domNode);
}
// get coords of mouseEvent relative to svg container to allow for panning
relCoords = d3.mouse($('svg').get(0));
if (relCoords[0] < panBoundary) {
panTimer = true;
pan(this, 'left');
} else if (relCoords[0] > ($('svg').width() - panBoundary)) {
panTimer = true;
pan(this, 'right');
} else if (relCoords[1] < panBoundary) {
panTimer = true;
pan(this, 'up');
} else if (relCoords[1] > ($('svg').height() - panBoundary)) {
panTimer = true;
pan(this, 'down');
} else {
try {
clearTimeout(panTimer);
} catch (e) {
}
}
d.x0 = d3.event.x;
d.y0 = d3.event.y;
var node = d3.select(this);
node.attr("transform", "translate(" + d.x0 + "," + (d.y0) + ")");
updateTempConnector();
})
.on("dragend", function (d) {
if (d == root) {
return;
}
domNode = this;
if (selectedNode) {
// now remove the element from the parent, and insert it into the new elements children
var index = draggingNode.parent.children.indexOf(draggingNode);
if (index > -1) {
draggingNode.parent.children.splice(index, 1);
}
if (typeof selectedNode.children !== 'undefined' || typeof selectedNode._children !== 'undefined') {
if (typeof selectedNode.children !== 'undefined') {
selectedNode.children.push(draggingNode);
} else {
selectedNode._children.push(draggingNode);
}
} else {
selectedNode.children = [];
selectedNode.children.push(draggingNode);
}
// Make sure that the node being added to is expanded so user can see added node is correctly moved
expand(selectedNode);
sortTree();
endDrag();
} else {
endDrag();
}
});
function endDrag() {
selectedNode = null;
d3.selectAll('.ghostCircle').attr('class', 'ghostCircle');
d3.select(domNode).attr('class', 'node');
// now restore the mouseover event or we won't be able to drag a 2nd time
d3.select(domNode).select('.ghostCircle').attr('pointer-events', '');
updateTempConnector();
if (draggingNode !== null) {
update(root);
//centerNode(draggingNode);
draggingNode = null;
}
}
// 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) {
console.log(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: $('svg g').first().offset().left + selectedNode.position.left,
y: selectedNode.position.top
},
target: {
x: draggingNode.x0,
y: draggingNode.y0
}
}];
}
var link = svgGroup.selectAll(".templink").data(data);
link.enter().append("path")
.attr("class", "templink")
.attr("d", d3.svg.diagonal.radial())
.attr('pointer-events', 'none');
link.attr("d", d3.svg.diagonal.radial());
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.x0;
y = -source.y0;
x = x * scale + width / 2;
y = y * scale + height / 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);
//dofocus([{ name : 'o_id' , value : d.o_id }]);
}
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, width]);
// 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("foreignObject")
.attr("class", "smallcircle")
.attr("width", function (d) {
var f = document.createElement("span");
f.id = "hiddenText";
f.style.display = 'hidden';
f.style.padding = '0px';
f.innerHTML = d.name;
document.body.appendChild(f);
textWidth = f.offsetWidth;
var f1 = document.getElementById('hiddenText');
f1.parentNode.removeChild(f1);
return textWidth + 50;
})
.attr("overflow", "visible")
.attr("height", 50)
.attr("y", - 50 / 2)
.attr("x", - 50)
.append("xhtml:div").attr("class", "mainDiv")
.html(function (d) {
var htmlString = "";
htmlString += "<div class='userImage' style='border-color:red '><img src='https://www.gravatar.com/avatar/6d2db975d856b8799a6198bab4777aed?s=32&d=identicon&r=PG' width='50' height='50'></div>";
htmlString += "<div class='content' style='color:red;'>" + d.name + "</div>";
htmlString += "<div style='clear:both;'></div>";
return htmlString;
})
nodeEnter.append("text")
.text(function (d) {
return d.name;
})
.style("font", "8px serif")
.style("opacity", 0.9)
.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) {
node.position = $(this).position();
node.offset = $(this).offset();
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";
});
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function (d) {
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
});
nodeUpdate.select("circle")
.attr("r", 4.5)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff";
});
// Fade the text in
// nodeUpdate.select("text")
// .style("fill-opacity", 1);
nodeUpdate.select("text")
.style("fill-opacity", 1)
// .attr("transform", function(d) { return d.x < 180 ? "translate(0)" : "rotate(180)translate(-" + (d.name.length + 50) + ")"; })
.attr("dy", ".35em")
.attr("text-anchor", function (d) {
return d.x < 180 ? "start" : "end";
})
.attr("transform", function (d) {
return d.x < 180 ? "translate(8)" : "rotate(180)translate(-8)";
});
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + source.x + "," + source.y + ")";
})
.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").attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")");
// Collapse all children of roots children before rendering.
root.children.forEach(function (child) {
collapse(child);
});
// Layout the tree initially and center on the root node.
update(root);
d3.select(self.frameElement).style("height", width);
The rotation is set in line 1221 of your fiddle:
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
The rotation here is required for the positioning, as you're rotating around the origin of the non-translated coordinate system. So simply removing the rotate(...) won't work. However, you can rotate the elements back after positioning:
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")rotate(" + (-d.x + 90) + ")";
Complete fiddle here.