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 thesvg
element. This requires thesvg
element to havepointer-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 yourdragmove
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-inpanExtent
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 anotherg
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.
来源:https://stackoverflow.com/questions/17405638/d3-js-zooming-and-panning-a-collapsible-tree-diagram