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

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

问题描述

如何在地图标签上施加力斥力,以便它们自动找到正确的位置?

<小时>

博斯托克的让我们制作地图"

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("文本").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"; });//地区的标签:多边形对象.svg.selectAll(".subunit-label").data(topojson.object(de, de.objects.subunits).geometries).enter().append("文本").attr("class", function(d) { return "subunit-label " + d.properties.name; }).attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; }).attr("dy", 函数(d){//手工制作的IFif( d.properties.name==="萨克森"||d.properties.name==="图林根"|| d.properties.name==="萨克森-安哈尔特"||d.properties.name==="莱茵兰-普法尔茨"){返回.9em"}else if(d.properties.name==="勃兰登堡"||d.properties.name==="汉堡"){返回1.5em"}else if(d.properties.name==="柏林"||d.properties.name==="不来梅"){return "-1em"}else{return ".35em"}}).text(function(d) { return d.properties.name; });

需要更好的解决方案

这对于较大的地图和标签集是无法管理的.如何为这两个类添加力斥力:.place-label.subunit-label?

这个问题是一场头脑风暴,因为我没有截止日期,但我对此很好奇.我正在考虑将此问题作为 Migurski/Dymo.py 的基本 D3js 实现.Dymo.py 的 README.md 文档设定了大量目标,从中选择核心需求和功能(20% 的工作,80% 的结果).

  1. 初始位置:Bostock 为相对于地理点的左/右定位提供了良好的开端.
  2. 标签间排斥:不同的方法是可能的,Lars &Navarrc 提出了一个,
  3. 标签湮灭:当一个标签的整体排斥力太强时的标签湮灭函数,因为它被挤压在其他标签之间,湮灭的优先级要么是随机的,要么是基于种群 数据值,我们可以通过 NaturalEarth 的 .shp 文件获得.
  4. [豪华] 标签到点的排斥:带有固定点和移动标签.但这是一种奢侈.

我忽略标签排斥是否适用于标签的层和类别.但让国家标签和城市标签不重叠也可能是一种奢侈.

解决方案

在我看来,强制布局不适合在地图上放置标签.原因很简单——标签应该尽可能靠近它们标记的地方,但是强制布局没有强制执行这一点.事实上,就模拟而言,混淆标签并没有什么坏处,这对于地图来说显然是不可取的.

可以在力布局之上实现一些东西,将地点本身作为固定节点和地点与其标签之间的吸引力,而标签之间的力则是排斥力.这可能需要修改力布局实现(或同时进行多个力布局),所以我不会走这条路.

我的解决方案仅依赖于碰撞检测:对于每对标签,检查它们是否重叠.如果是这种情况,请将它们移开,其中移动的方向和幅度来自重叠.这样,只有实际重叠的标签才会被移动,而标签只会移动一点点.重复这个过程,直到没有移动发生.

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

函数排列标签(){无功移动= 1;而(移动> 0){移动 = 0;svg.selectAll(".place-label").each(function() {var that = this,a = this.getBoundingClientRect();svg.selectAll(".place-label").each(function() {如果(这个!=那个){var b = this.getBoundingClientRect();如果(重叠){//确定移动量,移动标签}}});});}}

整个事情远非完美——请注意,有些标签离它们标记的地方很远,但方法是通用的,至少应该避免标签重叠.

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天全站免登陆