Что такое D3.js

D3.js - это библиотека JavaScript с открытым исходным кодом для создания динамических интерактивных визуализаций данных в веб-браузерах с использованием SVG, HTML и CSS.

Помимо D3, существуют другие популярные и мощные библиотеки, такие как ECharts и Chart.js. Однако они сильно инкапсулированы, что оставляет слишком мало места для настройки.

Напротив, D3 легко настраивается благодаря поддержке обработки событий для элементов SVG. Он может связывать произвольные данные с объектной моделью документа (DOM) или напрямую управлять W3C DOM API в DOM.

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

Направленный график D3-Force

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

Из-за отталкивания и тяги частицы в D3-силе непрерывно переходят от начального состояния случайного беспорядка к сбалансированному и упорядоченному расположению. Численный интегратор Velocity Verlet контролирует порядок частиц и ребер. Граф, созданный с помощью d3-force, содержит только узлы и ребра, и существует только небольшая коллекция образцов графа для справки, потому что большинство графов настраиваются.

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

Построение ориентированного графа D3-Force

В Nebula Graph Studio мы используем направленный граф D3-force для анализа взаимосвязей данных, потому что узлы и ребра интуитивно показывают связи данных, и это позволяет исследовать граф с помощью языка запросов графа. Более того, данные графа могут быть обновлены путем синхронизации операций в модели DOM с базой данных, что заслуживает отдельной статьи.

Давайте построим простой направленный граф D3-force, чтобы проиллюстрировать, как D3.js отображает подключения к данным, и поделимся некоторыми идеями по оптимизации макета на основе этого примера.

this.force = d3
        .forceSimulation()
        // Assign coordinates to nodes
        .nodes(data.vertexes)
        // Connect edges
        .force('link', linkForce)
        // The instance center
        .force('center', d3.forceCenter(width / 2, height / 2))
        // Gravitation
        .force('charge', d3.forceManyBody().strength(-20))
        // Collide force to prevent nodes overlap
        .force('collide',d3.forceCollide().radius(60).iterations(2));

Приведенный выше код создает график, как показано ниже:

Оптимизация макета исследования графика

На приведенном выше графике показаны только односкачковые отношения от начальных узлов. Как насчет двух- или трехходовых отношений? Ответ - API enter () D3.js.

API enter () D3.js независимо обрабатывает новые узлы. Когда новые узлы запрашиваются и помещаются в массив узлов, API отображает их в соответствии с координатами, назначенными экземпляром D3-force, без изменения информации (включая координаты x, y) существующих узлов.

С точки зрения API это вполне понятно. Но новые добавленные узлы не могут быть обработаны простым нажатием на существующий экземпляр D3-force, потому что модуль d3.forceSimulation () назначает координаты местоположения случайным образом.

Координаты, назначенные d3.forceSimulation (). Node (), случайны, как и местоположения исследуемых узлов. Вместе с параметром collied и link, узлы, связанные с новыми, находятся близко друг к другу под действием тягового усилия. Кроме того, в процессе приближения возникают столкновения между другими узлами. Когда на принудительно-ориентированном графе есть узлы, эти вновь добавленные узлы заставят весь граф сталкиваться под действием столкновения и сцепления, пока каждый узел не найдет свое собственное место. Это означает, что движение останавливается только тогда, когда и столкновение, и тяга соответствуют требованиям. Это похоже на Большой взрыв?

В описанном выше процессе есть две проблемы:

  1. Добавление нового узла приведет к постоянному перемещению всего графа.
  2. Требуется относительно много времени, чтобы быть стабильным

Однако именно так разработан API enter ().

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

Другое распространенное решение - уменьшить столкновение и увеличить тягу экземпляра D3-force. Таким образом, узлы быстрее находят состояние баланса, так что весь граф является стабильным. Это лучшее решение, но недостатком является то, что из-за него соединение между узлами заметно различается по длине, а размер графа огромен. Так что это не идеальное решение для случаев с огромным объемом данных.

Мы придумали новое решение.

Идея состоит в том, чтобы убедиться, что новый узел находится вокруг исходного узла. Вместо выделения D3.forceSimulation (). Node () установите координаты нового узла такими же, как у исходного узла. Столкновение узлов экземпляра D3-force гарантирует, что появление новых узлов не будет перезаписано и в конечном итоге появится вокруг исходного узла.

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

# Set the new nodes coordinates as the source node center or the entire graph center
addVertexes.map(d => {
  d.x = _.meanBy(selectVertexes, 'x') || svg.style('width') / 2;
  d.y = _.meanBy(selectVertexes, 'y') || svg.style('heigth') / 2;
});

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

Оптимизация макета отображения нескольких краев между двумя узлами

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

  1. Прямые линии по умолчанию будут перезаписывать друг друга. Таким образом, кривая - лучший выбор в таких обстоятельствах.
  2. Как определить кривизну кривой, чтобы избежать перезаписи?
  3. Если у вас несколько кривых, как убедиться, что средняя полукруглая дуга не находится на определенном полукруге?

Ниже описано, как мы решаем указанные выше проблемы:

  1. Сначала подсчитайте границы между любыми двумя узлами и сгруппируйте их на карту. Ключ на карте основан на имени двух задействованных узлов.
  2. Затем разделите края одной карты на две группы по их направлению. Существует множество способов определить направление ребра, и здесь мы применяем метод для сравнения ASCII-кода source.name и target.name узла. Linknum устанавливается для каждого ребра с положительным направлением. Точно так же -linknum устанавливается для каждого края с отрицательным направлением. Установленное нами значение linknum предназначено для определения кривизны и направления изгиба дуги.

Обратитесь к следующему коду для лучшего понимания:

const linkGroup = {};
  // Set the edges between two nodes as the same key based on their name property. 
  // Then add the key to the linkGroup, making all edges a group
  edges.forEach((link: any) => {
    const key =
      link.source.name < link.target.name
        ? link.source.name + ':' + link.target.name
        : link.target.name + ':' + link.source.name;
    if (!linkGroup.hasOwnProperty(key)) {
      linkGroup[key] = [];
    }
    linkGroup[key].push(link);
  });
  // Traverse each group to call setLinkNumbers to allocate linknum
  edges.forEach((link: any) => {
    const key = setLinkName(link);
    link.size = linkGroup[key].length;
    const group = linkGroup[key];
    if (group[group.length - 1] === link) {
      setLinkNumbers(group);
    }
  });
// Divide the edges into linkA and linkB based on their directions.
// Then allocate two kinds of linknum to control the upper and lower elliptical arc.

export function setLinkNumbers(group) {
  const len = group.length;
  const linksA: any = [];
  const linksB: any = [];
  for (let i = 0; i < len; i++) {
    const link = group[i];
    if (link.source.name < link.target.name) {
      linksA.push(link);
    } else {
      linksB.push(link);
    }
  }
  let startLinkANumber = 1;
  linksA.forEach(linkA=> {
    linkA.linknum = startLinkANumber++;
  }
  let startLinkBNumber = -1;
  linksB.forEach(linkB=> {
    linkB.linknum = startLinkBNumber--;
  }
}

После присвоения linknum каждому краю знак linknum оценивается в функции события тика, которая отслеживает края. Нам нужно только судить и задавать кривизну и направление пути.

Вот как это выглядит:

Вывод

Вот как мы оптимизировали принудительно-ориентированный граф D3.js. Если вы строите сложные взаимоотношения, существует множество проблем, которые следует учитывать и оптимизировать.

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

Попробуйте Nebula Graph с Nebula Graph Studio, чтобы испытать визуализацию D3.js. Оставьте нам комментарий ниже или зайдите на наш форум, если возникнут вопросы.

Привет, я Нико, интерфейсный инженер Nebula Graph. Меня интересует визуализация данных, и я хотел бы поделиться своим опытом в этом отношении. Надеюсь, мой пост вам поможет. Пожалуйста, дайте мне знать, если у вас есть какие-либо идеи по этому поводу. Спасибо!

Вам также может понравиться

Нравится то, что мы делаем? Пометьте нас на GitHub. Https://github.com/vesoft-inc/nebula

Первоначально опубликовано на https://nebula-graph.io 29 апреля 2020 г.