React Force Graph: графы связей и практические примеры
Исследуйте связи материалов, команды и модулей: интерактивная мастерская на react-force-graph-2d, понятная модель данных, настройки раскладки и собственные примеры для React.

Кто связан с задачей? Какой модуль используют сразу несколько частей приложения? Какие материалы стоит читать вместе? Интерактивный граф помогает исследовать такие вопросы: выбрать объект, увидеть его соседей и постепенно разобраться в сети связей.
Разберём react-force-graph-2d на понятных данных и собственных примерах. Ниже работает наша мастерская на настоящем React-компоненте: здесь можно менять раскладку, изучать связи и сохранять результат. Затем посмотрим, как встроить похожий граф в свой проект.
Собственная объясняющая схема: данные задают отношения, движок подбирает положение, а выбор узла помогает прочитать его прямые связи. Расстояние на холсте не доказывает важность или причинность.
Увеличить изображение ⤢Когда граф полезнее обычного списка
Допустим, у команды есть список проектов и список исполнителей. В таблице удобно искать фамилию или считать загрузку. Но если один человек участвует в нескольких проектах, а проекты используют общие модули, отдельные списки скрывают пересечения. Граф показывает объекты как узлы, а отношения между ними — как линии.
Перед визуализацией сформулируйте вопрос. Для базы знаний это может быть «какие статьи связаны с этой темой?». Для приложения — «что зависит от модуля авторизации?». Для команды — «какие задачи объединяют этих участников?». Такой вопрос определяет, какие объекты включать и что именно считать связью. Не соединяйте всё только ради эффектной картинки.
react-force-graph — семейство компонентов. В этой статье используется двумерный вариант с отрисовкой на Canvas. У проекта также есть отдельные пакеты для 3D, VR и AR; их дополнительные возможности требуют своих примеров и условий работы. Наш стенд посвящён именно 2D.
Первое упражнение: выбрать объект и исследовать связи
Начните с небольшого примера. В мастерской доступны «Связи материалов», «Команда», «Зависимости», «Сообщества» и «Цепочка». Сначала изучайте смысл узлов, затем пробуйте настройки. При переходе к собственным данным полезно сохранить исходный JSON отдельным файлом.
Мастерская интерактивных графов
Выберите пример, исследуйте связи и настройте раскладку. Можно загрузить свои данные, сохранить JSON или изображение. Бесплатная площадка без регистрации: данные обрабатываются в этой вкладке.
Открыть площадку отдельно ↗Отдельная вкладка удобна для работы с графом на широком экране.
- Выберите заготовку с материалами и найдите знакомую тему.
- Выберите узел на холсте или в текстовом списке. Прочитайте сведения и посмотрите прямые связи.
- Измените отталкивание и расстояние между связанными узлами по очереди. Сравните удобство чтения.
- Приблизьте нужный участок, затем впишите граф обратно в область просмотра.
- Сохраните JSON, если собираетесь продолжить работу, или PNG, если нужна иллюстрация.
У каждого сюжета своя проверка. В команде найдите роль, взаимодействующую с несколькими направлениями. В зависимостях проследите входящие и исходящие обращения модулей. В сообществах сравните связи внутри группы и между группами. В цепочке найдите обратную связь от отзывов к плану: здесь есть цикл, а последовательные шаги не гарантируют строго прямую линию.
Нажмите «Проверить JSON», прежде чем применять свои данные: мастерская сначала покажет состав будущего графа. Кнопка «Применить данные» выполнит замену после проверки. Рабочая карта хранится в текущей вкладке без автосохранения; перед обновлением или закрытием страницы скачайте JSON.
Двигайте узел, когда нужно разнести плотный участок, но не меняйте данные по впечатлению от расстояний. Два несвязанных узла могут оказаться рядом. Отдельная группа может отдалиться после перезапуска. Проверять отношение следует по линии и списку связей, а положение использовать как вспомогательный ориентир.
Данные графа: nodes, links и устойчивые ID
Минимальный граф содержит два массива. В nodes перечислены объекты, в links — пары их идентификаторов. Для первой проверки возьмём три темы и две связи. Это самостоятельный JSON, который можно вставить в мастерскую.
{
"nodes": [
{ "id": "react", "name": "React", "group": "Интерфейс" },
{ "id": "canvas", "name": "Canvas", "group": "Графика" },
{ "id": "graph", "name": "Граф связей", "group": "Графика" }
],
"links": [
{ "source": "react", "target": "graph", "label": "управляет интерфейсом" },
{ "source": "graph", "target": "canvas", "label": "рисуется на" }
]
}
id связывает данные, name подписывает объект. Название можно поменять, сохранив ID и все отношения. Поэтому лучше использовать стабильные ключи, а не положение в массиве или подпись, которую пользователь редактирует. В нашей мастерской ID должны быть уникальными строками; source и target должны указывать на существующие узлы.
group помогает различать категории. Дополнительное description поясняет объект, label — отношение, value хранит число для связи. Само наличие числа не меняет автоматически бизнес-смысл или длину линии: нужно отдельно решить, как его визуализировать. Например, толщиной можно показывать число совместных задач, но это следует объяснить в легенде.
Импорт проверяйте до замены текущего графа. Посмотрите количество узлов и связей, неизвестные ID и повторяющиеся ключи. Ошибка «связь указывает в никуда» обычно исправляется в исходных данных, а не подбором сил. Не склеивайте два разных объекта из-за одинаковых имён: у двух сотрудников может быть одно имя, но разные ID.
Как отталкивание и связи собирают раскладку
Силовая раскладка подбирает координаты через последовательные шаги. Удобно представить узлы как точки, которые раздвигаются, а связи — как пружины, стремящиеся к выбранной длине. Эта аналогия объясняет движение, но здесь нет расчёта реальных масс, прочности, электрических цепей или поведения конструкции.
У отрицательной силы charge узлы отталкиваются. Значение с большей отрицательной величиной, например переход от −30 к −95, обычно раздвигает рисунок. Положительное значение меняет характер взаимодействия на притяжение. Регулятор влияет на расположение всех узлов, поэтому слишком сильное отталкивание может унести небольшие группы далеко от области интереса.
link.distance задаёт желаемую длину связи. Итоговое расстояние зависит и от других сил: оно не обязано точно совпасть с указанным числом. Начните с умеренных параметров и меняйте один за раз. Если увеличить и отталкивание, и длину одновременно, станет сложнее понять причину изменения.
В компонент GraphView из раздела ниже можно добавить этот эффект перед return. Он использует существующие runtime, width и graphRef. После изменения параметров симуляция запускается повторно.
useEffect(() => {
if (width <= 0) return;
const graph = graphRef.current;
if (!graph) return;
graph.d3Force("charge")?.strength(-95);
graph.d3Force("link")?.distance(75);
graph.d3ReheatSimulation();
}, [runtime, width]);
Симуляция постепенно остывает: величина шага уменьшается, рисунок успокаивается. После изменения данных или сил повторный запуск позволяет ей найти новое расположение. В мастерской для этого есть «Пересчитать»; используйте эту кнопку и после изменения сил на паузе. Пауза полезна для чтения и сохранения картинки. Перезапуск не исправляет неверные связи, а дополнительные итерации не превращают результат в доказательство правильности данных.
Подписи тоже требуют внимания. Даже при разнесённых кружках длинные названия могут пересекаться. Сначала попробуйте показывать подпись выбранного узла и его соседей, увеличить масштаб или сократить видимую часть графа. Одно только увеличение силы редко решает все проблемы чтения.
Подсветка соседей: полезный ответ вместо клубка линий
На обзорном графе трудно проследить каждую линию. Выбор объекта должен отвечать на небольшой вопрос: «что непосредственно связано с ним?». Выделите сам узел, подсветите его соседей, остальные элементы приглушите. В отдельном блоке покажите названия и типы отношений, чтобы человеку не приходилось угадывать смысл по цвету.
Следующий компонент подсвечивает соседей на один шаг. Отношение учитывается в обе стороны для обзора; для задачи «кто зависит от этого модуля?» следует отдельно отбирать входящие или исходящие связи. Кнопки под холстом дают тот же выбор через клавиатуру. data — чистая модель из предыдущего раздела, width — измеренная ширина контейнера.
import { useMemo, useState } from "react";
import ForceGraph2D from "react-force-graph-2d";
const idOf = value =>
value && typeof value === "object" ? value.id : value;
export function NeighborsGraph({ data, width }) {
const [selected, setSelected] = useState(null);
const runtime = useMemo(() => ({
nodes: data.nodes.map(node => ({ ...node })),
links: data.links.map(link => ({ ...link }))
}), [data]);
const neighbors = new Set(selected ? [selected] : []);
data.links.forEach(link => {
if (link.source === selected) neighbors.add(link.target);
if (link.target === selected) neighbors.add(link.source);
});
return <section>
<ForceGraph2D
width={width} height={340} graphData={runtime}
nodeLabel={() => ""}
onNodeClick={node => setSelected(node.id)}
onBackgroundClick={() => setSelected(null)}
nodeColor={node => node.id === selected ? "#f0b75c"
: !selected || neighbors.has(node.id) ? "#53bda7" : "#93a1a5"}
linkWidth={link => selected &&
[idOf(link.source), idOf(link.target)].includes(selected) ? 3 : 1}
/>
<p role="status">{selected
? `Выбран: ${data.nodes.find(node => node.id === selected)?.name}`
: "Выберите объект"}</p>
<div>{data.nodes.map(node => <button key={node.id}
type="button" aria-pressed={node.id === selected}
onClick={() => setSelected(node.id)}>{node.name}</button>)}</div>
<button type="button" onClick={() => setSelected(null)}>Снять выбор</button>
</section>;
}
Сравните подсветку и фильтрацию. Подсветка сохраняет контекст. Фильтрация оставляет только нужные данные и может запустить новую раскладку. Если исключить промежуточный узел, связь через него перестанет быть видна. Поэтому интерфейсу полезно показывать, что скрыто, и иметь понятный сброс.
В этом коротком примере соседство вычисляется по чистым строковым ID, а стили линий работают с изменяемыми данными движка. Вспомогательная idOf учитывает обе формы концов связи. Подсказка компонента отключена; имена выводятся обычным текстом React, без вставки пользовательской строки как HTML.
Подключение в React: размер контейнера, ref и копия модели
В существующем React-проекте установите пакет react-force-graph-2d; примеры статьи сверены с версией 1.29.2. Компонент ниже получает data из JSON и измеряет собственный контейнер через ResizeObserver. Это удобно для колонки с боковой панелью: ширина окна и доступная ширина графа могут различаться.
import { useEffect, useMemo, useRef, useState } from "react";
import ForceGraph2D from "react-force-graph-2d";
export function GraphView({ data }) {
const hostRef = useRef(null);
const graphRef = useRef(undefined);
const [width, setWidth] = useState(0);
const runtime = useMemo(() => ({
nodes: data.nodes.map(node => ({ ...node })),
links: data.links.map(link => ({ ...link }))
}), [data]);
useEffect(() => {
const host = hostRef.current;
if (!host) return;
setWidth(Math.floor(host.clientWidth));
const observer = new ResizeObserver(([entry]) => {
setWidth(Math.max(0, Math.floor(entry.contentRect.width)));
});
observer.observe(host);
return () => observer.disconnect();
}, []);
const names = new Map(data.nodes.map(node => [node.id, node.name]));
return <section ref={hostRef} style={{ width: "100%", minWidth: 0 }}>
{width > 0 && <ForceGraph2D ref={graphRef}
width={width} height={340} graphData={runtime}
nodeAutoColorBy="group" nodeLabel={() => ""} />}
<button type="button" onClick={() =>
graphRef.current?.zoomToFit(350, 24)}>Вписать граф</button>
<p>Связи в данных:</p>
<ul>{data.links.map((link, index) => <li key={index}>
{names.get(link.source)} → {names.get(link.target)}
{link.label ? `: ${link.label}` : ""}
</li>)}</ul>
</section>;
}
useMemo удерживает рабочую копию между обычными обновлениями интерфейса. В родителе передавайте новую модель при фактической замене данных, а не создавайте объект заново на каждом наведении. Это уменьшает ненужные сбросы раскладки. Наша плоская схема копируется по объектам; вложенные изменяемые структуры требуют отдельного решения.
ref нужен для команд вроде «вписать граф» и «перезапустить». Вызывайте их из обработчиков или эффектов, когда компонент уже создан. Изменение ref.current само по себе не обновляет React-интерфейс. Выбранный ID, текст поиска и настройки формы удобно хранить в состоянии, а движение узлов оставлять движку.
Проверяйте не только первый запуск. Сузьте контейнер, сверните боковую панель, замените модель, затем верните прежнюю ширину. Холст должен получить новые размеры, а наблюдатель — отключаться при удалении компонента. В разработке дополнительные вызовы эффектов React помогают обнаружить неправильную очистку; не убирайте её ради исчезновения симптома.
Читаемость на телефоне и доступное управление
Уменьшить холст недостаточно. На телефоне сложно попасть в маленький узел, а длинная подпись закрывает соседей. Показывайте детали выбранного объекта под графом, оставляйте достаточно места кнопкам и дайте поиск по названию. Выбор касанием должен работать без предварительного наведения.
В нашей мастерской на телефоне управление холстом включается отдельной кнопкой «Включить управление»: это помогает сначала спокойно прокручивать статью. Список узлов работает и без управления Canvas. Кнопка «Выключить управление» возвращает обычный режим прокрутки страницы.
Граф на Canvas — изображение из пикселей, а не набор подписанных HTML-кнопок. Для клавиатуры и вспомогательных технологий добавьте отдельный список объектов и связей. Полезный список позволяет выбрать узел, прочитать соседей и снять выбор. Если сотни записей перегружают страницу, показывайте результаты поиска и связи выбранного объекта с понятным количеством.
Продумайте движение. Пользователь должен иметь возможность остановить рисунок и спокойно читать детали. Учтите системное предпочтение уменьшенной анимации, сохраняйте смысл цветов в текстовой легенде. Пауза и спокойные переходы требуют проверки в реальном интерфейсе; одно свойство компонента не делает всю страницу доступной.
Для плотных сетей начинайте с небольшого подграфа. Отключите постоянные частицы, показывайте меньше подписей и измеряйте работу на устройстве, для которого делаете интерфейс. Скорость зависит от числа узлов, связей, оформления и оборудования; универсальной гарантии для любого мобильного устройства здесь нет.
Сохранение: чистый JSON и изображение PNG
JSON хранит смысл графа: объекты, ID, отношения и пояснения. PNG сохраняет видимую картинку для статьи, презентации или обсуждения. По PNG нельзя восстановить исходные ID и редактируемую модель. При передаче коллеге лучше приложить данные и, если требуется, отдельную иллюстрацию.
Предпочтительный экспорт берётся из исходной модели, которую симуляция не меняла. Если доступна только рабочая копия, восстановите концы связей и перечислите разрешённые поля явно. Следующий пример сохраняет поля нашей схемы и отбрасывает координаты, скорости и внутренние ссылки движка.
export function cleanGraph(runtime) {
const idOf = value =>
value && typeof value === "object" ? value.id : value;
return {
nodes: runtime.nodes.map(node => ({
id: node.id, name: node.name, group: node.group,
...(typeof node.description === "string"
? { description: node.description } : {})
})),
links: runtime.links.map(link => ({
source: idOf(link.source), target: idOf(link.target),
...(typeof link.label === "string" ? { label: link.label } : {}),
...(Number.isFinite(link.value) ? { value: link.value } : {})
}))
};
}
Это преобразование уже проверенной модели, а не проверка произвольного файла. После сохранения откройте JSON заново и сравните количество объектов, направления и подписи. Если проект должен сохранять ручную расстановку, координаты стоит добавить отдельной версионированной схемой. Не подмешивайте случайно накопленные поля симуляции в основной формат.
В нашей мастерской PNG сохраняет последний показанный кадр холста. Используется onRenderFramePost, затем стандартный canvas.toBlob; отдельного метода exportPNG у рассматриваемого компонента нет. Настройте масштаб, впишите нужные узлы и дождитесь готового вида. Фон мастерской фиксирован светлым. Затем можно прокрутить страницу к кнопке «Скачать PNG»: сохранится последний показанный кадр. Если изменили данные, когда карта была вне экрана, сначала вернитесь к ней, чтобы появился новый кадр. Панели страницы и текстовый список в PNG не входят.
Перед импортом незнакомого JSON проверьте размер и структуру. Пользовательские подписи выводите текстом. Не превращайте описание узла в HTML-подсказку без обработки: API допускает HTML-содержимое, а данные могут прийти из внешнего источника. Наличие файлового импорта само по себе не делает любой текст безопасным.
Границы подхода и что проверить в своём проекте
Граф хорошо показывает пересечения и помогает исследовать связи. Для точных чисел, сравнения сумм или длинных описаний рядом часто нужна таблица. Для строгой организационной структуры может подойти иерархическая схема. Свободная силовая раскладка не обещает фиксированного порядка уровней и одинакового расположения после каждой замены данных.
Наша мастерская ограничивает импорт 200 узлами и 500 связями, чтобы учебные сценарии оставались управляемыми. Это ограничения данного инструмента, а не заявленный предел библиотеки и не результат теста производительности. На реальных данных проверяйте скорость, читаемость и полезность выбора независимо друг от друга.
Скачиваемый пример проекта позволяет изучить нашу оболочку и запустить её локально. В архиве закреплены зависимости и инструкция: установить их через npm ci, затем запустить npm run dev. Для своего сайта отдельно продумайте источник данных, сохранение изменений, права доступа и понятный сценарий возврата к исходному графу.
| Наблюдение | Что проверить первым |
|---|---|
| Узел пропал после импорта | Уникальность ID и существование концов каждой связи. |
| Подписи и линии сливаются | Подграф, выбор соседей и масштаб; затем параметры сил. |
| Граф прыгает при выборе узла | Не создаётся ли новая graphData при каждом обновлении. |
| JSON сохранился с объектами вместо ID | Экспорт из чистой модели или восстановление source/target. |
| На телефоне сложно разобраться | Поиск, текстовый список, детали выбора и управление касанием. |
Компоненты проекта react-force-graph созданы Vasco Asturiano и распространяются по MIT. Для уточнения механики полезны первичные руководства D3: сила связей, отталкивание и притяжение, симуляция. Учебные данные, задания, примеры кода и оформление мастерской подготовлены для ae-nekrasov.ru.


