
Людей можно условно разделить на тех, кто лучше воспринимает информацию на слух, и тех, кто эффективнее усваивает ее визуально. В мире разработки визуализация — это не просто удобство, а необходимый инструмент. Это особенно актуально, когда речь идёт о работе со сложными моделями данных.
Привет! Меня зовут Илья Чубко, я технический архитектор в К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
🔗 Удобный визуальный редактор 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) => ({})), );
Для хранения элементов и линий создадим файлы 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/
Добавить комментарий