Skip to content

@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.

This package provides two complementary components for visualizing entity relationships:

  1. <mj-entity-erd> — A higher-level wrapper that accepts MemberJunction EntityInfo[] directly, handles automatic relationship discovery, and supports configurable depth traversal.
  2. <mj-erd-diagram> — A generic, lower-level component that works with plain ERDNode[] 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
Terminal window
npm install @memberjunction/ng-entity-relationship-diagram
Use CaseComponentWhy
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 { EntityRelationshipDiagramModule } from '@memberjunction/ng-entity-relationship-diagram';
@NgModule({
imports: [EntityRelationshipDiagramModule]
})
export class YourModule { }
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
}
}
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;
}
}

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>
DepthDescription
0Show only the focus node
1Focus node + directly related nodes
2Focus node + nodes within 2 relationship hops
3+Continue expanding the relationship graph
InputTypeDefaultDescription
entitiesEntityInfo[][]Primary entities to display
allEntitiesEntityInfo[][]All entities for relationship discovery
selectedEntityIdstring | nullnullID of currently selected entity
depthnumber1Relationship hops to include
includeIncomingbooleantrueInclude entities referencing primary entities
includeOutgoingbooleantrueInclude entities referenced by primary entities
showHeaderbooleantrueShow header with zoom controls
headerTitlestring'Entity Relationship Diagram'Header title
isRefreshingbooleanfalseShow loading overlay
readOnlybooleanfalseDisable interactions
configERDConfig{}Configuration options
OutputTypeDescription
entitySelectedEntityInfoEntity was selected
entityDeselectedvoidSelection was cleared
openRecordEntityInfoEntity double-clicked
stateChangeERDStateDiagram state changed
InputTypeDefaultDescription
nodesERDNode[][]Nodes (entities) to display
selectedNodeIdstring | nullnullCurrently selected node
highlightedNodeIdsstring[][]Highlighted node IDs
focusNodeIdstring | nullnullFocus mode target
focusDepthnumber1Relationship hops in focus mode
isRefreshingbooleanfalseShow loading overlay
readOnlybooleanfalseDisable dragging and selection
configERDConfig{}Configuration options
showHeaderbooleantrueShow header with controls
headerTitlestring'Entity Relationship Diagram'Title in header

Selection Events:

OutputTypeDescription
nodeClickERDNodeClickEventNode clicked (cancelable)
nodeDoubleClickERDNodeDoubleClickEventNode double-clicked
nodeSelectedERDNodeNode selected
nodeDeselectedvoidSelection cleared
linkClickERDLinkClickEventRelationship link clicked

Hover Events:

OutputTypeDescription
nodeHoverERDNodeHoverEventMouse entered a node
nodeHoverEndERDNodeMouse left a node
linkHoverERDLinkHoverEventMouse entered a link
linkHoverEndERDLinkMouse left a link

Context Menu Events:

OutputTypeDescription
nodeContextMenuERDNodeContextMenuEventRight-click on node
linkContextMenuERDLinkContextMenuEventRight-click on link
diagramContextMenuERDDiagramContextMenuEventRight-click on background

Diagram Events:

OutputTypeDescription
nodeDragStartERDNodeDragEventDrag started (cancelable)
nodeDragEndERDNodeDragEventDrag ended
zoomChangeERDZoomEventZoom level or pan changed
refreshRequestedvoidRefresh button clicked
layoutCompletevoidForce simulation completed
stateChangeERDStateAny state change

Access via @ViewChild(ERDDiagramComponent):

// Zoom control
zoomIn(): void
zoomOut(): void
resetZoom(): void
zoomToNode(nodeId: string, scale?: number): void
zoomToFit(padding?: number): void
centerDiagram(): void
// Selection
selectNode(nodeId: string): boolean
deselectAll(): void
// Highlighting
highlightNode(nodeId: string): void
clearHighlights(): void
highlightRelated(nodeId: string, depth?: number): void
getRelatedNodes(nodeId: string, depth?: number): ERDRelationshipInfo[]
// State management
getState(): ERDState
setState(state: Partial<ERDState>, restorePositions?: boolean): void
// Layout control
freezeLayout(): void
unfreezeLayout(): void
// Utilities
refresh(): void
triggerResize(): void
getZoomState(): ERDZoomEvent
exportAsSVG(): string
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'
}
};
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 }>;
}

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 discovery
const { nodes, links } = buildERDDataFromEntities(primaryEntities, {
allEntities: md.Entities,
includeIncoming: true,
includeOutgoing: true,
depth: 2
});
// Save diagram state
const state = this.erdDiagram.getState();
localStorage.setItem('erd-state', JSON.stringify(state));
// Restore state on init
ngAfterViewInit() {
const savedState = localStorage.getItem('erd-state');
if (savedState) {
this.erdDiagram.setState(JSON.parse(savedState));
}
}
  • For 100+ node diagrams, use focus mode to show subsets
  • Call freezeLayout() after the initial layout settles for static diagrams
  • Adjust chargeStrength and linkDistance for different node densities
  • Set fitOnLoad: false when restoring state to avoid double-animation
PackageDescription
@memberjunction/coreCore framework (for entity wrapper)
d3D3.js force-directed graph engine
@dagrejs/dagreDAG layout algorithms
@progress/kendo-angular-layoutLayout components
rxjsReactive extensions
  • @angular/common ^21.x
  • @angular/core ^21.x
  • @angular/platform-browser ^21.x
Terminal window
cd packages/Angular/Generic/entity-relationship-diagram
npm run build

ISC