Разница между svg и canvas в d3.js

Я новичок в d3.js. Я понял, что есть два способа получить нарисованные объекты - SVG и Canvas. Мой вариант использования - около 100 узлов и краев. Я уже пробовал несколько примеров с использованием холста, и он отлично выглядит.

Я вижу, что есть сообщение SO вокруг разницы между SVG и Canvas.

Оба кажутся хорошо для моего использования, однако, я склонен к холсту (как я уже немного работал над примером). пожалуйста, исправьте меня, если мне не хватает чего-либо в контексте d3.js?

Ответ 1

Различия, перечисленные в связанных вопросах/ответах, объясняют общие различия между svg и canvas (вектор/растр и т.д.). Однако с d3 эти различия имеют дополнительные последствия, особенно учитывая, что основной частью d3 является привязка данных.

Связывание данных

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

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

С SVG привязка данных проста - мы можем назначить привязку к отдельному элементу svg, а затем использовать эту базу данных для установки ее атрибутов/обновления /etc. Это основано на состоянии svg - мы можем повторно выбрать круг и изменить его или получить доступ к его свойствам.

С Canvas холст не имеет состояния, поэтому мы не можем привязывать данные к фигурам внутри холста, поскольку холст состоит только из пикселей. Таким образом, мы не можем выбирать и обновлять элементы внутри холста, потому что в холсте нет элементов для выбора.

Исходя из вышесказанного, мы видим, что цикл ввода/обновления/выхода (или базовые инструкции добавления) необходим для svg в идиоматическом D3: нам нужно вводить элементы, чтобы их видеть, и мы часто ставим их в зависимости от их привязки. С холстом нам не нужно вводить ничего, то же самое с выходом/обновлением. Нет никаких элементов для добавления, чтобы видеть, поэтому мы можем рисовать визуализации без подходов ввода/обновления/выхода или добавления/вставки, используемых в визуализации d3 svg, если мы хотим.

Холст без привязки данных

Я буду использовать пример bl.ock в последнем вопросе, здесь. Поскольку нам не нужно добавлять элементы вообще (или добавлять данные к ним), мы используем цикл forEach для рисования каждой функции (что противоречит идиоматическому D3 с SVG). Поскольку нет элементов для обновления, мы должны перерисовать каждую функцию каждого тика - перерисовку всего кадра (обратите внимание на очистку холста от каждого тика). Что касается перетаскивания, d3.drag и d3.force обладают некоторой функциональностью, ожидающей использования с холстом, и могут позволить нам изменять массив данных непосредственно через события перетаскивания - обходя любую потребность в элементах узла в DOM, чтобы напрямую взаимодействовать с мышью (d3.force также модифицирует массив данных напрямую, но он также делает это в примере svg).

Без привязки данных мы непосредственно рисуем элементы на основе данных:

data.forEach(function(d) {
    // drawing instructions:
    context.beginPath()....
})

Если данные изменятся, мы, вероятно, перекроем данные.

Холст с привязкой данных

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

Чтобы создать фиктивный родительский контейнер, мы можем использовать:

// container for dummy elements:
var faux = d3.select(document.createElement("custom"));

Затем мы можем сделать выбор по мере необходимости, используя enter/exit/update/append/remove/transition/etc:

// treat as any other DOM elements:
var bars = faux.selectAll(".bar").data(data).enter()....

Но поскольку элементы в этих выборках не отображаются, нам нужно указать, как и когда их рисовать. Без привязки данных и Canvas мы нарисовали элементы на основе данных напрямую, с привязкой данных и Canvas, которые мы рисуем, основываясь на элементе selection/element в искусственной DOM:

bars.each(function() {
  var selection = d3.select(this);
  context.beginPath();
  context.fillRect(selection.attr("x"), selection.attr("y")...
  ...
})

Здесь мы можем перерисовывать элементы всякий раз, когда мы выходим/вводим/обновляем и т.д., Которые могут иметь некоторые преимущества. Это также позволяет переходы D3 путем непрерывной перерисовки при переходе свойств на элементы faux.

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

var canvas = d3.select("body")
  .append("canvas")
  .attr("width", 600)
  .attr("height", 200);
  
var context = canvas.node().getContext("2d");

var data = [1,2,3,4,5];

// container for dummy elements:
var faux = d3.select(document.createElement("custom"));

// normal update exit selection with dummy elements:
function update() {
  // modify data:
  manipulateData();
  
  
  var selection = faux.selectAll("circle")
    .data(data, function(d) { return d;});
    
  var exiting = selection.exit().size();
  var exit = selection.exit()
    .transition()
    .attr("r",0)
	  .attr("cy", 70)
	  .attr("fill","white")
    .duration(1200)
	  .remove();
    
  var enter = selection.enter()
    .append("circle")
    .attr("cx", function(d,i) { 
       return (i + exiting) * 20 + 20; 
    })
    .attr("cy", 50)
    .attr("r", 0)
	.attr("fill",function(d) { return ["orange","steelblue","crimson","violet","yellow"][d%5]; });
	
	enter.transition()
    .attr("r", 8)
	.attr("cx", function(d,i) { 
       return i * 20 + 20; 
    })
    .duration(1200);
    
  selection
    .transition()
    .attr("cx", function(d,i) {
      return i * 20 + 20;
    })
    .duration(1200);
	
}


// update every 1.3 seconds
setInterval(update,1300);


// rendering function, called repeatedly:
function render() {
  context.clearRect(0, 0, 600, 200);
  faux.selectAll("circle").each(function() {
    var sel = d3.select(this);
    context.beginPath();
    context.arc(sel.attr("cx"),sel.attr("cy"),sel.attr("r"),0,2*Math.PI);
	context.fillStyle = sel.attr("fill");
    context.fill();
	context.stroke();
  })
  window.requestAnimationFrame(render) 
}

window.requestAnimationFrame(render)

// to manipulate data:
var index = 6; // to keep track of elements.
function manipulateData() {
  data.forEach(function(d,i) {
    var r = Math.random();
    if (r < 0.5 && data.length > 1) {
      data.splice(i,1);
    }
    else {
      data.push(index++);
    }
  })
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>