@memberjunction/ng-entity-relationship-diagram 5.30.1 → 5.31.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.
Files changed (40) hide show
  1. package/dist/__tests__/compute-erd-layout.test.d.ts +2 -0
  2. package/dist/__tests__/compute-erd-layout.test.d.ts.map +1 -0
  3. package/dist/__tests__/compute-erd-layout.test.js +207 -0
  4. package/dist/__tests__/compute-erd-layout.test.js.map +1 -0
  5. package/dist/lib/components/entity-details/entity-details.component.d.ts +2 -1
  6. package/dist/lib/components/entity-details/entity-details.component.d.ts.map +1 -1
  7. package/dist/lib/components/entity-details/entity-details.component.js +7 -7
  8. package/dist/lib/components/entity-details/entity-details.component.js.map +1 -1
  9. package/dist/lib/components/entity-filter-panel/entity-filter-panel.component.d.ts +10 -4
  10. package/dist/lib/components/entity-filter-panel/entity-filter-panel.component.d.ts.map +1 -1
  11. package/dist/lib/components/entity-filter-panel/entity-filter-panel.component.js +60 -72
  12. package/dist/lib/components/entity-filter-panel/entity-filter-panel.component.js.map +1 -1
  13. package/dist/lib/components/erd-composite/erd-composite.component.d.ts +4 -1
  14. package/dist/lib/components/erd-composite/erd-composite.component.d.ts.map +1 -1
  15. package/dist/lib/components/erd-composite/erd-composite.component.js +22 -8
  16. package/dist/lib/components/erd-composite/erd-composite.component.js.map +1 -1
  17. package/dist/lib/components/erd-diagram.component.d.ts +169 -443
  18. package/dist/lib/components/erd-diagram.component.d.ts.map +1 -1
  19. package/dist/lib/components/erd-diagram.component.js +916 -1777
  20. package/dist/lib/components/erd-diagram.component.js.map +1 -1
  21. package/dist/lib/components/mj-entity-erd.component.d.ts +3 -2
  22. package/dist/lib/components/mj-entity-erd.component.d.ts.map +1 -1
  23. package/dist/lib/components/mj-entity-erd.component.js +6 -6
  24. package/dist/lib/components/mj-entity-erd.component.js.map +1 -1
  25. package/dist/lib/entity-relationship-diagram.module.d.ts +2 -1
  26. package/dist/lib/entity-relationship-diagram.module.d.ts.map +1 -1
  27. package/dist/lib/entity-relationship-diagram.module.js +9 -3
  28. package/dist/lib/entity-relationship-diagram.module.js.map +1 -1
  29. package/dist/lib/interfaces/erd-types.d.ts +38 -7
  30. package/dist/lib/interfaces/erd-types.d.ts.map +1 -1
  31. package/dist/lib/interfaces/erd-types.js.map +1 -1
  32. package/dist/lib/layout/compute-dagre-layout.d.ts +32 -0
  33. package/dist/lib/layout/compute-dagre-layout.d.ts.map +1 -0
  34. package/dist/lib/layout/compute-dagre-layout.js +168 -0
  35. package/dist/lib/layout/compute-dagre-layout.js.map +1 -0
  36. package/dist/lib/layout/compute-erd-layout.d.ts +103 -0
  37. package/dist/lib/layout/compute-erd-layout.d.ts.map +1 -0
  38. package/dist/lib/layout/compute-erd-layout.js +254 -0
  39. package/dist/lib/layout/compute-erd-layout.js.map +1 -0
  40. package/package.json +6 -5
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compute-dagre-layout.js","sourceRoot":"","sources":["../../../src/lib/layout/compute-dagre-layout.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Classic hierarchical (dagre) ERD layout — the \"old school\" option.\n *\n * Produces the same `ErdLayout` shape as `compute-erd-layout.ts` so the\n * component's template and CSS render identical cards; only position\n * (x / y) and edge routes differ. Schema bands are omitted in this mode\n * — the whole point of the hierarchical layout is that the vertical/\n * horizontal axis communicates relationship direction, not schema\n * grouping.\n *\n * Implementation:\n * 1. Measure every card using the same `fieldsForNode` logic the\n * schema-grid uses, so card dimensions + visible fields match\n * exactly.\n * 2. Feed nodes + FK edges into dagre with `rankdir: 'LR'` (left-to-\n * right hierarchy: sources on the left, leaves on the right).\n * 3. Convert dagre's centred coords to top-left card coords.\n * 4. Build orthogonal 3-segment edges (same router the schema-grid\n * uses) so markers + hit targets match.\n */\n\nimport dagre from '@dagrejs/dagre';\nimport type { ERDNode, ERDField } from '../interfaces/erd-types';\nimport type { ErdLayout, ErdLayoutOptions, LaidOutNode, LaidOutEdge } from './compute-erd-layout';\n\n// Mirrors the private helper in compute-erd-layout.ts — kept in sync so\n// both layouts produce identical card dimensions for the same input.\nfunction fieldsForNode(\n node: ERDNode,\n expanded: boolean,\n showAll: boolean,\n headerH: number,\n fieldH: number,\n moreH: number,\n): { visibleFields: ERDField[]; height: number } {\n const visibleFields = expanded || showAll\n ? node.fields\n : node.fields.filter(f => f.isPrimaryKey || !!f.relatedNodeId);\n\n const hasMore = !expanded && !showAll && node.fields.length > visibleFields.length;\n const rowCount = Math.max(visibleFields.length, 1);\n const height = headerH + rowCount * fieldH + 8 + (hasMore ? moreH : 4);\n return { visibleFields, height };\n}\n\nexport interface DagreLayoutOptions extends ErdLayoutOptions {\n /** Direction of the hierarchy. Default 'LR' (left-to-right). */\n rankDir?: 'TB' | 'BT' | 'LR' | 'RL';\n /** Horizontal separation between nodes in the same rank. Default 80. */\n nodeSep?: number;\n /** Separation between ranks. Default 120. */\n rankSep?: number;\n}\n\nexport function computeDagreLayout(nodes: ERDNode[], options: DagreLayoutOptions = {}): ErdLayout {\n if (nodes.length === 0) {\n return { nodes: [], edges: [], bands: [], totalWidth: 0, totalHeight: 0 };\n }\n\n const nodeW = options.nodeWidth ?? 220;\n const headerH = options.headerHeight ?? 36;\n const fieldH = options.fieldHeight ?? 22;\n const moreH = options.moreToggleHeight ?? 22;\n const canvasPad = options.canvasPad ?? 40;\n const rankDir = options.rankDir ?? 'LR';\n const nodeSep = options.nodeSep ?? 80;\n const rankSep = options.rankSep ?? 120;\n const expandedIds = options.expandedNodeIds ?? new Set<string>();\n const showAll = options.showAllFields ?? false;\n\n // Step 1: measure cards.\n type Measured = { node: ERDNode; visibleFields: ERDField[]; height: number };\n const measured: Measured[] = nodes.map(n => {\n const expanded = expandedIds.has(n.id) || showAll;\n const { visibleFields, height } = fieldsForNode(n, expanded, showAll, headerH, fieldH, moreH);\n return { node: n, visibleFields, height };\n });\n\n // Step 2: build dagre graph.\n const g = new dagre.graphlib.Graph<{ label: string }>({ directed: true, multigraph: true });\n g.setGraph({ rankdir: rankDir, nodesep: nodeSep, ranksep: rankSep, marginx: canvasPad, marginy: canvasPad });\n g.setDefaultEdgeLabel(() => ({}));\n\n for (const m of measured) {\n g.setNode(m.node.id, { width: nodeW, height: m.height });\n }\n\n const visibleIds = new Set(measured.map(m => m.node.id));\n for (const m of measured) {\n for (const f of m.node.fields) {\n if (!f.relatedNodeId || !visibleIds.has(f.relatedNodeId)) continue;\n // Dagre doesn't love self-loops for ranking; skip — they're still\n // rendered via the edge router below.\n if (f.relatedNodeId === m.node.id) continue;\n g.setEdge(m.node.id, f.relatedNodeId, { fieldName: f.name }, `${m.node.id}--${f.name}--${f.relatedNodeId}`);\n }\n }\n\n dagre.layout(g);\n\n // Step 3: convert to LaidOutNode (top-left coords).\n const laidOutNodes: LaidOutNode[] = measured.map(m => {\n const { x: cx, y: cy } = g.node(m.node.id);\n return {\n ...m.node,\n x: cx - nodeW / 2,\n y: cy - m.height / 2,\n width: nodeW,\n height: m.height,\n visibleFields: m.visibleFields,\n hasMore: !showAll && !expandedIds.has(m.node.id) && m.node.fields.length > m.visibleFields.length,\n };\n });\n\n // Step 4: build edges using the same orthogonal router schema-grid uses.\n const edges = buildEdges(laidOutNodes, headerH, fieldH);\n\n // Canvas size = graph bounds (dagre already accounted for margin).\n const { width: totalWidth, height: totalHeight } = g.graph() as unknown as { width: number; height: number };\n\n return {\n nodes: laidOutNodes,\n edges,\n bands: [],\n totalWidth: totalWidth || 0,\n totalHeight: totalHeight || 0,\n };\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Edge routing — same 3/5-segment router as the schema-grid layout. Kept\n// as a local copy to avoid coupling the two files; the output shape of\n// `LaidOutEdge` is the public contract.\n// ──────────────────────────────────────────────────────────────────────────\n\nfunction buildEdges(nodes: LaidOutNode[], headerH: number, fieldH: number): LaidOutEdge[] {\n const byId = new Map<string, LaidOutNode>();\n for (const n of nodes) byId.set(n.id, n);\n\n const edges: LaidOutEdge[] = [];\n\n for (const src of nodes) {\n for (const f of src.fields) {\n if (!f.relatedNodeId) continue;\n const tgt = byId.get(f.relatedNodeId);\n if (!tgt) continue;\n\n const fieldIdx = src.visibleFields.findIndex(vf => vf.name === f.name);\n const sourceY = fieldIdx >= 0\n ? src.y + headerH + fieldIdx * fieldH + fieldH / 2\n : src.y + src.height / 2;\n\n edges.push(routeEdge({\n id: `${src.id}--${f.name}--${tgt.id}`,\n sourceId: src.id,\n targetId: tgt.id,\n sourceY,\n sourceField: f,\n src,\n tgt,\n }));\n }\n }\n\n return edges;\n}\n\ninterface RouteInput {\n id: string;\n sourceId: string;\n targetId: string;\n sourceY: number;\n sourceField: ERDField;\n src: LaidOutNode;\n tgt: LaidOutNode;\n}\n\nfunction routeEdge(r: RouteInput): LaidOutEdge {\n const { src, tgt, sourceY } = r;\n const selfReference = src.id === tgt.id;\n\n if (selfReference) {\n const loopR = 18;\n const sx = src.x + src.width;\n const points: Array<[number, number]> = [\n [sx, sourceY],\n [sx + loopR, sourceY],\n [sx + loopR, src.y - 10],\n [src.x + src.width / 2, src.y - 10],\n [src.x + src.width / 2, src.y],\n ];\n return {\n id: r.id, sourceId: r.sourceId, targetId: r.targetId,\n sourceY, sourceField: r.sourceField, points,\n selfReference: true, goingRight: true,\n };\n }\n\n const sCenterX = src.x + src.width / 2;\n const tCenterX = tgt.x + tgt.width / 2;\n const goingRight = tCenterX >= sCenterX;\n const sx = goingRight ? src.x + src.width : src.x;\n const tx = goingRight ? tgt.x : tgt.x + tgt.width;\n const ty = tgt.y + 18;\n const midX = goingRight\n ? sx + Math.max(24, (tx - sx) / 2)\n : sx - Math.max(24, (sx - tx) / 2);\n\n const points: Array<[number, number]> = [\n [sx, sourceY], [midX, sourceY], [midX, ty], [tx, ty],\n ];\n\n return {\n id: r.id, sourceId: r.sourceId, targetId: r.targetId,\n sourceY, sourceField: r.sourceField, points,\n selfReference: false, goingRight,\n };\n}\n"]}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Pure schema-grouped grid layout for the ERD canvas.
3
+ *
4
+ * Given an array of `ERDNode`s and layout options, this function returns:
5
+ * - laid-out nodes (x, y, width, height, visibleFields, hasMore)
6
+ * - edges with orthogonal 3-segment paths
7
+ * - schema bands (background rectangles per schema)
8
+ * - total canvas dimensions
9
+ *
10
+ * No Angular, no D3, no side effects — easily unit-tested.
11
+ *
12
+ * Ported from the Claude Design prototype's `layout.js`. Semantics:
13
+ * - Entities grouped by schema (nulls go into a `"_"` synthetic schema).
14
+ * - Per-schema columns: ≤3 entities → 1 col, ≤8 → 2 cols, else 3 cols.
15
+ * - Band heights aligned to the tallest band for visual coherence.
16
+ * - Edge routing: exit source on nearer side → horizontal → enter target.
17
+ * - Self-reference: loop above the source card.
18
+ */
19
+ import type { ERDNode, ERDField } from '../interfaces/erd-types';
20
+ export interface LaidOutNode extends ERDNode {
21
+ x: number;
22
+ y: number;
23
+ width: number;
24
+ height: number;
25
+ /** Fields rendered on the card (subset of `fields` based on expansion + keys-only mode). */
26
+ visibleFields: ERDField[];
27
+ /** True when the node has more fields than `visibleFields` shows. */
28
+ hasMore: boolean;
29
+ }
30
+ export interface LaidOutEdge {
31
+ id: string;
32
+ sourceId: string;
33
+ targetId: string;
34
+ /** Source Y coordinate (row of the source field in the card). */
35
+ sourceY: number;
36
+ /** Polyline points for the edge, screen coordinates. */
37
+ points: Array<[number, number]>;
38
+ /** True when source === target. */
39
+ selfReference: boolean;
40
+ /** True when the edge exits right of the source (vs. left). */
41
+ goingRight: boolean;
42
+ /** The source field that owns the FK. */
43
+ sourceField: ERDField;
44
+ }
45
+ export interface LaidOutBand {
46
+ schemaName: string;
47
+ x: number;
48
+ y: number;
49
+ width: number;
50
+ height: number;
51
+ }
52
+ export interface ErdLayout {
53
+ nodes: LaidOutNode[];
54
+ edges: LaidOutEdge[];
55
+ bands: LaidOutBand[];
56
+ totalWidth: number;
57
+ totalHeight: number;
58
+ }
59
+ export interface ErdLayoutOptions {
60
+ /** Width of every node card. Default 220. */
61
+ nodeWidth?: number;
62
+ /** Header row height. Default 36. */
63
+ headerHeight?: number;
64
+ /** Per-field row height. Default 22. */
65
+ fieldHeight?: number;
66
+ /** Reserved footer height for the "+N more" toggle. Default 22. */
67
+ moreToggleHeight?: number;
68
+ /** Horizontal gap between nodes in the same schema. Default 48. */
69
+ nodeGapX?: number;
70
+ /** Vertical gap between nodes in the same schema column. Default 30. */
71
+ nodeGapY?: number;
72
+ /** Gap between schema bands. Default 80. */
73
+ schemaGapX?: number;
74
+ /** Inner horizontal padding inside a band. Default 28. */
75
+ bandPadX?: number;
76
+ /** Height reserved for the band title row. Default 56. */
77
+ bandTitleHeight?: number;
78
+ /** Bottom padding inside a band. Default 28. */
79
+ bandPadBottom?: number;
80
+ /** Outer canvas padding. Default 40. */
81
+ canvasPad?: number;
82
+ /** Set of node IDs currently expanded to show all fields. */
83
+ expandedNodeIds?: ReadonlySet<string>;
84
+ /**
85
+ * When true, all fields are shown on each node regardless of PK/FK status —
86
+ * "+N more" toggle is not drawn. Default: false.
87
+ */
88
+ showAllFields?: boolean;
89
+ /** Order of schemas from left to right. Inferred from nodes if not provided. */
90
+ schemaOrder?: ReadonlyArray<string>;
91
+ }
92
+ /**
93
+ * Compute a schema-grouped grid layout for the given nodes. Pure function.
94
+ */
95
+ export declare function computeErdLayout(nodes: ERDNode[], options?: ErdLayoutOptions): ErdLayout;
96
+ /** Convert a polyline to an SVG path string. */
97
+ export declare function pointsToPath(points: ReadonlyArray<readonly [number, number]>): string;
98
+ /**
99
+ * Return the set of node IDs one hop from the given node (inclusive).
100
+ * Used for focus and hover highlight modes.
101
+ */
102
+ export declare function getNeighbors(nodes: ReadonlyArray<ERDNode>, id: string): Set<string>;
103
+ //# sourceMappingURL=compute-erd-layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compute-erd-layout.d.ts","sourceRoot":"","sources":["../../../src/lib/layout/compute-erd-layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAMjE,MAAM,WAAW,WAAY,SAAQ,OAAO;IACxC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,4FAA4F;IAC5F,aAAa,EAAE,QAAQ,EAAE,CAAC;IAC1B,qEAAqE;IACrE,OAAO,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,MAAM,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChC,mCAAmC;IACnC,aAAa,EAAE,OAAO,CAAC;IACvB,+DAA+D;IAC/D,UAAU,EAAE,OAAO,CAAC;IACpB,yCAAyC;IACzC,WAAW,EAAE,QAAQ,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACtB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC7B,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wCAAwC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gDAAgD;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,eAAe,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACtC;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iFAAiF;IACjF,WAAW,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACvC;AAQD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS,CA4F5F;AA6ID,gDAAgD;AAChD,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAKrF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAYnF"}
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Pure schema-grouped grid layout for the ERD canvas.
3
+ *
4
+ * Given an array of `ERDNode`s and layout options, this function returns:
5
+ * - laid-out nodes (x, y, width, height, visibleFields, hasMore)
6
+ * - edges with orthogonal 3-segment paths
7
+ * - schema bands (background rectangles per schema)
8
+ * - total canvas dimensions
9
+ *
10
+ * No Angular, no D3, no side effects — easily unit-tested.
11
+ *
12
+ * Ported from the Claude Design prototype's `layout.js`. Semantics:
13
+ * - Entities grouped by schema (nulls go into a `"_"` synthetic schema).
14
+ * - Per-schema columns: ≤3 entities → 1 col, ≤8 → 2 cols, else 3 cols.
15
+ * - Band heights aligned to the tallest band for visual coherence.
16
+ * - Edge routing: exit source on nearer side → horizontal → enter target.
17
+ * - Self-reference: loop above the source card.
18
+ */
19
+ // ──────────────────────────────────────────────────────────────────────────
20
+ // Implementation
21
+ // ──────────────────────────────────────────────────────────────────────────
22
+ const DEFAULT_SCHEMA = '_';
23
+ /**
24
+ * Compute a schema-grouped grid layout for the given nodes. Pure function.
25
+ */
26
+ export function computeErdLayout(nodes, options = {}) {
27
+ const nodeW = options.nodeWidth ?? 220;
28
+ const headerH = options.headerHeight ?? 36;
29
+ const fieldH = options.fieldHeight ?? 22;
30
+ const moreH = options.moreToggleHeight ?? 22;
31
+ const gapX = options.nodeGapX ?? 48;
32
+ const gapY = options.nodeGapY ?? 30;
33
+ const schemaGap = options.schemaGapX ?? 80;
34
+ const bandPadX = options.bandPadX ?? 28;
35
+ const bandTitleH = options.bandTitleHeight ?? 56;
36
+ const bandPadBottom = options.bandPadBottom ?? 28;
37
+ const canvasPad = options.canvasPad ?? 40;
38
+ const expandedIds = options.expandedNodeIds ?? new Set();
39
+ const showAll = options.showAllFields ?? false;
40
+ // 1. Group by schema.
41
+ const bySchema = new Map();
42
+ for (const n of nodes) {
43
+ const s = n.schemaName || DEFAULT_SCHEMA;
44
+ const list = bySchema.get(s);
45
+ if (list)
46
+ list.push(n);
47
+ else
48
+ bySchema.set(s, [n]);
49
+ }
50
+ // 2. Determine schema order.
51
+ const schemas = options.schemaOrder?.slice() ?? [...bySchema.keys()].sort();
52
+ for (const [s] of bySchema) {
53
+ if (!schemas.includes(s))
54
+ schemas.push(s);
55
+ }
56
+ // 3. Sort entities inside each schema alphabetically by name.
57
+ for (const list of bySchema.values()) {
58
+ list.sort((a, b) => a.name.localeCompare(b.name));
59
+ }
60
+ // 4. Lay out nodes within each band.
61
+ const laidOutNodes = [];
62
+ const bands = [];
63
+ let curX = canvasPad;
64
+ let maxBandH = 0;
65
+ for (const schema of schemas) {
66
+ const list = bySchema.get(schema) ?? [];
67
+ if (list.length === 0)
68
+ continue;
69
+ const cols = colsFor(list.length);
70
+ const bandW = bandPadX * 2 + cols * nodeW + (cols - 1) * gapX;
71
+ // Column-major placement: column i gets every `cols`-th entry.
72
+ const colLists = Array.from({ length: cols }, () => []);
73
+ list.forEach((ent, i) => colLists[i % cols].push(ent));
74
+ const bandStartX = curX + bandPadX;
75
+ const bandStartY = canvasPad + bandTitleH;
76
+ const colHeights = [];
77
+ colLists.forEach((col, colIdx) => {
78
+ let y = 0;
79
+ for (const ent of col) {
80
+ const expanded = expandedIds.has(ent.id) || showAll;
81
+ const { visibleFields, height } = fieldsForNode(ent, expanded, showAll, headerH, fieldH, moreH);
82
+ laidOutNodes.push({
83
+ ...ent,
84
+ x: bandStartX + colIdx * (nodeW + gapX),
85
+ y: bandStartY + y,
86
+ width: nodeW,
87
+ height,
88
+ visibleFields,
89
+ hasMore: !expanded && ent.fields.length > visibleFields.length,
90
+ });
91
+ y += height + gapY;
92
+ }
93
+ colHeights.push(y - gapY);
94
+ });
95
+ const bandH = Math.max(...colHeights, 100) + bandTitleH + bandPadBottom;
96
+ bands.push({ schemaName: schema, x: curX, y: canvasPad, width: bandW, height: bandH });
97
+ maxBandH = Math.max(maxBandH, bandH);
98
+ curX += bandW + schemaGap;
99
+ }
100
+ // 5. Align all band heights to the tallest.
101
+ for (const b of bands)
102
+ b.height = maxBandH;
103
+ const totalWidth = Math.max(curX - schemaGap + canvasPad, canvasPad * 2);
104
+ const totalHeight = canvasPad + maxBandH + canvasPad;
105
+ // 6. Build edges from FK fields.
106
+ const edges = buildEdges(laidOutNodes, headerH, fieldH);
107
+ return { nodes: laidOutNodes, edges, bands, totalWidth, totalHeight };
108
+ }
109
+ /**
110
+ * Column count for a schema band. Targets a roughly square aspect ratio,
111
+ * capped at 12 columns so cards remain recognizable when the user zooms
112
+ * out to see the whole band. A single schema with hundreds of entities
113
+ * would otherwise produce an unreadable vertical strip.
114
+ */
115
+ function colsFor(count) {
116
+ if (count <= 3)
117
+ return 1;
118
+ if (count <= 8)
119
+ return 2;
120
+ if (count <= 20)
121
+ return 3;
122
+ if (count <= 40)
123
+ return 4;
124
+ if (count <= 75)
125
+ return 6;
126
+ if (count <= 120)
127
+ return 8;
128
+ if (count <= 200)
129
+ return 10;
130
+ return 12;
131
+ }
132
+ /**
133
+ * Return the fields the node will display and the resulting card height.
134
+ *
135
+ * When `showAll` or the node is expanded, every field is shown. Otherwise
136
+ * only PK + FK fields (classic schema-relationship view).
137
+ */
138
+ function fieldsForNode(node, expanded, showAll, headerH, fieldH, moreH) {
139
+ const visibleFields = expanded || showAll
140
+ ? node.fields
141
+ : node.fields.filter(f => f.isPrimaryKey || !!f.relatedNodeId);
142
+ const hasMore = !expanded && !showAll && node.fields.length > visibleFields.length;
143
+ const rowCount = Math.max(visibleFields.length, 1);
144
+ const height = headerH + rowCount * fieldH + 8 + (hasMore ? moreH : 4);
145
+ return { visibleFields, height };
146
+ }
147
+ function buildEdges(nodes, headerH, fieldH) {
148
+ const byId = new Map();
149
+ for (const n of nodes)
150
+ byId.set(n.id, n);
151
+ const edges = [];
152
+ for (const src of nodes) {
153
+ for (const f of src.fields) {
154
+ if (!f.relatedNodeId)
155
+ continue;
156
+ const tgt = byId.get(f.relatedNodeId);
157
+ if (!tgt)
158
+ continue;
159
+ const fieldIdx = src.visibleFields.findIndex(vf => vf.name === f.name);
160
+ const sourceY = fieldIdx >= 0
161
+ ? src.y + headerH + fieldIdx * fieldH + fieldH / 2
162
+ : src.y + src.height / 2;
163
+ edges.push(routeEdge({
164
+ id: `${src.id}--${f.name}--${tgt.id}`,
165
+ sourceId: src.id,
166
+ targetId: tgt.id,
167
+ sourceY,
168
+ sourceField: f,
169
+ src,
170
+ tgt,
171
+ }));
172
+ }
173
+ }
174
+ return edges;
175
+ }
176
+ function routeEdge(r) {
177
+ const { src, tgt, sourceY } = r;
178
+ const selfReference = src.id === tgt.id;
179
+ if (selfReference) {
180
+ // Loop above the source card.
181
+ const loopR = 18;
182
+ const sx = src.x + src.width;
183
+ const points = [
184
+ [sx, sourceY],
185
+ [sx + loopR, sourceY],
186
+ [sx + loopR, src.y - 10],
187
+ [src.x + src.width / 2, src.y - 10],
188
+ [src.x + src.width / 2, src.y],
189
+ ];
190
+ return {
191
+ id: r.id,
192
+ sourceId: r.sourceId,
193
+ targetId: r.targetId,
194
+ sourceY,
195
+ sourceField: r.sourceField,
196
+ points,
197
+ selfReference: true,
198
+ goingRight: true,
199
+ };
200
+ }
201
+ const sCenterX = src.x + src.width / 2;
202
+ const tCenterX = tgt.x + tgt.width / 2;
203
+ const goingRight = tCenterX >= sCenterX;
204
+ const sx = goingRight ? src.x + src.width : src.x;
205
+ const tx = goingRight ? tgt.x : tgt.x + tgt.width;
206
+ const ty = tgt.y + 18;
207
+ const midX = goingRight
208
+ ? sx + Math.max(24, (tx - sx) / 2)
209
+ : sx - Math.max(24, (sx - tx) / 2);
210
+ const points = [
211
+ [sx, sourceY],
212
+ [midX, sourceY],
213
+ [midX, ty],
214
+ [tx, ty],
215
+ ];
216
+ return {
217
+ id: r.id,
218
+ sourceId: r.sourceId,
219
+ targetId: r.targetId,
220
+ sourceY,
221
+ sourceField: r.sourceField,
222
+ points,
223
+ selfReference: false,
224
+ goingRight,
225
+ };
226
+ }
227
+ /** Convert a polyline to an SVG path string. */
228
+ export function pointsToPath(points) {
229
+ if (!points || points.length === 0)
230
+ return '';
231
+ let d = `M${points[0][0]} ${points[0][1]}`;
232
+ for (let i = 1; i < points.length; i++)
233
+ d += ` L${points[i][0]} ${points[i][1]}`;
234
+ return d;
235
+ }
236
+ /**
237
+ * Return the set of node IDs one hop from the given node (inclusive).
238
+ * Used for focus and hover highlight modes.
239
+ */
240
+ export function getNeighbors(nodes, id) {
241
+ const result = new Set([id]);
242
+ for (const n of nodes) {
243
+ for (const f of n.fields) {
244
+ if (!f.relatedNodeId)
245
+ continue;
246
+ if (n.id === id || f.relatedNodeId === id) {
247
+ result.add(n.id);
248
+ result.add(f.relatedNodeId);
249
+ }
250
+ }
251
+ }
252
+ return result;
253
+ }
254
+ //# sourceMappingURL=compute-erd-layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compute-erd-layout.js","sourceRoot":"","sources":["../../../src/lib/layout/compute-erd-layout.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure schema-grouped grid layout for the ERD canvas.\n *\n * Given an array of `ERDNode`s and layout options, this function returns:\n * - laid-out nodes (x, y, width, height, visibleFields, hasMore)\n * - edges with orthogonal 3-segment paths\n * - schema bands (background rectangles per schema)\n * - total canvas dimensions\n *\n * No Angular, no D3, no side effects — easily unit-tested.\n *\n * Ported from the Claude Design prototype's `layout.js`. Semantics:\n * - Entities grouped by schema (nulls go into a `\"_\"` synthetic schema).\n * - Per-schema columns: ≤3 entities → 1 col, ≤8 → 2 cols, else 3 cols.\n * - Band heights aligned to the tallest band for visual coherence.\n * - Edge routing: exit source on nearer side → horizontal → enter target.\n * - Self-reference: loop above the source card.\n */\n\nimport type { ERDNode, ERDField } from '../interfaces/erd-types';\n\n// ──────────────────────────────────────────────────────────────────────────\n// Public types\n// ──────────────────────────────────────────────────────────────────────────\n\nexport interface LaidOutNode extends ERDNode {\n x: number;\n y: number;\n width: number;\n height: number;\n /** Fields rendered on the card (subset of `fields` based on expansion + keys-only mode). */\n visibleFields: ERDField[];\n /** True when the node has more fields than `visibleFields` shows. */\n hasMore: boolean;\n}\n\nexport interface LaidOutEdge {\n id: string;\n sourceId: string;\n targetId: string;\n /** Source Y coordinate (row of the source field in the card). */\n sourceY: number;\n /** Polyline points for the edge, screen coordinates. */\n points: Array<[number, number]>;\n /** True when source === target. */\n selfReference: boolean;\n /** True when the edge exits right of the source (vs. left). */\n goingRight: boolean;\n /** The source field that owns the FK. */\n sourceField: ERDField;\n}\n\nexport interface LaidOutBand {\n schemaName: string;\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface ErdLayout {\n nodes: LaidOutNode[];\n edges: LaidOutEdge[];\n bands: LaidOutBand[];\n totalWidth: number;\n totalHeight: number;\n}\n\nexport interface ErdLayoutOptions {\n /** Width of every node card. Default 220. */\n nodeWidth?: number;\n /** Header row height. Default 36. */\n headerHeight?: number;\n /** Per-field row height. Default 22. */\n fieldHeight?: number;\n /** Reserved footer height for the \"+N more\" toggle. Default 22. */\n moreToggleHeight?: number;\n /** Horizontal gap between nodes in the same schema. Default 48. */\n nodeGapX?: number;\n /** Vertical gap between nodes in the same schema column. Default 30. */\n nodeGapY?: number;\n /** Gap between schema bands. Default 80. */\n schemaGapX?: number;\n /** Inner horizontal padding inside a band. Default 28. */\n bandPadX?: number;\n /** Height reserved for the band title row. Default 56. */\n bandTitleHeight?: number;\n /** Bottom padding inside a band. Default 28. */\n bandPadBottom?: number;\n /** Outer canvas padding. Default 40. */\n canvasPad?: number;\n /** Set of node IDs currently expanded to show all fields. */\n expandedNodeIds?: ReadonlySet<string>;\n /**\n * When true, all fields are shown on each node regardless of PK/FK status —\n * \"+N more\" toggle is not drawn. Default: false.\n */\n showAllFields?: boolean;\n /** Order of schemas from left to right. Inferred from nodes if not provided. */\n schemaOrder?: ReadonlyArray<string>;\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Implementation\n// ──────────────────────────────────────────────────────────────────────────\n\nconst DEFAULT_SCHEMA = '_';\n\n/**\n * Compute a schema-grouped grid layout for the given nodes. Pure function.\n */\nexport function computeErdLayout(nodes: ERDNode[], options: ErdLayoutOptions = {}): ErdLayout {\n const nodeW = options.nodeWidth ?? 220;\n const headerH = options.headerHeight ?? 36;\n const fieldH = options.fieldHeight ?? 22;\n const moreH = options.moreToggleHeight ?? 22;\n const gapX = options.nodeGapX ?? 48;\n const gapY = options.nodeGapY ?? 30;\n const schemaGap = options.schemaGapX ?? 80;\n const bandPadX = options.bandPadX ?? 28;\n const bandTitleH = options.bandTitleHeight ?? 56;\n const bandPadBottom = options.bandPadBottom ?? 28;\n const canvasPad = options.canvasPad ?? 40;\n const expandedIds = options.expandedNodeIds ?? new Set<string>();\n const showAll = options.showAllFields ?? false;\n\n // 1. Group by schema.\n const bySchema = new Map<string, ERDNode[]>();\n for (const n of nodes) {\n const s = n.schemaName || DEFAULT_SCHEMA;\n const list = bySchema.get(s);\n if (list) list.push(n);\n else bySchema.set(s, [n]);\n }\n\n // 2. Determine schema order.\n const schemas = options.schemaOrder?.slice() ?? [...bySchema.keys()].sort();\n for (const [s] of bySchema) {\n if (!schemas.includes(s)) schemas.push(s);\n }\n\n // 3. Sort entities inside each schema alphabetically by name.\n for (const list of bySchema.values()) {\n list.sort((a, b) => a.name.localeCompare(b.name));\n }\n\n // 4. Lay out nodes within each band.\n const laidOutNodes: LaidOutNode[] = [];\n const bands: LaidOutBand[] = [];\n let curX = canvasPad;\n let maxBandH = 0;\n\n for (const schema of schemas) {\n const list = bySchema.get(schema) ?? [];\n if (list.length === 0) continue;\n\n const cols = colsFor(list.length);\n const bandW = bandPadX * 2 + cols * nodeW + (cols - 1) * gapX;\n\n // Column-major placement: column i gets every `cols`-th entry.\n const colLists: ERDNode[][] = Array.from({ length: cols }, () => []);\n list.forEach((ent, i) => colLists[i % cols].push(ent));\n\n const bandStartX = curX + bandPadX;\n const bandStartY = canvasPad + bandTitleH;\n\n const colHeights: number[] = [];\n\n colLists.forEach((col, colIdx) => {\n let y = 0;\n for (const ent of col) {\n const expanded = expandedIds.has(ent.id) || showAll;\n const { visibleFields, height } = fieldsForNode(ent, expanded, showAll, headerH, fieldH, moreH);\n laidOutNodes.push({\n ...ent,\n x: bandStartX + colIdx * (nodeW + gapX),\n y: bandStartY + y,\n width: nodeW,\n height,\n visibleFields,\n hasMore: !expanded && ent.fields.length > visibleFields.length,\n });\n y += height + gapY;\n }\n colHeights.push(y - gapY);\n });\n\n const bandH = Math.max(...colHeights, 100) + bandTitleH + bandPadBottom;\n bands.push({ schemaName: schema, x: curX, y: canvasPad, width: bandW, height: bandH });\n maxBandH = Math.max(maxBandH, bandH);\n curX += bandW + schemaGap;\n }\n\n // 5. Align all band heights to the tallest.\n for (const b of bands) b.height = maxBandH;\n\n const totalWidth = Math.max(curX - schemaGap + canvasPad, canvasPad * 2);\n const totalHeight = canvasPad + maxBandH + canvasPad;\n\n // 6. Build edges from FK fields.\n const edges = buildEdges(laidOutNodes, headerH, fieldH);\n\n return { nodes: laidOutNodes, edges, bands, totalWidth, totalHeight };\n}\n\n/**\n * Column count for a schema band. Targets a roughly square aspect ratio,\n * capped at 12 columns so cards remain recognizable when the user zooms\n * out to see the whole band. A single schema with hundreds of entities\n * would otherwise produce an unreadable vertical strip.\n */\nfunction colsFor(count: number): number {\n if (count <= 3) return 1;\n if (count <= 8) return 2;\n if (count <= 20) return 3;\n if (count <= 40) return 4;\n if (count <= 75) return 6;\n if (count <= 120) return 8;\n if (count <= 200) return 10;\n return 12;\n}\n\n/**\n * Return the fields the node will display and the resulting card height.\n *\n * When `showAll` or the node is expanded, every field is shown. Otherwise\n * only PK + FK fields (classic schema-relationship view).\n */\nfunction fieldsForNode(\n node: ERDNode,\n expanded: boolean,\n showAll: boolean,\n headerH: number,\n fieldH: number,\n moreH: number,\n): { visibleFields: ERDField[]; height: number } {\n const visibleFields = expanded || showAll\n ? node.fields\n : node.fields.filter(f => f.isPrimaryKey || !!f.relatedNodeId);\n\n const hasMore = !expanded && !showAll && node.fields.length > visibleFields.length;\n const rowCount = Math.max(visibleFields.length, 1);\n const height = headerH + rowCount * fieldH + 8 + (hasMore ? moreH : 4);\n return { visibleFields, height };\n}\n\nfunction buildEdges(nodes: LaidOutNode[], headerH: number, fieldH: number): LaidOutEdge[] {\n const byId = new Map<string, LaidOutNode>();\n for (const n of nodes) byId.set(n.id, n);\n\n const edges: LaidOutEdge[] = [];\n\n for (const src of nodes) {\n for (const f of src.fields) {\n if (!f.relatedNodeId) continue;\n const tgt = byId.get(f.relatedNodeId);\n if (!tgt) continue;\n\n const fieldIdx = src.visibleFields.findIndex(vf => vf.name === f.name);\n const sourceY = fieldIdx >= 0\n ? src.y + headerH + fieldIdx * fieldH + fieldH / 2\n : src.y + src.height / 2;\n\n edges.push(routeEdge({\n id: `${src.id}--${f.name}--${tgt.id}`,\n sourceId: src.id,\n targetId: tgt.id,\n sourceY,\n sourceField: f,\n src,\n tgt,\n }));\n }\n }\n\n return edges;\n}\n\ninterface RouteInput {\n id: string;\n sourceId: string;\n targetId: string;\n sourceY: number;\n sourceField: ERDField;\n src: LaidOutNode;\n tgt: LaidOutNode;\n}\n\nfunction routeEdge(r: RouteInput): LaidOutEdge {\n const { src, tgt, sourceY } = r;\n const selfReference = src.id === tgt.id;\n\n if (selfReference) {\n // Loop above the source card.\n const loopR = 18;\n const sx = src.x + src.width;\n const points: Array<[number, number]> = [\n [sx, sourceY],\n [sx + loopR, sourceY],\n [sx + loopR, src.y - 10],\n [src.x + src.width / 2, src.y - 10],\n [src.x + src.width / 2, src.y],\n ];\n return {\n id: r.id,\n sourceId: r.sourceId,\n targetId: r.targetId,\n sourceY,\n sourceField: r.sourceField,\n points,\n selfReference: true,\n goingRight: true,\n };\n }\n\n const sCenterX = src.x + src.width / 2;\n const tCenterX = tgt.x + tgt.width / 2;\n const goingRight = tCenterX >= sCenterX;\n const sx = goingRight ? src.x + src.width : src.x;\n const tx = goingRight ? tgt.x : tgt.x + tgt.width;\n const ty = tgt.y + 18;\n const midX = goingRight\n ? sx + Math.max(24, (tx - sx) / 2)\n : sx - Math.max(24, (sx - tx) / 2);\n\n const points: Array<[number, number]> = [\n [sx, sourceY],\n [midX, sourceY],\n [midX, ty],\n [tx, ty],\n ];\n\n return {\n id: r.id,\n sourceId: r.sourceId,\n targetId: r.targetId,\n sourceY,\n sourceField: r.sourceField,\n points,\n selfReference: false,\n goingRight,\n };\n}\n\n/** Convert a polyline to an SVG path string. */\nexport function pointsToPath(points: ReadonlyArray<readonly [number, number]>): string {\n if (!points || points.length === 0) return '';\n let d = `M${points[0][0]} ${points[0][1]}`;\n for (let i = 1; i < points.length; i++) d += ` L${points[i][0]} ${points[i][1]}`;\n return d;\n}\n\n/**\n * Return the set of node IDs one hop from the given node (inclusive).\n * Used for focus and hover highlight modes.\n */\nexport function getNeighbors(nodes: ReadonlyArray<ERDNode>, id: string): Set<string> {\n const result = new Set<string>([id]);\n for (const n of nodes) {\n for (const f of n.fields) {\n if (!f.relatedNodeId) continue;\n if (n.id === id || f.relatedNodeId === id) {\n result.add(n.id);\n result.add(f.relatedNodeId);\n }\n }\n }\n return result;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/ng-entity-relationship-diagram",
3
- "version": "5.30.1",
3
+ "version": "5.31.0",
4
4
  "description": "MemberJunction: Entity Relationship Diagram (ERD) component for visualizing entity relationships using D3.js force-directed graphs",
5
5
  "main": "./dist/public-api.js",
6
6
  "typings": "./dist/public-api.d.ts",
@@ -8,7 +8,7 @@
8
8
  "/dist"
9
9
  ],
10
10
  "scripts": {
11
- "test": "echo \"No tests configured yet\"",
11
+ "test": "vitest run",
12
12
  "build": "ngc",
13
13
  "test:watch": "vitest"
14
14
  },
@@ -36,9 +36,10 @@
36
36
  "dependencies": {
37
37
  "@angular/forms": "21.1.3",
38
38
  "@dagrejs/dagre": "^1.1.8",
39
- "@memberjunction/core": "5.30.1",
40
- "@memberjunction/global": "5.30.1",
41
- "@memberjunction/ng-ui-components": "5.30.1",
39
+ "@memberjunction/core": "5.31.0",
40
+ "@memberjunction/ng-base-types": "5.31.0",
41
+ "@memberjunction/global": "5.31.0",
42
+ "@memberjunction/ng-ui-components": "5.31.0",
42
43
  "@types/d3": "^7.4.3",
43
44
  "angular-split": "^20.0.0",
44
45
  "d3": "^7.9.0",