D3js: label Otomatis penempatan untuk menghindari tumpang tindih? (gaya tolakan)

Cara menerapkan gaya tolakan pada peta's label sehingga mereka menemukan tempat yang tepat mereka secara otomatis ?


Bostock', "Let's Membuat Peta"

Mike Bostock's Let's Membuat Peta (gambar di bawah). Secara default, label yang diletakkan pada titik's koordinat dan poligon/multipolygons's path.centroid(d) + sederhana kiri atau rata kanan, sehingga mereka sering masuk dalam konflik.

Handmade penempatan label

Salah satu perbaikan aku bertemu membutuhkan untuk menambahkan aplikasi yang dibuat manusia JIKA perbaikan, dan untuk menambahkan sebanyak yang dibutuhkan, seperti :

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

Seluruh menjadi semakin kotor sebagai jumlah label untuk reajust meningkatkan :

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

Kebutuhan untuk solusi yang lebih baik

Yang's tidak hanya dikelola untuk peta yang lebih besar dan set label. Bagaimana untuk menambah kekuatan repulsions untuk kedua kelas: .tempat-label dan .subunit-label?

Masalah ini cukup brain storming karena saya belum't tenggat waktu ini, tapi saya'saya cukup penasaran tentang hal itu. Aku sedang berpikir tentang pertanyaan ini sebagai dasar D3js pelaksanaan Migurski/Dymo.py. Dymo.py's README.md dokumentasi set set besar dari tujuan, dari yang untuk memilih inti kebutuhan dan fungsi (20% dari pekerjaan, 80% dari hasilnya).

  1. Penempatan awal: Bostock memberikan awal yang baik dengan kiri/kanan positionning relatif terhadap geopoint.
  2. Inter-label tolakan: pendekatan yang berbeda yang mungkin, Lars & Navarrc diusulkan masing-masing,
  3. Label pemusnahan: label pemusnahan fungsi ketika salah satu label's secara keseluruhan tolakan terlalu kuat, karena terjepit di antara label lain, dengan prioritas pemusnahan yang secara acak atau didasarkan pada penduduk data nilai, yang dapat kita dapatkan melalui NaturalEarth's .shp file.
  4. [Rental] Label-ke-titik tolakan: dengan titik-titik tetap dan mobile label. Tapi ini agak mewah.

Aku mengabaikan jika label tolakan akan bekerja di seluruh lapisan dan kelas label. Tapi mendapatkan label negara-negara dan kota-kota label tidak tumpang tindih mungkin yang mewah juga.

Mengomentari pertanyaan (6)

Menurut saya, kekuatan tata letak ini tidak cocok untuk tujuan menempatkan label pada sebuah peta. Alasannya sederhana -- label harus sedekat mungkin dengan tempat-tempat yang mereka label, tapi gaya tata letak memiliki apa-apa untuk menegakkan ini. Memang, sejauh simulasi yang bersangkutan, tidak ada salahnya mencampur label, yang jelas tidak diinginkan untuk peta.

Mungkin ada sesuatu yang diimplementasikan di atas kekuatan tata letak yang menempatkan diri sebagai node tetap dan menarik kekuatan antara tempat dan label, sementara kekuatan antara label akan menjadi menjijikkan. Ini kemungkinan akan memerlukan modifikasi gaya tata letak implementasi (atau beberapa kekuatan layout pada waktu yang sama), jadi saya'm tidak akan pergi rute itu.

Solusi saya hanya bergantung pada deteksi tabrakan: untuk setiap pasangan dari label, memeriksa apakah mereka tumpang tindih. Jika ini adalah kasus, memindahkan mereka keluar dari jalan, di mana arah dan besarnya gerakan ini berasal dari tumpang tindih. Dengan cara ini, hanya label yang benar-benar tumpang tindih yang bisa bergerak sama sekali, dan label hanya bergerak sedikit. Proses ini adalah melakukan iterasi sampai tidak ada gerakan yang terjadi.

Kode ini agak berbelit-belit karena memeriksa tumpang tindih cukup berantakan. Saya tidak't pos seluruh kode di sini, hal ini dapat ditemukan di demo (perhatikan bahwa I've membuat label yang jauh lebih besar untuk membesar-besarkan efek). Kunci bit yang terlihat seperti ini:

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

Seluruh hal ini jauh dari sempurna -- perhatikan bahwa beberapa label yang cukup jauh dari tempat mereka label, tetapi metode yang lebih universal dan setidaknya harus menghindari tumpang tindih dari label.

Komentar (9)

Salah satu pilihan adalah dengan menggunakan angkatan tata letak dengan beberapa fokus. Masing-masing fokus harus terletak di fitur's centroid, mengatur label yang akan menarik hanya dengan sesuai fokus. Dengan cara ini, setiap label akan cenderung berada di dekat fitur's centroid, tapi tolakan dengan label lain yang dapat menghindari masalah tumpang tindih.

Untuk perbandingan:

Kode yang relevan:

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

Komentar (6)

Sementara ShareMap-dymo.js dapat bekerja, itu tidak muncul untuk menjadi sangat baik didokumentasikan. Saya telah menemukan sebuah perpustakaan yang bekerja untuk kasus yang lebih umum, didokumentasikan dengan baik dan juga menggunakan simulated annealing: D3-Labeler

I've menempatkan bersama-sama penggunaan sampel dengan ini [jsfiddle][2].D3-Labeler halaman contoh menggunakan 1,000 iterasi. Saya telah menemukan ini agak tidak perlu dan bahwa 50 iterasi tampaknya bekerja cukup baik - ini adalah sangat cepat bahkan untuk beberapa ratus titik data. Saya percaya ada ruang untuk perbaikan baik dalam cara ini perpustakaan terintegrasi dengan D3 dan dalam hal efisiensi, tetapi saya tidak't telah mampu sampai sejauh ini pada saya sendiri. I'll update thread ini saya harus menemukan waktu untuk mengajukan PR.

Berikut adalah kode yang relevan (lihat bagian D3-Labeler link untuk dokumentasi lebih lanjut):

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

Untuk lebih mendalam melihat bagaimana D3-Labeler bekerja, lihat "D3 plug-in otomatis untuk penempatan label menggunakan simulasi anil"

Jeff Heaton's "Kecerdasan Buatan bagi Manusia, Volume 1" juga melakukan pekerjaan yang sangat baik di menjelaskan simulated annealing proses.

Komentar (3)

Anda mungkin tertarik dalam d3fc-label-letak komponen (untuk D3v5) yang dirancang dengan tepat untuk tujuan ini. Komponen ini menyediakan mekanisme untuk mengatur anak komponen berdasarkan persegi panjang bounding box. Anda dapat menerapkan salah serakah atau simulated annealing strategi dalam rangka untuk meminimalkan tumpang tindih.

Berikut ini's sebuah cuplikan kode yang menunjukkan cara untuk menerapkan tata letak komponen untuk Mike Bostock's peta contoh:

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

Dan ini screenshot hasilnya:

Anda bisa melihat secara lengkap contoh berikut:

http://bl.ocks.org/ColinEberhardt/389c76c6a544af9f0cab

Keterangan: Seperti yang dibahas di komentar di bawah ini, saya inti kontributor proyek ini, jadi jelas saya agak bias. Kredit penuh untuk jawaban lain untuk pertanyaan ini yang memberi kita inspirasi!

Komentar (5)

Salah satu pilihan adalah dengan menggunakan Voronoi tata Letak untuk menghitung di mana ada ruang antara poin. Ada's sebuah contoh yang baik dari Mike Bostock di sini.

Komentar (2)

Untuk kasus 2D berikut ini adalah beberapa contoh yang melakukan sesuatu yang sangat mirip:

satu http://bl.ocks.org/1691430 dua http://bl.ocks.org/1377729

terima kasih Alexander Skaburskis yang dibesarkan ini di sini


Untuk kasus 1D Bagi mereka yang mencari solusi untuk masalah yang sama dalam 1-D aku bisa berbagi saya sandbox JSfiddle di mana saya mencoba untuk menyelesaikannya. It's jauh dari sempurna, tapi itu semacam melakukan hal tersebut.

Kiri: sandbox model, Kanan: contoh penggunaan

Berikut adalah potongan kode yang bisa anda jalankan dengan menekan tombol di akhir posting, dan juga kode itu sendiri. Ketika berjalan, klik di medan untuk posisi node tetap.

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.xmouse[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>
Komentar (0)