D3.js 缩放和平移可折叠的树状图 [英] D3.js Zooming and panning a collapsible tree diagram

查看:34
本文介绍了D3.js 缩放和平移可折叠的树状图的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用 D3.js 绘制一个可折叠的树图,如 例子.它工作得很好,但是当它进入正常功能时,图表的大小可能会发生巨大变化(即,我将拥有更多的很多节点,而不是我现在拥有的几个节点).

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).

我想让 SVG 区域滚动,我已经尝试了我在网上找到的所有方法来使它工作,但没有成功.我得到的最好的工作是使用 d3.behaviour.drag,我在其中拖动整个图表.它远非最佳且故障很多,但还可以使用.

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.

即便如此,我还是想稍微清理一下,我意识到 d3.behaviour.zoom 也可以用于 平移 SVG 区域,根据 API 文档.

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?

我希望能够平移带有图表的 SVG 区域,如果可能的话,让它对一些误用做出反应,即尝试将图表平移出视口,并启用缩放到最大视口的尺寸...

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...

这是我目前的代码:

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)+"&euro;<br/>"+"<br/>"+node_type_desc+" cost at this node: "+d3.round(d.part_cost, 2)+"&euro;<br/>"+"Total cost added by this node: "+d3.round(d.part_cost*d.part_multi, 2)+"&euro;<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 + ")"); 
}
}

推荐答案

你可以在这里看到(大部分)一个有效的实现:http://jsfiddle.net/nrabinowitz/fF4L4/2/

You can see (most of) a working implementation here: http://jsfiddle.net/nrabinowitz/fF4L4/2/

这里的关键部分:

  • svg 元素上调用 d3.behavior.zoom().这需要 svg 元素设置 pointer-events: all .您也可以在子元素上调用它,但如果您希望整个事物平移和缩放,我看不出有什么理由,因为您基本上希望整个 SVG 区域响应平移/缩放事件.

  • 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));

  • 在此处设置 scaleExtent 使您能够限制缩放比例.您可以将其设置为 [1, 1] 以完全禁用缩放,或者以编程方式将其设置为内容的最大大小,如果这是您想要的(我不确定这里到底需要什么).

  • 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).

    zoom 函数类似于您的 dragmove 函数,但包括比例因子并设置平移偏移的限制(据我所知,d3 没有任何内置的 panExtent 支持):

    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 :).)

    为了让事情更简单,让 g.drawarea 元素没有初始转换会有所帮助 - 所以我添加了另一个 g 元素到外部用于设置边距偏移量的包装器:

    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] + ")");
    

  • 这里的其余更改只是为了让您的代码在 JSFiddle 中更好地工作.这里缺少一些细节,但希望这足以让您入门.

    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.

    这篇关于D3.js 缩放和平移可折叠的树状图的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆