Как я построил свое собственное дерево навыков для своего сайта-портфолио

Мотивация

У меня есть веб-сайт портфолио, который я использую в основном как динамическое резюме, чтобы продемонстрировать свои технические способности и достижения. Я построил его с помощью Gridsome, который написан на Vue, и Buefy в качестве основной библиотеки компонентов просто потому, что я просто хотел опробовать Vue и любые существующие фреймворки. Один из разделов на странице - это раздел Ключевые навыки, где я перечисляю технические навыки, которые, как мне кажется, мне наиболее удобны и на которые большинство компаний будут обращать внимание.

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

В этом примере дерева навыков «пограничного стиля» используется библиотека с именем beautiful-skill-tree и он создан для React. Поскольку это несовместимо с моим сайтом Vue, мне нужна была альтернатива, но, к сожалению, хороших альтернатив там нет.

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

Технология

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



У Observable есть довольно подробное и хорошо написанное руководство / руководство по использованию D3, но мне было трудно настроить его и работать с Vue, в основном из-за системы реактивности Vue.



Затем я начал искать руководства по Vue + D3 и, к счастью, нашел этот очень полезный учебник. На этом веб-сайте объясняется, как заставить D3 хорошо взаимодействовать с системой реактивности, и на самом деле это довольно просто, используя концепцию watchers в Vue.

Поигравшись с D3, я узнал, что D3 на самом деле не предоставляет вам часть визуализации, то есть гистограммы, древовидные диаграммы и т. Д. Большая часть части визуализации создается вручную с svg компонентами ... слабый у. 😅 Итак, пришло время выучить и svg! После долгой возни я наконец закончил «строительные леса»:

Код

Я немного объясню, как работает код:

// skills data
const skills = [
{ id: "tech", label: 'Tech Tree' },
{ parentId: "tech", id: "languages", label: 'Languages',},
{
  parentId: "languages",
  id: "python",
  label: 'Python',
  iconHref:
  require('../assets/icons/python.png'),
  skillLevel: 90,
  descriptions: ['Mainly worked with webserver backend frameworks with it.', 'Used in some simple machine learning applications and projects such as Gestice League and Project 21.']
},
...]

Отображаемые данные должны иметь определенную структуру, в частности структуру, основанную на родительско-дочерних. Каждая точка данных (навык) является либо корнем, либо дочерним элементом другой точки данных. Чтобы построить это, у меня есть список объектов, у которых есть свои собственные id и, возможно, parentId, чтобы указать, что это родительский объект, если он у него есть.

Организация данных

...
let hierarchy = d3.stratify()(this.skills);
const treeLayout = d3.tree().size([
    1280,
    500,
  ]);
hierarchy = treeLayout(hierarchy);

Затем я использую мощные инструменты обработки данных D3. Во-первых, использование d3.stratify() помогает обрабатывать точки данных и формализовать структуру ссылок иерархии. В частности, он помогает идентифицировать корень и его дочерние элементы, а также вычислять метаданные и структуры, такие как глубина и высота результирующего графа. Он также выдаст ошибку, если обнаружит несколько корней или другие структурные проблемы. В результате получается то, что D3 называет объектом «иерархии». На этом этапе результирующая иерархия не имеет определенного макета, а это означает, что точки данных еще не «расположены» относительно друг друга в пространстве.

Затем я использую d3.tree() для преобразования иерархии в структуру дерева сверху вниз. Это помогает назначить координаты точкам данных, которые мы затем можем использовать для визуального построения всего дерева.

Рисуем дерево

Во-первых, в шаблоне у меня есть простая структура, такая как:

<template>
  <div>
    <svg />
  </div>
</template>

и в установленном методе компонента Vue:

...
mounted() {
  this.svg = d3
  .select("svg")
  .attr("width", "100%")
  .attr("height", 500)
  .attr("cursor", "grab")
  .attr("position", "relative");
  this.skillTree = this.svg.append("g");
  this.drawTree();
},

Здесь мы видим первый пример использования библиотеки D3 для обработки манипуляций с DOM. Используя d3.select("svg"), мы в основном пытаемся найти первый <svg /> объект в документе, а затем манипулировать им.

Первое, что мы делаем с <svg />, - это добавляем к нему элемент <g />, который является просто собственным элементом svg, который действует как контейнер для других компонентов. Выполнение this.svg.append("g") также вернет результирующий элемент <g />, который я затем назначу как объект skillTree, который будет построен позже.

Затем мы рисуем дерево в методе drawTree(), и вот как это работает:

// hierarchy object previously made
const links = hierarchy.links();
const nodes = hierarchy.descendants();
...
this.skillTree
  .selectAll("rect")
  .data(nodes)
  .enter()
  .append("rect")
  .attr("width", (d) => rectWidth)
  .attr("x", (d) => d.x)
  .attr("y", (d) => d.y)
... //etc.
...

Во-первых, мы можем получить доступ к объектам ссылки и узла через hierarchy.links() и hierarchy.descendants() соответственно.

Далее мы видим одну из самых мощных функций библиотеки D3. В D3 есть особая концепция ввода-обновления-выхода данных, которая очень хорошо объяснена здесь. Я еще не полностью с ним знаком, но в основном, делая .data(nodes), вы сообщаете набору данных, что вы ссылаетесь на следующий, а выполняя .enter() после, список элементов, которые необходимо обновить, будет быть возвращенным. Таким образом, для первоначального вызова .enter() в основном вернет весь список узлов, поскольку ранее в иерархии не было данных.

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

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

Делаем дерево навыков более интерактивным

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



Создание эффекта панорамирования для SVG | CSS-уловки
Ранее в этом месяце в Slack «Анимация на работе
мы обсуждали, как разрешить пользователям перемещаться внутри… css-tricks.com »



Вся идея состоит в том, чтобы использовать атрибут viewBox компонента <svg />, который в основном контролирует отображаемую область svg. Изменяя viewBox параметры, мы можем имитировать движущуюся «камеру» или что-то подобное в игре, сдвигая область всего дерева для отображения.

Следующие шаги - вычислить, на сколько нужно изменить viewBox. Для этого нам нужно выполнить последовательность шагов:
1. Записать исходные / начальные координаты курсора, когда пользователь нажимает на svg.
2. Отслеживать и изменять окно просмотра по смещению новые координаты курсора и исходные координаты курсора
3. Сохраните последнее смещение окна просмотра и учитывайте его при последующих расчетах начала координат.

Когда все будет сделано правильно, у нас в основном будет развернутое дерево навыков!

Позвольте мне объяснить его код:

...
this.svg.on("pointerdown", this.onPointerDown);
this.svg.on("pointerup", this.onPointerUp);
this.svg.on("pointerleave", this.onPointerUp);
this.svg.on("pointermove", this.onPointerMove);
...

Мы хотим отслеживать различные действия курсора / касания на всем <svg /> компоненте. Для этого нам просто нужно назначить разные обратные вызовы различным событиям. Обратите внимание, как мы относимся к указателю, покидающему <svg />, аналогично тому, как курсор «не нажат».

onPointerDown: function () {
  this.isClicked = true;
  const x = d3.event.pageX;
  const y = d3.event.pageY;
  this.viewBoxPointer= {
    x: x + this.viewBoxOffset.x, 
    y: y + this.viewBoxOffset.y,
  };
},
onPointerUp: function () {
  this.isClicked = false;
  const x = d3.event.pageX;
  const y = d3.event.pageY;
},
onPointerMove: function () {
  if (this.isClicked) {
    const x = d3.event.pageX;
    const y = d3.event.pageY;
    this.viewBoxOffset = {
      x: this.viewBoxPointer.x - x,
      y: this.viewBoxPointer.y - y,
    };
    this.svg.attr("viewBox", `${x} ${y} ${width} ${height}`);
}
},
...

Далее я кратко объясню, как работает панорамирование. Во-первых, в методе onPointerDown мы хотим отслеживать, когда пользователь все еще «нажимает» на <svg />, и мы делаем это с помощью флага this.isClicked. Затем мы также отслеживаем текущую позицию курсора в this.viewBoxPointer. Пока не обращайте внимания на viewBoxOffset.

После этого мы наблюдаем, когда пользователь начинает перемещать курсор вокруг <svg />. Это делается методом onPointerMove. Сначала мы проверяем, действительно ли пользователь все еще нажимает <svg />, что означает, что пользователь перетаскивает компонент. Затем мы отслеживаем новые координаты курсора, а затем вычисляем смещение относительно исходной позиции, которая сохраняется в this.viewBoxPointer. Мы также сохраняем текущее смещение в viewBoxOffset. Это будет имитировать "перемещение" дерева навыков.

Наконец, когда пользователь прекращает перетаскивание и перестает щелкать мышью, мы просто снимаем флаг this.isClicked.

Когда пользователь снова хочет панорамировать дерево навыков, мы затем учитываем смещение при вычислении viewBoxPointer в методе onPointerDown, так что мы начинаем с правильной последней известной позиции.

Заключительные детали

Последняя часть всего дерева навыков - это отображение панели «описания» сбоку от нее, когда пользователь нажимает на один из узлов. Для этого нужно поместить компонент карты Buefy (через <div class="card" ...) и обернуть его Buefy <b-collapse>, чтобы показать / скрыть его при щелчке по узлам. Данные выбранного узла передаются, а затем детали отображаются красиво, с красивой маленькой «полосой опыта», построенной с использованием <b-progress> Buefy.

Итак, после всего этого мы получаем следующее:

Заключение

Я очень доволен конечным результатом, и я выполнил то, что намеревался достичь, научившись использовать D3 и svg. Я хочу изучить еще кое-что, а именно, как лучше поддерживать поддержку мобильного сенсорного ввода для svg и, возможно, добавить для него функцию масштабирования. Может быть, после того, как я немного поправлю код, я смогу опубликовать его как пакет для использования другими🤔. Но в целом это был забавный маленький проект! Вы можете увидеть это на моем основном сайте, если хотите: 😃 https://zexuan.netlify.app/