7

我基于 Mike Bostock 的节点链接树在 D3.js 中创建了一个树。我遇到的问题以及我在 Mike's Tree 中也看到的问题是,当空间不足时,文本标签与圆形节点重叠/下重叠,而不是扩展链接以留出一些空间。

作为一个新用户,我不允许上传图片,所以这里有一个指向 Mike's Tree 的链接,您可以在其中看到前面节点的标签与后面的节点重叠。

我尝试了各种方法来通过检测文本的像素长度来解决问题:

d3.select('.nodeText').node().getComputedTextLength();

但是,这仅在我渲染页面后才有效,当我需要渲染之前最长文本项的长度时。

在渲染之前获取最长的文本项:

nodes = tree.nodes(root).reverse();

var longest = nodes.reduce(function (a, b) { 
  return a.label.length > b.label.length ? a : b; 
});

node = vis.selectAll('g.node').data(nodes, function(d, i){
  return d.id || (d.id = ++i); 
});

nodes.forEach(function(d) {
  d.y = (longest.label.length + 200);
});

仅返回字符串长度,同时使用

d.y = (d.depth * 200);

使每个链接都具有静态长度,并且在打开或关闭新节点时不会调整大小。

有没有办法避免这种重叠?如果是这样,什么是最好的方法来做到这一点并保持树的动态结构?

我可以提出 3 种可能的解决方案,但并不是那么简单:

  1. 检测标签长度并在超出子节点的地方使用省略号。(这会使标签的可读性降低)
  2. 通过检测标签长度并告诉链接进行相应调整来动态缩放布局。(这将是最好的,但似乎真的很难
  3. scale the svg element and use a scroll bar when the labels start to run over. (not sure this is possible as I have been working on the assumption that the SVG needs to have a set height and width).
4

1 回答 1

8

So the following approach can give different levels of the layout different "heights". You have to take care that with a radial layout you risk not having enough spread for small circles to fan your text without overlaps, but let's ignore that for now.

The key is to realize that the tree layout simply maps things to an arbitrary space of width and height and that the diagonal projection maps width (x) to angle and height (y) to radius. Moreover the radius is a simple function of the depth of the tree.

So here is a way to reassign the depths based on the text lengths:

First of all, I use the following (jQuery) to compute maximum text sizes for:

var computeMaxTextSize = function(data, fontSize, fontName){
    var maxH = 0, maxW = 0;

    var div = document.createElement('div');
    document.body.appendChild(div);
    $(div).css({
        position: 'absolute',
        left: -1000,
        top: -1000,
        display: 'none',
        margin:0, 
        padding:0
    });

    $(div).css("font", fontSize + 'px '+fontName);

    data.forEach(function(d) {
        $(div).html(d);
        maxH = Math.max(maxH, $(div).outerHeight());
        maxW = Math.max(maxW, $(div).outerWidth());
    });

    $(div).remove();
    return {maxH: maxH, maxW: maxW};
}

Now I will recursively build an array with an array of strings per level:

var allStrings = [[]];
var childStrings = function(level, n) {
    var a = allStrings[level];
    a.push(n.name);

    if(n.children && n.children.length > 0) {
        if(!allStrings[level+1]) {
            allStrings[level+1] = [];
        }
        n.children.forEach(function(d) {
            childStrings(level + 1, d);
        });
    }
};
childStrings(0, root);

And then compute the maximum text length per level.

var maxLevelSizes = [];
allStrings.forEach(function(d, i) {
    maxLevelSizes.push(computeMaxTextSize(allStrings[i], '10', 'sans-serif'));
});

Then I compute the total text width for all the levels (adding spacing for the little circle icons and some padding to make it look nice). This will be the radius of the final layout. Note that I will use this same padding amount again later on.

var padding = 25; // Width of the blue circle plus some spacing
var totalRadius = d3.sum(maxLevelSizes, function(d) { return d.maxW + padding});

var diameter = totalRadius * 2; // was 960;

var tree = d3.layout.tree()
    .size([360, totalRadius])
    .separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });

Now we can call the layout as usual. There is one last piece: to figure out the radius for the different levels we will need a cumulative sum of the radii of the previous levels. Once we have that we simply assign the new radii to the computed nodes.

// Compute cummulative sums - these will be the ring radii
var newDepths = maxLevelSizes.reduce(function(prev, curr, index) {
    prev.push(prev[index] + curr.maxW + padding);                 
    return prev;
},[0]);                                                      

var nodes = tree.nodes(root);

// Assign new radius based on depth
nodes.forEach(function(d) {
    d.y = newDepths[d.depth];
});

Eh voila! This is maybe not the cleanest solution, and perhaps does not address every concern, but it should get you started. Have fun!

于 2012-12-13T21:44:22.947 回答