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