@memberjunction/ng-entity-relationship-diagram 0.0.1 → 2.130.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 +754 -27
- package/dist/lib/components/entity-details/entity-details.component.d.ts +70 -0
- package/dist/lib/components/entity-details/entity-details.component.d.ts.map +1 -0
- package/dist/lib/components/entity-details/entity-details.component.js +694 -0
- package/dist/lib/components/entity-details/entity-details.component.js.map +1 -0
- package/dist/lib/components/entity-filter-panel/entity-filter-panel.component.d.ts +48 -0
- package/dist/lib/components/entity-filter-panel/entity-filter-panel.component.d.ts.map +1 -0
- package/dist/lib/components/entity-filter-panel/entity-filter-panel.component.js +179 -0
- package/dist/lib/components/entity-filter-panel/entity-filter-panel.component.js.map +1 -0
- package/dist/lib/components/erd-composite/erd-composite.component.d.ts +170 -0
- package/dist/lib/components/erd-composite/erd-composite.component.d.ts.map +1 -0
- package/dist/lib/components/erd-composite/erd-composite.component.js +391 -0
- package/dist/lib/components/erd-composite/erd-composite.component.js.map +1 -0
- package/dist/lib/components/erd-diagram.component.d.ts +486 -0
- package/dist/lib/components/erd-diagram.component.d.ts.map +1 -0
- package/dist/lib/components/erd-diagram.component.js +1936 -0
- package/dist/lib/components/erd-diagram.component.js.map +1 -0
- package/dist/lib/components/mj-entity-erd.component.d.ts +261 -0
- package/dist/lib/components/mj-entity-erd.component.d.ts.map +1 -0
- package/dist/lib/components/mj-entity-erd.component.js +416 -0
- package/dist/lib/components/mj-entity-erd.component.js.map +1 -0
- package/dist/lib/entity-relationship-diagram.module.d.ts +83 -0
- package/dist/lib/entity-relationship-diagram.module.d.ts.map +1 -0
- package/dist/lib/entity-relationship-diagram.module.js +121 -0
- package/dist/lib/entity-relationship-diagram.module.js.map +1 -0
- package/dist/lib/interfaces/erd-types.d.ts +491 -0
- package/dist/lib/interfaces/erd-types.d.ts.map +1 -0
- package/dist/lib/interfaces/erd-types.js +6 -0
- package/dist/lib/interfaces/erd-types.js.map +1 -0
- package/dist/lib/utils/entity-to-erd-adapter.d.ts +64 -0
- package/dist/lib/utils/entity-to-erd-adapter.d.ts.map +1 -0
- package/dist/lib/utils/entity-to-erd-adapter.js +231 -0
- package/dist/lib/utils/entity-to-erd-adapter.js.map +1 -0
- package/dist/public-api.d.ts +9 -0
- package/dist/public-api.d.ts.map +1 -0
- package/dist/public-api.js +16 -0
- package/dist/public-api.js.map +1 -0
- package/package.json +45 -6
package/README.md
CHANGED
|
@@ -1,45 +1,772 @@
|
|
|
1
1
|
# @memberjunction/ng-entity-relationship-diagram
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A package providing **two Angular components** for rendering interactive Entity Relationship Diagrams (ERD) using D3.js force-directed graphs:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
1. **`<mj-entity-erd>`** - Higher-level MemberJunction wrapper that accepts `EntityInfo[]` directly
|
|
6
|
+
2. **`<mj-erd-diagram>`** - Generic, reusable component that works with any data
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
## Which Component Should I Use?
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
| Use Case | Component | Why |
|
|
11
|
+
|----------|-----------|-----|
|
|
12
|
+
| **MemberJunction project** | `<mj-entity-erd>` | Auto-discovers relationships, handles transformation |
|
|
13
|
+
| **Custom data sources** | `<mj-erd-diagram>` | Full control over node/link data |
|
|
14
|
+
| **Single entity + related** | `<mj-entity-erd>` | Built-in depth/relationship discovery |
|
|
15
|
+
| **Non-MJ Angular project** | `<mj-erd-diagram>` | No MJ dependencies in your code |
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## `<mj-entity-erd>` - MemberJunction Entity Wrapper
|
|
20
|
+
|
|
21
|
+
The higher-level wrapper component designed specifically for MemberJunction projects. It accepts `EntityInfo[]` directly and handles all the transformation logic internally.
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
- **Direct EntityInfo Input** - No manual transformation needed
|
|
26
|
+
- **Automatic Relationship Discovery** - Finds related entities automatically
|
|
27
|
+
- **Configurable Depth** - Control how many relationship hops to include
|
|
28
|
+
- **Parent-Controlled State** - Emits state changes for persistence by parent
|
|
29
|
+
- **Bidirectional Relationships** - Optionally include incoming and outgoing relationships
|
|
30
|
+
|
|
31
|
+
### Basic Usage
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { EntityRelationshipDiagramModule } from '@memberjunction/ng-entity-relationship-diagram';
|
|
35
|
+
import { EntityInfo, Metadata } from '@memberjunction/core';
|
|
36
|
+
|
|
37
|
+
@Component({
|
|
38
|
+
selector: 'my-entity-viewer',
|
|
39
|
+
template: `
|
|
40
|
+
<mj-entity-erd
|
|
41
|
+
[entities]="selectedEntities"
|
|
42
|
+
[allEntities]="allEntities"
|
|
43
|
+
[selectedEntityId]="currentEntityId"
|
|
44
|
+
[depth]="1"
|
|
45
|
+
[includeIncoming]="true"
|
|
46
|
+
[includeOutgoing]="true"
|
|
47
|
+
(entitySelected)="onEntitySelected($event)"
|
|
48
|
+
(openRecord)="onOpenEntity($event)">
|
|
49
|
+
</mj-entity-erd>
|
|
50
|
+
`
|
|
51
|
+
})
|
|
52
|
+
export class MyEntityViewerComponent {
|
|
53
|
+
allEntities: EntityInfo[] = [];
|
|
54
|
+
selectedEntities: EntityInfo[] = [];
|
|
55
|
+
currentEntityId: string | null = null;
|
|
56
|
+
|
|
57
|
+
constructor() {
|
|
58
|
+
const md = new Metadata();
|
|
59
|
+
this.allEntities = md.Entities;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
onEntitySelected(entity: EntityInfo) {
|
|
63
|
+
this.currentEntityId = entity.ID;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
onOpenEntity(entity: EntityInfo) {
|
|
67
|
+
// 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:
|
|
15
103
|
|
|
16
|
-
|
|
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
|
+
```
|
|
17
114
|
|
|
18
|
-
|
|
115
|
+
### Multi-Entity Mode (Dashboards)
|
|
19
116
|
|
|
20
|
-
|
|
117
|
+
Display multiple entities with full navigation:
|
|
21
118
|
|
|
22
|
-
|
|
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
|
+
```
|
|
23
133
|
|
|
24
|
-
|
|
25
|
-
2. Configure the trusted publisher (e.g., GitHub Actions)
|
|
26
|
-
3. Specify the repository and workflow that should be allowed to publish
|
|
27
|
-
4. Use the configured workflow to publish your actual package
|
|
134
|
+
### State Persistence Example
|
|
28
135
|
|
|
29
|
-
|
|
136
|
+
The wrapper emits state changes but doesn't persist them. Handle persistence in your container:
|
|
30
137
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- Exists only for administrative purposes
|
|
138
|
+
```typescript
|
|
139
|
+
@Component({...})
|
|
140
|
+
export class EntityDashboardComponent {
|
|
141
|
+
savedState: ERDState | null = null;
|
|
36
142
|
|
|
37
|
-
|
|
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
|
+
}
|
|
38
150
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
-
|
|
151
|
+
onStateChange(state: ERDState) {
|
|
152
|
+
// Save to user preferences
|
|
153
|
+
localStorage.setItem('entity-erd-state', JSON.stringify(state));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Utility Functions
|
|
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
|
+
```
|
|
42
195
|
|
|
43
196
|
---
|
|
44
197
|
|
|
45
|
-
|
|
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
|
+
```
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { ERDNode, ERDNodeClickEvent } from '@memberjunction/ng-entity-relationship-diagram';
|
|
249
|
+
|
|
250
|
+
export class YourComponent {
|
|
251
|
+
entityNodes: ERDNode[] = [
|
|
252
|
+
{
|
|
253
|
+
id: 'users',
|
|
254
|
+
name: 'Users',
|
|
255
|
+
fields: [
|
|
256
|
+
{ id: 'id', name: 'ID', isPrimaryKey: true, type: 'uuid' },
|
|
257
|
+
{ id: 'email', name: 'Email', isPrimaryKey: false, type: 'string' },
|
|
258
|
+
{ id: 'roleId', name: 'RoleID', isPrimaryKey: false, relatedNodeId: 'roles', relatedFieldName: 'ID' }
|
|
259
|
+
]
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: 'roles',
|
|
263
|
+
name: 'Roles',
|
|
264
|
+
fields: [
|
|
265
|
+
{ id: 'id', name: 'ID', isPrimaryKey: true, type: 'uuid' },
|
|
266
|
+
{ id: 'name', name: 'Name', isPrimaryKey: false, type: 'string' }
|
|
267
|
+
]
|
|
268
|
+
}
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
selectedId: string | null = null;
|
|
272
|
+
|
|
273
|
+
onNodeSelected(node: ERDNode) {
|
|
274
|
+
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
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Template:**
|
|
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
|
+
```
|
|
338
|
+
|
|
339
|
+
## Focus Mode - Entity-Centric Views
|
|
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
|
|
345
|
+
|
|
346
|
+
```html
|
|
347
|
+
<!-- Show Users entity with directly related entities -->
|
|
348
|
+
<mj-erd-diagram
|
|
349
|
+
[nodes]="allNodes"
|
|
350
|
+
[focusNodeId]="'users'"
|
|
351
|
+
[focusDepth]="1"
|
|
352
|
+
(nodeSelected)="onNodeSelected($event)">
|
|
353
|
+
</mj-erd-diagram>
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Focus Depth Options
|
|
357
|
+
|
|
358
|
+
| Depth | Description |
|
|
359
|
+
|-------|-------------|
|
|
360
|
+
| `0` | Show only the focus node |
|
|
361
|
+
| `1` | Focus node + directly related nodes |
|
|
362
|
+
| `2` | Focus node + nodes within 2 relationship hops |
|
|
363
|
+
| `3+` | Continue expanding the relationship graph |
|
|
364
|
+
|
|
365
|
+
## API Reference
|
|
366
|
+
|
|
367
|
+
### Inputs
|
|
368
|
+
|
|
369
|
+
#### Data Inputs
|
|
370
|
+
|
|
371
|
+
| Input | Type | Default | Description |
|
|
372
|
+
|-------|------|---------|-------------|
|
|
373
|
+
| `nodes` | `ERDNode[]` | `[]` | The nodes (entities) to display |
|
|
374
|
+
| `selectedNodeId` | `string \| null` | `null` | ID of the currently selected node |
|
|
375
|
+
| `highlightedNodeIds` | `string[]` | `[]` | IDs of nodes to highlight |
|
|
376
|
+
| `focusNodeId` | `string \| null` | `null` | ID of node to focus on (enables focus mode) |
|
|
377
|
+
| `focusDepth` | `number` | `1` | Relationship hops to include in focus mode |
|
|
378
|
+
|
|
379
|
+
#### State Inputs
|
|
380
|
+
|
|
381
|
+
| Input | Type | Default | Description |
|
|
382
|
+
|-------|------|---------|-------------|
|
|
383
|
+
| `isRefreshing` | `boolean` | `false` | Shows loading overlay when true |
|
|
384
|
+
| `readOnly` | `boolean` | `false` | Disables dragging and selection |
|
|
385
|
+
|
|
386
|
+
#### Configuration Inputs
|
|
387
|
+
|
|
388
|
+
| Input | Type | Default | Description |
|
|
389
|
+
|-------|------|---------|-------------|
|
|
390
|
+
| `config` | `ERDConfig` | `{}` | Configuration options (see below) |
|
|
391
|
+
| `showHeader` | `boolean` | `true` | Show header with zoom controls |
|
|
392
|
+
| `headerTitle` | `string` | `'Entity Relationship Diagram'` | Title in header |
|
|
393
|
+
|
|
394
|
+
### Outputs (Events)
|
|
395
|
+
|
|
396
|
+
#### Selection Events
|
|
397
|
+
|
|
398
|
+
| Output | Type | Description |
|
|
399
|
+
|--------|------|-------------|
|
|
400
|
+
| `nodeClick` | `ERDNodeClickEvent` | Node clicked (set `cancel=true` to prevent selection) |
|
|
401
|
+
| `nodeDoubleClick` | `ERDNodeDoubleClickEvent` | Node double-clicked |
|
|
402
|
+
| `nodeSelected` | `ERDNode` | Node selected after click |
|
|
403
|
+
| `nodeDeselected` | `void` | Current node deselected |
|
|
404
|
+
| `linkClick` | `ERDLinkClickEvent` | Relationship link clicked |
|
|
405
|
+
|
|
406
|
+
#### Hover Events
|
|
407
|
+
|
|
408
|
+
| Output | Type | Description |
|
|
409
|
+
|--------|------|-------------|
|
|
410
|
+
| `nodeHover` | `ERDNodeHoverEvent` | Mouse entered a node |
|
|
411
|
+
| `nodeHoverEnd` | `ERDNode` | Mouse left a node |
|
|
412
|
+
| `linkHover` | `ERDLinkHoverEvent` | Mouse entered a link |
|
|
413
|
+
| `linkHoverEnd` | `ERDLink` | Mouse left a link |
|
|
414
|
+
|
|
415
|
+
#### Context Menu Events
|
|
416
|
+
|
|
417
|
+
| Output | Type | Description |
|
|
418
|
+
|--------|------|-------------|
|
|
419
|
+
| `nodeContextMenu` | `ERDNodeContextMenuEvent` | Right-click on node |
|
|
420
|
+
| `linkContextMenu` | `ERDLinkContextMenuEvent` | Right-click on link |
|
|
421
|
+
| `diagramContextMenu` | `ERDDiagramContextMenuEvent` | Right-click on background |
|
|
422
|
+
|
|
423
|
+
#### Drag Events
|
|
424
|
+
|
|
425
|
+
| Output | Type | Description |
|
|
426
|
+
|--------|------|-------------|
|
|
427
|
+
| `nodeDragStart` | `ERDNodeDragEvent` | Drag started (set `cancel=true` to prevent) |
|
|
428
|
+
| `nodeDragEnd` | `ERDNodeDragEvent` | Drag ended |
|
|
429
|
+
|
|
430
|
+
#### Diagram Events
|
|
431
|
+
|
|
432
|
+
| Output | Type | Description |
|
|
433
|
+
|--------|------|-------------|
|
|
434
|
+
| `zoomChange` | `ERDZoomEvent` | Zoom level or pan position changed |
|
|
435
|
+
| `refreshRequested` | `void` | Refresh button clicked |
|
|
436
|
+
| `layoutComplete` | `void` | Force simulation completed layout |
|
|
437
|
+
| `stateChange` | `ERDState` | Any state change (selection, zoom, etc.) |
|
|
438
|
+
|
|
439
|
+
### Public Methods
|
|
440
|
+
|
|
441
|
+
Access via `@ViewChild(ERDDiagramComponent)`:
|
|
442
|
+
|
|
443
|
+
#### Zoom Control
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
zoomIn(): void // Zoom in 1.5x
|
|
447
|
+
zoomOut(): void // Zoom out 0.67x
|
|
448
|
+
resetZoom(): void // Reset to default zoom
|
|
449
|
+
zoomToNode(nodeId: string, scale?: number): void // Center on specific node
|
|
450
|
+
zoomToFit(padding?: number): void // Fit all nodes in view
|
|
451
|
+
centerDiagram(): void // Center without changing zoom
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
#### Selection
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
selectNode(nodeId: string): boolean // Programmatically select
|
|
458
|
+
deselectAll(): void // Clear all selections
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
#### Highlighting
|
|
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
|
|
467
|
+
getRelatedNodes(nodeId: string, depth?: number): ERDRelationshipInfo[]
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
#### State Management
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
getState(): ERDState // Get current state for persistence
|
|
474
|
+
setState(state: Partial<ERDState>, restorePositions?: boolean): void
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
#### Layout Control
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
freezeLayout(): void // Stop simulation, fix positions
|
|
481
|
+
unfreezeLayout(): void // Resume natural movement
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
#### Utilities
|
|
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
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
const config: ERDConfig = {
|
|
497
|
+
// Node Sizing
|
|
498
|
+
nodeWidth: 180, // Width of each node box
|
|
499
|
+
nodeBaseHeight: 60, // Base height before adding fields
|
|
500
|
+
fieldHeight: 20, // Height per field row
|
|
501
|
+
maxNodeHeight: 300, // Maximum node height
|
|
502
|
+
|
|
503
|
+
// Force Simulation
|
|
504
|
+
chargeStrength: -800, // Repulsion between nodes (negative)
|
|
505
|
+
linkDistance: 80, // Base distance between linked nodes
|
|
506
|
+
collisionPadding: 20, // Extra padding for collision detection
|
|
507
|
+
|
|
508
|
+
// Display Options
|
|
509
|
+
showFieldDetails: true, // Show PK/FK fields in nodes
|
|
510
|
+
showRelationshipLabels: true, // Show field names on links
|
|
511
|
+
showHeader: true, // Show header with controls
|
|
512
|
+
showNodeCount: true, // Show count in header
|
|
513
|
+
showMinimap: false, // Show navigation minimap
|
|
514
|
+
showLegend: false, // Show color legend
|
|
515
|
+
|
|
516
|
+
// Interaction
|
|
517
|
+
enableDragging: true, // Allow node dragging
|
|
518
|
+
enableZoom: true, // Enable mouse wheel zoom
|
|
519
|
+
enablePan: true, // Enable drag-to-pan
|
|
520
|
+
minZoom: 0.1, // Minimum zoom level
|
|
521
|
+
maxZoom: 4, // Maximum zoom level
|
|
522
|
+
initialZoom: 1, // Starting zoom level
|
|
523
|
+
enableMultiSelect: false, // Ctrl+click multi-select
|
|
524
|
+
|
|
525
|
+
// Animation
|
|
526
|
+
animationDuration: 750, // Transition duration in ms
|
|
527
|
+
fitOnLoad: true, // Auto-fit diagram on initial load
|
|
528
|
+
|
|
529
|
+
// Empty State
|
|
530
|
+
emptyStateMessage: 'No entities to display',
|
|
531
|
+
emptyStateIcon: 'fa-solid fa-diagram-project',
|
|
532
|
+
|
|
533
|
+
// Layout
|
|
534
|
+
layoutAlgorithm: 'force', // 'force' | 'horizontal' | 'vertical' | 'radial'
|
|
535
|
+
|
|
536
|
+
// Colors
|
|
537
|
+
colors: {
|
|
538
|
+
nodeBackground: '#f8f9fa',
|
|
539
|
+
nodeBorder: '#333',
|
|
540
|
+
nodeHeader: '#007bff',
|
|
541
|
+
nodeHeaderText: 'white',
|
|
542
|
+
primaryKeyBackground: '#fff3cd',
|
|
543
|
+
primaryKeyText: '#856404',
|
|
544
|
+
foreignKeyBackground: '#cce5ff',
|
|
545
|
+
foreignKeyText: '#004085',
|
|
546
|
+
linkColor: '#666',
|
|
547
|
+
selectedBorder: '#4CAF50',
|
|
548
|
+
highlightBorder: '#ff9800',
|
|
549
|
+
relatedBorder: '#ff6b35'
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
## Data Interfaces
|
|
555
|
+
|
|
556
|
+
### ERDNode
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
interface ERDNode {
|
|
560
|
+
id: string; // Unique identifier
|
|
561
|
+
name: string; // Display name
|
|
562
|
+
schemaName?: string; // Optional schema/namespace
|
|
563
|
+
description?: string; // Optional description
|
|
564
|
+
status?: string; // e.g., 'Active', 'Deprecated'
|
|
565
|
+
baseTable?: string; // Underlying table name
|
|
566
|
+
fields: ERDField[]; // All fields in this node
|
|
567
|
+
customData?: Record<string, unknown>; // Additional data
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### ERDField
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
interface ERDField {
|
|
575
|
+
id: string; // Unique identifier
|
|
576
|
+
name: string; // Display name
|
|
577
|
+
type?: string; // Data type (e.g., 'string', 'int')
|
|
578
|
+
isPrimaryKey: boolean; // Is this a PK?
|
|
579
|
+
relatedNodeId?: string; // FK target node ID
|
|
580
|
+
relatedNodeName?: string; // FK target node name
|
|
581
|
+
relatedFieldName?: string; // FK target field name
|
|
582
|
+
description?: string;
|
|
583
|
+
allowsNull?: boolean;
|
|
584
|
+
defaultValue?: string;
|
|
585
|
+
length?: number; // For string fields
|
|
586
|
+
precision?: number; // For numeric fields
|
|
587
|
+
scale?: number;
|
|
588
|
+
isVirtual?: boolean; // Computed field
|
|
589
|
+
autoIncrement?: boolean;
|
|
590
|
+
possibleValues?: ERDFieldValue[];
|
|
591
|
+
customData?: Record<string, unknown>;
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### ERDLink
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
interface ERDLink {
|
|
599
|
+
sourceNodeId: string;
|
|
600
|
+
targetNodeId: string;
|
|
601
|
+
sourceField: ERDField;
|
|
602
|
+
targetField?: ERDField;
|
|
603
|
+
isSelfReference: boolean;
|
|
604
|
+
relationshipType?: 'one-to-one' | 'one-to-many' | 'many-to-one' | 'many-to-many';
|
|
605
|
+
label?: string;
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
## State Persistence
|
|
610
|
+
|
|
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
|
+
interface ERDState {
|
|
631
|
+
selectedNodeId: string | null;
|
|
632
|
+
highlightedNodeIds: string[];
|
|
633
|
+
zoomLevel: number;
|
|
634
|
+
translateX: number;
|
|
635
|
+
translateY: number;
|
|
636
|
+
focusNodeId: string | null;
|
|
637
|
+
focusDepth: number;
|
|
638
|
+
nodePositions: Record<string, {
|
|
639
|
+
x: number;
|
|
640
|
+
y: number;
|
|
641
|
+
fx?: number | null;
|
|
642
|
+
fy?: number | null;
|
|
643
|
+
}>;
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
## Event Handling Examples
|
|
648
|
+
|
|
649
|
+
### Custom Context Menu
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
onNodeContextMenu(event: ERDNodeContextMenuEvent) {
|
|
653
|
+
event.cancel = true; // Prevent browser context menu
|
|
654
|
+
|
|
655
|
+
this.contextMenuItems = [
|
|
656
|
+
{ label: 'View Details', action: () => this.viewDetails(event.node) },
|
|
657
|
+
{ label: 'Edit Entity', action: () => this.editEntity(event.node) },
|
|
658
|
+
{ label: 'Show Related', action: () => this.erdDiagram.highlightRelated(event.node.id) }
|
|
659
|
+
];
|
|
660
|
+
|
|
661
|
+
this.contextMenuPosition = event.position;
|
|
662
|
+
this.showContextMenu = true;
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### Tooltips on Hover
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
onNodeHover(event: ERDNodeHoverEvent) {
|
|
670
|
+
this.tooltip = {
|
|
671
|
+
visible: true,
|
|
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
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
onNodeClick(event: ERDNodeClickEvent) {
|
|
687
|
+
if (event.node.customData?.['locked']) {
|
|
688
|
+
event.cancel = true;
|
|
689
|
+
this.showMessage('This entity is locked');
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
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
|
+
## Performance Considerations
|
|
742
|
+
|
|
743
|
+
- For diagrams with 100+ nodes, consider using focus mode to show subsets
|
|
744
|
+
- Use `freezeLayout()` after the initial layout settles for static diagrams
|
|
745
|
+
- Adjust `chargeStrength` and `linkDistance` for different node densities
|
|
746
|
+
- Set `fitOnLoad: false` if you're restoring state to avoid double-animation
|
|
747
|
+
|
|
748
|
+
## Browser Support
|
|
749
|
+
|
|
750
|
+
- Chrome 80+
|
|
751
|
+
- Firefox 75+
|
|
752
|
+
- Safari 13+
|
|
753
|
+
- Edge 80+
|
|
754
|
+
|
|
755
|
+
Requires support for:
|
|
756
|
+
- SVG
|
|
757
|
+
- ResizeObserver
|
|
758
|
+
- CSS Grid/Flexbox
|
|
759
|
+
|
|
760
|
+
## Dependencies
|
|
761
|
+
|
|
762
|
+
- Angular 18+
|
|
763
|
+
- D3.js 7+
|
|
764
|
+
- RxJS 7+
|
|
765
|
+
|
|
766
|
+
## License
|
|
767
|
+
|
|
768
|
+
MIT
|
|
769
|
+
|
|
770
|
+
## Contributing
|
|
771
|
+
|
|
772
|
+
Issues and PRs welcome at https://github.com/MemberJunction/MJ
|