D3js: автоматическое размещение меток, чтобы избежать дублирования? (силы отталкивания)

Как применить силу отталкивания на карте'С надписи так, чтобы они автоматически найти свои места ?


Босток' "Давайте's сделать карту"по

Майк Босток'ы не's сделать на карте](http://bost.ocks.org/mike/map/) (скриншот ниже). По умолчанию, метки ставятся на точки'ы координат и полигоны/мультиполигоны'пути s.центроид(г) + простая влево или вправо, выровнять, поэтому они часто вводят в конфликт.

Ручной размещения надписей ## Одно улучшение я встретил требует, чтобы добавить человека в "если" исправления, и добавить столько, сколько нужно, например :

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

В целом становятся все более грязными, как количество ярлыков для reajust увеличение :

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

Необходимо для лучшего решения

Что's просто не управляем на больших картах и наборы наклеек. Как добавить отталкивания силу этих обоих классов: .место-этикетка "и".субъединицы-метка?

Эта проблема вполне себе мозговой штурм, как я не'т срок, но я'я довольно любопытно. Я думал об этом вопрос, как базовая реализация D3js из Migurski/Dymo.py. Дымо.пы's в файле README.МД набор документов большой набор задач, из которых для выбора основных потребностей и функций (20% работы, 80% результата).

  1. Первичное размещение: Босток дать хороший старт с левым/правым размещением относительно геопоинт.
  2. Интер-метки отталкивания: иной подход, возможно, Ларс & Navarrc предложил один друг,
  3. Метки аннигиляции: метка уничтожение функцию, когда одна надпись's общий отталкивания является слишком интенсивным, так как зажатый между другими надписями, с приоритетом на уничтожение либо случайным образом или на основе "население" значение данных, которые мы можем получить через NaturalEarth'ов .файл SHP.
  4. [Роскошные] ярлык-в-точку отталкивания: с фиксированными точками и мобильных меток. Но это скорее роскошь.

Я игнорирую, если метка отталкивание будет работать через слои и классы метки. Но получать ярлыки странам и городам надписи не перекрываются могут быть класса люкс, а также.

Комментарии к вопросу (6)

На мой взгляд, раскладка сил непригодна для целей размещения меток на карте. Причина проста -- этикетки должны быть как можно ближе к местам их метки, но раскладка сил не имеет ничего, чтобы осуществить это. Действительно, что касается моделирования, нет никакого вреда в смешение меток, которые явно не желательно на карте.

Там может быть что-то реализовано на верхней части макета силу, что и сами места как основные узлы и силами притяжения между местом и его этикетке, в то время как силы между метками будет отталкивающим. Этого, вероятно, потребуется модифицированная раскладка сил по выполнению (или несколько сил макетов в то же время), поэтому я'м не собираюсь идти по этому пути.

Мое решение основывается только на обнаружение столкновений: для каждой пары меток, проверить, если они пересекаются. Если это так, переместите их в сторону, где направление и величина движения происходит от перекрытия. Таким образом, только метки, которые фактически дублируют друг друга, перемещаются на всех, и ярлыки только отойти немного. Этот процесс повторяется, пока никакого движения не происходит.

Код является немного запутанным, потому что проверка на перекрытие довольно неаккуратно. Я выиграл'т размещать весь код здесь, он может быть найден в демо (заметьте, что я'ве сделал ярлыки гораздо больше преувеличивать эффект). Ключ биты выглядят так:

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

Все это далеко от совершенства-обратите внимание, что некоторые надписи достаточно далеко от места их метки, но метод универсален и должен, по крайней мере, избежать дублирования меток.

Комментарии (9)

Один вариант заключается в использовании макет силу с множественными очагами. Каждый очаг должен быть расположен в'ы центроид, создать ярлык для того чтобы быть привлечены только соответствующие очаги. Таким образом, каждая метка будет стремиться быть рядом с функцией'ы центроид, но отталкивание с другими надписями могут избежать дублирования вопроса.

Для сравнения:

Соответствующий код:

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

Комментарии (6)

А ShareMap-dymo.js может работать, он не появляется, чтобы быть очень хорошо документированы. Я нашел библиотеку, которая работает для более общего случая, подтвержден документально, а также использует моделируемый отжиг: Д3-Этикетировочная

Я'вэ собрал образец использования с этим [jsfiddle][2].Д3-Этикетировочная образец страница использует 1000 итераций. Я нашел это довольно ненужным и что 50 итераций, кажется, работает очень хорошо - это очень быстро, даже за несколько сотен точек данных. Я считаю, что есть возможности для совершенствования, как в том, как эта библиотека интегрируется с Д3 и с точки зрения эффективности, но я бы'т быть в состоянии сделать это на мою собственную. Я'будете обновлять этот поток, я должен найти время, чтобы представить себе пиар.

Вот соответствующий код (см. Д3-Этикетировочная ссылке для дальнейшей документации):

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

Для более глубокий взгляд на то, как Д3-Этикетировочная работ, см. "а Д3 плагин для автоматического размещения надписей с использованием моделирования отжиг на"

Джефф Хитон'ы "и искусственный интеллект для человека, 1 с&quot объеме; также делает отличную работу, объясняя имитации отжига процесс.

Комментарии (3)

Вы могли бы быть заинтересованы в d3fc-лейбл-макет компонента (для D3v5), которая предназначена именно для этой цели. Компонент обеспечивает механизм для организации дочерние компоненты, основанные на их ящики прямоугольной области. Вы можете применять либо жадные, либо сымитированный отжиг стратегии в целях минимизации дублирования.

Здесь'с фрагментом кода, который демонстрирует, как применить этот макет компонента Майк Босток's карты пример:

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

Дисклеймер: как говорилось в комментарии ниже, я являюсь основным разработчиком этого проекта, поэтому, очевидно, что я несколько предвзята. Полный кредит на другие ответы на этот вопрос, который дал нам вдохновение!

Комментарии (5)

Одним из вариантов является использование макета Вороного, чтобы вычислить, где есть пространство между точками. Там'хороший пример из Майк Босток здесь.

Комментарии (2)

Для 2D случая вот некоторые примеры, которые сделать что-то очень похожее:

один http://bl.ocks.org/1691430 два http://bl.ocks.org/1377729

спасибо Александр Skaburskis, кто принес это здесь


Для случая 1Д Для тех, кто ищет решение аналогичной задачи в 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.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>

в

Комментарии (0)