マップのラベルに力の反発力を適用して、適切な場所を自動的に見つける方法?
Mike Bostockの Let's Make a Map (下のスクリーンショット)。デフォルトでは、ラベルはポイントの座標とポリゴン/マルチポリゴンのpath.centroid(d)
+単純な左または右に配置されるため、頻繁に競合します。
1つの改善 私が会った は、人間が作ったIF
修正を追加し、必要なだけ追加する必要があります。
.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; });
これは、大きなマップやラベルのセットでは管理できません。 これら両方のクラスに力の反発力を追加する方法:.place-label
および.subunit-label
?
私はこれに期限を設定していないので、この問題は非常にブレインストーミングですが、私はそれについて非常に興味があります。私はこの質問をMigurski/ Dymo.py の基本的なD3js実装として考えていました。 Dymo.pyのREADME.mdドキュメントは、コアのニーズと機能を選択するための大きな目標セットを設定します(作業の20%、結果の80%)。
population
データ値。ラベルの反発がレイヤーとラベルのクラス間で機能するかどうかは無視します。しかし、国のラベルと都市のラベルが重ならないようにすることも贅沢かもしれません。
私の意見では、力のレイアウトは地図上にラベルを配置する目的には適していません。その理由は簡単です。ラベルはラベルを付ける場所にできるだけ近づける必要がありますが、強制レイアウトにはこれを強制するものは何もありません。実際、シミュレーションに関する限り、ラベルを混同しても害はありません。これは、マップにとって明らかに望ましくありません。
力のレイアウトの上に実装されたものがあり、固定ノードとしての場所自体と、場所とそのラベルの間の引力を持ち、ラベル間の力は反発します。これには、変更された強制レイアウト実装(または同時に複数の強制レイアウト)が必要になる可能性が高いため、そのルートをたどりません。
私の解決策は、単に衝突検出に依存しています。ラベルの各ペアについて、それらが重複しているかどうかを確認します。この場合、移動の方向と大きさがオーバーラップから得られる邪魔にならない場所に移動します。この方法では、実際にオーバーラップするラベルのみがまったく移動され、ラベルは少しだけ移動します。このプロセスは、移動が発生しなくなるまで繰り返されます。
重複のチェックは非常に面倒なので、コードはやや複雑です。ここにコード全体を掲載するのではなく、 このデモ で見つけることができます(効果を誇張するためにラベルを大きくしたことに注意してください)。キービットは次のようになります。
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
}
}
});
});
}
}
全体は完璧とはほど遠いです-一部のラベルはラベルを貼る場所からかなり離れていますが、この方法は普遍的であり、少なくともラベルの重複を避ける必要があります。
1つのオプションは、 複数のフォーカスを持つ強制レイアウト を使用することです。各焦点はフィーチャの重心に配置され、対応する焦点のみによって引き付けられるようにラベルを設定する必要があります。この方法では、各ラベルはフィーチャの重心に近い傾向がありますが、他のラベルとの反発力により、重複する問題を回避できます。
比較のために:
関連するコード:
// 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();
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プラグイン」 を参照してください。
ジェフヒートンの「人工知能、第1巻」も、シミュレーテッドアニーリングプロセスの説明に優れています。
この目的のために正確に設計された 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);
そして、これは結果の小さなスクリーンショットです:
ここで完全な例を見ることができます:
http://bl.ocks.org/ColinEberhardt/389c76c6a544af9f0cab
開示:以下のコメントで説明したように、私はこのプロジェクトの中心的な貢献者であるため、明らかにいくらか偏見があります。私たちにインスピレーションを与えたこの質問に対する他の答えへの完全な信用!
1つのオプションは、ボロノイレイアウトを使用して、ポイント間にスペースがある場所を計算することです。 Mike Bostockから良い例があります ここ 。
2Dの場合、非常によく似た動作を行ういくつかの例があります。
one http://bl.ocks.org/16914
two http://bl.ocks.org/1377729
これを育てたアレクサンダー・スカブルスキーに感謝します ここ
1Dの場合1-Dで同様の問題の解決策を検索する人のために、サンドボックスJSfiddleを共有して解決しようとしています。完璧とはほど遠いですが、それは一種のことです。
左:サンドボックスモデル、右:使用例
投稿の最後にあるボタンを押すと実行できるコードスニペットと、コード自体を次に示します。実行中に、フィールドをクリックして固定ノードを配置します。
var width = 700,
height = 500;
var mouse = [0,0];
var force = d3.layout.force()
.size([width*2, height])
.gravity(0.05)
.chargeDistance(30)
.friction(0.2)
.charge(function(d){return d.fixed?0:-1000})
.linkDistance(5)
.on("tick", tick);
var drag = force.drag()
.on("dragstart", dragstart);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.on("click", function(){
mouse = d3.mouse(d3.select(this).node()).map(function(d) {
return parseInt(d);
});
graph.links.forEach(function(d,i){
var rn = Math.random()*200 - 100;
d.source.fixed = true;
d.source.px = mouse[0];
d.source.py = mouse[1] + rn;
d.target.y = mouse[1] + rn;
})
force.resume();
d3.selectAll("circle").classed("fixed", function(d){ return d.fixed});
});
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
var graph = {
"nodes": [
{"x": 469, "y": 410},
{"x": 493, "y": 364},
{"x": 442, "y": 365},
{"x": 467, "y": 314},
{"x": 477, "y": 248},
{"x": 425, "y": 207},
{"x": 402, "y": 155},
{"x": 369, "y": 196},
{"x": 350, "y": 148},
{"x": 539, "y": 222},
{"x": 594, "y": 235},
{"x": 582, "y": 185}
],
"links": [
{"source": 0, "target": 1},
{"source": 2, "target": 3},
{"source": 4, "target": 5},
{"source": 6, "target": 7},
{"source": 8, "target": 9},
{"source": 10, "target": 11}
]
}
function tick() {
graph.nodes.forEach(function (d) {
if(d.fixed) return;
if(d.x<mouse[0]) d.x = mouse[0]
if(d.x>mouse[0]+50) d.x--
})
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
function dblclick(d) {
d3.select(this).classed("fixed", d.fixed = false);
}
function dragstart(d) {
d3.select(this).classed("fixed", d.fixed = true);
}
force
.nodes(graph.nodes)
.links(graph.links)
.start();
link = link.data(graph.links)
.enter().append("line")
.attr("class", "link");
node = node.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 10)
.on("dblclick", dblclick)
.call(drag);
.link {
stroke: #ccc;
stroke-width: 1.5px;
}
.node {
cursor: move;
fill: #ccc;
stroke: #000;
stroke-width: 1.5px;
opacity: 0.5;
}
.node.fixed {
fill: #f00;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<body></body>