在D3.js中打开子元素时,如何避免TreeMap上文本元素的重叠?

时间:2012-09-07 08:49:24

标签: svg d3.js label overlap treemap

我在D3.js中根据Mike Bostock的Node-link Tree创建了一个Tree。我在Mike的树中看到的问题是,当没有足够的空间而不是扩展链接以留出一些空间时,文本标签会重叠/重叠圆形节点。

作为新用户,我不允许上传图片,因此这里是迈克树的link,您可以在其中看到前面节点的标签与以下节点重叠。

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

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. 缩放svg元素并在标签开始运行时使用滚动条。 (不确定这是可能的,因为我一直在假设SVG需要设置高度和宽度)。

1 个答案:

答案 0 :(得分:8)

因此,以下方法可以给出不同级别的布局不同的“高度”。你必须注意,使用径向布局你可能没有足够的传播让小圆圈散开文本而没有重叠,但是现在让我们忽略它。

关键是要认识到树形布局只是将事物映射到宽度和高度的任意空间,并且对角线投影将宽度(x)映射到角度和高度(y)到半径。此外,半径是树深度的简单函数。

所以here是一种根据文本长度重新分配深度的方法:

首先,我使用以下(jQuery)来计算:

的最大文本大小
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};
}

现在我将递归地构建一个数组,每个级别包含一个字符串数组:

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

然后计算每个级别的最大文本长度。

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

然后我计算所有级别的总文本宽度(为小圆圈图标添加间距和一些填充以使其看起来很漂亮)。这将是最终布局的半径。请注意,我稍后会再次使用相同的填充量。

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

现在我们可以像往常一样调用布局。还有最后一部分:要找出不同等级的半径,我们需要先前等级半径的累积和。一旦我们有了,我们只需将新的半径分配给计算的节点。

// 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];
});
瞧瞧!这可能不是最干净的解决方案,也许并不能解决所有问题,但它应该让你开始。玩得开心!