Связка, ничья и интерактивность

Допустим, вы создаете визуализацию данных, используя D3 и SVG. Вы можете удариться о потолок, если попытаетесь отобразить несколько тысяч элементов одновременно. Ваш браузер может начать работать под тяжестью всех этих элементов DOM.

На помощь приходит HTML5 Canvas! Это намного быстрее, поэтому с его помощью можно решить проблемы с зависанием вашего браузера.

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

Но не бойтесь - все не так сложно. Любой опыт, который у вас был при создании визуальных эффектов с помощью D3 и SVG или приближении к D3 с другим средством визуализации, вам очень поможет.

Этот урок построен на плечах гигантов, которые уже хорошо освоили Canvas. Я выучил эти три урока наизусть и рекомендую вам тоже:

Так зачем же тогда продолжать читать это? Что ж, когда я хочу узнать что-то новое, мне очень помогает взглянуть на один и тот же предмет под немного разными углами. И это руководство немного под другим углом.

Кроме того, в этом руководстве рассматриваются три ключевых этапа: привязка данных, рисование элементов и добавление интерактивности - и все это делается за один раз. с добавленным пошаговым руководством по настройке.

Что строим?

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

Ментальная модель

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

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

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

Менее бесплотно, вы вводите данные в еще несуществующую DOM, а D3 создает новые элементы по вашему выбору в соответствии с данными, которые вы вводите. Обычно один элемент на точку данных. Если вы хотите ввести новые данные в DOM, вы можете сделать это, и D3 определяет, какие элементы должны быть созданы заново, каким элементам разрешено оставаться, а какие элементы должны быть упакованы и покинуть экран.

D3 обычно используется вместе с SVG или иногда с HTML-элементами. В этом традиционном случае вы можете увидеть данные в модели DOM, если, например, решите просмотреть их через консоль. Вы можете захватить его, вы можете переместить его вверх или вниз по DOM, и вы можете - что важно - добавить интерактивность каждому элементу, который вы хотите показать, например, всплывающей подсказке.

Но, с другой стороны, вы не можете показать много элементов. Почему? Потому что чем больше элементов вы вставляете в DOM, тем тяжелее браузеру приходится работать, чтобы отобразить их все. Позвольте им также перемещаться, и браузеру необходимо постоянно их пересчитывать. Чем более измотан браузер, тем ниже ваша частота кадров или FPS (кадров в секунду), которая измеряет, сколько кадров браузер может отображать в секунду. Частота кадров 60 - это хорошо и обеспечивает плавное восприятие, пока не пропущены кадры - частота кадров ниже 30 может равняться скачкообразной поездке. Поэтому, когда вы хотите показать больше элементов, вы можете вернуться к холсту.

Почему холст? Canvas - это элемент HTML5, который имеет собственный API для рисования на нем. Все элементы, нарисованные на элементе холста, не проявятся в модели DOM и сэкономят много времени браузеру. Они нарисованы в немедленном режиме. Это означает, что визуализированные элементы не сохраняются в DOM, но ваши инструкции рисуют их непосредственно в конкретном кадре. DOM знает только один элемент холста; все на нем только в памяти. Если вы хотите изменить элементы холста, вам нужно перерисовать сцену для следующего кадра.

Проблема, конечно, в том, что вы не можете напрямую общаться с этими нематериальными элементами, живущими в памяти. Вы должны найти способ поговорить с ними косвенно. Здесь на помощь приходит модель D3, а также пользовательские или «виртуальные» DOM-элементы. В основном вы будете:

  1. Свяжите свои данные с настраиваемыми элементами DOM. Они живут не в DOM, а только в памяти (в «виртуальной» DOM) и описывают жизненный цикл этих элементов известным способом D3.
  2. Используйте холст, чтобы нарисовать эти элементы.
  3. Добавьте интерактивности с помощью техники, называемой «подбор».

Давай сделаем это.

Данные

Прежде чем мы начнем писать код, давайте создадим некоторые данные. Допустим, вам нужно 5000 точек данных. Итак, давайте создадим массив из 5000 элементов, каждый из которых является объектом с единственным значением свойства, содержащим индекс элемента. Вот как вы его создаете с помощью d3.range(). d3.range() - служебная функция D3, которая создает массив на основе своего аргумента:

var data = [];
d3.range(5000).forEach(function(el) {
  data.push({ value: el }); 
});

Вот как данные выглядят в консоли

Кайф!

Контейнер холста и его инструменты

Элемент холста - это элемент HTML. Концептуально он очень похож на любой родительский элемент SVG, который я, по крайней мере, обычно добавляю в простой контейнерный div, например:

<div id=“container”></div>

Итак, давайте добавим его в ваш контейнер с D3, как в…

var width = 750, height = 400;
var canvas = d3.select('#container')
  .append('canvas')
  .attr('width', width)
  .attr('height', height);
var context = canvas.node().getContext('2d');

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

HTML

…это просто. Основная HTML-структура вашего сайта будет следующей:

<!-- A title -->
<h3>Coloured grids</h3>
<!-- An input field with a default value. --> 
<input type="text" id="text-input" value="5000">
<!-- An explanation... --> 
<div id="text-explain">...takes numbers between 1 and 10k</div>
<!-- ...and a container for the canvas element. --> 
<div id="container"></div>

Структура Javascript

На верхнем уровне вам понадобятся всего 2 функции:

databind(data) {
  // Bind data to custom elements.
}
draw() {
  // Draw the elements on the canvas.
}

Пока все довольно просто.

Свяжите элементы

Чтобы привязать данные к элементам, вы сначала создаете базовый элемент для всех ваших настраиваемых элементов, которые вы будете создавать и рисовать. Если вы хорошо знаете D3, подумайте о нем как о замене элемента SVG:

var customBase = document.createElement('custom');
var custom = d3.select(customBase); 
// This is your SVG replacement and the parent of all other elements

Затем вы добавляете некоторые настройки для своей сетки. Короче говоря, эти настройки позволяют рисовать сетку из квадратов. 100 квадратов составляют «участок», и после 10 участков (или после 1000 квадратов) идет разрыв строки. Вы можете настроить это для разного «разбиения» квадратов или разного разделения строк. Или просто не беспокойтесь об этом. Я предлагаю второе ...

// Settings for a grid with 10 cells in a row, 
// 100 cells in a block and 1000 cells in a row.
var groupSpacing = 4; 
var cellSpacing = 2; 
var offsetTop = height / 5; 
var cellSize = Math.floor((width - 11 * groupSpacing) / 100) - cellSpacing;

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

function databind(data) {
// Get a scale for the colours - not essential but nice.
colourScale = d3.scaleSequential(d3.interpolateSpectral)      
                .domain(d3.extent(data, function(d) { return d; }));

Теперь давайте объединим ваши данные в «замещающий SVG», который вы назвали custom выше, и добавим еще несуществующие пользовательские элементы с классом .rect

var join = custom.selectAll('custom.rect')
  .data(data);

Вы вводите пользовательские элементы (помните, что ничего не входит в DOM, это все в памяти).

var enterSel = join.enter()
  .append('custom')
  .attr('class', 'rect')
  .attr("x", function(d, i) {
    var x0 = Math.floor(i / 100) % 10, x1 = Math.floor(i % 10);     
    return groupSpacing * x0 + (cellSpacing + cellSize) * (x1 + x0 * 10); })
  .attr("y", function(d, i) {
  var y0 = Math.floor(i / 1000), y1 = Math.floor(i % 100 / 10); 
  return groupSpacing * y0 + (cellSpacing + cellSize) * (y1 + y0 * 10); })
  .attr('width', 0)
  .attr('height', 0);

Когда элемент входит в вашу модель, вы просто задаете ему координаты x и y, а также ширину и высоту, равные 0, которые вы измените в следующем обновлении.

Вы объединяете выбор для ввода с выбором для обновления, определяете все атрибуты для обновления и вводите выбор. Это включает значение ширины и высоты, а также цвет из цветовой шкалы, которую вы создали ранее:

join 
  .merge(enterSel)
  .transition()
  .attr('width', cellSize)
  .attr('height', cellSize)
  .attr('fillStyle', function(d) { return colourScale(d); });

Об этой последней строке следует отметить два момента. Когда вы работаете с SVG, эта строка будет

.style('color', function(d) { return colourScale(d); })

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

Это очень важное различие: при работе с SVG или HTML вы можете привязывать данные к элементам и рисовать или применять стили к элементам за один шаг. В холсте нужно два шага. Сначала вы связываете данные, затем рисуете данные. Вы не можете стилизовать элементы во время привязки. Они существуют только в памяти, и холст не может быть стилизован с помощью свойств стиля CSS, а это именно то, к чему вы получаете доступ при использовании .style().

Сначала это может показаться ограничивающим, поскольку вы можете сделать меньше за один шаг, но концептуально это почти чище, а также дает вам некоторую свободу. .attr() позволяет нам отправлять любые пары "ключ-значение" в пути. Вы можете использовать другие методы, например свойство HTML .dataset, но .attr() подойдет.

Обратите внимание, мы говорим не color, а fillStyle. Если честно, вы можете использовать color или chooChooTrain здесь. Вам нужно будет помнить об этом только тогда, когда вы получите информацию позже во время рисования. Однако, поскольку холст использует свойство fillStyle для стилизации элементов, в данном случае это кажется более подходящим.

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

var exitSel = join.exit()
  .transition()
  .attr('width', 0)
  .attr('height', 0)
  .remove();

Вот и все! Вы можете закрыть свою databind() функцию и двигаться дальше ...

} // databind()

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

Рисование элементов

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

function draw() {

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

Если вы переместите прямоугольник с x = 0 на x = 1 в определенный момент времени (например, после нажатия кнопки), браузер переместит прямоугольник с 0 на 1 за один тик или раскрашивание кадра (что составляет примерно 16 мс) ). Если вы переместите его с 0 на 10, это произойдет за время, зависящее от продолжительности, которую вы просили, чтобы этот переход произошел, может быть, 1 пиксель за тик, может быть 8 пикселей за тик (подробнее читайте в этом сообщении в блоге).

Но он сообщит пикселю в 0, что прямоугольник исчез, а пикселю в 1, что теперь есть прямоугольник. Canvas этого не делает. Вам нужно указать холсту, что рисовать, и если вы рисуете что-то новое, вам нужно сказать ему, чтобы он удалил предыдущую краску.

Итак, давайте начнем с очистки всего, что может быть на холсте, прежде чем рисовать. Вот как:

context.clearRect(0, 0, width, height); // Clear the canvas.

Простой.

Теперь ваша очередь…

  1. … Получить все элементы, чтобы
  2. перебрать все элементы и
  3. возьмите информацию, которую вы сохранили в функции databind(), чтобы нарисовать элемент:
// Draw each individual custom element with their properties.
var elements = custom.selectAll('custom.rect');
// Grab all elements you bound data to in the databind() function.
elements.each(function(d,i) { // For each virtual/custom element...
  var node = d3.select(this); 
  // This is each individual element in the loop. 
  
  context.fillStyle = node.attr('fillStyle'); 
  // Here you retrieve the colour from the individual in-memory node and set the fillStyle for the canvas paint
  context.fillRect(node.attr('x'), node.attr('y'), node.attr('width'), node.attr('height'));
  // Here you retrieve the position of the node and apply it to the fillRect context function which will fill and paint the square.
}); // Loop through each element.

Вот и все! Вы можете закрыть функцию draw()

} // draw()

Когда я начал с холста после некоторого желания погрузиться в него, эта простота действительно подняла мне настроение.

Однако в браузере пока ничего не произошло. У нас есть инструменты в функциях databind() и draw(), но еще ничего не нарисовано. Как ты делаешь это? Если вы просто хотите нарисовать статический визуал или изображение, вы просто вызываете:

databind(data);
draw();

Это свяжет данные с пользовательскими элементами, которые будут жить в памяти, а затем отрисовать их - один раз!

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

Это снова просто. Вы просто вызываете databind(data), чтобы создать наши пользовательские элементы, прежде чем повторно вызывать draw(), пока выполняется переход. Так что в нашем случае минимум 250 мс. Вы можете использовать setInterval() для этого, но нам действительно следует использовать requestAnimationFrame(), чтобы добиться максимальной производительности (подробнее прочтите это). Есть несколько способов его использования, но, придерживаясь духа D3, я предлагаю использовать d3.timer(), который реализует requestAnimationFrame(), а также прост в использовании. Итак, начнем:

// === First call === //
databind(d3.range(value)); // Build the custom elements in memory.
var t = d3.timer(function(elapsed) {
  draw();
  if (elapsed > 300) t.stop();
}); // Timer running the draw function repeatedly for 300 ms.

d3.timer() вызывает обратный вызов несколько раз, пока elapsed (время, прошедшее с момента создания экземпляра в миллисекундах) не превысит 300, а затем таймер останавливается. За эти 300 миллисекунд он запускает draw() на каждом тике (примерно каждые 16 мс). draw() затем просматривает атрибуты каждого элемента и рисует их соответствующим образом.

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

Обратите внимание, что draw() должен стоять сразу после функции databind(). Вы не можете попросить машину запустить databind(), затем сделать что-то еще в течение секунды, а затем позвонить draw(). Потому что через 1 секунду все переходные состояния, вычисленные вашей databind() функцией, уже перешли. Сделано, запылено и забыто.

Вот и все! Вы связали данные с пользовательскими элементами и нарисовали их на холсте.

Позвольте пользователю обновить количество квадратов

Чтобы дать пользователю возможность повторить этот подвиг с произвольным количеством элементов (нормально, полу-кастомный, максимум 10 000), вы добавляете следующий слушатель и обработчик в свое текстовое поле ввода:

// === Listeners/handlers === //
d3.select('#text-input').on('keydown', function() {
if (d3.event.keyCode === 13) { 
// Only do something if the user hits return (keycode 13).
  if (+this.value < 1 || +this.value > 10000) { 
  // If the user goes lower than 1 or higher than 10k...
     
    d3.select('#text-explain').classed('alert', true); 
    // ... highlight the note about the range and return.
    return;
  } else { 
  // If the user types in a sensible number...
    d3.select('#text-explain').classed('alert', false); 
    // ...remove potential alert colours from the note...
    value = +this.value; // ...set the value...
    databind(d3.range(value)); // ...and bind the data.
    var t = d3.timer(function(elapsed) {
      draw();
  
      if (elapsed > 300) t.stop();
    }); // Timer running the draw function repeatedly for 300 ms. 
  
  } // If user hits return.
}); // Text input listener/handler

Вот и снова наша красочная сетка из квадратов холста, готовая к обновлению и перерисовке:

Интерактивность

Самая большая "проблема" холста по сравнению с SVG или HTML заключается в том, что в DOM нет материальных элементов. Если бы они были, вы могли бы просто зарегистрировать слушателей к элементам и добавить обработчики к слушателям. Например, вы можете вызвать наведение указателя мыши на элемент SVG rect, и всякий раз, когда срабатывает слушатель, вы можете что-то сделать с прямоугольником. Как отображение значений данных, хранящихся с rect, во всплывающей подсказке.

С холстом вам нужно найти другой способ сделать событие услышанным в наших элементах холста. К счастью, есть несколько умных людей, которые придумали косвенный, но логичный путь.

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

Сбор

Есть несколько шагов (хотя все логично). Но вкратце для этого вы создадите два полотна. Один основной холст, который создает наш визуальный элемент, и один скрытый холст (например, мы его не можем видеть), который создает тот же визуальный элемент. Ключевым моментом здесь является то, что все элементы на втором холсте будут находиться в одном и том же положении по отношению к исходному положению холста по сравнению с первым холстом. Таким образом, квадрат 1 начинается с 0,0 на основном холсте, а также на скрытом холсте. Квадрат 2 начинается с 8,0 на основном холсте, а также на скрытом холсте и так далее.

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

Почему? Потому что затем мы присоединяем слушателя движения мыши к основному холсту, чтобы получить поток положений мыши. В каждой позиции мыши мы можем использовать собственный метод холста, чтобы «выбрать» цвет именно в этой позиции. Затем мы просто ищем цвет в нашем ассоциативном массиве, и у нас есть данные! И мы летим ...

Вы могли бы сказать «ну, у моих квадратов уже есть уникальный цвет, я могу их использовать?» И действительно, вы могли их использовать. Однако ваша интерактивность исчезнет из окна, как только вы решите лишить свои квадраты цветов. Поэтому вы должны всегда иметь один холст - скрытый холст - с гарантированным набором уникальных цветов для квадратов.

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

1. Подготовьте скрытый холст.

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

1.1 Создайте скрытый элемент холста и установите его CSS на { display: none; }.

// Rename the main canvas and add a 'mainCanvas' class to it.
var mainCanvas = d3.select('#container')
  .append('canvas')
  .classed('mainCanvas', true)
  .attr('width', width) .attr('height', height);
 
// new ----------------------------------- 
// Add the hidden canvas and give it the 'hiddenCanvas' class.
var hiddenCanvas = d3.select('#container')
  .append('canvas')
  .classed('hiddenCanvas', true) 
  .attr('width', width) 
  .attr('height', height);

На самом деле, в этом примере я не буду скрывать холст, чтобы показать, что происходит. Но для этого просто добавьте .hiddenCanvas { display: none; } в свой CSS, и дело сделано.

1.2 Создайте переменную контекста в функции draw() и передайте функции два аргумента: холст, а также логическое значение с именем 'hidden', определяющее, какой холст мы создаем (hidden = true || false), как в:

function draw(canvas, hidden) {

1.3 Теперь вам нужно адаптировать все функции рисования для включения двух новых draw() аргументов. Так что с этого момента вы не просто звоните draw(), вы звоните либо draw(mainCanvas, false), либо draw(hiddenCanvas, true)

2. Примените уникальные цвета к скрытым элементам и сопоставьте их

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

2.1 Включите функцию для генерации нового уникального цвета каждый раз, когда она вызывается (через Stack Overflow).

// Function to create new colours for the picking.
var nextCol = 1;
function genColor(){ 
  
  var ret = [];
  if(nextCol < 16777215){ 
    
    ret.push(nextCol & 0xff); // R 
    ret.push((nextCol & 0xff00) >> 8); // G 
    ret.push((nextCol & 0xff0000) >> 16); // B
    nextCol += 1; 
  
  }
var col = "rgb(" + ret.join(',') + ")";
return col;
}

genColour() создает строку определения цвета в форме rgb (0,0,0). Каждый раз, когда он вызывается, он увеличивает значение R на единицу. Когда он достигает 255, он увеличивает значение G на 1 и сбрасывает значение R на 0. Как только он достигает r (255,255,0), он увеличивает значение B на 1, сбрасывая R и G на 0 и так далее.

Таким образом, всего у вас может быть 256 * 256 * 256 = 16,777,216 элементов, чтобы сохранить уникальный цвет. Тем не менее, я могу заверить вас, что ваш браузер умрет заранее. Даже с холстом (руководство по webGL).

2.2 Создайте объект карты, который будет отслеживать, какой пользовательский элемент имеет уникальный цвет:

var colourToNode = {}; // Map to track the colour of nodes.

Вы можете добавить функцию genColour() в любом месте сценария, если она находится за пределами области действия функций databind() и draw(). Но обратите внимание, что ваша переменная карты должна быть создана до и за пределами функции databind().

2.3. Добавьте уникальный цвет для каждого настраиваемого элемента, например, .attr('fillStyleHidden') и
2.4. Создайте объект карты во время создания элемента.

Здесь вы будете использовать свой «канон цвета» genColour() в нашей databind() функции при назначении fillStyle нашим элементам. Поскольку у вас также есть доступ к каждой точке данных, когда она привязана к каждому элементу, вы можете объединить цвет и данные на своей colourToNode карте.

join 
  .merge(enterSel) 
  .transition() 
  .attr('width', cellSize) 
  .attr('height', cellSize) 
  .attr('fillStyle', function(d) { 
    return colorScale(d.value); 
  });
  // new -----------------------------------------------------     
  
  .attr('fillStyleHidden', function(d) { 
    if (!d.hiddenCol) {
      d.hiddenCol = genColor(); 
      colourToNode[d.hiddenCol] = d;
    }
    // Here you (1) add a unique colour as property to each element 
    // and(2) map the colour to the node in the colourToNode-map.
    return d.hiddenCol;
});

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

context.fillStyle = hidden ? node.attr('fillStyleHidden') : node.attr('fillStyle');
// The node colour depends on the canvas you draw.

Основной холст, конечно же, выглядит так же:

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

3. Подбирайте цвета мышью.

3.1 Во-первых, просто зарегистрируйте слушателя на главном холсте, слушающего события перемещения мыши.

d3.select('.mainCanvas').on('mousemove', function() {
});

Почему mousemove? Поскольку вы не можете зарегистрировать слушателей с отдельными квадратами, но должны использовать весь холст, вы не сможете работать с событиями наведения курсора или -out, поскольку они будут срабатывать только при входе в холст, а не элементы. Чтобы получить положение мыши на холсте, вы можете сделать mousemove или щелкнуть / mousedown.

d3.select('.mainCanvas').on('mousemove', function() {
  draw(hiddenCanvas, true); // Draw the hidden canvas.
});

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

Цвета на основном холсте варьируются от черного до красного, от rgb (0,0,0) до rgb (255,0,0), а затем кажется, что тот же диапазон от черного до красного повторяется. Однако теперь цвет варьируется от чуть более зеленого черного, а именно от rgb (0,1,0) до rgb (255,1,0):

Если увеличить первую пару сотен квадратов, вот цвета первого, 256-го и 257-го квадрата:

3.3 Поскольку наш скрытый холст структурно является точной копией нашего основного холста, все скрытые элементы холста будут находиться в том же положении, что и элементы на нашем основном холсте. Итак, теперь вы можете использовать позиции мыши по осям x и y, которые вы получаете от слушателя на основном холсте, чтобы установить то же положение на скрытом холсте. Вернувшись в слушатель, вы добавляете:

d3.select('.mainCanvas').on('mousemove', function() {   
  
  // Draw the hidden canvas.
  draw(hiddenCanvas, true);
  // Get mouse positions from the main canvas.
  var mouseX = d3.event.layerX || d3.event.offsetX; 
  var mouseY = d3.event.layerY || d3.event.offsetY; });

Обратите внимание, что здесь мы берем свойства event.layerX и event.layerY, которые возвращают положение мыши, включая прокрутку. Это может сломаться, поэтому используйте offsetX как запасной вариант (или просто используйте offsetX).

3.4 Выбор: Canvas в значительной степени обеспечивает доступ к пиксельным данным, над которыми наводит курсор мыши, с помощью функции getImageData() и ее свойства .data. В полном расцвете это будет выглядеть так:

getImageData(posX, posY, 1, 1).data .

Он вернет массив с четырьмя числами: R, G, B и значение альфа. Поскольку вы старательно создавали карту colourToNode, назначая данные элемента каждому из его скрытых цветов, теперь вы можете получить доступ к данным этого элемента, просто просмотрев цвет на карте!

d3.select('.mainCanvas').on('mousemove', function() {
  // Draw the hidden canvas.
  draw(hiddenCanvas, true);
  // Get mouse positions from the main canvas.
  var mouseX = d3.event.layerX || d3.event.offsetX; 
  var mouseY = d3.event.layerY || d3.event.offsetY;
// new -----------------------------------------------
  // Get the toolbox for the hidden canvas.
  var hiddenCtx = hiddenCanvas.node().getContext('2d');
  // Pick the colour from the mouse position. 
  var col = hiddenCtx.getImageData(mouseX, mouseY, 1, 1).data; 
  // Then stringify the values in a way our map-object can read it.
  var colKey = 'rgb(' + col[0] + ',' + col[1] + ',' + col[2] + ')';
  // Get the data from our map! 
  var nodeData = colourToNode[colKey];
  console.log(nodeData);
});

И действительно, вывод nodeData на консоль возвращает объект каждый раз, когда вы наводите курсор на квадрат:

Данные для каждого узла теперь показывают value, который составляет исходные данные, а также ключ hiddenCol, показывающий цвет этого узла для скрытого холста:

3.5. Наконец - и это формальность - вы добавляете всплывающую подсказку

d3.select('.mainCanvas').on('mousemove', function() {
  // Draw the hidden canvas.
  draw(hiddenCanvas, true);
  // Get mouse positions from the main canvas.
  var mouseX = d3.event.layerX || d3.event.offsetX; 
  var mouseY = d3.event.layerY || d3.event.offsetY;
  // Get the toolbox for the hidden canvas.
  var hiddenCtx = hiddenCanvas.node().getContext('2d');
  // Pick the colour from the mouse position. 
  var col = hiddenCtx.getImageData(mouseX, mouseY, 1, 1).data;
  // Then stringify the values in a way our map-object can read it.
  var colKey = 'rgb(' + col[0] + ',' + col[1] + ',' + col[2] + ')';
  // Get the data from our map! 
  var nodeData = colourToNode[colKey]; 
  
  console.log(nodeData);
  // new -----------------------------------------------
  if (nodeData) { 
  // Show the tooltip only when there is nodeData found by the mouse
    d3.select('#tooltip') 
      .style('opacity', 0.8) 
      .style('top', d3.event.pageY + 5 + 'px') 
      .style('left', d3.event.pageX + 5 + 'px')   
      .html(nodeData.value); 
  } else { 
  // Hide the tooltip when the mouse doesn't find nodeData.
  
    d3.select('#tooltip').style('opacity', 0); 
  
  }
}); // canvas listener/handler

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

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

… И вот еще раз полный код.

Надеюсь, вам понравилось это читать, и, пожалуйста, скажите привет и / или…

Ларс Верспол www.datamake.io @lars_vers https://www.linkedin.com/in/larsverspohl

… всегда благодарен за лайк💚 или подписку, которую он может вернуть.