d3.js-D3js:自动放置标签以避免重叠? (推斥力)

如何在地图标签上施加排斥力,以便他们自动找到合适的位置?


博斯托克(Bostock)“让我们来绘制地图”

迈克·博斯托克(Mike Bostock)的《让我们做一张地图》(以下屏幕截图)。 默认情况下,标签放置在点的坐标处,而多边形/多多边形的population +简单的向左或向右对齐,因此它们经常会发生冲突。

enter image description here

手工标签放置

我遇到的一项改进要求添加一个人工制作的population修复程序,并根据需要添加尽可能多的修复程序,例如:

.attr("dy", function(d){ if(d.properties.name==="Berlin") {return ".9em"} })

随着要调整的标签数量的增加,整体变得越来越脏:

//places's labels: point objects
svg.selectAll(".place-label")
    .data(topojson.object(de, de.objects.places).geometries)
  .enter().append("text")
    .attr("class", "place-label")
    .attr("transform", function(d) { return "translate(" + projection(d.coordinates) + ")"; })
    .attr("dy", ".35em")
    .text(function(d) { if (d.properties.name!=="Berlin"&&d.properties.name!=="Bremen"){return d.properties.name;} })
    .attr("x", function(d) { return d.coordinates[0] > -1 ? 6 : -6; })
    .style("text-anchor", function(d) { return d.coordinates[0] > -1 ? "start" : "end"; });

//districts's labels: polygons objects.
svg.selectAll(".subunit-label")
    .data(topojson.object(de, de.objects.subunits).geometries)
  .enter().append("text")
    .attr("class", function(d) { return "subunit-label " + d.properties.name; })
    .attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
    .attr("dy", function(d){
    //handmade IF
        if( d.properties.name==="Sachsen"||d.properties.name==="Thüringen"|| d.properties.name==="Sachsen-Anhalt"||d.properties.name==="Rheinland-Pfalz")
            {return ".9em"}
        else if(d.properties.name==="Brandenburg"||d.properties.name==="Hamburg")
            {return "1.5em"}
        else if(d.properties.name==="Berlin"||d.properties.name==="Bremen")
            {return "-1em"}else{return ".35em"}}
    )
    .text(function(d) { return d.properties.name; });

需要更好的解决方案

对于较大的地图和标签集而言,这是无法管理的。 如何在这两个类别中添加力排斥:population.subunit-label

这个问题真是令人费解,因为我没有对此进行最后期限,但对此我感到很好奇。 我正在考虑将此问题作为Migurski / Dymo.py的基本D3js实现。 Dymo.py的README.md文档设定了一系列目标,从中可以选择核心需求和功能(工作的20%,结果的80%)。

  1. 初始放置:Bostock从相对于geopoint的左/右定位开始。
  2. 标签间的排斥:可能采用不同的方法,Lars&Navarrc提出了一种,
  3. 标签an灭:当一个标签的整体排斥力太强时(由于挤压在其他标签之间),标签an灭的功能为label灭,优先级是随机的或基于population数据值,我们可以通过NaturalEarth的.shp文件获得该值。
  4. [豪华]标签到点的排斥:带有固定点和移动标签。 但这是一种奢侈。

我忽略标签排斥是否可以在标签的各个层和类别之间起作用。 但是让国家标签和城市标签不重叠也可能是一种奢侈。

Hugolpz asked 2020-08-01T16:03:09Z
6个解决方案
35 votes

我认为,部队布局不适合在地图上放置标签。 原因很简单-标签应尽可能靠近标签的位置,但是强制布局没有强制执行此操作的原因。 实际上,就模拟而言,混合标签没有任何危害,这显然是地图所不希望的。

可能会在力量布局的顶部实施一些措施,将地点本身作为固定节点,并在地点与其标签之间施加吸引力,而标签之间的作用力是相互排斥的。 这可能需要修改后的部队布局实现(或同时进行多个部队布局),因此我不会沿这条路线走。

我的解决方案仅依赖于冲突检测:对于每对标签,检查它们是否重叠。 如果是这种情况,请将它们移开,而运动的方向和幅度则取决于重叠。 这样,只有实际上重叠的标签才会移动,而标签只会移动一点。 重复此过程,直到没有移动发生。

该代码有些混乱,因为检查重叠是否很乱。 我不会在此处发布完整的代码,可以在此演示中找到它(请注意,我已经将标签做得更大了,以夸大效果)。 关键部分如下所示:

function arrangeLabels() {
  var move = 1;
  while(move > 0) {
    move = 0;
    svg.selectAll(".place-label")
       .each(function() {
         var that = this,
             a = this.getBoundingClientRect();
         svg.selectAll(".place-label")
            .each(function() {
              if(this != that) {
                var b = this.getBoundingClientRect();
                if(overlap) {
                  // determine amount of movement, move labels
                }
              }
            });
       });
  }
}

整个过程还远非完美-请注意,有些标签与其标签所处的位置相距甚远,但是该方法是通用的,至少应避免标签重叠。

enter image description here

Lars Kotthoff answered 2020-08-01T16:03:40Z
21 votes

一种选择是将力布局与多个焦点一起使用。 每个焦点必须位于要素的质心中,并将标签设置为仅被相应的焦点吸引。 这样,每个标签趋向于靠近特征的质心,但是与其他标签的排斥可以避免重叠问题。

为了比较:

  • M. Bostock的“让我们制作地图”教程(生成地图),
  • 我的重点是实现焦点策略的“自动标签放置”版本(结果图)。

相关代码:

// Place and label location
var foci = [],
    labels = [];

// Store the projected coordinates of the places for the foci and the labels
places.features.forEach(function(d, i) {
    var c = projection(d.geometry.coordinates);
    foci.push({x: c[0], y: c[1]});
    labels.push({x: c[0], y: c[1], label: d.properties.name})
});

// Create the force layout with a slightly weak charge
var force = d3.layout.force()
    .nodes(labels)
    .charge(-20)
    .gravity(0)
    .size([width, height]);

// Append the place labels, setting their initial positions to
// the feature's centroid
var placeLabels = svg.selectAll('.place-label')
    .data(labels)
    .enter()
    .append('text')
    .attr('class', 'place-label')
    .attr('x', function(d) { return d.x; })
    .attr('y', function(d) { return d.y; })
    .attr('text-anchor', 'middle')
    .text(function(d) { return d.label; });

force.on("tick", function(e) {
    var k = .1 * e.alpha;
    labels.forEach(function(o, j) {
        // The change in the position is proportional to the distance
        // between the label and the corresponding place (foci)
        o.y += (foci[j].y - o.y) * k;
        o.x += (foci[j].x - o.x) * k;
    });

    // Update the position of the text element
    svg.selectAll("text.place-label")
        .attr("x", function(d) { return d.x; })
        .attr("y", function(d) { return d.y; });
});

force.start();

enter image description here

Pablo Navarro answered 2020-08-01T16:04:18Z
14 votes

虽然ShareMap-dymo.js可能有效,但似乎没有很好的文档记录。 我发现了一个适用于更一般情况的库,该库有充分的文档记录,还使用了模拟退火:D3-Labeler

我将这个用法示例与该jsfiddle放在一起。D3-Labeler示例页面使用1,000次迭代。 我发现这是不必要的,并且50次迭代似乎效果很好-即使对于几百个数据点,这也非常快。 我相信该库与D3集成的方式以及效率方面都存在改进的空间,但我无法独自做到这一点。 如果我有时间提交PR,我将更新此线程。

以下是相关代码(有关更多文档,请参阅D3-Labeler链接):

var label_array = [];
var anchor_array = [];

//Create circles
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("id", function(d){
    var text = getRandomStr();
    var id = "point-" + text;
    var point = { x: xScale(d[0]), y: yScale(d[1]) }
    var onFocus = function(){
        d3.select("#" + id)
            .attr("stroke", "blue")
            .attr("stroke-width", "2");
    };
    var onFocusLost = function(){
        d3.select("#" + id)
            .attr("stroke", "none")
            .attr("stroke-width", "0");
    };
    label_array.push({x: point.x, y: point.y, name: text, width: 0.0, height: 0.0, onFocus: onFocus, onFocusLost: onFocusLost});
    anchor_array.push({x: point.x, y: point.y, r: rScale(d[1])});
    return id;                                   
})
.attr("fill", "green")
.attr("cx", function(d) {
    return xScale(d[0]);
})
.attr("cy", function(d) {
    return yScale(d[1]);
})
.attr("r", function(d) {
    return rScale(d[1]);
});

//Create labels
var labels = svg.selectAll("text")
.data(label_array)
.enter()
.append("text")
.attr("class", "label")
.text(function(d) {
    return d.name;
})
.attr("x", function(d) {
    return d.x;
})
.attr("y", function(d) {
    return d.y;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "black")
.on("mouseover", function(d){
    d3.select(this).attr("fill","blue");
    d.onFocus();
})
.on("mouseout", function(d){
    d3.select(this).attr("fill","black");
    d.onFocusLost();
});

var links = svg.selectAll(".link")
.data(label_array)
.enter()
.append("line")
.attr("class", "link")
.attr("x1", function(d) { return (d.x); })
.attr("y1", function(d) { return (d.y); })
.attr("x2", function(d) { return (d.x); })
.attr("y2", function(d) { return (d.y); })
.attr("stroke-width", 0.6)
.attr("stroke", "gray");

var index = 0;
labels.each(function() {
    label_array[index].width = this.getBBox().width;
    label_array[index].height = this.getBBox().height;
    index += 1;
});

d3.labeler()
    .label(label_array)
    .anchor(anchor_array)
    .width(w)
    .height(h)
    .start(50);

labels
    .transition()
    .duration(800)
    .attr("x", function(d) { return (d.x); })
    .attr("y", function(d) { return (d.y); });

links
    .transition()
    .duration(800)
    .attr("x2",function(d) { return (d.x); })
    .attr("y2",function(d) { return (d.y); });

要更深入地了解D3-Labeler的工作原理,请参见“用于通过使用模拟功能自动放置标签的D3插件退火”

Jeff Heaton的“人类人工智能,第1卷”在解释模拟退火过程方面也做得非常出色。

Jordan answered 2020-08-01T16:04:56Z
11 votes

您可能对为此目的专门设计的d3fc-label-layout组件(对于D3v5)感兴趣。 该组件提供了一种根据子组件的矩形边界框排列子组件的机制。 您可以应用贪婪或模拟退火策略,以最大程度地减少重叠。

这是一个代码片段,演示了如何将此布局组件应用于Mike Bostock的地图示例:

const labelPadding = 2;

// the component used to render each label
const textLabel = layoutTextLabel()
  .padding(labelPadding)
  .value(d => d.properties.name);

// a strategy that combines simulated annealing with removal
// of overlapping labels
const strategy = layoutRemoveOverlaps(layoutGreedy());

// create the layout that positions the labels
const labels = layoutLabel(strategy)
    .size((d, i, g) => {
        // measure the label and add the required padding
        const textSize = g[i].getElementsByTagName('text')[0].getBBox();
        return [textSize.width + labelPadding * 2, textSize.height + labelPadding * 2];
    })
    .position(d => projection(d.geometry.coordinates))
    .component(textLabel);

// render!
svg.datum(places.features)
     .call(labels);

这是结果的小屏幕截图:

enter image description here

您可以在此处看到完整的示例:

[HTTP://本来.OC考试.org/Colin Eberhard T/389从76从6啊544阿凡9发0cab]

披露:正如下面评论中所讨论的,我是该项目的核心贡献者,因此显然我有些偏见。 完全归功于该问题的其他答案,这给了我们启发!

ColinE answered 2020-08-01T16:05:39Z
3 votes

一种选择是使用Voronoi布局来计算点之间的空间。 这里有Mike Bostock的一个很好的例子。

RobinL answered 2020-08-01T16:05:59Z
2 votes

对于2D外壳这是一些执行类似操作的示例:

一[http://bl.ocks.org/1691430]
两个[http://bl.ocks.org/1377729]

感谢Alexander Skaburskis在这里提出的建议


对于一维盒对于那些在1-D中寻找类似问题的解决方案的人,我可以共享我试图解决它的沙箱JSfiddle。 这远非完美,但确实可以做到。

左:沙盒模型,右:示例用法enter image description here

这是您可以通过按文章末尾的按钮运行的代码片段,以及代码本身。 运行时,单击字段以定位固定节点。

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<body></body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<body></body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<body></body>

Angie answered 2020-08-01T16:06:46Z
translate from https://stackoverflow.com:/questions/17425268/d3js-automatic-labels-placement-to-avoid-overlaps-force-repulsion