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

ale*_*gel 6 svg label overlap treemap d3.js

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

作为新用户,我不允许上传图像,因此这里是Mike's Tree 的链接,您可以在其中看到前面节点的标签与以下节点重叠.

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

d3.select('.nodeText').node().getComputedTextLength();
Run Code Online (Sandbox Code Playgroud)

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

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

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);
});
Run Code Online (Sandbox Code Playgroud)

使用时只返回字符串长度

d.y = (d.depth * 200);
Run Code Online (Sandbox Code Playgroud)

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

有没有办法避免这种重叠?如果是这样,那么最好的方法是保持树的动态结构?

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

  1. 检测标签长度并使用省略号覆盖子节点.(这会使标签的可读性降低)
  2. 通过检测标签长度并告知链接进行相应调整来动态缩放布局.(这将是最好的,但似乎真的很难
  3. 缩放svg元素并在标签开始运行时使用滚动条.(不确定这是可能的,因为我一直在假设SVG需要设置高度和宽度).

Sup*_*gly 8

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

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

所以这里有一种根据文本长度重新分配深度的方法:

首先,我使用以下(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};
}
Run Code Online (Sandbox Code Playgroud)

现在我将以递归方式构建一个包含每个级别字符串数组的数组:

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);
Run Code Online (Sandbox Code Playgroud)

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

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

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

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; });
Run Code Online (Sandbox Code Playgroud)

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

// 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];
});
Run Code Online (Sandbox Code Playgroud)

呃瞧!这可能不是最干净的解决方案,也许并不能解决所有问题,但它应该让你开始.玩得开心!