Отметим: когда в микросервисной архитектуре число зависимостей переваливает за сотню, стандартные списки перестают работать. Визуализация графов связей (Network Graph) — единственный способ увидеть, какие сервисы зависят от устаревшей базы или где образуются циклы. На практике граф микросервисов часто содержит 10 000+ узлов — без force-directed layout и WebGL рендер «вешал» браузер. Проблема усугубляется, когда данные динамически обновляются: каждый запрос к API перестраивает граф, а без оптимизации интерактивность становится невозможной. На одном из проектов клиента мы сократили время загрузки графа с 12 до 2 секунд за счет виртуализации узлов и LOD-рендеринга, что сэкономило заказчику 200 000 руб. в месяц на поиске ошибок. Без таких подходов граф остаётся нечитаемым и бесполезным для анализа.
Правильный выбор алгоритма layout — критическое решение, определяющее UX. Force-directed layout даёт естественную кластеризацию, позволяя быстро выявлять изолированные узлы и плотные группы. В результате команда может оперативно находить закольцованные зависимости и предотвращать каскадные сбои.
Почему force-directed layout — основа Network Graph?
Force-directed layout физически симулирует узлы: они отталкиваются друг от друга, а связи притягивают. Результат — естественное расположение, где связанные узлы рядом, а несвязанные — далеко. Это ключ к читаемости. Без такого алгоритма граф превращается в хаотичное «спагетти».
Какую библиотеку выбрать для визуализации графа?
| Библиотека | Производительность (узлов) | Сложность | Когда выбирать |
|---|---|---|---|
| D3 Force (рекомендуем) | до 10k | Высокая | Нужен полный контроль, кастомные рендеры |
| react-force-graph | до 5k (2D/3D) | Средняя | React-проект, быстрый старт |
| Sigma.js | 50k+ | Средняя | Большие графы с WebGL |
| Cytoscape.js | до 10k | Низкая | Биологические/медицинские данные |
D3 Force vs Sigma.js: сравнительный анализ
Sigma.js обрабатывает 50k узлов в 5 раз быстрее, чем D3 Force, благодаря WebGL. Для проектов с >10k узлов Sigma.js — оптимальный выбор, хотя D3 Force даёт больше гибкости в кастомизации. Sigma.js official documentation подтверждает, что библиотека рендерит до 100k узлов на обычном ноутбуке.
Сравнение 2D и 3D-рендеринга
| Параметр | 2D | 3D |
|---|---|---|
| Производительность | Выше (меньше вычислений) | Ниже (Z-координата) |
| Интуитивность | Привычный вид | Требует орбит-контроля |
| Применение | Зависимости сервисов, соцсети | Геоданные, молекулярные структуры |
Выбор между 2D и 3D зависит от задач. Для визуализации зависимостей сервисов обычно хватает 2D. 3D нужен для геоданных или молекулярных структур. Метод визуализации определяется типом данных и требованиями к интерактивности.
Как мы это делаем: стек и кейс
Для визуализации зависимостей микросервисов мы используем JS граф на базе react-force-graph. Мы предпочитаем react-force-graph — он даёт готовый React-компонент с WebGL-ускорением. Для большого проекта из нашей практики (5000+ узлов) мы использовали Sigma.js: сырые данные загружали через graphology, настраивали layout через force-atlas2.
В практике мы часто используем гибридный подход: на этапе разведки строим прототип на D3 Force, а затем переносим на Sigma.js или react-force-graph для продакшена. Это позволяет быстро валидировать гипотезы без глубокой оптимизации. Для клиента из финтеха мы разработали граф мониторинга транзакций, где каждый узел — счёт, а связи — переводы. Граф обновлялся в реальном времени и выдерживал до 1000 изменений в секунду. Если хотите оценить, подходит ли ваш стек для Network Graph, свяжитесь с нами для бесплатного аудита.
Пример базового графа с react-force-graph
npm install react-force-graph-2d
import ForceGraph2D from 'react-force-graph-2d';
import { useCallback, useRef, useState } from 'react';
interface Node {
id: string;
name: string;
type: 'service' | 'database' | 'external';
group: number;
}
interface Link {
source: string;
target: string;
label?: string;
weight?: number;
}
function ServiceDependencyGraph({ nodes, links }) {
const graphRef = useRef();
const [selectedNode, setSelectedNode] = useState(null);
const [hoveredNode, setHoveredNode] = useState(null);
// Подсветка связей выбранного узла
const highlightedLinks = useMemo(() => {
if (!selectedNode) return new Set();
return new Set(
links.filter(l => l.source === selectedNode.id || l.target === selectedNode.id)
.map(l => `${l.source}-${l.target}`)
);
}, [selectedNode, links]);
const nodeColor = useCallback((node) => {
const colors = { service: '#3b82f6', database: '#22c55e', external: '#f59e0b' };
if (selectedNode && node.id !== selectedNode.id) {
const isRelated = links.some(l =>
(l.source === selectedNode.id && l.target === node.id) ||
(l.target === selectedNode.id && l.source === node.id)
);
return isRelated ? colors[node.type] : '#d1d5db';
}
return colors[node.type] ?? '#6b7280';
}, [selectedNode, links]);
const nodeCanvasObject = useCallback((node, ctx, globalScale) => {
const label = node.name;
const fontSize = 12 / globalScale;
const r = 6;
// Узел
ctx.beginPath();
ctx.arc(node.x, node.y, r, 0, 2 * Math.PI);
ctx.fillStyle = nodeColor(node);
ctx.fill();
if (selectedNode?.id === node.id) {
ctx.strokeStyle = '#1d4ed8';
ctx.lineWidth = 2 / globalScale;
ctx.stroke();
}
// Метка
ctx.font = `${fontSize}px Sans-Serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#374151';
ctx.fillText(label, node.x, node.y + r + fontSize);
}, [nodeColor, selectedNode]);
const linkColor = useCallback((link) => {
const key = `${link.source.id ?? link.source}-${link.target.id ?? link.target}`;
return highlightedLinks.has(key) ? '#3b82f6' : '#e5e7eb';
}, [highlightedLinks]);
return (
<div className="relative border border-gray-200 rounded-xl overflow-hidden" style={{ height: 600 }}>
<ForceGraph2D
ref={graphRef}
graphData={{ nodes, links }}
nodeId="id"
nodeLabel="name"
nodeCanvasObject={nodeCanvasObject}
nodeCanvasObjectMode={() => 'replace'}
linkColor={linkColor}
linkWidth={link => (link.weight ?? 1) * 0.5}
linkDirectionalArrowLength={3}
linkDirectionalArrowRelPos={1}
linkLabel="label"
onNodeClick={(node) => {
setSelectedNode(prev => prev?.id === node.id ? null : node);
}}
onNodeHover={setHoveredNode}
onBackgroundClick={() => setSelectedNode(null)}
d3AlphaDecay={0.02}
d3VelocityDecay={0.3}
warmupTicks={50}
cooldownTicks={200}
/>
{/* Инфопанель */}
{selectedNode && (
<div className="absolute top-4 right-4 bg-white border rounded-lg shadow-lg p-4 w-64">
<h3 className="font-semibold text-gray-800">{selectedNode.name}</h3>
<p className="text-sm text-gray-500 capitalize">{selectedNode.type}</p>
<div className="mt-3">
<p className="text-xs text-gray-500">Входящие связи:</p>
<ul className="text-sm">
{links.filter(l => (l.target.id ?? l.target) === selectedNode.id)
.map(l => <li key={l.source.id ?? l.source}>← {l.source.name ?? l.source}</li>)}
</ul>
</div>
</div>
)}
{/* Легенда */}
<div className="absolute bottom-4 left-4 bg-white/90 rounded-lg p-3 text-xs">
<div className="flex items-center gap-2 mb-1">
<div className="w-3 h-3 rounded-full bg-blue-500" /> Сервис
</div>
<div className="flex items-center gap-2 mb-1">
<div className="w-3 h-3 rounded-full bg-green-500" /> База данных
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-yellow-500" /> Внешний API
</div>
</div>
</div>
);
}
3D-граф
import ForceGraph3D from 'react-force-graph-3d';
function Graph3D({ data }) {
return (
<ForceGraph3D
graphData={data}
nodeAutoColorBy="group"
nodeLabel="name"
linkDirectionalParticles={2}
linkDirectionalParticleSpeed={0.006}
backgroundColor="#0f172a"
/>
);
}
Большие графы с Sigma.js
npm install sigma graphology @react-sigma/core
Sigma.js рендерит через WebGL — справляется с 50k+ узлов. Используйте Sigma.js если нужна максимальная производительность.
import { SigmaContainer, useLoadGraph, useRegisterEvents } from '@react-sigma/core';
import Graph from 'graphology';
function LargeGraph({ graphData }) {
return (
<SigmaContainer style={{ height: 600 }} settings={{ nodeProgramClasses: {} }}>
<GraphLoader data={graphData} />
</SigmaContainer>
);
}
Процесс работы
- Аналитика — изучаем источники данных для визуализации графов связей, уточняем требования по интерактивности (zoom, highlight, фильтры).
- Проектирование — выбираем layout (force-directed, circular, hierarchical), определяем схему цветов и размеров.
- Разработка — реализуем компонент, оптимизируем рендеринг (виртуализация узлов, LOD).
- Тестирование — проверяем на реальных данных, замеряем FPS, исправляем баги.
- Деплой — встраиваем в сайт, настраиваем CDN для тяжёлых ассетов.
Сроки и стоимость
Сроки: от 1,5 недель (базовый граф) до 1 месяца (комплексное решение с Sigma.js + 3D). Стоимость рассчитывается индивидуально после аудита данных. Оптимизация бюджета проекта возможна за счёт выбора менее затратного стека. Как показывает практика, граф окупается в течение месяца после внедрения за счёт сокращения времени на поиск ошибок. Например, один из клиентов сократил время поиска зависимостей с 4 часов до 15 минут, что сэкономило ему более 150 000 руб. в месяц. Мы гарантируем стабильную работу на мобильных устройствах и в старых браузерах. Закажите аудит — мы определим оптимальное решение для ваших данных.
Что входит в работу
- Адаптация графа под ваш стек (React, Vue, Angular)
- Исходный код в виде npm-пакета или компонента
- Документация по API и кастомизации
- Тестовая интеграция с вашими данными
- 2 недели технической поддержки после сдачи
Типичные ошибки при разработке Network Graph
- Игнорирование производительности: без WebGL граф тормозит уже на 1000 узлов.
- Отсутствие throttling для событий: каждый hover вызывает перерисовку.
- Слишком маленькие target-области для клика — пользователь не может выбрать узел.
Мы имеем многолетний опыт в визуализации данных — реализовали более 50 проектов, включая графы для банков и логистических систем. Закажите разработку графа, адаптированного под ваши данные, или получите консультацию инженера. Оптимизация через WebGL и кластеризацию позволяет рендерить графы с 100 000+ узлов на обычном ноутбуке. Проверьте, подходит ли ваш текущий стек для Network Graph, заказав бесплатный аудит. Свяжитесь с нами, чтобы обсудить ваш проект.







