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
Related
I am implementing a d3 code in power bi for a collapsible tree. Each node in my tree is a rectangular box. But for some reason the nodes get overlapped if the node size is large. Here is my code:
// ADD: translate function for the data
function toJSON(data) {
var flare = { name: "All Products", children: [] },
levels = ["productcategory","productsubcategory"];
// For each data row, loop through the expected levels traversing the output tree
data.forEach(function(d){
// Keep this as a reference to the current level
var depthCursor = flare.children;
// Go down one level at a time
levels.forEach(function( property, depth ){
// Look to see if a branch has already been created
var index;
depthCursor.forEach(function(child,i){
if ( d[property] == child.name ) index = i;
});
// Add a branch if it isn't there
if ( isNaN(index) ) {
depthCursor.push({ name : d[property], children : []});
index = depthCursor.length - 1;
}
// Now reference the new child array as we go deeper into the tree
depthCursor = depthCursor[index].children;
// This is a leaf, so add the last element to the specified branch
if ( depth === levels.length - 1 ) depthCursor.push({ name : d.product, size : d.revenue, revenue : d.revenue});
});
});
// End of conversion
return flare;
}
// Aggregate the revenue at each level
function aggregateRevenue(node) {
if(node.children) {
node.children.forEach(function(child) {
aggregateRevenue(child);
node.revenue = d3.sum(node.children, function(d) { return d.revenue; });
});
}
}
// Set the margins
var margin = {top: 20, right: 120, bottom: 20, left: 120},
width = pbi.width - margin.left - margin.right, // ALTER: Changed fixed width with the 'pbi.width' variable
height = pbi.height - margin.top - margin.bottom; // ALTER: Changed fixed height with the 'pbi.height' variable
var i = 0,
duration = 750,
root;
var tree = d3.layout.tree()
.size([height, width]);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
// Zoom functionality:
var zoom = d3.behavior.zoom()
.scaleExtent([0.1, 10])
.on("zoom", zoomed);
function zoomed() {
svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
// SVG creation
var svg = d3.select("#chart") // ALTER: Select SVG object; no need to create it
.attr("width", width + margin.left + margin.right) // ALTER: Add complete width
.attr("height", height + margin.top + margin.bottom) // ALTER: Add complete height
.call(zoom) // Add zoom behavior here
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// ALTER: Replaced the d3.json function with the pbi variant: pbi.dsv
pbi.dsv(function(data) {
var flare = toJSON(data); // ALTER: add extra convertion step to parent/child JSON
root = flare;
aggregateRevenue(flare);
root.x0 = height / 2;
root.y0 = 0;
// collapse the tree
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
root.children.forEach(collapse);
update(root);
});
d3.select(self.frameElement).style("height", height + margin.top + margin.bottom);
function update(source) {
// 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);
// Total revenue ( to calculate individual perncentages )
var totalRevenue = d3.sum(data, function(d) { return d.revenue; });
// Append rectangular node
nodeEnter.append("rect")
.attr("width", 150)
.attr("height", 80)
.attr("x", -75)
.attr("y", -40)
.style("fill", function(d) { return d._children ? pbi.colors[0] : pbi.colors[1]; });
// Append name of the node
nodeEnter.append("text")
.text(function(d) { return d.name; })
.attr("text-anchor", "middle")
.attr("dominant-baseline", "central")
.style("fill", "white")
.style("font-weight", "bold")
.attr("x", 0)
.attr("y", -15);
// Append the green stacked bar line
nodeEnter.append("rect")
.attr("class", "percentage")
.attr("width", function(d) { return ((d.revenue / totalRevenue) * 100).toFixed(2); })
.attr("height", 10)
.attr("x", -50)
.attr("y", -5)
.attr("fill", "green");
// Append the blue stacked bar line
nodeEnter.append("rect")
.attr("class", "non-percentage")
.attr("width", function(d) { return 100 - ((d.revenue / totalRevenue) * 100).toFixed(2); })
.attr("height", 10)
.attr("x", function(d) { return ((d.revenue / totalRevenue) * 100).toFixed(2) - 50; })
.attr("y", -5)
.attr("fill", "blue");
// Append the text for green bar
nodeEnter.append("text")
.attr("x", -70)
.attr("y", 0)
.attr("text-anchor", "start")
.attr("dominant-baseline", "central")
.text(function(d) { return ((d.revenue / totalRevenue) * 100).toFixed(2) + "%"; })
.style("fill", "white")
.style("font-weight", "bold");
// Append the text for blue bar
nodeEnter.append("text")
.attr("x", 35)
.attr("y", 0)
.attr("text-anchor", "start")
.attr("dominant-baseline", "central")
.text(function(d) { return (100 - ((d.revenue / totalRevenue) * 100)).toFixed(2) + "%"; })
.style("fill", "white")
.style("font-weight", "bold");
// Append node's revenue inside the node
nodeEnter.append("text")
.text(function(d) { return "Revenue = $" + d.revenue; })
.attr("text-anchor", "middle")
.attr("dominant-baseline", "central")
.style("fill", "white")
.style("font-weight", "bold")
.attr("x", 0)
.attr("y", 15);
// Append % of total share shared by the node
//nodeEnter.append("text")
//.text(function(d) { return "% Total Share = " + ((d.revenue / totalRevenue) * 100).toFixed(2) + "%"; })
//.attr("text-anchor", "middle")
//.attr("dominant-baseline", "central")
//.style("fill", "white")
//.style("font-weight", "bold")
//.attr("x", 0)
//.attr("y", 15);
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
// 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();
// 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;
});
}
// 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);
}
I also tried of following code but this does not seem to work also:
var treemap = d3.tree()
.size([10*height, width])
.separation(function separation(a, b) { return a.parent == b.parent ? 2 : 2; });
A help in this regard shall be appreciated.
You need to change the node size to your box(rectangle size) that is 80
var treemap = d3.tree()
.size([80, width])
.separation(function separation(a, b) { return a.parent == b.parent ? 2 : 2; });
I have created a family tree using d3.js. Can anyone please guide me on how to change the color of children? like Relativistic Physics or Modern Physics? I don't know how to select a single child and change the color. Thank you
Code Link: https://codepen.io/woldsl/pen/abEQJYP
You can change the color of the node based on the text and you can maintain a color schema for the text that you needed to change (If that's the goal u want to achieve).
var treeData = {
name: " physic ",
children: [
{
name: "Classical Physics ",
children: [
{
name: "Relativistic Physics"
},
{
name: "Quantum Mechanics"
}
]
},
{
name: "Modern Physics"
},
{
name: "Atomic Physics"
}
]
};
var colorSchema = {
"Relativistic Physics": "red",
"Modern Physics": "yellow"
};
// Set the dimensions and margins of the diagram
var margin = { top: 20, right: 120, bottom: 30, left: 120 },
width = 3000 - margin.left - margin.right,
height = 800 - 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 ? "red" : "yellow";
});
// 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 (
colorSchema[d.data.name] || (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 (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text {
font: 12px sans-serif;
font-weight: bold;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
.color {
color: #ff0000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
You can always add the color that u needed for a text in the colorSchema.
<meta charset="UTF-8">
<style>
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text {
font: 12px sans-serif;
font-weight: bold;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
.color {
color: #FF0000 ;
}
</style>
<!-- load the d3.js library -->
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var treeData =
{
"name": " physic ",
"level":"red",
"children": [
{
"name": "Classical Physics ",
"level":"green",
"children" : [
{
"name" : "Relativistic Physics",
},
{
"name" : "Quantum Mechanics",
},
]
},
{
"name": "Modern Physics",
},
{
"name": "Atomic Physics",
},
]}
// Set the dimensions and margins of the diagram
var margin = {top: 20, right: 120, bottom: 30, left: 120},
width = 3000 - margin.left - margin.right,
height = 800 - 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", d => d.data.level)
.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 (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
}
</script>
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 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.
I'm using D3.js to plot a collapsible tree diagram like in the example. It's working mostly well, but the diagram might change dramatically in size when it enters its normal function (ie instead of the few nodes I have now, I'll have a lot more).
I wanted to make the SVG area scroll, I've tried everything I found online to make it work, but with no success. The best I got working was using the d3.behaviour.drag, in which I drag the whole diagram around. It is far from optimal and glitches a lot, but it is kinda usable.
Even so, I'm trying to clean it up a little bit and I realised the d3.behaviour.zoom can also be used to pan the SVG area, according to the API docs.
Question: Can anyone explain how to adapt it to my code?
I would like to be able to pan the SVG area with the diagram, if possible making it react to some misuses, namely, trying to pan the diagram out of the viewport, and enabling to zoom to the maximum viewport's dimensions...
This is my code so far:
var realWidth = window.innerWidth;
var realHeight = window.innerHeight;
function load(){
callD3();
}
var m = [40, 240, 40, 240],
w = realWidth -m[0] -m[0],
h = realHeight -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("#box").append("svg:svg")
.attr("class","svg_container")
.attr("width", w)
.attr("height", h)
.style("overflow", "scroll")
.style("background-color","#EEEEEE")
.append("svg:g")
.attr("class","drawarea")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")")
;
var botao = d3.select("#form #button");
function callD3() {
//d3.json(filename, function(json) {
d3.json("D3_NEWCO_tree.json", function(json) {
root = json;
d3.select("#processName").html(root.text);
root.x0 = h / 2;
root.y0 = 0;
botao.on("click", function(){toggle(root); update(root);});
update(root);
});
function update(source) {
var duration = d3.event && d3.event.altKey ? 5000 : 500;
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse();
// Normalize for fixed-depth.
nodes.forEach(function(d) { d.y = d.depth * 50; });
// Update the nodes…
var node = vis.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("svg:g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
.on("click", function(d) { toggle(d); update(d); });
nodeEnter.append("svg:circle")
.attr("r", function(d){
return Math.sqrt((d.part_cc_p*1))+4;
})
.attr("class", function(d) { return "level"+d.part_level; })
.style("stroke", function(d){
if(d._children){return "blue";}
})
;
nodeEnter.append("svg:text")
.attr("x", function(d) { return d.children || d._children ? -((Math.sqrt((d.part_cc_p*1))+6)+this.getComputedTextLength() ) : Math.sqrt((d.part_cc_p*1))+6; })
.attr("y", function(d) { return d.children || d._children ? -7 : 0; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text(function(d) {
if(d.part_level>0){return d.name;}
else
if(d.part_multi>1){return "Part " + d.name+ " ["+d.part_multi+"]";}
else{return "Part " + d.name;}
})
.attr("title",
function(d){
var node_type_desc;
if(d.part_level!=0){node_type_desc = "Labour";}else{node_type_desc = "Component";}
return ("Part Name: "+d.text+"<br/>Part type: "+d.part_type+"<br/>Cost so far: "+d3.round(d.part_cc, 2)+"€<br/>"+"<br/>"+node_type_desc+" cost at this node: "+d3.round(d.part_cost, 2)+"€<br/>"+"Total cost added by this node: "+d3.round(d.part_cost*d.part_multi, 2)+"€<br/>"+"Node multiplicity: "+d.part_multi);
})
.style("fill-opacity", 1e-6);
// 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", function(d){
return Math.sqrt((d.part_cc_p*1))+4;
})
.attr("class", function(d) { return "level"+d.part_level; })
.style("stroke", function(d){
if(d._children){return "blue";}else{return null;}
})
;
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", function(d){
return Math.sqrt((d.part_cc_p*1))+4;
});
nodeExit.select("text")
.style("fill-opacity", 1e-6);
// Update the links…
var link = vis.selectAll("path.link")
.data(tree.links(nodes), function(d) { return d.target.id; });
// Enter any new links at the parent's previous position.
link.enter().insert("svg:path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {x: source.x0, y: source.y0};
return diagonal({source: o, target: o});
})
.transition()
.duration(duration)
.attr("d", diagonal);
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
})
.remove();
$('svg text').tipsy({
fade:true,
gravity: 'nw',
html:true
});
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
var drag = d3.behavior.drag()
.origin(function() {
var t = d3.select(this);
return {x: t.attr("x"), y: t.attr("y")};
})
.on("drag", dragmove);
d3.select(".drawarea").call(drag);
}
// Toggle children.
function toggle(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
}
function dragmove(){
d3.transition(d3.select(".drawarea"))
.attr("transform", "translate(" + d3.event.x +"," + d3.event.y + ")");
}
}
You can see (most of) a working implementation here: http://jsfiddle.net/nrabinowitz/fF4L4/2/
The key pieces here:
Call d3.behavior.zoom() on the svg element. This requires the svg element to have pointer-events: all set. You can also call this on a subelement, but I don't see a reason if you want the whole thing to pan and zoom, since you basically want the whole SVG area to respond to pan/zoom events.
d3.select("svg")
.call(d3.behavior.zoom()
.scaleExtent([0.5, 5])
.on("zoom", zoom));
Setting scaleExtent here gives you the ability to limit the zoom scale. You can set it to [1, 1] to disable zooming entirely, or set it programmatically to the max size of your content, if that's what you want (I wasn't sure exactly what was desired here).
The zoom function is similar to your dragmove function, but includes the scale factor and sets limits on the pan offset (as far as I can tell, d3 doesn't have any built-in panExtent support):
function zoom() {
var scale = d3.event.scale,
translation = d3.event.translate,
tbound = -h * scale,
bbound = h * scale,
lbound = (-w + m[1]) * scale,
rbound = (w - m[3]) * scale;
// limit translation to thresholds
translation = [
Math.max(Math.min(translation[0], rbound), lbound),
Math.max(Math.min(translation[1], bbound), tbound)
];
d3.select(".drawarea")
.attr("transform", "translate(" + translation + ")" +
" scale(" + scale + ")");
}
(To be honest, I don't think I have the logic quite right here for the correct left and right thresholds - this doesn't seem to limit dragging properly when zoomed in. Left as an exercise for the reader :).)
To make things simpler here, it helps to have the g.drawarea element not have an initial transform - so I added another g element to the outer wrappers to set the margin offset:
vis // snip
.append("svg:g")
.attr("class","drawarea")
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
The rest of the changes here are just to make your code work better in JSFiddle. There are a few missing details here, but hopefully this is enough to get you started.