@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
- A package providing **two Angular components** for rendering interactive Entity Relationship Diagrams (ERD) using D3.js force-directed graphs:
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
- 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
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
- | **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 |
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
- ## `<mj-entity-erd>` - MemberJunction Entity Wrapper
53
+ ## Usage
20
54
 
21
- The higher-level wrapper component designed specifically for MemberJunction projects. It accepts `EntityInfo[]` directly and handles all the transformation logic internally.
55
+ ### Import the Module
22
56
 
23
- ### Features
57
+ ```typescript
58
+ import { EntityRelationshipDiagramModule } from '@memberjunction/ng-entity-relationship-diagram';
24
59
 
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
60
+ @NgModule({
61
+ imports: [EntityRelationshipDiagramModule]
62
+ })
63
+ export class YourModule { }
64
+ ```
30
65
 
31
- ### Basic Usage
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
- ### 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
- ```
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
- export class YourComponent {
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
- **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
- ```
150
+ ### Focus Mode
338
151
 
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
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
- | `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 |
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
- #### State Inputs
188
+ ### `<mj-entity-erd>` Outputs
380
189
 
381
- | Input | Type | Default | Description |
382
- |-------|------|---------|-------------|
383
- | `isRefreshing` | `boolean` | `false` | Shows loading overlay when true |
384
- | `readOnly` | `boolean` | `false` | Disables dragging and selection |
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
- #### Configuration Inputs
197
+ ### `<mj-erd-diagram>` Inputs
387
198
 
388
199
  | Input | Type | Default | Description |
389
200
  |-------|------|---------|-------------|
390
- | `config` | `ERDConfig` | `{}` | Configuration options (see below) |
391
- | `showHeader` | `boolean` | `true` | Show header with zoom controls |
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 (Events)
212
+ ### `<mj-erd-diagram>` Outputs
395
213
 
396
- #### Selection Events
214
+ **Selection Events:**
397
215
 
398
216
  | Output | Type | Description |
399
217
  |--------|------|-------------|
400
- | `nodeClick` | `ERDNodeClickEvent` | Node clicked (set `cancel=true` to prevent selection) |
218
+ | `nodeClick` | `ERDNodeClickEvent` | Node clicked (cancelable) |
401
219
  | `nodeDoubleClick` | `ERDNodeDoubleClickEvent` | Node double-clicked |
402
- | `nodeSelected` | `ERDNode` | Node selected after click |
403
- | `nodeDeselected` | `void` | Current node deselected |
220
+ | `nodeSelected` | `ERDNode` | Node selected |
221
+ | `nodeDeselected` | `void` | Selection cleared |
404
222
  | `linkClick` | `ERDLinkClickEvent` | Relationship link clicked |
405
223
 
406
- #### Hover Events
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
- #### Context Menu Events
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
- #### Drag Events
241
+ **Diagram Events:**
424
242
 
425
243
  | Output | Type | Description |
426
244
  |--------|------|-------------|
427
- | `nodeDragStart` | `ERDNodeDragEvent` | Drag started (set `cancel=true` to prevent) |
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 layout |
437
- | `stateChange` | `ERDState` | Any state change (selection, zoom, etc.) |
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
- 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
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
- ```typescript
473
- getState(): ERDState // Get current state for persistence
275
+ // State management
276
+ getState(): ERDState
474
277
  setState(state: Partial<ERDState>, restorePositions?: boolean): void
475
- ```
476
278
 
477
- #### Layout Control
279
+ // Layout control
280
+ freezeLayout(): void
281
+ unfreezeLayout(): void
478
282
 
479
- ```typescript
480
- freezeLayout(): void // Stop simulation, fix positions
481
- unfreezeLayout(): void // Resume natural movement
283
+ // Utilities
284
+ refresh(): void
285
+ triggerResize(): void
286
+ getZoomState(): ERDZoomEvent
287
+ exportAsSVG(): string
482
288
  ```
483
289
 
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
290
+ ## Configuration
494
291
 
495
292
  ```typescript
496
293
  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
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, // 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
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, // 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',
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; // 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
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; // 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
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; // For string fields
586
- precision?: number; // For numeric fields
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
- ## Event Handling Examples
398
+ ## Utility Functions
648
399
 
649
- ### Custom Context Menu
400
+ The package exports utility functions for transforming MemberJunction entities to ERD data:
650
401
 
651
402
  ```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
- ];
403
+ import {
404
+ buildERDDataFromEntities,
405
+ entityInfoToERDNode,
406
+ entityFieldToERDField,
407
+ entitiesToERDNodes,
408
+ getOriginalEntityFromERDNode,
409
+ findEntityByNodeId
410
+ } from '@memberjunction/ng-entity-relationship-diagram';
660
411
 
661
- this.contextMenuPosition = event.position;
662
- this.showContextMenu = true;
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
- ### Tooltips on Hover
421
+ ## State Persistence
667
422
 
668
423
  ```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
424
+ // Save diagram state
425
+ const state = this.erdDiagram.getState();
426
+ localStorage.setItem('erd-state', JSON.stringify(state));
684
427
 
685
- ```typescript
686
- onNodeClick(event: ERDNodeClickEvent) {
687
- if (event.node.customData?.['locked']) {
688
- event.cancel = true;
689
- this.showMessage('This entity is locked');
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 diagrams with 100+ nodes, consider using focus mode to show subsets
744
- - Use `freezeLayout()` after the initial layout settles for static diagrams
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` if you're restoring state to avoid double-animation
747
-
748
- ## Browser Support
442
+ - Set `fitOnLoad: false` when restoring state to avoid double-animation
749
443
 
750
- - Chrome 80+
751
- - Firefox 75+
752
- - Safari 13+
753
- - Edge 80+
444
+ ## Dependencies
754
445
 
755
- Requires support for:
756
- - SVG
757
- - ResizeObserver
758
- - CSS Grid/Flexbox
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
- ## Dependencies
454
+ ### Peer Dependencies
761
455
 
762
- - Angular 21+
763
- - D3.js 7+
764
- - RxJS 7+
456
+ - `@angular/common` ^21.x
457
+ - `@angular/core` ^21.x
458
+ - `@angular/platform-browser` ^21.x
765
459
 
766
- ## License
460
+ ## Build
767
461
 
768
- MIT
462
+ ```bash
463
+ cd packages/Angular/Generic/entity-relationship-diagram
464
+ npm run build
465
+ ```
769
466
 
770
- ## Contributing
467
+ ## License
771
468
 
772
- Issues and PRs welcome at https://github.com/MemberJunction/MJ
469
+ ISC