Карта мира d3 с щелчком по стране и масштабированием почти не работает

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

Примечание. Если отключить функцию перехода, масштабирование и центрирование работают, только при добавлении поворота отображается неправильно.

Что не так с моим кодом?

Я создал plunker для удобства http://plnkr.co/edit/tgIHG76bM3cbBLktjTX0?p=preview< /а>

<!DOCTYPE html>
<meta charset="utf-8">
<style>

.background {
  fill: none;
  pointer-events: all;
  stroke:grey;
}

.feature, {
  fill: #ccc;
  cursor: pointer;
}

.feature.active {
  fill: orange;
}

.mesh,.land {
  fill: black;
  stroke: #ddd;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.water {
  fill: #00248F;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script src="//d3js.org/queue.v1.min.js"></script>
<script>

var width = 960,
    height = 600,
    active = d3.select(null);

var projection = d3.geo.orthographic()
    .scale(250)
    .translate([width / 2, height / 2])
    .clipAngle(90);

var path = d3.geo.path()
    .projection(projection);

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);

svg.append("rect")
    .attr("class", "background")
    .attr("width", width)
    .attr("height", height)
    .on("click", reset);

var g = svg.append("g")
    .style("stroke-width", "1.5px");

var countries;
var countryIDs;

 queue()
  .defer(d3.json, "js/world-110m.json")
  .defer(d3.tsv, "js/world-110m-country-names.tsv")
  .await(ready)

function ready(error, world, countryData) {
  if (error) throw error;

  countries = topojson.feature(world, world.objects.countries).features;
  countryIDs = countryData;

    //Adding water
    g.append("path")
      .datum({type: "Sphere"})
      .attr("class", "water")
      .attr("d", path);

    var world = g.selectAll("path.land")
    .data(countries)
    .enter().append("path")
    .attr("class", "land")
    .attr("d", path)
    .on("click", clicked)

};

function clicked(d) {
  if (active.node() === this) return reset();
  active.classed("active", false);
  active = d3.select(this).classed("active", true);

  var bounds = path.bounds(d),
      dx = bounds[1][0] - bounds[0][0],
      dy = bounds[1][1] - bounds[0][1],
      x = (bounds[0][0] + bounds[1][0]) / 2,
      y = (bounds[0][1] + bounds[1][1]) / 2,
      scale = 0.5 / Math.max(dx / width, dy / height),
      translate = [width / 2 - scale * x, height / 2 - scale * y];

  g.transition()
      .duration(750)
      .style("stroke-width", 1.5 / scale + "px")
      .attr("transform", "translate(" + translate + ")scale(" + scale + ")");

  var countryCode;

  for (i=0;i<countryIDs.length;i++) {
    if(countryIDs[i].id==d.id) {
      countryCode = countryIDs[i];
    }
  }


  var rotate = projection.rotate();
  var focusedCountry = country(countries, countryCode);
  var p = d3.geo.centroid(focusedCountry);


  (function transition() {
    d3.transition()
    .duration(2500)
    .tween("rotate", function() {
      var r = d3.interpolate(projection.rotate(), [-p[0], -p[1]]);

      return function(t) {
        projection.rotate(r(t));
        g.selectAll("path").attr("d", path)
        //.classed("focused", function(d, i) { return d.id == focusedCountry.id ? focused = d : false; });
      };
    })
    })();

    function country(cnt, sel) {
      for(var i = 0, l = cnt.length; i < l; i++) {
        console.log(sel.id)
        if(cnt[i].id == sel.id) {
          return cnt[i];
        }
      }
    };
}

function reset() {
  active.classed("active", false);
  active = d3.select(null);

  g.transition()
      .duration(750)
      .style("stroke-width", "1.5px")
      .attr("transform", "");
}

</script>

person sn4ke    schedule 09.08.2017    source источник


Ответы (1)


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

Текущий подход

В настоящее время вы панорамируете и масштабируете g, это панорамирует и масштабирует g до предполагаемого места назначения. После щелчка g размещается так, чтобы функция находилась посередине, а затем масштабируется, чтобы продемонстрировать функцию. Следовательно, g больше не находится в центре svg (поскольку он был масштабирован и переведен), другими словами, земной шар перемещается и растягивается так, чтобы объект находился в центре. Пути не изменены.

В этот момент вы поворачиваете проекцию, которая пересчитывает пути на основе нового поворота. Это перемещает выбранные объекты в центр g, который больше не находится в центре svg — поскольку объект уже был в центре svg, любое перемещение приведет к его децентрации. Например, если вы удалите код, который изменяет масштаб и переводит g, вы заметите, что ваша функция центрируется по клику.

Возможное решение

Вы, кажется, после двух преобразований:

  1. вращение
  2. шкала

Панорамирование (/перевод) — это не то, что вы, вероятно, захотите здесь сделать, так как это перемещает земной шар, когда вы просто хотите его повернуть.

Вращение может быть выполнено только с проекцией d3, а масштабирование может быть выполнено либо с g, либо с проекцией d3. Поэтому, вероятно, проще просто использовать проекцию d3 для обработки ваших преобразований карты.

Кроме того, проблема с текущим подходом заключается в том, что, используя path.bounds для получения bbox, для получения как масштаба, так и перевода, вы вычисляете значения, которые могут измениться по мере обновления проекции (тип проекции также будет изменять дисперсию) . Например, если визуализируется только часть объекта (потому что он частично находится за горизонтом), ограничительная рамка будет отличаться от должной, что вызовет проблемы с масштабированием и переводом. Чтобы преодолеть это ограничение в моем предложенном решении, сначала поверните глобус, рассчитайте границы и масштабируйте его до этого коэффициента. Вы можете рассчитать масштаб, фактически не обновляя вращение путей на глобусе, просто обновите path и переместите нарисованные пути позже.

Внедрение решения

Я немного изменил ваш код, и я думаю, что в конечном итоге будет чище реализовать код:

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

 // Store the current rotation and scale:
  var currentRotate = projection.rotate();
  var currentScale = projection.scale();

Используя вашу переменную p, чтобы получить центроид объекта, к которому мы приближаемся, я определяю ограничивающую рамку объекта с примененным вращением (но на самом деле я еще не поворачиваю карту). С помощью bbox я получаю масштаб, необходимый для приближения к выбранному объекту:

  projection.rotate([-p[0], -p[1]]);
  path.projection(projection);

  // calculate the scale and translate required:
  var b = path.bounds(d);
  var nextScale = currentScale * 1 / Math.max((b[1][0] - b[0][0]) / (width/2), (b[1][1] - b[0][1]) / (height/2));
  var nextRotate = projection.rotate(); // as projection has already been updated.

Для получения дополнительной информации о расчете параметров см. этот ответ.

Затем я выполняю анимацию между текущим масштабом и поворотом и целевым (следующим) масштабом и поворотом:

  // Update the map:
  d3.selectAll("path")
   .transition()
   .attrTween("d", function(d) {
      var r = d3.interpolate(currentRotate, nextRotate);
      var s = d3.interpolate(currentScale, nextScale);
        return function(t) {
          projection
            .rotate(r(t))
            .scale(s(t));
          path.projection(projection);
          return path(d);
        }
   })
   .duration(1000);

Теперь мы одновременно переходим к обоим свойствам:

Плункер

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

Другие уточнения

Вы можете получить центроид страны/функции только с помощью этого:

  // Clicked on feature:
  var p = d3.geo.centroid(d);

Обновлен плункер или Bl.ock

Вы также можете поэкспериментировать с ослаблением — вместо того, чтобы просто использовать линейную интерполяцию — например, в этом plunker или bl.ock. Это может помочь сохранить функции в поле зрения во время перехода.

Альтернативная реализация

Если вы действительно хотите сохранить масштабирование как манипулирование g, а не проекцию, то вы можете добиться этого, но масштабирование должно быть после поворота, так как объект будет центрирован в g, который будет центрирован. в svg. См. этот планкер. Вы можете рассчитать bbox перед вращением, но тогда масштабирование временно сместит земной шар от центра, если одновременно выполняются оба перехода (вращение и масштабирование).

Зачем использовать функции анимации для поворота и масштабирования?

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

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

path.transition()
  .attrTween("d", function()...) // set rotation
  .attr("d", path) // set scale

Масштабирование SVG и масштабирование проекции

Многие цилиндрические проекции можно панорамировать и масштабировать, манипулируя путями/svg напрямую, без обновления проекции. Поскольку это не пересчитывает пути с помощью geoPath, это должно быть менее требовательным.

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

person Andrew Reid    schedule 10.08.2017
comment
Хороший ответ. Я смотрел на это некоторое время этим утром и не мог найти хорошее решение. - person Mark; 10.08.2017
comment
Спасибо, я тоже видел это раньше. Это оставалось в моей памяти весь день - оказалось немного сложнее для меня, чем я ожидал сначала, но это сделало это еще более интересным испытанием. Всегда приятно видеть еще одного северянина. - person Andrew Reid; 10.08.2017