Как мы реализовали визуализацию связей в ER-дизайнере на Angular

от автора

Людей можно условно разделить на тех, кто лучше воспринимает информацию на слух, и тех, кто эффективнее усваивает ее визуально. В мире разработки визуализация — это не просто удобство, а необходимый инструмент. Это особенно актуально, когда речь идёт о работе со сложными моделями данных.

Привет! Меня зовут Илья Чубко, я технический архитектор в К2Тех. В этой статье расскажу, как мы подошли к разработке визуального ER-дизайнера на Angular — от первых набросков до архитектурных решений, с акцентом на визуализацию связей между сущностями.

🎯 Зачем это нужно?

Когда создаётся модель данных с множеством сущностей и связей, важно не просто отобразить это в виде JSON или таблицы. Нужно создать наглядное визуальное представление — с линиями, подсветкой, возможностью удобно перемещать элементы, выделять связи и видеть контекст.

Это особенно критично, если:

  • у вас есть несколько десятков сущностей;

  • между ними — множество связей;

  • требуется отладка, анализ или документирование.

Визуализация — это не просто графика, это инструмент понимания архитектуры.

🧱 Архитектура ER-дизайнера

Проект поделён на три основных слоя:

1. Модель данных

Используем простые, но расширяемые интерфейсы:

export interface NodeItem {   id: Guid;   name: string;   position: { x: number; y: number };   columns: NodeColumn[]; }  export interface Edge {   id: Guid;   sourceId: Guid;   targetId: Guid; } 

Каждая сущность (NodeItem) имеет координаты, список колонок, уникальный идентификатор и имя.
Связи (Edge) описывают, откуда и куда направлена линия — это основа для рендера кривых.

2. Интерфейс пользователя

Технологии: Angular, TailwindCSS, NgRx Signals
Основные компоненты:

  • Холст — зона рисования (svg или canvas, мы выбрали SVG для гибкости);

  • Окно свойств — редактирование полей, названий, связей;

  • Панель инструментов — создание сущностей, связей, сохранение схемы;

  • Модальные окна — подтверждение удаления, выбор типа связи и т.п.;

  • Меню действий — context-menu по клику: «Добавить поле», «Удалить» и т.д.

3. Взаимодействие

Мы продумали UX для ER-дизайнера так, чтобы работа с диаграммами была максимально естественной:

  • Drag&Drop сущностей — перемещение объектов на canvas;

  • Drag&Drop полей — перетаскивание полей в пределах одного объект;

  • Выделение — выделение объектов и полей;

  • Панорамирование холста — перетаскивание рабочей области с помощью мыши.

🧩 Отрисовка связей: линии, дуги, кривые

На этапе визуализации связей между объектами мы начали экспериментировать с подходами к отрисовке:

  • прямые линии — слишком жёстко и нечитабельно при пересечениях;

  • ломаные линии — уже лучше, но визуально перегружают холст;

  • дуги — красиво, но иногда затруднительно понять направление связи.

💡 Кривые Безье — наш выбор

Почему:

  • гладкие изгибы;

  • адаптивность к положению сущностей;

  • простота в реализации через SVG path.

Пример SVG Path:

M 660 450 S 760 450 880 350 S 1100 250, 1100 250
Кривая безье с типом Smooth

Кривая безье с типом Smooth

🔗 Удобный визуальный редактор SVG Path — svg-path-visualizer

Связь рисуется как кривая от первого объекта ко второму объекту через автоматически рассчитанные контрольные точки.

Пример svg path
<svg class="absolute w-full h-full pointer-events-none">     <defs>         <marker             id="arrow"             viewBox="0 0 10 10"             refX="10"             refY="5"             markerWidth="6"             markerHeight="6"             orient="auto-start-reverse"         >             <path d="M 0 0 L 10 5 L 0 10 z" fill="green"></path>         </marker>     </defs>      <path         [attr.d]="path()"         [attr.stroke-width]="2.0"         [attr.stroke]="lineColor"         fill="none"         marker-end="url(#arrow)"     ></path>      <circle         [attr.cx]="sourceX()"         [attr.cy]="sourceY()"         r="5"         fill="green"     ></circle> </svg>

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

😤 Сложности

  • Изгибы «уходят» при перекрытии сущностей — решили, смещая контрольные точки за элемент;

  • SVG в Angular — пришлось вынести генерацию path в отдельный сервис;

  • Пересечения линий — пока не решено полностью, думаем над auto-routing.

Для четырех кейсов, когда элементы располагаются друг под другом, необходимо изменить контрольную точку для предотвращения сильного изгиба. Таким образом SVG Path будет следующим:

M 500 450 S 400 450 400 320.5 S 618 191, 618 191
Смещение контрольных точек

Смещение контрольных точек

Наглядный пример представлен ниже:

Перерисовка линий при перемещении объектов

Перерисовка линий при перемещении объектов

💾 Хранение схемы

Для хранения данных можно использовать систему хранения состояния NgRx Signals. Создаем файл common.store.ts.

common.store.ts
import { signalStore, withMethods } from '@ngrx/signals'; import { withNodes } from './nodes.feature'; import { withEdges } from './edges.feature';  export const CommonStore = signalStore( { providedIn: 'root' }, withNodes(), withEdges(), withMethods((store) =&gt; ({})), ); 

Для хранения элементов и линий создадим файлы nodes.feature.ts и edges.feature.ts соответственно

nodes.feature.ts
import { patchState, signalStoreFeature, type, withMethods, } from '@ngrx/signals'; import { SelectEntityId, setAllEntities, updateEntity, withEntities, } from '@ngrx/signals/entities'; import { NodeItem } from '../model/node-item.interface'; import { Guid } from 'guid-typescript';  const selectId: SelectEntityId = (item) => item.id.toString();  export function withNodes() { return signalStoreFeature( withEntities({ entity: type(), collection: 'nodes', }), withMethods((store) => ({ setNodes(nodes: NodeItem[]) { patchState( store, setAllEntities(nodes, { collection: 'nodes', selectId }), ); },  getNodeById(id: Guid) { return store.nodesEntities().find((node) => node.id === id); },  updateNodePosition( id: Guid, position: { x: number; y: number }, ): void { patchState( store, updateEntity( { id: id.toString(), changes: () => ({ position: { ...position } }), }, { collection: 'nodes', selectId }, ), ); }, })), ); } 
edges.feature.ts
import { patchState, signalStoreFeature, type, withMethods, } from '@ngrx/signals'; import { SelectEntityId, setAllEntities, withEntities, } from '@ngrx/signals/entities'; import { Guid } from 'guid-typescript';  export interface Edge { id: Guid; sourceId: Guid; targetId: Guid; }  const selectId: SelectEntityId = (item) => item.id.toString();  export function withEdges() { return signalStoreFeature( withEntities({ entity: type(), collection: 'edges', }), withMethods((store) => ({ setEdges(edges: Edge[]) { patchState( store, setAllEntities(edges, { collection: 'edges', selectId }) ); } })), ); } 

✅ Выводы

Angular отлично подходит для визуальных редакторов — особенно в связке с SVG и signals. Визуальное представление моделей — мощный инструмент. И даже если вы делаете его «для себя», в какой-то момент это становится полноценным продуктом.

Пример реализации ER-дизайнера вы можете посмотреть здесь.
Исходный код примера линий расположен на github, stackblitz.


ссылка на оригинал статьи https://habr.com/ru/articles/897284/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *