Как стать автором
Обновить
233.24

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

Уровень сложностиСредний
Время на прочтение4 мин
Количество просмотров1.4K

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

Привет! Меня зовут Илья Чубко, я технический архитектор в К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.

Теги:
Хабы:
+8
Комментарии2

Публикации

Информация

Сайт
k2.tech
Дата регистрации
Численность
101–200 человек
Местоположение
Россия