D3js:自动标签放置以避免重叠? (力排斥) [英] D3js: Automatic labels placement to avoid overlaps? (force repulsion)

查看:2903
本文介绍了D3js:自动标签放置以避免重叠? (力排斥)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

如何对地图标签应用强制排斥,以便自动找到正确的地点?






< h2> Bostock让我们制作一张地图

Mike Bostock的需要添加一个人为的 IF 修复,并添加尽可能多的需要,如:

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

随着标签数量的增加增加:

  //地方的标签:点对象
svg.selectAll(。place-label)
.data(topojson.object(de,de.objects.places).geometries)
.enter()。append(text)
.attr(class,place-label )
.attr(transform,function(d){returntranslate(+ 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;})
.style(text-anchor,function(d){return d.coordinates [0]> -1?start:end;});

// districts的标签:多边形对象。
svg.selectAll(。subunit-label)
.data(topojson.object(de,de.objects.subunits).geometries)
.enter )
.attr(transform,function(d){return(class,function(d){returnsubunit-label+ d.properties.name; translate(+ path.centroid(d)+);})
.attr(dy,function(d){
// handmade IF
if 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)
{return1.5em}
else if(d.properties.name ===Berlin|| d.properties.name ===Bremen)
{return-1em} else {return.35em}}

.text(function(d){return d.properties.name;});



需要更好的解决方案



不能管理更大的地图和标签集。 如何在这两个类中添加强制排斥: .place-label .subunit-label



这个问题相当大脑,因为我没有最后期限,但我很好奇。我正在考虑这个问题作为基本的D3js实现Migurski / Dymo.py 。 Dymo.py的README.md文档设置了一大组目标,从中选择核心需求和功能(20%的工作,80%的结果)。


  1. 初始展示位置: Bostock提供了一个相对于地理位置左/右定位的良好开始。

  2. 标签间排斥:可能有不同的方法,Lars&
  3. 标签湮灭功能:当一个标签的整体排斥力过强时,由于其他标签之间的挤压,湮灭的优先级是随机的或基于群体数据值,这可以通过NaturalEarth的.shp文件获得。
  4. >标签到点排斥:固定点和移动标签。但这是一种奢侈。

如果标签排斥会在图层和类别的标签之间起作用,我将忽略。

解决方案

在我看来,强制布局是不适合的以便在地图上放置标签。原因很简单 - 标签应该尽可能接近它们标记的位置,但强制布局没有任何强制执行。事实上,就模拟而言,混合标签没有任何危害,这对于地图是显然不可取的。



的力布局,有地方本身作为固定节点和地方和其标签之间的吸引力,而标签之间的力是排斥的。这可能需要一个修改的强制布局实现(或几个强制布局同时),所以我不会走这条路线。



我的解决方案依赖简单的碰撞检测:对于每对标签,检查它们是否重叠。如果是这种情况,请将它们移出路径,其中移动的方向和大小源自重叠。这样,只有实际重叠的标签才会移动,而标签只会移动一点。此过程被迭代,直到不发生移动。



代码有点复杂,因为检查重叠是很乱。我不会在这里发布整个代码,可以在此演示中找到(请注意,我已经使标签更大,以夸大的效果)。关键位如下:

  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){
//确定移动量,移动标签
}
}
});
});
}
}

一些标签离他们标签的地方相当远,但是该方法是通用的,并且应该至少避免标签重叠。




How to apply force repulsion on map's labels so they find their right places automatically ?


Bostock' "Let's Make a Map"

Mike Bostock's Let's Make a Map (screenshot below). By default, labels are put at the point's coordinates and polygons/multipolygons's path.centroid(d) + a simple left or right align, so they frequently enter in conflict.

Handmade label placements

One improvement I met requires to add an human made IF fixes, and to add as many as needed, such :

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

The whole become increasingly dirty as the number of labels to reajust increase :

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

Need for better solution

That's just not manageable for larger maps and sets of labels. How to add force repulsions to these both classes: .place-label and .subunit-label?

This issue is quite a brain storming as I haven't deadline on this, but I'am quite curious about it. I was thinking about this question as a basic D3js implementation of Migurski/Dymo.py. Dymo.py's README.md documentation set a large set of objectives, from which to select the core needs and functions (20% of the work, 80% of the result).

  1. Initial placement: Bostock give a good start with left/right positionning relative to the geopoint.
  2. Inter-labels repulsion: different approach are possible, Lars & Navarrc proposed one each,
  3. Labels annihilation: A label annihilation function when one label's overall repulsion is too intense, since squeezed between other labels, with the priority of annihilation being either random or based on a population data value, which we can get via NaturalEarth's .shp file.
  4. [Luxury] Label-to-dots repulsion: with fixed dots and mobile labels. But this is rather a luxury.

I ignore if label repulsion will work across layers and classes of labels. But getting countries labels and cities labels not overlapping may be a luxury as well.

解决方案

In my opinion, the force layout is unsuitable for the purpose of placing labels on a map. The reason is simple -- labels should be as close as possible to the places they label, but the force layout has nothing to enforce this. Indeed, as far as the simulation is concerned, there is no harm in mixing up labels, which is clearly not desirable for a map.

There could be something implemented on top of the force layout that has the places themselves as fixed nodes and attractive forces between the place and its label, while the forces between labels would be repulsive. This would likely require a modified force layout implementation (or several force layouts at the same time), so I'm not going to go down that route.

My solution relies simply on collision detection: for each pair of labels, check if they overlap. If this is the case, move them out of the way, where the direction and magnitude of the movement is derived from the overlap. This way, only labels that actually overlap are moved at all, and labels only move a little bit. This process is iterated until no movement occurs.

The code is somewhat convoluted because checking for overlap is quite messy. I won't post the entire code here, it can be found in this demo (note that I've made the labels much larger to exaggerate the effect). The key bits look like this:

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

The whole thing is far from perfect -- note that some labels are quite far away from the place they label, but the method is universal and should at least avoid overlap of labels.

这篇关于D3js:自动标签放置以避免重叠? (力排斥)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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