обратный порядок масштабирования и перевода d3.zoom - PullRequest
0 голосов
/ 25 июня 2018

Если вы нажмете красную кнопку в этом примере:

https://bl.ocks.org/interwebjill/fe782e6f195b17f6fe6798a24c390d90

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

Я попытался изменить порядок шкалы и перевести как в определении масштабирования, так и в функции zoomToExtent, но эффект не изменился.

1 Ответ

0 голосов
/ 25 июня 2018

Источником проблемы является d3.interpolateZoom. Этот интерполятор имеет масштабную интерполяцию быстрее, чем переводить, даже если они в основном оба переходят одновременно. Шаблон, реализованный с помощью d3.interpolateZoom, основан на этой бумаге .

Поскольку масштабирование и перевод оба интерполируются по-разному в d3.interpolateZoom, вы получаете разрыв в стороне от вашей диаграммы, поскольку масштаб уменьшается / увеличивается быстрее, чем значения перевода.

d3.interpolateZoom используется при вызове увеличения при переходе.

Однако, если вы примените преобразование непосредственно к переходу, используя .attr(), переход d3 будет использовать d3.interpolateString, который будет искать в начальной и конечной строках соответствующие числа и использовать d3.interpolateNumber для них. Это будет применять одну и ту же интерполяцию как для масштабирования, так и для перевода.

Используя оба метода, мы можем сравнить несоответствие между d3.interpolateZoom и d3.interpolateString. Ниже черный прямоугольник использует d3.interpolateString, а оранжевый прямоугольник использует d3.interpolateZoom. Нажмите на прямоугольник, чтобы начать переход:

var svg = d3.select("body").append("svg")
   .attr("width", 500)
   .attr("height", 300);
   
var g1 = svg.append("g"), g2 = svg.append("g");

var zoom1 = d3.zoom().on("zoom", function() { 
   g1.attr("transform", d3.event.transform);
});

var zoom2 = d3.zoom().on("zoom", function() {
   g2.attr("transform", d3.event.transform);
});

g1.call(zoom1.transform, d3.zoomIdentity  
       .translate(150, 100)
       .scale(2));
       
g2.call(zoom2.transform, d3.zoomIdentity
       .translate(150,100)
       .scale(2));

g1.append("rect")
   .attr("x", 20)
   .attr("y", 20)
   .attr("width", 50)
   .attr("height", 50);
   
g2.append("rect")
  .attr("x", 22)
  .attr("y", 22)
  .attr("width", 46)
  .attr("height",46)
  .attr("fill","orange");
   
d3.selectAll("rect").on("click", function() {
                                                               
   g1.transition()
      .duration(6000)
      .attr("transform", d3.zoomIdentity)
      .on("end", function() {
				d3.select(this).call(zoom1.transform, d3.zoomIdentity);			  
			})
      
   g2.transition()
      .duration(6000)
      .call(zoom2.transform, d3.zoomIdentity)
      

});
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>

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

Сравнивая эти два, получаем:

enter image description here enter image description here

(ось Y указывает процент, оставшийся при переходе от начального атрибута к конечному атрибуту)


Вы хотите, чтобы масштабирование и перевод перемещались одновременно с одинаковой скоростью при переходе. Мы можем сделать это, если мы используем функцию анимации движения. В отличие от выше, мы не можем просто использовать transition().attr("transform",newTransfrom), потому что вы также рисуете холст и обновляете ось. Поэтому нам нужно создать собственную функцию анимации, которая может использовать текущее преобразование и масштаб, применить ее к оси, холсту и маркерам.

Например, вместо того, чтобы вызывать зум (который будет использовать d3.interpolateZoom):

function zoomToExtent(d0, d1) {
  zoomRect.call(zoom).transition()
    .duration(1500)
    .call(zoom.transform, d3.zoomIdentity  
       .translate(-xSVG(d0), 0)
       .scale(width / (xSVG(d1) - xSVG(d0))));
  }

Вместо этого мы можем использовать функцию анимации, которая управляет преобразованием элемента и применяет тот же интерполятор для масштабирования и преобразования:

function zoomToExtent(d0, d1) {
  //get transition start and end values:
  var startScale = d3.zoomTransform(zoomRect.node()).k;
  var startTranslate = d3.zoomTransform(zoomRect.node()).x;
  var endTranslate = -xSVG(d0);
  var endScale = width / (xSVG(d1) - xSVG(d0));

  zoomRect.call(zoom).transition()
    .duration(1500)
    .tween("transform", function() {
      var interpolateScale = d3.interpolateNumber(startScale,endScale);
      var interpolateTranslate = d3.interpolateNumber(startTranslate,endTranslate);

      return function(t) { 
          var t = d3.zoomIdentity.translate(interpolateTranslate(t),0).scale(interpolateScale(t));
          zoomed(t);
        }
      })
      .on("end", function() {    // update the zoom identity on end:
        d3.select(this).call(zoom.transform, d3.zoomIdentity  
       .translate(endTranslate, 0)
       .scale(endScale));
      })

  }

Вы можете заметить, что я передаю значение преобразования в функцию масштабирования, так как для этого нет d3.event.transform, нам нужно изменить функцию масштабирования, чтобы использовать переданный параметр, если он доступен, иначе использовать преобразование события:

function zoomed(transform) {
   var t = transform || d3.event.transform;
   ...

В целом, это может выглядеть примерно так .


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

var svg = d3.select("body").append("svg")
   .attr("width", 510)
   .attr("height", 310);
   
var g1 = svg.append("g");
var g2 = svg.append("g");
   
var rectangles1 = g1.selectAll()
  .data(d3.range(750))
  .enter()
  .append("rect")
  .attr("x", function(d) { return d%25*20; })
  .attr("y", function(d) { return Math.floor(d/25)*20; })
  .attr("width", 20)
  .attr("height", 20)
  .attr("fill","#ccc")
  .attr("stroke","white")
  .attr("stroke-width", 2);
  
var rectangles2 = g2.selectAll()
  .data(d3.range(750))
  .enter()
  .append("rect")
  .attr("x", function(d) { return d%25*20; })
  .attr("y", function(d) { return Math.floor(d/25)*20; })
  .attr("width", 20)
  .attr("height", 20)
  .attr("fill","none")
  .attr("stroke","#444")
  .attr("stroke-width", 1);
  
var startZoom = d3.zoomIdentity
  .translate(-250,-200)
  .scale(4);

var endZoom = d3.zoomIdentity
  .translate(-100,-100)
  .scale(5);
  
var zoom1 = d3.zoom().on("zoom", function() { g1.attr("transform", d3.event.transform); });
var zoom2 = d3.zoom().on("zoom", function() { g2.attr("transform", d3.event.transform); });

g1.call(zoom1.transform, startZoom);
g2.call(zoom2.transform, startZoom);

var toggle = true;

svg.on("click", function() {
  toggle = !toggle;
  g1.transition()
    .duration(5000)
    .call(zoom1.transform, toggle ? startZoom: endZoom)
    
  g2.transition()
    .duration(5000)
    .attr("transform", toggle ? startZoom: endZoom)
    .on("end", function() {
      d3.select(this).call(zoom2.transform,  toggle ? startZoom: endZoom);
    })
    
    
})
rect {
  opacity: 0.5;
}
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>
...