@memberjunction/ng-entity-relationship-diagram 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -1,37 +1,71 @@
|
|
|
1
1
|
# @memberjunction/ng-entity-relationship-diagram
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Angular components for rendering interactive Entity Relationship Diagrams (ERD) using D3.js force-directed graphs, with both a MemberJunction-specific wrapper and a generic reusable component.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides two complementary components for visualizing entity relationships:
|
|
8
|
+
|
|
9
|
+
1. **`<mj-entity-erd>`** -- A higher-level wrapper that accepts MemberJunction `EntityInfo[]` directly, handles automatic relationship discovery, and supports configurable depth traversal.
|
|
10
|
+
2. **`<mj-erd-diagram>`** -- A generic, lower-level component that works with plain `ERDNode[]` data, suitable for any project regardless of MemberJunction usage.
|
|
11
|
+
|
|
12
|
+
```mermaid
|
|
13
|
+
flowchart LR
|
|
14
|
+
subgraph MJWrapper["mj-entity-erd"]
|
|
15
|
+
EI[EntityInfo Array] --> TRANSFORM[Transform & Discover]
|
|
16
|
+
TRANSFORM --> NODES[ERDNode + ERDLink]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
subgraph Generic["mj-erd-diagram"]
|
|
20
|
+
NODES --> D3[D3.js Force Simulation]
|
|
21
|
+
D3 --> SVG[Interactive SVG]
|
|
22
|
+
SVG --> EVENTS[Selection / Zoom / Drag Events]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
subgraph Features["Capabilities"]
|
|
26
|
+
FOCUS[Focus Mode]
|
|
27
|
+
STATE[State Persistence]
|
|
28
|
+
EXPORT[SVG Export]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
SVG --> Features
|
|
32
|
+
|
|
33
|
+
style MJWrapper fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
34
|
+
style Generic fill:#7c5295,stroke:#563a6b,color:#fff
|
|
35
|
+
style Features fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install @memberjunction/ng-entity-relationship-diagram
|
|
42
|
+
```
|
|
7
43
|
|
|
8
44
|
## Which Component Should I Use?
|
|
9
45
|
|
|
10
46
|
| Use Case | Component | Why |
|
|
11
47
|
|----------|-----------|-----|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
17
|
-
---
|
|
48
|
+
| MemberJunction project | `<mj-entity-erd>` | Auto-discovers relationships, handles transformation |
|
|
49
|
+
| Custom data sources | `<mj-erd-diagram>` | Full control over node/link data |
|
|
50
|
+
| Single entity + related | `<mj-entity-erd>` | Built-in depth/relationship discovery |
|
|
51
|
+
| Non-MJ Angular project | `<mj-erd-diagram>` | No MJ dependencies in your code |
|
|
18
52
|
|
|
19
|
-
##
|
|
53
|
+
## Usage
|
|
20
54
|
|
|
21
|
-
|
|
55
|
+
### Import the Module
|
|
22
56
|
|
|
23
|
-
|
|
57
|
+
```typescript
|
|
58
|
+
import { EntityRelationshipDiagramModule } from '@memberjunction/ng-entity-relationship-diagram';
|
|
24
59
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
60
|
+
@NgModule({
|
|
61
|
+
imports: [EntityRelationshipDiagramModule]
|
|
62
|
+
})
|
|
63
|
+
export class YourModule { }
|
|
64
|
+
```
|
|
30
65
|
|
|
31
|
-
###
|
|
66
|
+
### MemberJunction Entity Wrapper
|
|
32
67
|
|
|
33
68
|
```typescript
|
|
34
|
-
import { EntityRelationshipDiagramModule } from '@memberjunction/ng-entity-relationship-diagram';
|
|
35
69
|
import { EntityInfo, Metadata } from '@memberjunction/core';
|
|
36
70
|
|
|
37
71
|
@Component({
|
|
@@ -65,189 +99,27 @@ export class MyEntityViewerComponent {
|
|
|
65
99
|
|
|
66
100
|
onOpenEntity(entity: EntityInfo) {
|
|
67
101
|
// Navigate to entity record
|
|
68
|
-
this.router.navigate(['/entities', entity.ID]);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### API Reference
|
|
74
|
-
|
|
75
|
-
#### Inputs
|
|
76
|
-
|
|
77
|
-
| Input | Type | Default | Description |
|
|
78
|
-
|-------|------|---------|-------------|
|
|
79
|
-
| `entities` | `EntityInfo[]` | `[]` | Primary entities to display |
|
|
80
|
-
| `allEntities` | `EntityInfo[]` | `[]` | All entities for relationship discovery |
|
|
81
|
-
| `selectedEntityId` | `string \| null` | `null` | ID of currently selected entity |
|
|
82
|
-
| `depth` | `number` | `1` | Relationship hops to include (0 = only primary entities) |
|
|
83
|
-
| `includeIncoming` | `boolean` | `true` | Include entities that reference primary entities |
|
|
84
|
-
| `includeOutgoing` | `boolean` | `true` | Include entities referenced by primary entities via FK |
|
|
85
|
-
| `showHeader` | `boolean` | `true` | Show header with zoom controls |
|
|
86
|
-
| `headerTitle` | `string` | `'Entity Relationship Diagram'` | Header title |
|
|
87
|
-
| `isRefreshing` | `boolean` | `false` | Show loading overlay |
|
|
88
|
-
| `readOnly` | `boolean` | `false` | Disable interactions |
|
|
89
|
-
| `config` | `ERDConfig` | `{}` | Configuration options |
|
|
90
|
-
|
|
91
|
-
#### Outputs
|
|
92
|
-
|
|
93
|
-
| Output | Type | Description |
|
|
94
|
-
|--------|------|-------------|
|
|
95
|
-
| `entitySelected` | `EntityInfo` | Entity was selected |
|
|
96
|
-
| `entityDeselected` | `void` | Selection was cleared |
|
|
97
|
-
| `openRecord` | `EntityInfo` | Entity double-clicked (typically for navigation) |
|
|
98
|
-
| `stateChange` | `ERDState` | Diagram state changed (zoom, pan, selection) |
|
|
99
|
-
|
|
100
|
-
### Single Entity Mode (Entity Forms)
|
|
101
|
-
|
|
102
|
-
Perfect for showing one entity with its relationships:
|
|
103
|
-
|
|
104
|
-
```html
|
|
105
|
-
<mj-entity-erd
|
|
106
|
-
[entities]="[currentEntity]"
|
|
107
|
-
[allEntities]="allEntities"
|
|
108
|
-
[selectedEntityId]="currentEntity.ID"
|
|
109
|
-
[depth]="1"
|
|
110
|
-
[showHeader]="false"
|
|
111
|
-
(openRecord)="navigateToEntity($event)">
|
|
112
|
-
</mj-entity-erd>
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### Multi-Entity Mode (Dashboards)
|
|
116
|
-
|
|
117
|
-
Display multiple entities with full navigation:
|
|
118
|
-
|
|
119
|
-
```html
|
|
120
|
-
<mj-entity-erd
|
|
121
|
-
[entities]="filteredEntities"
|
|
122
|
-
[allEntities]="allEntities"
|
|
123
|
-
[selectedEntityId]="selectedId"
|
|
124
|
-
[depth]="1"
|
|
125
|
-
[includeIncoming]="true"
|
|
126
|
-
[includeOutgoing]="true"
|
|
127
|
-
(entitySelected)="onSelect($event)"
|
|
128
|
-
(entityDeselected)="onDeselect()"
|
|
129
|
-
(openRecord)="openEntityForm($event)"
|
|
130
|
-
(stateChange)="saveUserPreferences($event)">
|
|
131
|
-
</mj-entity-erd>
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
### State Persistence Example
|
|
135
|
-
|
|
136
|
-
The wrapper emits state changes but doesn't persist them. Handle persistence in your container:
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
@Component({...})
|
|
140
|
-
export class EntityDashboardComponent {
|
|
141
|
-
savedState: ERDState | null = null;
|
|
142
|
-
|
|
143
|
-
ngOnInit() {
|
|
144
|
-
// Load saved preferences
|
|
145
|
-
const saved = localStorage.getItem('entity-erd-state');
|
|
146
|
-
if (saved) {
|
|
147
|
-
this.savedState = JSON.parse(saved);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
onStateChange(state: ERDState) {
|
|
152
|
-
// Save to user preferences
|
|
153
|
-
localStorage.setItem('entity-erd-state', JSON.stringify(state));
|
|
154
102
|
}
|
|
155
103
|
}
|
|
156
104
|
```
|
|
157
105
|
|
|
158
|
-
###
|
|
159
|
-
|
|
160
|
-
The package also exports utility functions for advanced use cases:
|
|
161
|
-
|
|
162
|
-
```typescript
|
|
163
|
-
import {
|
|
164
|
-
buildERDDataFromEntities,
|
|
165
|
-
entityInfoToERDNode,
|
|
166
|
-
entityFieldToERDField,
|
|
167
|
-
entitiesToERDNodes,
|
|
168
|
-
getOriginalEntityFromERDNode,
|
|
169
|
-
findEntityByNodeId
|
|
170
|
-
} from '@memberjunction/ng-entity-relationship-diagram';
|
|
171
|
-
|
|
172
|
-
// Build complete ERD data with automatic relationship discovery
|
|
173
|
-
const { nodes, links } = buildERDDataFromEntities(primaryEntities, {
|
|
174
|
-
allEntities: md.Entities,
|
|
175
|
-
includeIncoming: true,
|
|
176
|
-
includeOutgoing: true,
|
|
177
|
-
depth: 2
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// Convert single entity
|
|
181
|
-
const erdNode = entityInfoToERDNode(entityInfo);
|
|
182
|
-
|
|
183
|
-
// Convert entity field
|
|
184
|
-
const erdField = entityFieldToERDField(entityFieldInfo);
|
|
185
|
-
|
|
186
|
-
// Batch convert entities
|
|
187
|
-
const erdNodes = entitiesToERDNodes(entityInfoArray);
|
|
188
|
-
|
|
189
|
-
// Extract original EntityInfo from ERDNode (if available)
|
|
190
|
-
const originalEntity = getOriginalEntityFromERDNode(erdNode);
|
|
191
|
-
|
|
192
|
-
// Find entity by node ID
|
|
193
|
-
const entity = findEntityByNodeId(nodeId, allEntities);
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
---
|
|
197
|
-
|
|
198
|
-
## `<mj-erd-diagram>` - Generic ERD Component
|
|
199
|
-
|
|
200
|
-
The lower-level generic component for full control over the data.
|
|
201
|
-
|
|
202
|
-
## Features
|
|
203
|
-
|
|
204
|
-
- **Interactive Force-Directed Layout** - Nodes automatically arrange themselves using physics-based simulation
|
|
205
|
-
- **Zoom and Pan** - Mouse wheel zoom and drag-to-pan with configurable limits
|
|
206
|
-
- **Node Selection** - Click to select nodes with visual highlighting
|
|
207
|
-
- **Focus Mode** - Show a single entity with its N-hop related entities (entity-centric view)
|
|
208
|
-
- **Relationship Visualization** - Arrows show foreign key relationships with field-level connections
|
|
209
|
-
- **Customizable Styling** - Full control over colors, sizes, and appearance via configuration
|
|
210
|
-
- **Rich Event System** - Comprehensive events for clicks, hovers, context menus, and drags
|
|
211
|
-
- **State Persistence** - Save and restore diagram state (zoom, pan, selection, node positions)
|
|
212
|
-
- **Read-Only Mode** - Disable interactions while keeping visualization intact
|
|
213
|
-
- **SVG Export** - Export the diagram as SVG for embedding or printing
|
|
214
|
-
|
|
215
|
-
## Installation
|
|
216
|
-
|
|
217
|
-
```bash
|
|
218
|
-
npm install @memberjunction/ng-entity-relationship-diagram
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
## Quick Start
|
|
222
|
-
|
|
223
|
-
### 1. Import the Module
|
|
224
|
-
|
|
225
|
-
```typescript
|
|
226
|
-
import { EntityRelationshipDiagramModule } from '@memberjunction/ng-entity-relationship-diagram';
|
|
227
|
-
|
|
228
|
-
@NgModule({
|
|
229
|
-
imports: [
|
|
230
|
-
EntityRelationshipDiagramModule
|
|
231
|
-
]
|
|
232
|
-
})
|
|
233
|
-
export class YourModule { }
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
### 2. Basic Usage
|
|
237
|
-
|
|
238
|
-
```html
|
|
239
|
-
<mj-erd-diagram
|
|
240
|
-
[nodes]="entityNodes"
|
|
241
|
-
[selectedNodeId]="selectedId"
|
|
242
|
-
(nodeSelected)="onNodeSelected($event)"
|
|
243
|
-
(nodeDoubleClick)="onNodeDoubleClick($event)">
|
|
244
|
-
</mj-erd-diagram>
|
|
245
|
-
```
|
|
106
|
+
### Generic ERD Component
|
|
246
107
|
|
|
247
108
|
```typescript
|
|
248
109
|
import { ERDNode, ERDNodeClickEvent } from '@memberjunction/ng-entity-relationship-diagram';
|
|
249
110
|
|
|
250
|
-
|
|
111
|
+
@Component({
|
|
112
|
+
selector: 'my-schema-viewer',
|
|
113
|
+
template: `
|
|
114
|
+
<mj-erd-diagram
|
|
115
|
+
[nodes]="entityNodes"
|
|
116
|
+
[selectedNodeId]="selectedId"
|
|
117
|
+
(nodeSelected)="onNodeSelected($event)"
|
|
118
|
+
(nodeDoubleClick)="onNodeDoubleClick($event)">
|
|
119
|
+
</mj-erd-diagram>
|
|
120
|
+
`
|
|
121
|
+
})
|
|
122
|
+
export class SchemaViewerComponent {
|
|
251
123
|
entityNodes: ERDNode[] = [
|
|
252
124
|
{
|
|
253
125
|
id: 'users',
|
|
@@ -267,84 +139,19 @@ export class YourComponent {
|
|
|
267
139
|
]
|
|
268
140
|
}
|
|
269
141
|
];
|
|
270
|
-
|
|
271
142
|
selectedId: string | null = null;
|
|
272
143
|
|
|
273
144
|
onNodeSelected(node: ERDNode) {
|
|
274
145
|
this.selectedId = node.id;
|
|
275
|
-
console.log('Selected:', node.name);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
onNodeDoubleClick(event: ERDNodeClickEvent) {
|
|
279
|
-
// Navigate to entity detail
|
|
280
|
-
this.router.navigate(['/entities', event.node.id]);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
## Real-World Example: Entity Admin Dashboard
|
|
286
|
-
|
|
287
|
-
This component is used in MemberJunction's Entity Admin Dashboard to visualize database schema. Here's how it's integrated:
|
|
288
|
-
|
|
289
|
-
**Location:** `packages/Angular/Explorer/dashboards/src/EntityAdmin/components/erd-composite.component.ts`
|
|
290
|
-
|
|
291
|
-
```typescript
|
|
292
|
-
import { ERDDiagramComponent, ERDNode, ERDNodeClickEvent } from '@memberjunction/ng-entity-relationship-diagram';
|
|
293
|
-
import { entitiesToERDNodes, findEntityByNodeId } from '../utils/entity-to-erd-adapter';
|
|
294
|
-
|
|
295
|
-
@Component({
|
|
296
|
-
selector: 'mj-erd-composite',
|
|
297
|
-
templateUrl: './erd-composite.component.html'
|
|
298
|
-
})
|
|
299
|
-
export class ERDCompositeComponent {
|
|
300
|
-
@ViewChild(ERDDiagramComponent) erdDiagram!: ERDDiagramComponent;
|
|
301
|
-
|
|
302
|
-
entities: EntityInfo[] = [];
|
|
303
|
-
erdNodes: ERDNode[] = [];
|
|
304
|
-
selectedEntity: EntityInfo | null = null;
|
|
305
|
-
|
|
306
|
-
async ngOnInit() {
|
|
307
|
-
const md = new Metadata();
|
|
308
|
-
this.entities = md.Entities;
|
|
309
|
-
|
|
310
|
-
// Convert MJ EntityInfo to generic ERDNode
|
|
311
|
-
this.erdNodes = entitiesToERDNodes(this.entities);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
onERDNodeSelected(node: ERDNode) {
|
|
315
|
-
// Convert back to EntityInfo for detail panel
|
|
316
|
-
const entity = findEntityByNodeId(node.id, this.entities);
|
|
317
|
-
if (entity) {
|
|
318
|
-
this.selectedEntity = entity;
|
|
319
|
-
}
|
|
320
146
|
}
|
|
321
147
|
}
|
|
322
148
|
```
|
|
323
149
|
|
|
324
|
-
|
|
325
|
-
```html
|
|
326
|
-
<mj-erd-diagram
|
|
327
|
-
[nodes]="filteredERDNodes"
|
|
328
|
-
[isRefreshing]="isRefreshingERD"
|
|
329
|
-
[selectedNodeId]="selectedEntity?.ID || null"
|
|
330
|
-
[showHeader]="true"
|
|
331
|
-
headerTitle="Entity Relationship Diagram"
|
|
332
|
-
(nodeSelected)="onERDNodeSelected($event)"
|
|
333
|
-
(nodeClick)="onERDNodeClick($event)"
|
|
334
|
-
(nodeDeselected)="onEntityDeselected()"
|
|
335
|
-
(refreshRequested)="refreshERD()">
|
|
336
|
-
</mj-erd-diagram>
|
|
337
|
-
```
|
|
150
|
+
### Focus Mode
|
|
338
151
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
Focus mode is perfect for showing a single entity as the center with its related entities. This is ideal for:
|
|
342
|
-
- Entity detail pages
|
|
343
|
-
- Relationship exploration
|
|
344
|
-
- Schema documentation
|
|
152
|
+
Show a single entity with its N-hop related entities:
|
|
345
153
|
|
|
346
154
|
```html
|
|
347
|
-
<!-- Show Users entity with directly related entities -->
|
|
348
155
|
<mj-erd-diagram
|
|
349
156
|
[nodes]="allNodes"
|
|
350
157
|
[focusNodeId]="'users'"
|
|
@@ -353,8 +160,6 @@ Focus mode is perfect for showing a single entity as the center with its related
|
|
|
353
160
|
</mj-erd-diagram>
|
|
354
161
|
```
|
|
355
162
|
|
|
356
|
-
### Focus Depth Options
|
|
357
|
-
|
|
358
163
|
| Depth | Description |
|
|
359
164
|
|-------|-------------|
|
|
360
165
|
| `0` | Show only the focus node |
|
|
@@ -364,46 +169,59 @@ Focus mode is perfect for showing a single entity as the center with its related
|
|
|
364
169
|
|
|
365
170
|
## API Reference
|
|
366
171
|
|
|
367
|
-
### Inputs
|
|
368
|
-
|
|
369
|
-
#### Data Inputs
|
|
172
|
+
### `<mj-entity-erd>` Inputs
|
|
370
173
|
|
|
371
174
|
| Input | Type | Default | Description |
|
|
372
175
|
|-------|------|---------|-------------|
|
|
373
|
-
| `
|
|
374
|
-
| `
|
|
375
|
-
| `
|
|
376
|
-
| `
|
|
377
|
-
| `
|
|
176
|
+
| `entities` | `EntityInfo[]` | `[]` | Primary entities to display |
|
|
177
|
+
| `allEntities` | `EntityInfo[]` | `[]` | All entities for relationship discovery |
|
|
178
|
+
| `selectedEntityId` | `string \| null` | `null` | ID of currently selected entity |
|
|
179
|
+
| `depth` | `number` | `1` | Relationship hops to include |
|
|
180
|
+
| `includeIncoming` | `boolean` | `true` | Include entities referencing primary entities |
|
|
181
|
+
| `includeOutgoing` | `boolean` | `true` | Include entities referenced by primary entities |
|
|
182
|
+
| `showHeader` | `boolean` | `true` | Show header with zoom controls |
|
|
183
|
+
| `headerTitle` | `string` | `'Entity Relationship Diagram'` | Header title |
|
|
184
|
+
| `isRefreshing` | `boolean` | `false` | Show loading overlay |
|
|
185
|
+
| `readOnly` | `boolean` | `false` | Disable interactions |
|
|
186
|
+
| `config` | `ERDConfig` | `{}` | Configuration options |
|
|
378
187
|
|
|
379
|
-
|
|
188
|
+
### `<mj-entity-erd>` Outputs
|
|
380
189
|
|
|
381
|
-
|
|
|
382
|
-
|
|
383
|
-
| `
|
|
384
|
-
| `
|
|
190
|
+
| Output | Type | Description |
|
|
191
|
+
|--------|------|-------------|
|
|
192
|
+
| `entitySelected` | `EntityInfo` | Entity was selected |
|
|
193
|
+
| `entityDeselected` | `void` | Selection was cleared |
|
|
194
|
+
| `openRecord` | `EntityInfo` | Entity double-clicked |
|
|
195
|
+
| `stateChange` | `ERDState` | Diagram state changed |
|
|
385
196
|
|
|
386
|
-
|
|
197
|
+
### `<mj-erd-diagram>` Inputs
|
|
387
198
|
|
|
388
199
|
| Input | Type | Default | Description |
|
|
389
200
|
|-------|------|---------|-------------|
|
|
390
|
-
| `
|
|
391
|
-
| `
|
|
201
|
+
| `nodes` | `ERDNode[]` | `[]` | Nodes (entities) to display |
|
|
202
|
+
| `selectedNodeId` | `string \| null` | `null` | Currently selected node |
|
|
203
|
+
| `highlightedNodeIds` | `string[]` | `[]` | Highlighted node IDs |
|
|
204
|
+
| `focusNodeId` | `string \| null` | `null` | Focus mode target |
|
|
205
|
+
| `focusDepth` | `number` | `1` | Relationship hops in focus mode |
|
|
206
|
+
| `isRefreshing` | `boolean` | `false` | Show loading overlay |
|
|
207
|
+
| `readOnly` | `boolean` | `false` | Disable dragging and selection |
|
|
208
|
+
| `config` | `ERDConfig` | `{}` | Configuration options |
|
|
209
|
+
| `showHeader` | `boolean` | `true` | Show header with controls |
|
|
392
210
|
| `headerTitle` | `string` | `'Entity Relationship Diagram'` | Title in header |
|
|
393
211
|
|
|
394
|
-
### Outputs
|
|
212
|
+
### `<mj-erd-diagram>` Outputs
|
|
395
213
|
|
|
396
|
-
|
|
214
|
+
**Selection Events:**
|
|
397
215
|
|
|
398
216
|
| Output | Type | Description |
|
|
399
217
|
|--------|------|-------------|
|
|
400
|
-
| `nodeClick` | `ERDNodeClickEvent` | Node clicked (
|
|
218
|
+
| `nodeClick` | `ERDNodeClickEvent` | Node clicked (cancelable) |
|
|
401
219
|
| `nodeDoubleClick` | `ERDNodeDoubleClickEvent` | Node double-clicked |
|
|
402
|
-
| `nodeSelected` | `ERDNode` | Node selected
|
|
403
|
-
| `nodeDeselected` | `void` |
|
|
220
|
+
| `nodeSelected` | `ERDNode` | Node selected |
|
|
221
|
+
| `nodeDeselected` | `void` | Selection cleared |
|
|
404
222
|
| `linkClick` | `ERDLinkClickEvent` | Relationship link clicked |
|
|
405
223
|
|
|
406
|
-
|
|
224
|
+
**Hover Events:**
|
|
407
225
|
|
|
408
226
|
| Output | Type | Description |
|
|
409
227
|
|--------|------|-------------|
|
|
@@ -412,7 +230,7 @@ Focus mode is perfect for showing a single entity as the center with its related
|
|
|
412
230
|
| `linkHover` | `ERDLinkHoverEvent` | Mouse entered a link |
|
|
413
231
|
| `linkHoverEnd` | `ERDLink` | Mouse left a link |
|
|
414
232
|
|
|
415
|
-
|
|
233
|
+
**Context Menu Events:**
|
|
416
234
|
|
|
417
235
|
| Output | Type | Description |
|
|
418
236
|
|--------|------|-------------|
|
|
@@ -420,115 +238,90 @@ Focus mode is perfect for showing a single entity as the center with its related
|
|
|
420
238
|
| `linkContextMenu` | `ERDLinkContextMenuEvent` | Right-click on link |
|
|
421
239
|
| `diagramContextMenu` | `ERDDiagramContextMenuEvent` | Right-click on background |
|
|
422
240
|
|
|
423
|
-
|
|
241
|
+
**Diagram Events:**
|
|
424
242
|
|
|
425
243
|
| Output | Type | Description |
|
|
426
244
|
|--------|------|-------------|
|
|
427
|
-
| `nodeDragStart` | `ERDNodeDragEvent` | Drag started (
|
|
245
|
+
| `nodeDragStart` | `ERDNodeDragEvent` | Drag started (cancelable) |
|
|
428
246
|
| `nodeDragEnd` | `ERDNodeDragEvent` | Drag ended |
|
|
429
|
-
|
|
430
|
-
#### Diagram Events
|
|
431
|
-
|
|
432
|
-
| Output | Type | Description |
|
|
433
|
-
|--------|------|-------------|
|
|
434
|
-
| `zoomChange` | `ERDZoomEvent` | Zoom level or pan position changed |
|
|
247
|
+
| `zoomChange` | `ERDZoomEvent` | Zoom level or pan changed |
|
|
435
248
|
| `refreshRequested` | `void` | Refresh button clicked |
|
|
436
|
-
| `layoutComplete` | `void` | Force simulation completed
|
|
437
|
-
| `stateChange` | `ERDState` | Any state change
|
|
249
|
+
| `layoutComplete` | `void` | Force simulation completed |
|
|
250
|
+
| `stateChange` | `ERDState` | Any state change |
|
|
438
251
|
|
|
439
252
|
### Public Methods
|
|
440
253
|
|
|
441
254
|
Access via `@ViewChild(ERDDiagramComponent)`:
|
|
442
255
|
|
|
443
|
-
#### Zoom Control
|
|
444
|
-
|
|
445
256
|
```typescript
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
```typescript
|
|
464
|
-
highlightNode(nodeId: string): void // Highlight a specific node
|
|
465
|
-
clearHighlights(): void // Clear all highlights
|
|
466
|
-
highlightRelated(nodeId: string, depth?: number): void // Highlight node + related
|
|
257
|
+
// Zoom control
|
|
258
|
+
zoomIn(): void
|
|
259
|
+
zoomOut(): void
|
|
260
|
+
resetZoom(): void
|
|
261
|
+
zoomToNode(nodeId: string, scale?: number): void
|
|
262
|
+
zoomToFit(padding?: number): void
|
|
263
|
+
centerDiagram(): void
|
|
264
|
+
|
|
265
|
+
// Selection
|
|
266
|
+
selectNode(nodeId: string): boolean
|
|
267
|
+
deselectAll(): void
|
|
268
|
+
|
|
269
|
+
// Highlighting
|
|
270
|
+
highlightNode(nodeId: string): void
|
|
271
|
+
clearHighlights(): void
|
|
272
|
+
highlightRelated(nodeId: string, depth?: number): void
|
|
467
273
|
getRelatedNodes(nodeId: string, depth?: number): ERDRelationshipInfo[]
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
#### State Management
|
|
471
274
|
|
|
472
|
-
|
|
473
|
-
getState(): ERDState
|
|
275
|
+
// State management
|
|
276
|
+
getState(): ERDState
|
|
474
277
|
setState(state: Partial<ERDState>, restorePositions?: boolean): void
|
|
475
|
-
```
|
|
476
278
|
|
|
477
|
-
|
|
279
|
+
// Layout control
|
|
280
|
+
freezeLayout(): void
|
|
281
|
+
unfreezeLayout(): void
|
|
478
282
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
283
|
+
// Utilities
|
|
284
|
+
refresh(): void
|
|
285
|
+
triggerResize(): void
|
|
286
|
+
getZoomState(): ERDZoomEvent
|
|
287
|
+
exportAsSVG(): string
|
|
482
288
|
```
|
|
483
289
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
```typescript
|
|
487
|
-
refresh(): void // Rebuild visualization
|
|
488
|
-
triggerResize(): void // Recalculate after container resize
|
|
489
|
-
getZoomState(): ERDZoomEvent // Get current zoom/pan state
|
|
490
|
-
exportAsSVG(): string // Export as SVG string
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
## Configuration Options
|
|
290
|
+
## Configuration
|
|
494
291
|
|
|
495
292
|
```typescript
|
|
496
293
|
const config: ERDConfig = {
|
|
497
|
-
// Node
|
|
498
|
-
nodeWidth: 180,
|
|
499
|
-
nodeBaseHeight: 60,
|
|
500
|
-
fieldHeight: 20,
|
|
501
|
-
maxNodeHeight: 300,
|
|
502
|
-
|
|
503
|
-
// Force
|
|
504
|
-
chargeStrength: -800,
|
|
505
|
-
linkDistance: 80,
|
|
506
|
-
collisionPadding: 20,
|
|
507
|
-
|
|
508
|
-
// Display
|
|
509
|
-
showFieldDetails: true,
|
|
510
|
-
showRelationshipLabels: true,
|
|
511
|
-
showHeader: true,
|
|
512
|
-
showNodeCount: true,
|
|
513
|
-
showMinimap: false,
|
|
514
|
-
showLegend: false,
|
|
294
|
+
// Node sizing
|
|
295
|
+
nodeWidth: 180,
|
|
296
|
+
nodeBaseHeight: 60,
|
|
297
|
+
fieldHeight: 20,
|
|
298
|
+
maxNodeHeight: 300,
|
|
299
|
+
|
|
300
|
+
// Force simulation
|
|
301
|
+
chargeStrength: -800,
|
|
302
|
+
linkDistance: 80,
|
|
303
|
+
collisionPadding: 20,
|
|
304
|
+
|
|
305
|
+
// Display
|
|
306
|
+
showFieldDetails: true,
|
|
307
|
+
showRelationshipLabels: true,
|
|
308
|
+
showHeader: true,
|
|
309
|
+
showNodeCount: true,
|
|
310
|
+
showMinimap: false,
|
|
311
|
+
showLegend: false,
|
|
515
312
|
|
|
516
313
|
// Interaction
|
|
517
|
-
enableDragging: true,
|
|
518
|
-
enableZoom: true,
|
|
519
|
-
enablePan: true,
|
|
520
|
-
minZoom: 0.1,
|
|
521
|
-
maxZoom: 4,
|
|
522
|
-
initialZoom: 1,
|
|
523
|
-
enableMultiSelect: false,
|
|
314
|
+
enableDragging: true,
|
|
315
|
+
enableZoom: true,
|
|
316
|
+
enablePan: true,
|
|
317
|
+
minZoom: 0.1,
|
|
318
|
+
maxZoom: 4,
|
|
319
|
+
initialZoom: 1,
|
|
320
|
+
enableMultiSelect: false,
|
|
524
321
|
|
|
525
322
|
// Animation
|
|
526
|
-
animationDuration: 750,
|
|
527
|
-
fitOnLoad: true,
|
|
528
|
-
|
|
529
|
-
// Empty State
|
|
530
|
-
emptyStateMessage: 'No entities to display',
|
|
531
|
-
emptyStateIcon: 'fa-solid fa-diagram-project',
|
|
323
|
+
animationDuration: 750,
|
|
324
|
+
fitOnLoad: true,
|
|
532
325
|
|
|
533
326
|
// Layout
|
|
534
327
|
layoutAlgorithm: 'force', // 'force' | 'horizontal' | 'vertical' | 'radial'
|
|
@@ -540,9 +333,7 @@ const config: ERDConfig = {
|
|
|
540
333
|
nodeHeader: '#007bff',
|
|
541
334
|
nodeHeaderText: 'white',
|
|
542
335
|
primaryKeyBackground: '#fff3cd',
|
|
543
|
-
primaryKeyText: '#856404',
|
|
544
336
|
foreignKeyBackground: '#cce5ff',
|
|
545
|
-
foreignKeyText: '#004085',
|
|
546
337
|
linkColor: '#666',
|
|
547
338
|
selectedBorder: '#4CAF50',
|
|
548
339
|
highlightBorder: '#ff9800',
|
|
@@ -553,48 +344,35 @@ const config: ERDConfig = {
|
|
|
553
344
|
|
|
554
345
|
## Data Interfaces
|
|
555
346
|
|
|
556
|
-
### ERDNode
|
|
557
|
-
|
|
558
347
|
```typescript
|
|
559
348
|
interface ERDNode {
|
|
560
|
-
id: string;
|
|
561
|
-
name: string;
|
|
562
|
-
schemaName?: string;
|
|
563
|
-
description?: string;
|
|
564
|
-
status?: string;
|
|
565
|
-
baseTable?: string;
|
|
566
|
-
fields: ERDField[];
|
|
567
|
-
customData?: Record<string, unknown>;
|
|
349
|
+
id: string;
|
|
350
|
+
name: string;
|
|
351
|
+
schemaName?: string;
|
|
352
|
+
description?: string;
|
|
353
|
+
status?: string;
|
|
354
|
+
baseTable?: string;
|
|
355
|
+
fields: ERDField[];
|
|
356
|
+
customData?: Record<string, unknown>;
|
|
568
357
|
}
|
|
569
|
-
```
|
|
570
358
|
|
|
571
|
-
### ERDField
|
|
572
|
-
|
|
573
|
-
```typescript
|
|
574
359
|
interface ERDField {
|
|
575
|
-
id: string;
|
|
576
|
-
name: string;
|
|
577
|
-
type?: string;
|
|
578
|
-
isPrimaryKey: boolean;
|
|
579
|
-
relatedNodeId?: string;
|
|
580
|
-
relatedNodeName?: string;
|
|
581
|
-
relatedFieldName?: string;
|
|
360
|
+
id: string;
|
|
361
|
+
name: string;
|
|
362
|
+
type?: string;
|
|
363
|
+
isPrimaryKey: boolean;
|
|
364
|
+
relatedNodeId?: string;
|
|
365
|
+
relatedNodeName?: string;
|
|
366
|
+
relatedFieldName?: string;
|
|
582
367
|
description?: string;
|
|
583
368
|
allowsNull?: boolean;
|
|
584
369
|
defaultValue?: string;
|
|
585
|
-
length?: number;
|
|
586
|
-
|
|
587
|
-
scale?: number;
|
|
588
|
-
isVirtual?: boolean; // Computed field
|
|
370
|
+
length?: number;
|
|
371
|
+
isVirtual?: boolean;
|
|
589
372
|
autoIncrement?: boolean;
|
|
590
|
-
possibleValues?: ERDFieldValue[];
|
|
591
373
|
customData?: Record<string, unknown>;
|
|
592
374
|
}
|
|
593
|
-
```
|
|
594
|
-
|
|
595
|
-
### ERDLink
|
|
596
375
|
|
|
597
|
-
```typescript
|
|
598
376
|
interface ERDLink {
|
|
599
377
|
sourceNodeId: string;
|
|
600
378
|
targetNodeId: string;
|
|
@@ -604,29 +382,7 @@ interface ERDLink {
|
|
|
604
382
|
relationshipType?: 'one-to-one' | 'one-to-many' | 'many-to-one' | 'many-to-many';
|
|
605
383
|
label?: string;
|
|
606
384
|
}
|
|
607
|
-
```
|
|
608
|
-
|
|
609
|
-
## State Persistence
|
|
610
385
|
|
|
611
|
-
Save and restore diagram state for user preferences:
|
|
612
|
-
|
|
613
|
-
```typescript
|
|
614
|
-
// Save to localStorage
|
|
615
|
-
const state = this.erdDiagram.getState();
|
|
616
|
-
localStorage.setItem('erd-state', JSON.stringify(state));
|
|
617
|
-
|
|
618
|
-
// Restore on component init
|
|
619
|
-
ngAfterViewInit() {
|
|
620
|
-
const savedState = localStorage.getItem('erd-state');
|
|
621
|
-
if (savedState) {
|
|
622
|
-
this.erdDiagram.setState(JSON.parse(savedState));
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
### ERDState Interface
|
|
628
|
-
|
|
629
|
-
```typescript
|
|
630
386
|
interface ERDState {
|
|
631
387
|
selectedNodeId: string | null;
|
|
632
388
|
highlightedNodeIds: string[];
|
|
@@ -635,138 +391,79 @@ interface ERDState {
|
|
|
635
391
|
translateY: number;
|
|
636
392
|
focusNodeId: string | null;
|
|
637
393
|
focusDepth: number;
|
|
638
|
-
nodePositions: Record<string, {
|
|
639
|
-
x: number;
|
|
640
|
-
y: number;
|
|
641
|
-
fx?: number | null;
|
|
642
|
-
fy?: number | null;
|
|
643
|
-
}>;
|
|
394
|
+
nodePositions: Record<string, { x: number; y: number; fx?: number | null; fy?: number | null }>;
|
|
644
395
|
}
|
|
645
396
|
```
|
|
646
397
|
|
|
647
|
-
##
|
|
398
|
+
## Utility Functions
|
|
648
399
|
|
|
649
|
-
|
|
400
|
+
The package exports utility functions for transforming MemberJunction entities to ERD data:
|
|
650
401
|
|
|
651
402
|
```typescript
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
403
|
+
import {
|
|
404
|
+
buildERDDataFromEntities,
|
|
405
|
+
entityInfoToERDNode,
|
|
406
|
+
entityFieldToERDField,
|
|
407
|
+
entitiesToERDNodes,
|
|
408
|
+
getOriginalEntityFromERDNode,
|
|
409
|
+
findEntityByNodeId
|
|
410
|
+
} from '@memberjunction/ng-entity-relationship-diagram';
|
|
660
411
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
412
|
+
// Build complete ERD data with automatic relationship discovery
|
|
413
|
+
const { nodes, links } = buildERDDataFromEntities(primaryEntities, {
|
|
414
|
+
allEntities: md.Entities,
|
|
415
|
+
includeIncoming: true,
|
|
416
|
+
includeOutgoing: true,
|
|
417
|
+
depth: 2
|
|
418
|
+
});
|
|
664
419
|
```
|
|
665
420
|
|
|
666
|
-
|
|
421
|
+
## State Persistence
|
|
667
422
|
|
|
668
423
|
```typescript
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
content: `${event.node.name}\n${event.relatedNodes.length} related entities`,
|
|
673
|
-
x: event.position.x + 10,
|
|
674
|
-
y: event.position.y + 10
|
|
675
|
-
};
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
onNodeHoverEnd() {
|
|
679
|
-
this.tooltip.visible = false;
|
|
680
|
-
}
|
|
681
|
-
```
|
|
682
|
-
|
|
683
|
-
### Prevent Selection for Locked Nodes
|
|
424
|
+
// Save diagram state
|
|
425
|
+
const state = this.erdDiagram.getState();
|
|
426
|
+
localStorage.setItem('erd-state', JSON.stringify(state));
|
|
684
427
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
this.
|
|
428
|
+
// Restore state on init
|
|
429
|
+
ngAfterViewInit() {
|
|
430
|
+
const savedState = localStorage.getItem('erd-state');
|
|
431
|
+
if (savedState) {
|
|
432
|
+
this.erdDiagram.setState(JSON.parse(savedState));
|
|
690
433
|
}
|
|
691
434
|
}
|
|
692
435
|
```
|
|
693
436
|
|
|
694
|
-
## Styling
|
|
695
|
-
|
|
696
|
-
The component uses Angular's view encapsulation but exposes CSS variables for customization:
|
|
697
|
-
|
|
698
|
-
```css
|
|
699
|
-
/* In your global styles or component styles */
|
|
700
|
-
mj-erd-diagram {
|
|
701
|
-
--erd-background: #f5f5f5;
|
|
702
|
-
--erd-header-background: #e0e0e0;
|
|
703
|
-
--erd-header-text: #333;
|
|
704
|
-
}
|
|
705
|
-
```
|
|
706
|
-
|
|
707
|
-
For deeper customization, the component uses `::ng-deep` compatible class names:
|
|
708
|
-
- `.erd-section` - Main container
|
|
709
|
-
- `.section-header` - Header bar
|
|
710
|
-
- `.erd-container` - SVG container
|
|
711
|
-
- `.node` - Node group
|
|
712
|
-
- `.entity-rect` - Node rectangle
|
|
713
|
-
- `.entity-header` - Node header bar
|
|
714
|
-
- `.link-group` - Link group
|
|
715
|
-
- `.link` - Link path
|
|
716
|
-
|
|
717
|
-
## Converting Custom Data to ERDNode
|
|
718
|
-
|
|
719
|
-
Create an adapter function to convert your domain types:
|
|
720
|
-
|
|
721
|
-
```typescript
|
|
722
|
-
// Example: Convert database schema to ERDNode
|
|
723
|
-
function tableToERDNode(table: DatabaseTable): ERDNode {
|
|
724
|
-
return {
|
|
725
|
-
id: table.id,
|
|
726
|
-
name: table.tableName,
|
|
727
|
-
schemaName: table.schema,
|
|
728
|
-
fields: table.columns.map(col => ({
|
|
729
|
-
id: col.name,
|
|
730
|
-
name: col.name,
|
|
731
|
-
type: col.dataType,
|
|
732
|
-
isPrimaryKey: col.isPrimaryKey,
|
|
733
|
-
relatedNodeId: col.foreignKeyTable,
|
|
734
|
-
relatedFieldName: col.foreignKeyColumn,
|
|
735
|
-
allowsNull: col.nullable
|
|
736
|
-
}))
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
```
|
|
740
|
-
|
|
741
437
|
## Performance Considerations
|
|
742
438
|
|
|
743
|
-
- For
|
|
744
|
-
-
|
|
439
|
+
- For 100+ node diagrams, use focus mode to show subsets
|
|
440
|
+
- Call `freezeLayout()` after the initial layout settles for static diagrams
|
|
745
441
|
- Adjust `chargeStrength` and `linkDistance` for different node densities
|
|
746
|
-
- Set `fitOnLoad: false`
|
|
747
|
-
|
|
748
|
-
## Browser Support
|
|
442
|
+
- Set `fitOnLoad: false` when restoring state to avoid double-animation
|
|
749
443
|
|
|
750
|
-
|
|
751
|
-
- Firefox 75+
|
|
752
|
-
- Safari 13+
|
|
753
|
-
- Edge 80+
|
|
444
|
+
## Dependencies
|
|
754
445
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
-
|
|
446
|
+
| Package | Description |
|
|
447
|
+
|---------|-------------|
|
|
448
|
+
| `@memberjunction/core` | Core framework (for entity wrapper) |
|
|
449
|
+
| `d3` | D3.js force-directed graph engine |
|
|
450
|
+
| `@dagrejs/dagre` | DAG layout algorithms |
|
|
451
|
+
| `@progress/kendo-angular-layout` | Layout components |
|
|
452
|
+
| `rxjs` | Reactive extensions |
|
|
759
453
|
|
|
760
|
-
|
|
454
|
+
### Peer Dependencies
|
|
761
455
|
|
|
762
|
-
-
|
|
763
|
-
-
|
|
764
|
-
-
|
|
456
|
+
- `@angular/common` ^21.x
|
|
457
|
+
- `@angular/core` ^21.x
|
|
458
|
+
- `@angular/platform-browser` ^21.x
|
|
765
459
|
|
|
766
|
-
##
|
|
460
|
+
## Build
|
|
767
461
|
|
|
768
|
-
|
|
462
|
+
```bash
|
|
463
|
+
cd packages/Angular/Generic/entity-relationship-diagram
|
|
464
|
+
npm run build
|
|
465
|
+
```
|
|
769
466
|
|
|
770
|
-
##
|
|
467
|
+
## License
|
|
771
468
|
|
|
772
|
-
|
|
469
|
+
ISC
|