@memberjunction/ng-entity-relationship-diagram
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.
Overview
Section titled “Overview”This package provides two complementary components for visualizing entity relationships:
<mj-entity-erd>— A higher-level wrapper that accepts MemberJunctionEntityInfo[]directly, handles automatic relationship discovery, and supports configurable depth traversal.<mj-erd-diagram>— A generic, lower-level component that works with plainERDNode[]data, suitable for any project regardless of MemberJunction usage.
flowchart LR
subgraph MJWrapper["mj-entity-erd"]
EI[EntityInfo Array] --> TRANSFORM[Transform & Discover]
TRANSFORM --> NODES[ERDNode + ERDLink]
end
subgraph Generic["mj-erd-diagram"]
NODES --> D3[D3.js Force Simulation]
D3 --> SVG[Interactive SVG]
SVG --> EVENTS[Selection / Zoom / Drag Events]
end
subgraph Features["Capabilities"]
FOCUS[Focus Mode]
STATE[State Persistence]
EXPORT[SVG Export]
end
SVG --> Features
style MJWrapper fill:#2d6a9f,stroke:#1a4971,color:#fff
style Generic fill:#7c5295,stroke:#563a6b,color:#fff
style Features fill:#2d8659,stroke:#1a5c3a,color:#fff
Installation
Section titled “Installation”npm install @memberjunction/ng-entity-relationship-diagramWhich Component Should I Use?
Section titled “Which Component Should I Use?”| Use Case | Component | Why |
|---|---|---|
| MemberJunction project | <mj-entity-erd> | Auto-discovers relationships, handles transformation |
| Custom data sources | <mj-erd-diagram> | Full control over node/link data |
| Single entity + related | <mj-entity-erd> | Built-in depth/relationship discovery |
| Non-MJ Angular project | <mj-erd-diagram> | No MJ dependencies in your code |
Import the Module
Section titled “Import the Module”import { EntityRelationshipDiagramModule } from '@memberjunction/ng-entity-relationship-diagram';
@NgModule({ imports: [EntityRelationshipDiagramModule]})export class YourModule { }MemberJunction Entity Wrapper
Section titled “MemberJunction Entity Wrapper”import { EntityInfo, Metadata } from '@memberjunction/core';
@Component({ selector: 'my-entity-viewer', template: ` <mj-entity-erd [entities]="selectedEntities" [allEntities]="allEntities" [selectedEntityId]="currentEntityId" [depth]="1" [includeIncoming]="true" [includeOutgoing]="true" (entitySelected)="onEntitySelected($event)" (openRecord)="onOpenEntity($event)"> </mj-entity-erd> `})export class MyEntityViewerComponent { allEntities: EntityInfo[] = []; selectedEntities: EntityInfo[] = []; currentEntityId: string | null = null;
constructor() { const md = new Metadata(); this.allEntities = md.Entities; }
onEntitySelected(entity: EntityInfo) { this.currentEntityId = entity.ID; }
onOpenEntity(entity: EntityInfo) { // Navigate to entity record }}Generic ERD Component
Section titled “Generic ERD Component”import { ERDNode, ERDNodeClickEvent } from '@memberjunction/ng-entity-relationship-diagram';
@Component({ selector: 'my-schema-viewer', template: ` <mj-erd-diagram [nodes]="entityNodes" [selectedNodeId]="selectedId" (nodeSelected)="onNodeSelected($event)" (nodeDoubleClick)="onNodeDoubleClick($event)"> </mj-erd-diagram> `})export class SchemaViewerComponent { entityNodes: ERDNode[] = [ { id: 'users', name: 'Users', fields: [ { id: 'id', name: 'ID', isPrimaryKey: true, type: 'uuid' }, { id: 'email', name: 'Email', isPrimaryKey: false, type: 'string' }, { id: 'roleId', name: 'RoleID', isPrimaryKey: false, relatedNodeId: 'roles', relatedFieldName: 'ID' } ] }, { id: 'roles', name: 'Roles', fields: [ { id: 'id', name: 'ID', isPrimaryKey: true, type: 'uuid' }, { id: 'name', name: 'Name', isPrimaryKey: false, type: 'string' } ] } ]; selectedId: string | null = null;
onNodeSelected(node: ERDNode) { this.selectedId = node.id; }}Focus Mode
Section titled “Focus Mode”Show a single entity with its N-hop related entities:
<mj-erd-diagram [nodes]="allNodes" [focusNodeId]="'users'" [focusDepth]="1" (nodeSelected)="onNodeSelected($event)"></mj-erd-diagram>| Depth | Description |
|---|---|
0 | Show only the focus node |
1 | Focus node + directly related nodes |
2 | Focus node + nodes within 2 relationship hops |
3+ | Continue expanding the relationship graph |
API Reference
Section titled “API Reference”<mj-entity-erd> Inputs
Section titled “<mj-entity-erd> Inputs”| Input | Type | Default | Description |
|---|---|---|---|
entities | EntityInfo[] | [] | Primary entities to display |
allEntities | EntityInfo[] | [] | All entities for relationship discovery |
selectedEntityId | string | null | null | ID of currently selected entity |
depth | number | 1 | Relationship hops to include |
includeIncoming | boolean | true | Include entities referencing primary entities |
includeOutgoing | boolean | true | Include entities referenced by primary entities |
showHeader | boolean | true | Show header with zoom controls |
headerTitle | string | 'Entity Relationship Diagram' | Header title |
isRefreshing | boolean | false | Show loading overlay |
readOnly | boolean | false | Disable interactions |
config | ERDConfig | {} | Configuration options |
<mj-entity-erd> Outputs
Section titled “<mj-entity-erd> Outputs”| Output | Type | Description |
|---|---|---|
entitySelected | EntityInfo | Entity was selected |
entityDeselected | void | Selection was cleared |
openRecord | EntityInfo | Entity double-clicked |
stateChange | ERDState | Diagram state changed |
<mj-erd-diagram> Inputs
Section titled “<mj-erd-diagram> Inputs”| Input | Type | Default | Description |
|---|---|---|---|
nodes | ERDNode[] | [] | Nodes (entities) to display |
selectedNodeId | string | null | null | Currently selected node |
highlightedNodeIds | string[] | [] | Highlighted node IDs |
focusNodeId | string | null | null | Focus mode target |
focusDepth | number | 1 | Relationship hops in focus mode |
isRefreshing | boolean | false | Show loading overlay |
readOnly | boolean | false | Disable dragging and selection |
config | ERDConfig | {} | Configuration options |
showHeader | boolean | true | Show header with controls |
headerTitle | string | 'Entity Relationship Diagram' | Title in header |
<mj-erd-diagram> Outputs
Section titled “<mj-erd-diagram> Outputs”Selection Events:
| Output | Type | Description |
|---|---|---|
nodeClick | ERDNodeClickEvent | Node clicked (cancelable) |
nodeDoubleClick | ERDNodeDoubleClickEvent | Node double-clicked |
nodeSelected | ERDNode | Node selected |
nodeDeselected | void | Selection cleared |
linkClick | ERDLinkClickEvent | Relationship link clicked |
Hover Events:
| Output | Type | Description |
|---|---|---|
nodeHover | ERDNodeHoverEvent | Mouse entered a node |
nodeHoverEnd | ERDNode | Mouse left a node |
linkHover | ERDLinkHoverEvent | Mouse entered a link |
linkHoverEnd | ERDLink | Mouse left a link |
Context Menu Events:
| Output | Type | Description |
|---|---|---|
nodeContextMenu | ERDNodeContextMenuEvent | Right-click on node |
linkContextMenu | ERDLinkContextMenuEvent | Right-click on link |
diagramContextMenu | ERDDiagramContextMenuEvent | Right-click on background |
Diagram Events:
| Output | Type | Description |
|---|---|---|
nodeDragStart | ERDNodeDragEvent | Drag started (cancelable) |
nodeDragEnd | ERDNodeDragEvent | Drag ended |
zoomChange | ERDZoomEvent | Zoom level or pan changed |
refreshRequested | void | Refresh button clicked |
layoutComplete | void | Force simulation completed |
stateChange | ERDState | Any state change |
Public Methods
Section titled “Public Methods”Access via @ViewChild(ERDDiagramComponent):
// Zoom controlzoomIn(): voidzoomOut(): voidresetZoom(): voidzoomToNode(nodeId: string, scale?: number): voidzoomToFit(padding?: number): voidcenterDiagram(): void
// SelectionselectNode(nodeId: string): booleandeselectAll(): void
// HighlightinghighlightNode(nodeId: string): voidclearHighlights(): voidhighlightRelated(nodeId: string, depth?: number): voidgetRelatedNodes(nodeId: string, depth?: number): ERDRelationshipInfo[]
// State managementgetState(): ERDStatesetState(state: Partial<ERDState>, restorePositions?: boolean): void
// Layout controlfreezeLayout(): voidunfreezeLayout(): void
// Utilitiesrefresh(): voidtriggerResize(): voidgetZoomState(): ERDZoomEventexportAsSVG(): stringConfiguration
Section titled “Configuration”const config: ERDConfig = { // Node sizing nodeWidth: 180, nodeBaseHeight: 60, fieldHeight: 20, maxNodeHeight: 300,
// Force simulation chargeStrength: -800, linkDistance: 80, collisionPadding: 20,
// Display showFieldDetails: true, showRelationshipLabels: true, showHeader: true, showNodeCount: true, showMinimap: false, showLegend: false,
// Interaction enableDragging: true, enableZoom: true, enablePan: true, minZoom: 0.1, maxZoom: 4, initialZoom: 1, enableMultiSelect: false,
// Animation animationDuration: 750, fitOnLoad: true,
// Layout layoutAlgorithm: 'force', // 'force' | 'horizontal' | 'vertical' | 'radial'
// Colors colors: { nodeBackground: '#f8f9fa', nodeBorder: '#333', nodeHeader: '#007bff', nodeHeaderText: 'white', primaryKeyBackground: '#fff3cd', foreignKeyBackground: '#cce5ff', linkColor: '#666', selectedBorder: '#4CAF50', highlightBorder: '#ff9800', relatedBorder: '#ff6b35' }};Data Interfaces
Section titled “Data Interfaces”interface ERDNode { id: string; name: string; schemaName?: string; description?: string; status?: string; baseTable?: string; fields: ERDField[]; customData?: Record<string, unknown>;}
interface ERDField { id: string; name: string; type?: string; isPrimaryKey: boolean; relatedNodeId?: string; relatedNodeName?: string; relatedFieldName?: string; description?: string; allowsNull?: boolean; defaultValue?: string; length?: number; isVirtual?: boolean; autoIncrement?: boolean; customData?: Record<string, unknown>;}
interface ERDLink { sourceNodeId: string; targetNodeId: string; sourceField: ERDField; targetField?: ERDField; isSelfReference: boolean; relationshipType?: 'one-to-one' | 'one-to-many' | 'many-to-one' | 'many-to-many'; label?: string;}
interface ERDState { selectedNodeId: string | null; highlightedNodeIds: string[]; zoomLevel: number; translateX: number; translateY: number; focusNodeId: string | null; focusDepth: number; nodePositions: Record<string, { x: number; y: number; fx?: number | null; fy?: number | null }>;}Utility Functions
Section titled “Utility Functions”The package exports utility functions for transforming MemberJunction entities to ERD data:
import { buildERDDataFromEntities, entityInfoToERDNode, entityFieldToERDField, entitiesToERDNodes, getOriginalEntityFromERDNode, findEntityByNodeId} from '@memberjunction/ng-entity-relationship-diagram';
// Build complete ERD data with automatic relationship discoveryconst { nodes, links } = buildERDDataFromEntities(primaryEntities, { allEntities: md.Entities, includeIncoming: true, includeOutgoing: true, depth: 2});State Persistence
Section titled “State Persistence”// Save diagram stateconst state = this.erdDiagram.getState();localStorage.setItem('erd-state', JSON.stringify(state));
// Restore state on initngAfterViewInit() { const savedState = localStorage.getItem('erd-state'); if (savedState) { this.erdDiagram.setState(JSON.parse(savedState)); }}Performance Considerations
Section titled “Performance Considerations”- For 100+ node diagrams, use focus mode to show subsets
- Call
freezeLayout()after the initial layout settles for static diagrams - Adjust
chargeStrengthandlinkDistancefor different node densities - Set
fitOnLoad: falsewhen restoring state to avoid double-animation
Dependencies
Section titled “Dependencies”| Package | Description |
|---|---|
@memberjunction/core | Core framework (for entity wrapper) |
d3 | D3.js force-directed graph engine |
@dagrejs/dagre | DAG layout algorithms |
@progress/kendo-angular-layout | Layout components |
rxjs | Reactive extensions |
Peer Dependencies
Section titled “Peer Dependencies”@angular/common^21.x@angular/core^21.x@angular/platform-browser^21.x
cd packages/Angular/Generic/entity-relationship-diagramnpm run buildLicense
Section titled “License”ISC