Что такое 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, узлы, связанные с новыми, находятся близко друг к другу под действием тягового усилия. Кроме того, в процессе приближения возникают столкновения между другими узлами. Когда на принудительно-ориентированном графе есть узлы, эти вновь добавленные узлы заставят весь граф сталкиваться под действием столкновения и сцепления, пока каждый узел не найдет свое собственное место. Это означает, что движение останавливается только тогда, когда и столкновение, и тяга соответствуют требованиям. Это похоже на Большой взрыв?
В описанном выше процессе есть две проблемы:
- Добавление нового узла приведет к постоянному перемещению всего графа.
- Требуется относительно много времени, чтобы быть стабильным
Однако именно так разработан 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;
});
Если исходного узла нет, новый узел появляется в центре графика. Это окажет меньшее влияние на существующие узлы, поэтому стоит подумать.
Оптимизация макета отображения нескольких краев между двумя узлами
Когда между двумя узлами имеется несколько ребер, возникают следующие проблемы:
- Прямые линии по умолчанию будут перезаписывать друг друга. Таким образом, кривая - лучший выбор в таких обстоятельствах.
- Как определить кривизну кривой, чтобы избежать перезаписи?
- Если у вас несколько кривых, как убедиться, что средняя полукруглая дуга не находится на определенном полукруге?
Ниже описано, как мы решаем указанные выше проблемы:
- Сначала подсчитайте границы между любыми двумя узлами и сгруппируйте их на карту. Ключ на карте основан на имени двух задействованных узлов.
- Затем разделите края одной карты на две группы по их направлению. Существует множество способов определить направление ребра, и здесь мы применяем метод для сравнения 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 г.