@memberjunction/ng-graph-view 0.0.0 → 6.1.0-edge.4
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/LICENSE +183 -0
- package/README.md +76 -31
- package/dist/lib/components/graph-view.component.d.ts +108 -0
- package/dist/lib/components/graph-view.component.d.ts.map +1 -0
- package/dist/lib/components/graph-view.component.js +1285 -0
- package/dist/lib/components/graph-view.component.js.map +1 -0
- package/dist/lib/events/graph-events.d.ts +117 -0
- package/dist/lib/events/graph-events.d.ts.map +1 -0
- package/dist/lib/events/graph-events.js +178 -0
- package/dist/lib/events/graph-events.js.map +1 -0
- package/dist/lib/graph-view.module.d.ts +10 -0
- package/dist/lib/graph-view.module.d.ts.map +1 -0
- package/dist/lib/graph-view.module.js +29 -0
- package/dist/lib/graph-view.module.js.map +1 -0
- package/dist/lib/models/graph-models.d.ts +132 -0
- package/dist/lib/models/graph-models.d.ts.map +1 -0
- package/dist/lib/models/graph-models.js +10 -0
- package/dist/lib/models/graph-models.js.map +1 -0
- package/dist/public-api.d.ts +8 -0
- package/dist/public-api.d.ts.map +1 -0
- package/dist/public-api.js +11 -0
- package/dist/public-api.js.map +1 -0
- package/package.json +44 -7
|
@@ -0,0 +1,1285 @@
|
|
|
1
|
+
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef, ViewChild, inject } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { FormsModule } from '@angular/forms';
|
|
4
|
+
import { NormalizeUUID, UUIDsEqual } from '@memberjunction/global';
|
|
5
|
+
import * as d3 from 'd3';
|
|
6
|
+
import { BeforeNodeSelectEventArgs, NodeSelectedEventArgs, BeforeEdgeSelectEventArgs, EdgeSelectedEventArgs, BeforeHopExpandEventArgs, HopExpandedEventArgs, BeforeLayoutChangeEventArgs, LayoutChangedEventArgs, BeforeNodeNavigateEventArgs, NodeNavigatedEventArgs, ViewportTransformEventArgs } from '../events/graph-events';
|
|
7
|
+
import * as i0 from "@angular/core";
|
|
8
|
+
import * as i1 from "@angular/forms";
|
|
9
|
+
const _c0 = ["wrapper"];
|
|
10
|
+
const _c1 = ["svgCanvas"];
|
|
11
|
+
const _forTrack0 = ($index, $item) => $item.ID;
|
|
12
|
+
const _forTrack1 = ($index, $item) => $item.Category;
|
|
13
|
+
function GraphViewComponent_Conditional_2_Conditional_20_Template(rf, ctx) { if (rf & 1) {
|
|
14
|
+
const _r4 = i0.ɵɵgetCurrentView();
|
|
15
|
+
i0.ɵɵelement(0, "div", 10);
|
|
16
|
+
i0.ɵɵelementStart(1, "div", 23);
|
|
17
|
+
i0.ɵɵelement(2, "i", 24);
|
|
18
|
+
i0.ɵɵelementStart(3, "input", 25);
|
|
19
|
+
i0.ɵɵtwoWayListener("ngModelChange", function GraphViewComponent_Conditional_2_Conditional_20_Template_input_ngModelChange_3_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r2 = i0.ɵɵnextContext(2); i0.ɵɵtwoWayBindingSet(ctx_r2.SearchQuery, $event) || (ctx_r2.SearchQuery = $event); return i0.ɵɵresetView($event); });
|
|
20
|
+
i0.ɵɵlistener("ngModelChange", function GraphViewComponent_Conditional_2_Conditional_20_Template_input_ngModelChange_3_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.SearchNodes($event)); });
|
|
21
|
+
i0.ɵɵelementEnd()();
|
|
22
|
+
} if (rf & 2) {
|
|
23
|
+
const ctx_r2 = i0.ɵɵnextContext(2);
|
|
24
|
+
i0.ɵɵadvance(3);
|
|
25
|
+
i0.ɵɵtwoWayProperty("ngModel", ctx_r2.SearchQuery);
|
|
26
|
+
} }
|
|
27
|
+
function GraphViewComponent_Conditional_2_Template(rf, ctx) { if (rf & 1) {
|
|
28
|
+
const _r2 = i0.ɵɵgetCurrentView();
|
|
29
|
+
i0.ɵɵelementStart(0, "div", 3)(1, "span", 8);
|
|
30
|
+
i0.ɵɵelement(2, "i", 9);
|
|
31
|
+
i0.ɵɵtext(3, " Network Graph ");
|
|
32
|
+
i0.ɵɵelementEnd();
|
|
33
|
+
i0.ɵɵelement(4, "div", 10);
|
|
34
|
+
i0.ɵɵelementStart(5, "button", 11);
|
|
35
|
+
i0.ɵɵlistener("click", function GraphViewComponent_Conditional_2_Template_button_click_5_listener() { i0.ɵɵrestoreView(_r2); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.SetLayoutMode("force")); });
|
|
36
|
+
i0.ɵɵelement(6, "i", 12);
|
|
37
|
+
i0.ɵɵtext(7, " Force ");
|
|
38
|
+
i0.ɵɵelementEnd();
|
|
39
|
+
i0.ɵɵelementStart(8, "button", 13);
|
|
40
|
+
i0.ɵɵlistener("click", function GraphViewComponent_Conditional_2_Template_button_click_8_listener() { i0.ɵɵrestoreView(_r2); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.SetLayoutMode("circular")); });
|
|
41
|
+
i0.ɵɵelement(9, "i", 14);
|
|
42
|
+
i0.ɵɵtext(10, " Circular ");
|
|
43
|
+
i0.ɵɵelementEnd();
|
|
44
|
+
i0.ɵɵelement(11, "div", 10);
|
|
45
|
+
i0.ɵɵelementStart(12, "button", 15);
|
|
46
|
+
i0.ɵɵlistener("click", function GraphViewComponent_Conditional_2_Template_button_click_12_listener() { i0.ɵɵrestoreView(_r2); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.ZoomIn()); });
|
|
47
|
+
i0.ɵɵelement(13, "i", 16);
|
|
48
|
+
i0.ɵɵelementEnd();
|
|
49
|
+
i0.ɵɵelementStart(14, "button", 17);
|
|
50
|
+
i0.ɵɵlistener("click", function GraphViewComponent_Conditional_2_Template_button_click_14_listener() { i0.ɵɵrestoreView(_r2); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.ZoomOut()); });
|
|
51
|
+
i0.ɵɵelement(15, "i", 18);
|
|
52
|
+
i0.ɵɵelementEnd();
|
|
53
|
+
i0.ɵɵelementStart(16, "button", 19);
|
|
54
|
+
i0.ɵɵlistener("click", function GraphViewComponent_Conditional_2_Template_button_click_16_listener() { i0.ɵɵrestoreView(_r2); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.FitToView()); });
|
|
55
|
+
i0.ɵɵelement(17, "i", 20);
|
|
56
|
+
i0.ɵɵelementEnd();
|
|
57
|
+
i0.ɵɵelementStart(18, "button", 21);
|
|
58
|
+
i0.ɵɵlistener("click", function GraphViewComponent_Conditional_2_Template_button_click_18_listener() { i0.ɵɵrestoreView(_r2); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.Rearrange()); });
|
|
59
|
+
i0.ɵɵelement(19, "i", 22);
|
|
60
|
+
i0.ɵɵelementEnd();
|
|
61
|
+
i0.ɵɵconditionalCreate(20, GraphViewComponent_Conditional_2_Conditional_20_Template, 4, 1);
|
|
62
|
+
i0.ɵɵelementEnd();
|
|
63
|
+
} if (rf & 2) {
|
|
64
|
+
const ctx_r2 = i0.ɵɵnextContext();
|
|
65
|
+
i0.ɵɵadvance(5);
|
|
66
|
+
i0.ɵɵclassProp("active", ctx_r2.LayoutMode === "force");
|
|
67
|
+
i0.ɵɵadvance(3);
|
|
68
|
+
i0.ɵɵclassProp("active", ctx_r2.LayoutMode === "circular");
|
|
69
|
+
i0.ɵɵadvance(12);
|
|
70
|
+
i0.ɵɵconditional(ctx_r2.Searchable ? 20 : -1);
|
|
71
|
+
} }
|
|
72
|
+
function GraphViewComponent_Conditional_3_For_2_Template(rf, ctx) { if (rf & 1) {
|
|
73
|
+
i0.ɵɵelementStart(0, "div", 26);
|
|
74
|
+
i0.ɵɵelement(1, "div", 27);
|
|
75
|
+
i0.ɵɵtext(2);
|
|
76
|
+
i0.ɵɵelementEnd();
|
|
77
|
+
} if (rf & 2) {
|
|
78
|
+
const cat_r5 = ctx.$implicit;
|
|
79
|
+
i0.ɵɵadvance();
|
|
80
|
+
i0.ɵɵstyleProp("background", cat_r5.Color);
|
|
81
|
+
i0.ɵɵadvance();
|
|
82
|
+
i0.ɵɵtextInterpolate1(" ", cat_r5.Label, " ");
|
|
83
|
+
} }
|
|
84
|
+
function GraphViewComponent_Conditional_3_Template(rf, ctx) { if (rf & 1) {
|
|
85
|
+
i0.ɵɵelementStart(0, "div", 4);
|
|
86
|
+
i0.ɵɵrepeaterCreate(1, GraphViewComponent_Conditional_3_For_2_Template, 3, 3, "div", 26, _forTrack1);
|
|
87
|
+
i0.ɵɵelementEnd();
|
|
88
|
+
} if (rf & 2) {
|
|
89
|
+
const ctx_r2 = i0.ɵɵnextContext();
|
|
90
|
+
i0.ɵɵadvance();
|
|
91
|
+
i0.ɵɵrepeater(ctx_r2.ActiveCategories);
|
|
92
|
+
} }
|
|
93
|
+
function GraphViewComponent_Conditional_4_Conditional_15_Template(rf, ctx) { if (rf & 1) {
|
|
94
|
+
i0.ɵɵelementStart(0, "span", 38);
|
|
95
|
+
i0.ɵɵtext(1, "Current Record");
|
|
96
|
+
i0.ɵɵelementEnd();
|
|
97
|
+
} }
|
|
98
|
+
function GraphViewComponent_Conditional_4_Conditional_21_Template(rf, ctx) { if (rf & 1) {
|
|
99
|
+
const _r7 = i0.ɵɵgetCurrentView();
|
|
100
|
+
i0.ɵɵelementStart(0, "button", 44);
|
|
101
|
+
i0.ɵɵlistener("click", function GraphViewComponent_Conditional_4_Conditional_21_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r7); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.NavigateToEntity(ctx_r2.SelectedNode)); });
|
|
102
|
+
i0.ɵɵelement(1, "i", 45);
|
|
103
|
+
i0.ɵɵtext(2, " Open Entity Form ");
|
|
104
|
+
i0.ɵɵelementEnd();
|
|
105
|
+
} }
|
|
106
|
+
function GraphViewComponent_Conditional_4_Template(rf, ctx) { if (rf & 1) {
|
|
107
|
+
const _r6 = i0.ɵɵgetCurrentView();
|
|
108
|
+
i0.ɵɵelementStart(0, "div", 5)(1, "div", 28)(2, "span", 29);
|
|
109
|
+
i0.ɵɵtext(3, "Node Inspector");
|
|
110
|
+
i0.ɵɵelementEnd();
|
|
111
|
+
i0.ɵɵelementStart(4, "button", 30);
|
|
112
|
+
i0.ɵɵlistener("click", function GraphViewComponent_Conditional_4_Template_button_click_4_listener() { i0.ɵɵrestoreView(_r6); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.ClearSelection()); });
|
|
113
|
+
i0.ɵɵelement(5, "i", 31);
|
|
114
|
+
i0.ɵɵelementEnd()();
|
|
115
|
+
i0.ɵɵelementStart(6, "div", 32)(7, "div", 33);
|
|
116
|
+
i0.ɵɵelement(8, "i");
|
|
117
|
+
i0.ɵɵelementEnd();
|
|
118
|
+
i0.ɵɵelementStart(9, "div", 34)(10, "div", 35);
|
|
119
|
+
i0.ɵɵtext(11);
|
|
120
|
+
i0.ɵɵelementEnd();
|
|
121
|
+
i0.ɵɵelementStart(12, "div", 36)(13, "span", 37);
|
|
122
|
+
i0.ɵɵtext(14);
|
|
123
|
+
i0.ɵɵelementEnd();
|
|
124
|
+
i0.ɵɵconditionalCreate(15, GraphViewComponent_Conditional_4_Conditional_15_Template, 2, 0, "span", 38);
|
|
125
|
+
i0.ɵɵelementEnd()()();
|
|
126
|
+
i0.ɵɵelementStart(16, "div", 39);
|
|
127
|
+
i0.ɵɵtext(17, " Direct Connected Edges: ");
|
|
128
|
+
i0.ɵɵelementStart(18, "strong");
|
|
129
|
+
i0.ɵɵtext(19);
|
|
130
|
+
i0.ɵɵelementEnd()();
|
|
131
|
+
i0.ɵɵelementStart(20, "div", 40);
|
|
132
|
+
i0.ɵɵconditionalCreate(21, GraphViewComponent_Conditional_4_Conditional_21_Template, 3, 0, "button", 41);
|
|
133
|
+
i0.ɵɵelementStart(22, "button", 42);
|
|
134
|
+
i0.ɵɵlistener("click", function GraphViewComponent_Conditional_4_Template_button_click_22_listener() { i0.ɵɵrestoreView(_r6); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.ExpandHops(ctx_r2.SelectedNode.ID, 1)); });
|
|
135
|
+
i0.ɵɵelement(23, "i", 43);
|
|
136
|
+
i0.ɵɵtext(24, " Expand 1-Hop Neighbors ");
|
|
137
|
+
i0.ɵɵelementEnd()()();
|
|
138
|
+
} if (rf & 2) {
|
|
139
|
+
const ctx_r2 = i0.ɵɵnextContext();
|
|
140
|
+
i0.ɵɵadvance(7);
|
|
141
|
+
i0.ɵɵstyleProp("background", ctx_r2.GetNodeColor(ctx_r2.SelectedNode));
|
|
142
|
+
i0.ɵɵadvance();
|
|
143
|
+
i0.ɵɵclassMap(ctx_r2.GetNodeIcon(ctx_r2.SelectedNode));
|
|
144
|
+
i0.ɵɵadvance(3);
|
|
145
|
+
i0.ɵɵtextInterpolate1(" ", ctx_r2.SelectedNode.Label, " ");
|
|
146
|
+
i0.ɵɵadvance(3);
|
|
147
|
+
i0.ɵɵtextInterpolate1(" ", ctx_r2.SelectedNode.Sublabel || ctx_r2.SelectedNode.Category, " ");
|
|
148
|
+
i0.ɵɵadvance();
|
|
149
|
+
i0.ɵɵconditional(ctx_r2.IsFocalNode(ctx_r2.SelectedNode) ? 15 : -1);
|
|
150
|
+
i0.ɵɵadvance(4);
|
|
151
|
+
i0.ɵɵtextInterpolate(ctx_r2.GetNodeEdgeCount(ctx_r2.SelectedNode.ID));
|
|
152
|
+
i0.ɵɵadvance(2);
|
|
153
|
+
i0.ɵɵconditional(!ctx_r2.IsFocalNode(ctx_r2.SelectedNode) ? 21 : -1);
|
|
154
|
+
} }
|
|
155
|
+
function GraphViewComponent_For_9_Template(rf, ctx) { if (rf & 1) {
|
|
156
|
+
const _r8 = i0.ɵɵgetCurrentView();
|
|
157
|
+
i0.ɵɵnamespaceSVG();
|
|
158
|
+
i0.ɵɵelementStart(0, "g")(1, "line", 46);
|
|
159
|
+
i0.ɵɵlistener("click", function GraphViewComponent_For_9_Template_line_click_1_listener($event) { const edge_r9 = i0.ɵɵrestoreView(_r8).$implicit; const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.OnEdgeClick(edge_r9, $event)); });
|
|
160
|
+
i0.ɵɵelementEnd();
|
|
161
|
+
i0.ɵɵelementStart(2, "text", 47);
|
|
162
|
+
i0.ɵɵtext(3);
|
|
163
|
+
i0.ɵɵelementEnd()();
|
|
164
|
+
} if (rf & 2) {
|
|
165
|
+
const edge_r9 = ctx.$implicit;
|
|
166
|
+
const ctx_r2 = i0.ɵɵnextContext();
|
|
167
|
+
i0.ɵɵadvance();
|
|
168
|
+
i0.ɵɵclassProp("is-highlighted", ctx_r2.IsEdgeHighlighted(edge_r9));
|
|
169
|
+
i0.ɵɵattribute("x1", ctx_r2.GetNodeX(edge_r9.SourceID))("y1", ctx_r2.GetNodeY(edge_r9.SourceID))("x2", ctx_r2.GetNodeX(edge_r9.TargetID))("y2", ctx_r2.GetNodeY(edge_r9.TargetID));
|
|
170
|
+
i0.ɵɵadvance();
|
|
171
|
+
i0.ɵɵattribute("x", (ctx_r2.GetNodeX(edge_r9.SourceID) + ctx_r2.GetNodeX(edge_r9.TargetID)) / 2)("y", (ctx_r2.GetNodeY(edge_r9.SourceID) + ctx_r2.GetNodeY(edge_r9.TargetID)) / 2 - 4);
|
|
172
|
+
i0.ɵɵadvance();
|
|
173
|
+
i0.ɵɵtextInterpolate1(" ", edge_r9.Label, " ");
|
|
174
|
+
} }
|
|
175
|
+
function GraphViewComponent_For_11_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
176
|
+
i0.ɵɵnamespaceSVG();
|
|
177
|
+
i0.ɵɵelement(0, "circle", 49);
|
|
178
|
+
} }
|
|
179
|
+
function GraphViewComponent_For_11_Conditional_2_Template(rf, ctx) { if (rf & 1) {
|
|
180
|
+
i0.ɵɵnamespaceSVG();
|
|
181
|
+
i0.ɵɵelement(0, "circle", 50);
|
|
182
|
+
} }
|
|
183
|
+
function GraphViewComponent_For_11_Conditional_7_Template(rf, ctx) { if (rf & 1) {
|
|
184
|
+
i0.ɵɵnamespaceSVG();
|
|
185
|
+
i0.ɵɵelementStart(0, "text", 53);
|
|
186
|
+
i0.ɵɵtext(1);
|
|
187
|
+
i0.ɵɵelementEnd();
|
|
188
|
+
} if (rf & 2) {
|
|
189
|
+
const node_r11 = i0.ɵɵnextContext().$implicit;
|
|
190
|
+
const ctx_r2 = i0.ɵɵnextContext();
|
|
191
|
+
i0.ɵɵattribute("y", (node_r11.Radius || 26) + 28);
|
|
192
|
+
i0.ɵɵadvance();
|
|
193
|
+
i0.ɵɵtextInterpolate1(" ", ctx_r2.TruncateLabel(node_r11.Sublabel, 20), " ");
|
|
194
|
+
} }
|
|
195
|
+
function GraphViewComponent_For_11_Template(rf, ctx) { if (rf & 1) {
|
|
196
|
+
const _r10 = i0.ɵɵgetCurrentView();
|
|
197
|
+
i0.ɵɵnamespaceSVG();
|
|
198
|
+
i0.ɵɵelementStart(0, "g", 48);
|
|
199
|
+
i0.ɵɵlistener("mousedown", function GraphViewComponent_For_11_Template_g_mousedown_0_listener($event) { const node_r11 = i0.ɵɵrestoreView(_r10).$implicit; const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.OnNodeMouseDown(node_r11, $event)); })("click", function GraphViewComponent_For_11_Template_g_click_0_listener($event) { const node_r11 = i0.ɵɵrestoreView(_r10).$implicit; const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.OnNodeClick(node_r11, $event)); })("dblclick", function GraphViewComponent_For_11_Template_g_dblclick_0_listener() { const node_r11 = i0.ɵɵrestoreView(_r10).$implicit; const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.NavigateToEntity(node_r11)); });
|
|
200
|
+
i0.ɵɵconditionalCreate(1, GraphViewComponent_For_11_Conditional_1_Template, 1, 0, ":svg:circle", 49)(2, GraphViewComponent_For_11_Conditional_2_Template, 1, 0, ":svg:circle", 50);
|
|
201
|
+
i0.ɵɵelement(3, "circle", 51)(4, "circle");
|
|
202
|
+
i0.ɵɵelementStart(5, "text", 52);
|
|
203
|
+
i0.ɵɵtext(6);
|
|
204
|
+
i0.ɵɵelementEnd();
|
|
205
|
+
i0.ɵɵconditionalCreate(7, GraphViewComponent_For_11_Conditional_7_Template, 2, 2, ":svg:text", 53);
|
|
206
|
+
i0.ɵɵelementEnd();
|
|
207
|
+
} if (rf & 2) {
|
|
208
|
+
const node_r11 = ctx.$implicit;
|
|
209
|
+
const ctx_r2 = i0.ɵɵnextContext();
|
|
210
|
+
i0.ɵɵattribute("transform", "translate(" + (node_r11.X || 0) + "," + (node_r11.Y || 0) + ")");
|
|
211
|
+
i0.ɵɵadvance();
|
|
212
|
+
i0.ɵɵconditional(ctx_r2.IsNodeSelected(node_r11) ? 1 : ctx_r2.IsFocalNode(node_r11) ? 2 : -1);
|
|
213
|
+
i0.ɵɵadvance(2);
|
|
214
|
+
i0.ɵɵattribute("r", node_r11.Radius || 26)("stroke", ctx_r2.GetNodeColor(node_r11));
|
|
215
|
+
i0.ɵɵadvance();
|
|
216
|
+
i0.ɵɵattribute("r", (node_r11.Radius || 26) - 8)("fill", ctx_r2.GetNodeColor(node_r11));
|
|
217
|
+
i0.ɵɵadvance();
|
|
218
|
+
i0.ɵɵattribute("y", (node_r11.Radius || 26) + 16);
|
|
219
|
+
i0.ɵɵadvance();
|
|
220
|
+
i0.ɵɵtextInterpolate1(" ", ctx_r2.TruncateLabel(node_r11.Label, 18), " ");
|
|
221
|
+
i0.ɵɵadvance();
|
|
222
|
+
i0.ɵɵconditional(node_r11.Sublabel ? 7 : -1);
|
|
223
|
+
} }
|
|
224
|
+
export const DEFAULT_GRAPH_CATEGORIES = [
|
|
225
|
+
{ Category: 'person', Label: 'Person', Color: '#10b981', IconClass: 'fa-solid fa-user' },
|
|
226
|
+
{ Category: 'organization', Label: 'Organization', Color: '#38bdf8', IconClass: 'fa-solid fa-building' },
|
|
227
|
+
{ Category: 'committee', Label: 'Committee', Color: '#8b5cf6', IconClass: 'fa-solid fa-landmark' },
|
|
228
|
+
{ Category: 'account', Label: 'Holding / Account', Color: '#f59e0b', IconClass: 'fa-solid fa-building-flag' },
|
|
229
|
+
{ Category: 'asset', Label: 'Asset', Color: '#ec4899', IconClass: 'fa-solid fa-gem' },
|
|
230
|
+
{ Category: 'group', Label: 'Group', Color: '#06b6d4', IconClass: 'fa-solid fa-users' },
|
|
231
|
+
{ Category: 'custom', Label: 'Custom', Color: '#64748b', IconClass: 'fa-solid fa-circle-nodes' }
|
|
232
|
+
];
|
|
233
|
+
const DYNAMIC_CATEGORY_PALETTE = [
|
|
234
|
+
'#10b981', '#38bdf8', '#8b5cf6', '#f59e0b', '#ec4899',
|
|
235
|
+
'#06b6d4', '#84cc16', '#f43f5e', '#a855f7', '#14b8a6'
|
|
236
|
+
];
|
|
237
|
+
const GRAPH_VIEW_CSS = `
|
|
238
|
+
:host {
|
|
239
|
+
display: block;
|
|
240
|
+
width: 100%;
|
|
241
|
+
height: 100%;
|
|
242
|
+
position: relative;
|
|
243
|
+
overflow: hidden;
|
|
244
|
+
background: var(--mj-bg-surface-sunken, #0b1220);
|
|
245
|
+
border-radius: var(--mj-radius-lg, 12px);
|
|
246
|
+
border: 1px solid var(--mj-border-default, #223254);
|
|
247
|
+
user-select: none;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.mj-graph-wrapper {
|
|
251
|
+
width: 100%;
|
|
252
|
+
height: 100%;
|
|
253
|
+
position: relative;
|
|
254
|
+
overflow: hidden;
|
|
255
|
+
display: flex;
|
|
256
|
+
flex-direction: column;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.mj-graph-svg-canvas {
|
|
260
|
+
width: 100%;
|
|
261
|
+
height: 100%;
|
|
262
|
+
cursor: grab;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.mj-graph-svg-canvas.is-panning {
|
|
266
|
+
cursor: grabbing;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* Floating Toolbar */
|
|
270
|
+
.mj-graph-toolbar {
|
|
271
|
+
position: absolute;
|
|
272
|
+
top: 14px;
|
|
273
|
+
left: 14px;
|
|
274
|
+
z-index: 20;
|
|
275
|
+
display: flex;
|
|
276
|
+
align-items: center;
|
|
277
|
+
gap: 8px;
|
|
278
|
+
background: rgba(20, 31, 54, 0.88);
|
|
279
|
+
backdrop-filter: blur(10px);
|
|
280
|
+
border: 1px solid var(--mj-border-default, #223254);
|
|
281
|
+
padding: 6px 12px;
|
|
282
|
+
border-radius: var(--mj-radius-lg, 10px);
|
|
283
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.mj-graph-tool-btn {
|
|
287
|
+
background: var(--mj-bg-surface-elevated, #1a2744);
|
|
288
|
+
border: 1px solid var(--mj-border-default, #223254);
|
|
289
|
+
color: var(--mj-text-primary, #f8fafc);
|
|
290
|
+
border-radius: 6px;
|
|
291
|
+
padding: 5px 10px;
|
|
292
|
+
font-size: 11.5px;
|
|
293
|
+
font-weight: 600;
|
|
294
|
+
cursor: pointer;
|
|
295
|
+
display: flex;
|
|
296
|
+
align-items: center;
|
|
297
|
+
gap: 6px;
|
|
298
|
+
transition: all 0.15s ease;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.mj-graph-tool-btn:hover {
|
|
302
|
+
border-color: var(--mj-brand-primary, #38bdf8);
|
|
303
|
+
background: var(--mj-border-hover, #334876);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.mj-graph-tool-btn.active {
|
|
307
|
+
background: var(--mj-brand-primary, #38bdf8);
|
|
308
|
+
color: #090e1a;
|
|
309
|
+
border-color: var(--mj-brand-primary, #38bdf8);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.mj-graph-separator {
|
|
313
|
+
width: 1px;
|
|
314
|
+
height: 18px;
|
|
315
|
+
background: var(--mj-border-default, #223254);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.mj-graph-search-input {
|
|
319
|
+
background: var(--mj-bg-surface-sunken, #090e1a);
|
|
320
|
+
border: 1px solid var(--mj-border-default, #223254);
|
|
321
|
+
border-radius: 6px;
|
|
322
|
+
color: var(--mj-text-primary, #f8fafc);
|
|
323
|
+
padding: 4px 8px 4px 24px;
|
|
324
|
+
font-size: 11.5px;
|
|
325
|
+
width: 150px;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.mj-graph-search-input:focus {
|
|
329
|
+
outline: none;
|
|
330
|
+
border-color: var(--mj-brand-primary, #38bdf8);
|
|
331
|
+
width: 190px;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* Floating Legend */
|
|
335
|
+
.mj-graph-legend {
|
|
336
|
+
position: absolute;
|
|
337
|
+
bottom: 14px;
|
|
338
|
+
left: 14px;
|
|
339
|
+
z-index: 20;
|
|
340
|
+
display: flex;
|
|
341
|
+
align-items: center;
|
|
342
|
+
gap: 12px;
|
|
343
|
+
background: rgba(20, 31, 54, 0.88);
|
|
344
|
+
backdrop-filter: blur(10px);
|
|
345
|
+
border: 1px solid var(--mj-border-default, #223254);
|
|
346
|
+
padding: 6px 14px;
|
|
347
|
+
border-radius: var(--mj-radius-lg, 10px);
|
|
348
|
+
font-size: 11.5px;
|
|
349
|
+
color: var(--mj-text-secondary, #94a3b8);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.mj-graph-legend-item {
|
|
353
|
+
display: flex;
|
|
354
|
+
align-items: center;
|
|
355
|
+
gap: 6px;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.mj-graph-legend-dot {
|
|
359
|
+
width: 10px;
|
|
360
|
+
height: 10px;
|
|
361
|
+
border-radius: 50%;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* Inspector Drawer */
|
|
365
|
+
.mj-graph-inspector {
|
|
366
|
+
position: absolute;
|
|
367
|
+
top: 14px;
|
|
368
|
+
right: 14px;
|
|
369
|
+
width: 300px;
|
|
370
|
+
max-height: calc(100% - 28px);
|
|
371
|
+
background: rgba(20, 31, 54, 0.95);
|
|
372
|
+
backdrop-filter: blur(14px);
|
|
373
|
+
border: 1px solid var(--mj-border-default, #223254);
|
|
374
|
+
border-radius: var(--mj-radius-lg, 12px);
|
|
375
|
+
padding: 16px;
|
|
376
|
+
display: flex;
|
|
377
|
+
flex-direction: column;
|
|
378
|
+
gap: 12px;
|
|
379
|
+
z-index: 25;
|
|
380
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
|
381
|
+
overflow-y: auto;
|
|
382
|
+
scrollbar-width: thin;
|
|
383
|
+
animation: mjGraphSlide 0.2s ease;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.mj-graph-inspector-header {
|
|
387
|
+
display: flex;
|
|
388
|
+
align-items: center;
|
|
389
|
+
justify-content: space-between;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.mj-graph-inspector-title {
|
|
393
|
+
font-size: 11px;
|
|
394
|
+
font-weight: 700;
|
|
395
|
+
text-transform: uppercase;
|
|
396
|
+
letter-spacing: 0.05em;
|
|
397
|
+
color: var(--mj-text-muted, #64748b);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.mj-graph-inspector-close {
|
|
401
|
+
background: none;
|
|
402
|
+
border: none;
|
|
403
|
+
color: var(--mj-text-muted, #64748b);
|
|
404
|
+
cursor: pointer;
|
|
405
|
+
font-size: 14px;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.mj-graph-inspector-card {
|
|
409
|
+
display: flex;
|
|
410
|
+
align-items: center;
|
|
411
|
+
gap: 12px;
|
|
412
|
+
background: var(--mj-bg-surface, #111a2e);
|
|
413
|
+
border: 1px solid var(--mj-border-default, #223254);
|
|
414
|
+
border-radius: 8px;
|
|
415
|
+
padding: 10px 12px;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.mj-graph-inspector-avatar {
|
|
419
|
+
width: 40px;
|
|
420
|
+
height: 40px;
|
|
421
|
+
border-radius: 8px;
|
|
422
|
+
display: flex;
|
|
423
|
+
align-items: center;
|
|
424
|
+
justify-content: center;
|
|
425
|
+
font-size: 16px;
|
|
426
|
+
color: #fff;
|
|
427
|
+
flex-shrink: 0;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.mj-graph-inspector-actions {
|
|
431
|
+
display: flex;
|
|
432
|
+
flex-direction: column;
|
|
433
|
+
gap: 6px;
|
|
434
|
+
margin-top: 4px;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/* SVG Rendering Elements */
|
|
438
|
+
.mj-edge-path {
|
|
439
|
+
stroke: var(--mj-border-hover, #2d416b);
|
|
440
|
+
stroke-width: 2;
|
|
441
|
+
fill: none;
|
|
442
|
+
transition: stroke 0.2s ease;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.mj-edge-path.is-highlighted {
|
|
446
|
+
stroke: var(--mj-brand-primary, #38bdf8);
|
|
447
|
+
stroke-width: 3;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.mj-edge-text {
|
|
451
|
+
font-size: 10px;
|
|
452
|
+
font-weight: 500;
|
|
453
|
+
fill: var(--mj-text-muted, #64748b);
|
|
454
|
+
text-anchor: middle;
|
|
455
|
+
pointer-events: none;
|
|
456
|
+
paint-order: stroke;
|
|
457
|
+
stroke: var(--mj-bg-surface-sunken, #0b1220);
|
|
458
|
+
stroke-width: 3px;
|
|
459
|
+
stroke-linecap: butt;
|
|
460
|
+
stroke-linejoin: miter;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.mj-node-group {
|
|
464
|
+
cursor: pointer;
|
|
465
|
+
transition: transform 0.1s ease;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.mj-node-group:hover .mj-node-circle {
|
|
469
|
+
filter: drop-shadow(0 0 10px var(--mj-brand-primary, #38bdf8));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.mj-node-circle {
|
|
473
|
+
transition: all 0.2s ease;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.mj-node-label {
|
|
477
|
+
font-size: 11px;
|
|
478
|
+
font-weight: 700;
|
|
479
|
+
fill: var(--mj-text-primary, #f8fafc);
|
|
480
|
+
text-anchor: middle;
|
|
481
|
+
pointer-events: none;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.mj-node-sublabel {
|
|
485
|
+
font-size: 9.5px;
|
|
486
|
+
font-weight: 500;
|
|
487
|
+
fill: var(--mj-text-secondary, #94a3b8);
|
|
488
|
+
text-anchor: middle;
|
|
489
|
+
pointer-events: none;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
@keyframes mjGraphSlide {
|
|
493
|
+
from { opacity: 0; transform: translateX(20px); }
|
|
494
|
+
to { opacity: 1; transform: translateX(0); }
|
|
495
|
+
}
|
|
496
|
+
`;
|
|
497
|
+
/**
|
|
498
|
+
* `<mj-graph-view>` — Interactive network and entity relationship graph visualization.
|
|
499
|
+
*
|
|
500
|
+
* Supports force-directed dynamic layout, multi-category entity badges, hop-expansion,
|
|
501
|
+
* zoom/pan controls, inspector drawer, search filters, and cancelable Before/After event pairs.
|
|
502
|
+
*/
|
|
503
|
+
export class GraphViewComponent {
|
|
504
|
+
cdr = inject(ChangeDetectorRef);
|
|
505
|
+
WrapperRef;
|
|
506
|
+
CanvasRef;
|
|
507
|
+
// ── Inputs ────────────────────────────────────────────────────────
|
|
508
|
+
Nodes = [];
|
|
509
|
+
Edges = [];
|
|
510
|
+
Categories = DEFAULT_GRAPH_CATEGORIES;
|
|
511
|
+
LayoutMode = 'force';
|
|
512
|
+
MaxHopDistance = 2;
|
|
513
|
+
SelectedNodeId;
|
|
514
|
+
FocalNodeId;
|
|
515
|
+
AutoOpenInspector = false;
|
|
516
|
+
ShowToolbar = true;
|
|
517
|
+
ShowLegend = true;
|
|
518
|
+
ShowInspector = true;
|
|
519
|
+
Searchable = true;
|
|
520
|
+
AutoFitOnLoad = true;
|
|
521
|
+
Physics = {
|
|
522
|
+
Repulsion: -500,
|
|
523
|
+
LinkDistance: 130,
|
|
524
|
+
Gravity: 0.05,
|
|
525
|
+
Damping: 0.88,
|
|
526
|
+
MaxIterations: 200
|
|
527
|
+
};
|
|
528
|
+
// ── Cancelable Before/After Outputs ────────────────────────────────
|
|
529
|
+
BeforeNodeSelect = new EventEmitter();
|
|
530
|
+
NodeSelected = new EventEmitter();
|
|
531
|
+
BeforeEdgeSelect = new EventEmitter();
|
|
532
|
+
EdgeSelected = new EventEmitter();
|
|
533
|
+
BeforeHopExpand = new EventEmitter();
|
|
534
|
+
HopExpanded = new EventEmitter();
|
|
535
|
+
BeforeLayoutChange = new EventEmitter();
|
|
536
|
+
LayoutChanged = new EventEmitter();
|
|
537
|
+
BeforeNodeNavigate = new EventEmitter();
|
|
538
|
+
NodeNavigated = new EventEmitter();
|
|
539
|
+
ViewportTransform = new EventEmitter();
|
|
540
|
+
// ── Internal State ────────────────────────────────────────────────
|
|
541
|
+
FilteredNodes = [];
|
|
542
|
+
SelectedNode = null;
|
|
543
|
+
SelectedEdge = null;
|
|
544
|
+
SearchQuery = '';
|
|
545
|
+
Scale = 1.0;
|
|
546
|
+
PanX = 200;
|
|
547
|
+
PanY = 150;
|
|
548
|
+
IsPanning = false;
|
|
549
|
+
panStartX = 0;
|
|
550
|
+
panStartY = 0;
|
|
551
|
+
draggedNode = null;
|
|
552
|
+
simulation = null;
|
|
553
|
+
ngOnInit() {
|
|
554
|
+
this.InitializePositions();
|
|
555
|
+
this.ApplyFilters();
|
|
556
|
+
this.SimulatePhysics();
|
|
557
|
+
}
|
|
558
|
+
ngOnChanges(changes) {
|
|
559
|
+
if (changes['Nodes'] || changes['Edges']) {
|
|
560
|
+
this.InitializePositions();
|
|
561
|
+
this.ApplyFilters();
|
|
562
|
+
this.SimulatePhysics();
|
|
563
|
+
}
|
|
564
|
+
if (changes['SelectedNodeId'] && this.SelectedNodeId) {
|
|
565
|
+
const found = this.Nodes.find(n => UUIDsEqual(n.ID, this.SelectedNodeId));
|
|
566
|
+
if (found && this.AutoOpenInspector) {
|
|
567
|
+
this.SelectedNode = found;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
ngOnDestroy() {
|
|
572
|
+
if (this.simulation) {
|
|
573
|
+
this.simulation.stop();
|
|
574
|
+
this.simulation = null;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
// ── Positioning & D3 Physics Engine ──────────────────────────────────
|
|
578
|
+
InitializePositions() {
|
|
579
|
+
const cx = this.GetContainerWidth() / 2 || 400;
|
|
580
|
+
const cy = this.GetContainerHeight() / 2 || 250;
|
|
581
|
+
const count = this.Nodes.length || 1;
|
|
582
|
+
this.Nodes.forEach((node, idx) => {
|
|
583
|
+
if (node.x === undefined || node.y === undefined) {
|
|
584
|
+
if (node.X !== undefined && node.Y !== undefined) {
|
|
585
|
+
node.x = node.X;
|
|
586
|
+
node.y = node.Y;
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
const angle = (idx / count) * 2 * Math.PI;
|
|
590
|
+
const r = 100 + Math.random() * 60;
|
|
591
|
+
node.x = cx + r * Math.cos(angle);
|
|
592
|
+
node.y = cy + r * Math.sin(angle);
|
|
593
|
+
node.X = node.x;
|
|
594
|
+
node.Y = node.y;
|
|
595
|
+
}
|
|
596
|
+
node.vx = 0;
|
|
597
|
+
node.vy = 0;
|
|
598
|
+
node.VX = 0;
|
|
599
|
+
node.VY = 0;
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
SimulatePhysics() {
|
|
604
|
+
if (this.LayoutMode === 'circular') {
|
|
605
|
+
this.ApplyCircularLayout();
|
|
606
|
+
if (this.AutoFitOnLoad) {
|
|
607
|
+
this.AutoFitToView();
|
|
608
|
+
}
|
|
609
|
+
this.cdr.markForCheck();
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (this.simulation) {
|
|
613
|
+
this.simulation.stop();
|
|
614
|
+
this.simulation = null;
|
|
615
|
+
}
|
|
616
|
+
const cx = this.GetContainerWidth() / 2 || 400;
|
|
617
|
+
const cy = this.GetContainerHeight() / 2 || 250;
|
|
618
|
+
// Resolve endpoints to node objects so a case-skewed UUID or dangling
|
|
619
|
+
// edge cannot throw "node not found" out of ngOnInit. Raw-string
|
|
620
|
+
// .id(d => d.ID) compared with ===, which UUIDsEqual elsewhere
|
|
621
|
+
// deliberately does not.
|
|
622
|
+
const nodeById = new Map();
|
|
623
|
+
for (const node of this.Nodes) {
|
|
624
|
+
const key = NormalizeUUID(node.ID);
|
|
625
|
+
if (key) {
|
|
626
|
+
nodeById.set(key, node);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const links = [];
|
|
630
|
+
for (const edge of this.Edges) {
|
|
631
|
+
const source = nodeById.get(NormalizeUUID(edge.SourceID));
|
|
632
|
+
const target = nodeById.get(NormalizeUUID(edge.TargetID));
|
|
633
|
+
if (source && target) {
|
|
634
|
+
links.push({ source, target });
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
this.simulation = d3.forceSimulation(this.Nodes)
|
|
638
|
+
.force('link', d3.forceLink(links)
|
|
639
|
+
.distance(this.Physics.LinkDistance || 120))
|
|
640
|
+
.force('charge', d3.forceManyBody()
|
|
641
|
+
.strength(this.Physics.Repulsion || -450)
|
|
642
|
+
.distanceMax(700))
|
|
643
|
+
.force('collide', d3.forceCollide((d) => (d.Radius || 26) + 16).iterations(2))
|
|
644
|
+
.force('center', d3.forceCenter(cx, cy));
|
|
645
|
+
// Headless Pre-Warming: run 80 ticks synchronously in memory so the SVG renders resting immediately
|
|
646
|
+
this.simulation.stop();
|
|
647
|
+
for (let i = 0; i < 90; ++i) {
|
|
648
|
+
this.simulation.tick();
|
|
649
|
+
}
|
|
650
|
+
this.SyncCoordinates();
|
|
651
|
+
// Auto-center & fit into view
|
|
652
|
+
if (this.AutoFitOnLoad) {
|
|
653
|
+
this.AutoFitToView();
|
|
654
|
+
}
|
|
655
|
+
// Attach live tick handler for interactive drag & drop
|
|
656
|
+
this.simulation.on('tick', () => {
|
|
657
|
+
this.SyncCoordinates();
|
|
658
|
+
this.cdr.markForCheck();
|
|
659
|
+
});
|
|
660
|
+
this.cdr.markForCheck();
|
|
661
|
+
}
|
|
662
|
+
SyncCoordinates() {
|
|
663
|
+
for (const n of this.Nodes) {
|
|
664
|
+
n.X = n.x;
|
|
665
|
+
n.Y = n.y;
|
|
666
|
+
n.VX = n.vx;
|
|
667
|
+
n.VY = n.vy;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
ApplyCircularLayout() {
|
|
671
|
+
const cx = this.GetContainerWidth() / 2 || 400;
|
|
672
|
+
const cy = this.GetContainerHeight() / 2 || 250;
|
|
673
|
+
const r = Math.min(cx, cy) * 0.7 || 160;
|
|
674
|
+
const count = this.Nodes.length || 1;
|
|
675
|
+
this.Nodes.forEach((node, idx) => {
|
|
676
|
+
const angle = (idx / count) * 2 * Math.PI;
|
|
677
|
+
node.x = cx + r * Math.cos(angle);
|
|
678
|
+
node.y = cy + r * Math.sin(angle);
|
|
679
|
+
node.X = node.x;
|
|
680
|
+
node.Y = node.y;
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
// ── Public Programmatic Methods ───────────────────────────────────
|
|
684
|
+
ZoomIn() {
|
|
685
|
+
this.Scale = Math.min(this.Scale * 1.2, 3.0);
|
|
686
|
+
this.EmitViewport();
|
|
687
|
+
}
|
|
688
|
+
ZoomOut() {
|
|
689
|
+
this.Scale = Math.max(this.Scale / 1.2, 0.3);
|
|
690
|
+
this.EmitViewport();
|
|
691
|
+
}
|
|
692
|
+
FitToView() {
|
|
693
|
+
this.AutoFitToView(40);
|
|
694
|
+
}
|
|
695
|
+
AutoFitToView(padding = 40) {
|
|
696
|
+
if (!this.Nodes || this.Nodes.length === 0)
|
|
697
|
+
return;
|
|
698
|
+
const width = this.GetContainerWidth() || 800;
|
|
699
|
+
const height = this.GetContainerHeight() || 420;
|
|
700
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
701
|
+
for (const n of this.Nodes) {
|
|
702
|
+
const nx = n.x ?? n.X ?? 0;
|
|
703
|
+
const ny = n.y ?? n.Y ?? 0;
|
|
704
|
+
const r = (n.Radius || 26) + 20; // include badge & labels
|
|
705
|
+
if (nx - r < minX)
|
|
706
|
+
minX = nx - r;
|
|
707
|
+
if (nx + r > maxX)
|
|
708
|
+
maxX = nx + r;
|
|
709
|
+
if (ny - r < minY)
|
|
710
|
+
minY = ny - r;
|
|
711
|
+
if (ny + r > maxY)
|
|
712
|
+
maxY = ny + r;
|
|
713
|
+
}
|
|
714
|
+
if (!isFinite(minX) || !isFinite(maxX) || !isFinite(minY) || !isFinite(maxY))
|
|
715
|
+
return;
|
|
716
|
+
const bboxW = Math.max(maxX - minX, 100);
|
|
717
|
+
const bboxH = Math.max(maxY - minY, 100);
|
|
718
|
+
const scaleX = (width - padding * 2) / bboxW;
|
|
719
|
+
const scaleY = (height - padding * 2) / bboxH;
|
|
720
|
+
const targetScale = Math.min(Math.max(Math.min(scaleX, scaleY), 0.4), 1.25);
|
|
721
|
+
this.Scale = targetScale;
|
|
722
|
+
this.PanX = (width - (minX + maxX) * this.Scale) / 2;
|
|
723
|
+
this.PanY = (height - (minY + maxY) * this.Scale) / 2;
|
|
724
|
+
this.EmitViewport();
|
|
725
|
+
this.cdr.markForCheck();
|
|
726
|
+
}
|
|
727
|
+
Rearrange() {
|
|
728
|
+
this.InitializePositions();
|
|
729
|
+
this.SimulatePhysics();
|
|
730
|
+
}
|
|
731
|
+
SetLayoutMode(mode) {
|
|
732
|
+
const beforeEvent = new BeforeLayoutChangeEventArgs(mode, this.LayoutMode);
|
|
733
|
+
this.BeforeLayoutChange.emit(beforeEvent);
|
|
734
|
+
if (beforeEvent.Cancel)
|
|
735
|
+
return;
|
|
736
|
+
const prev = this.LayoutMode;
|
|
737
|
+
this.LayoutMode = mode;
|
|
738
|
+
this.SimulatePhysics();
|
|
739
|
+
this.LayoutChanged.emit(new LayoutChangedEventArgs(mode, prev));
|
|
740
|
+
}
|
|
741
|
+
SelectNode(id) {
|
|
742
|
+
const node = this.Nodes.find(n => UUIDsEqual(n.ID, id));
|
|
743
|
+
if (node)
|
|
744
|
+
this.OnNodeClick(node, new MouseEvent('click'));
|
|
745
|
+
}
|
|
746
|
+
ClearSelection() {
|
|
747
|
+
this.SelectedNode = null;
|
|
748
|
+
this.SelectedEdge = null;
|
|
749
|
+
this.cdr.markForCheck();
|
|
750
|
+
}
|
|
751
|
+
SearchNodes(query) {
|
|
752
|
+
this.SearchQuery = query;
|
|
753
|
+
this.ApplyFilters();
|
|
754
|
+
}
|
|
755
|
+
ExpandHops(nodeId, depth) {
|
|
756
|
+
const node = this.Nodes.find(n => UUIDsEqual(n.ID, nodeId));
|
|
757
|
+
if (!node)
|
|
758
|
+
return;
|
|
759
|
+
const beforeEvent = new BeforeHopExpandEventArgs(node, (node.HopDistance || 0) + depth, node.HopDistance || 0);
|
|
760
|
+
this.BeforeHopExpand.emit(beforeEvent);
|
|
761
|
+
if (beforeEvent.Cancel)
|
|
762
|
+
return;
|
|
763
|
+
// Fire after event for host listener to fetch incremental relations
|
|
764
|
+
this.HopExpanded.emit(new HopExpandedEventArgs(node, [], [], (node.HopDistance || 0) + depth));
|
|
765
|
+
}
|
|
766
|
+
// ── Event Handlers ────────────────────────────────────────────────
|
|
767
|
+
OnNodeClick(node, event) {
|
|
768
|
+
event.stopPropagation();
|
|
769
|
+
const beforeEvent = new BeforeNodeSelectEventArgs(node, this.SelectedNode);
|
|
770
|
+
this.BeforeNodeSelect.emit(beforeEvent);
|
|
771
|
+
if (beforeEvent.Cancel)
|
|
772
|
+
return;
|
|
773
|
+
const prev = this.SelectedNode;
|
|
774
|
+
this.SelectedNode = node;
|
|
775
|
+
this.SelectedEdge = null;
|
|
776
|
+
this.NodeSelected.emit(new NodeSelectedEventArgs(node, prev));
|
|
777
|
+
this.cdr.markForCheck();
|
|
778
|
+
}
|
|
779
|
+
OnEdgeClick(edge, event) {
|
|
780
|
+
event.stopPropagation();
|
|
781
|
+
const src = this.Nodes.find(n => UUIDsEqual(n.ID, edge.SourceID)) || null;
|
|
782
|
+
const tgt = this.Nodes.find(n => UUIDsEqual(n.ID, edge.TargetID)) || null;
|
|
783
|
+
const beforeEvent = new BeforeEdgeSelectEventArgs(edge, src, tgt);
|
|
784
|
+
this.BeforeEdgeSelect.emit(beforeEvent);
|
|
785
|
+
if (beforeEvent.Cancel)
|
|
786
|
+
return;
|
|
787
|
+
this.SelectedEdge = edge;
|
|
788
|
+
this.EdgeSelected.emit(new EdgeSelectedEventArgs(edge, src, tgt));
|
|
789
|
+
this.cdr.markForCheck();
|
|
790
|
+
}
|
|
791
|
+
NavigateToEntity(node) {
|
|
792
|
+
// Focal-node dblclick must not navigate away from the record this
|
|
793
|
+
// graph is illustrating — the inspector already hides the navigate
|
|
794
|
+
// button for the same reason.
|
|
795
|
+
if (this.IsFocalNode(node)) {
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const rawId = this.stripCategoryPrefix(String(node.Data?.['ID'] || node.ID));
|
|
799
|
+
const entityName = String(node.Data?.['EntityName'] || (node.Category === 'person' ? 'MJ_BizApps_Common: People' : 'MJ_BizApps_Common: Organizations'));
|
|
800
|
+
const beforeEvent = new BeforeNodeNavigateEventArgs(node, entityName, rawId);
|
|
801
|
+
this.BeforeNodeNavigate.emit(beforeEvent);
|
|
802
|
+
if (beforeEvent.Cancel)
|
|
803
|
+
return;
|
|
804
|
+
// Hosts navigate. This widget is L1 and must not import Explorer's
|
|
805
|
+
// NavigationService — emit the intent and let the surface open the record.
|
|
806
|
+
this.NodeNavigated.emit(new NodeNavigatedEventArgs(node, entityName, rawId));
|
|
807
|
+
}
|
|
808
|
+
// ── Mouse Drag & Pan Handlers ─────────────────────────────────────
|
|
809
|
+
OnCanvasMouseDown(event) {
|
|
810
|
+
if (event.target === this.CanvasRef.nativeElement || event.target.tagName === 'svg') {
|
|
811
|
+
this.IsPanning = true;
|
|
812
|
+
this.panStartX = event.clientX - this.PanX;
|
|
813
|
+
this.panStartY = event.clientY - this.PanY;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
OnCanvasMouseMove(event) {
|
|
817
|
+
if (this.IsPanning) {
|
|
818
|
+
this.PanX = event.clientX - this.panStartX;
|
|
819
|
+
this.PanY = event.clientY - this.panStartY;
|
|
820
|
+
this.EmitViewport();
|
|
821
|
+
this.cdr.markForCheck();
|
|
822
|
+
}
|
|
823
|
+
else if (this.draggedNode) {
|
|
824
|
+
const coords = this.GetCanvasRelativeCoords(event);
|
|
825
|
+
this.draggedNode.fx = coords.x;
|
|
826
|
+
this.draggedNode.fy = coords.y;
|
|
827
|
+
this.draggedNode.x = coords.x;
|
|
828
|
+
this.draggedNode.y = coords.y;
|
|
829
|
+
this.draggedNode.X = coords.x;
|
|
830
|
+
this.draggedNode.Y = coords.y;
|
|
831
|
+
this.cdr.markForCheck();
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
OnCanvasMouseUp(_event) {
|
|
835
|
+
this.IsPanning = false;
|
|
836
|
+
if (this.draggedNode) {
|
|
837
|
+
this.draggedNode.fx = null;
|
|
838
|
+
this.draggedNode.fy = null;
|
|
839
|
+
this.draggedNode = null;
|
|
840
|
+
if (this.simulation) {
|
|
841
|
+
this.simulation.alphaTarget(0);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
OnCanvasWheel(event) {
|
|
846
|
+
event.preventDefault();
|
|
847
|
+
const delta = event.deltaY > 0 ? 0.9 : 1.1;
|
|
848
|
+
this.Scale = Math.min(Math.max(this.Scale * delta, 0.3), 3.0);
|
|
849
|
+
this.EmitViewport();
|
|
850
|
+
this.cdr.markForCheck();
|
|
851
|
+
}
|
|
852
|
+
OnNodeMouseDown(node, event) {
|
|
853
|
+
event.stopPropagation();
|
|
854
|
+
this.draggedNode = node;
|
|
855
|
+
node.fx = node.x ?? node.X;
|
|
856
|
+
node.fy = node.y ?? node.Y;
|
|
857
|
+
if (this.simulation) {
|
|
858
|
+
this.simulation.alphaTarget(0.3).restart();
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
EmitViewport() {
|
|
862
|
+
this.ViewportTransform.emit(new ViewportTransformEventArgs({ Scale: this.Scale, PanX: this.PanX, PanY: this.PanY }));
|
|
863
|
+
this.cdr.markForCheck();
|
|
864
|
+
}
|
|
865
|
+
GetCanvasRelativeCoords(event) {
|
|
866
|
+
const canvas = this.CanvasRef?.nativeElement;
|
|
867
|
+
if (!canvas) {
|
|
868
|
+
return {
|
|
869
|
+
x: (event.clientX - this.PanX) / this.Scale,
|
|
870
|
+
y: (event.clientY - this.PanY) / this.Scale
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
const rect = canvas.getBoundingClientRect();
|
|
874
|
+
const mouseX = event.clientX - rect.left;
|
|
875
|
+
const mouseY = event.clientY - rect.top;
|
|
876
|
+
return {
|
|
877
|
+
x: (mouseX - this.PanX) / this.Scale,
|
|
878
|
+
y: (mouseY - this.PanY) / this.Scale
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
GetContainerWidth() {
|
|
882
|
+
return this.CanvasRef?.nativeElement?.clientWidth || this.WrapperRef?.nativeElement?.clientWidth || 800;
|
|
883
|
+
}
|
|
884
|
+
GetContainerHeight() {
|
|
885
|
+
return this.CanvasRef?.nativeElement?.clientHeight || this.WrapperRef?.nativeElement?.clientHeight || 420;
|
|
886
|
+
}
|
|
887
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
888
|
+
ApplyFilters() {
|
|
889
|
+
const q = this.SearchQuery.trim().toLowerCase();
|
|
890
|
+
this.FilteredNodes = this.Nodes.filter(n => {
|
|
891
|
+
return !q || n.Label.toLowerCase().includes(q) || (n.Sublabel && n.Sublabel.toLowerCase().includes(q));
|
|
892
|
+
});
|
|
893
|
+
this.cdr.markForCheck();
|
|
894
|
+
}
|
|
895
|
+
GetNodeX(id) {
|
|
896
|
+
const node = this.Nodes.find(n => UUIDsEqual(n.ID, id));
|
|
897
|
+
return node ? (node.X ?? node.x ?? 0) : 0;
|
|
898
|
+
}
|
|
899
|
+
GetNodeY(id) {
|
|
900
|
+
const node = this.Nodes.find(n => UUIDsEqual(n.ID, id));
|
|
901
|
+
return node ? (node.Y ?? node.y ?? 0) : 0;
|
|
902
|
+
}
|
|
903
|
+
GetCategoryConfig(category) {
|
|
904
|
+
const key = String(category || '').toLowerCase();
|
|
905
|
+
const found = this.Categories?.find(c => c.Category.toLowerCase() === key);
|
|
906
|
+
if (found)
|
|
907
|
+
return found;
|
|
908
|
+
const builtin = DEFAULT_GRAPH_CATEGORIES.find(c => c.Category.toLowerCase() === key);
|
|
909
|
+
if (builtin)
|
|
910
|
+
return builtin;
|
|
911
|
+
// Deterministic palette hash for arbitrary consumer-defined categories
|
|
912
|
+
const hash = Math.abs(key.split('').reduce((acc, ch) => ((acc << 5) - acc) + ch.charCodeAt(0), 0));
|
|
913
|
+
const fallbackColor = DYNAMIC_CATEGORY_PALETTE[hash % DYNAMIC_CATEGORY_PALETTE.length];
|
|
914
|
+
const formattedLabel = category ? category.charAt(0).toUpperCase() + category.slice(1) : 'Item';
|
|
915
|
+
return {
|
|
916
|
+
Category: category || 'custom',
|
|
917
|
+
Label: formattedLabel,
|
|
918
|
+
Color: fallbackColor,
|
|
919
|
+
IconClass: 'fa-solid fa-circle-dot'
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
get ActiveCategories() {
|
|
923
|
+
const present = new Set();
|
|
924
|
+
for (const n of this.Nodes) {
|
|
925
|
+
if (n.Category)
|
|
926
|
+
present.add(n.Category.toLowerCase());
|
|
927
|
+
}
|
|
928
|
+
const result = [];
|
|
929
|
+
for (const catKey of present) {
|
|
930
|
+
result.push(this.GetCategoryConfig(catKey));
|
|
931
|
+
}
|
|
932
|
+
return result;
|
|
933
|
+
}
|
|
934
|
+
GetCategoryLabel(category) {
|
|
935
|
+
return this.GetCategoryConfig(category).Label;
|
|
936
|
+
}
|
|
937
|
+
GetNodeColor(node) {
|
|
938
|
+
return node.Color ?? this.GetCategoryConfig(node.Category).Color;
|
|
939
|
+
}
|
|
940
|
+
GetNodeIcon(node) {
|
|
941
|
+
return node.IconClass ?? this.GetCategoryConfig(node.Category).IconClass ?? 'fa-solid fa-circle-dot';
|
|
942
|
+
}
|
|
943
|
+
GetNodeEdgeCount(id) {
|
|
944
|
+
return this.Edges.filter(e => UUIDsEqual(e.SourceID, id) || UUIDsEqual(e.TargetID, id)).length;
|
|
945
|
+
}
|
|
946
|
+
IsEdgeHighlighted(edge) {
|
|
947
|
+
return UUIDsEqual(this.SelectedEdge?.ID, edge.ID) ||
|
|
948
|
+
UUIDsEqual(this.SelectedNode?.ID, edge.SourceID) ||
|
|
949
|
+
UUIDsEqual(this.SelectedNode?.ID, edge.TargetID);
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Whether this node is the selected one. Exists as a method because the template cannot call
|
|
953
|
+
* an imported function directly, and the comparison must go through UUIDsEqual rather than ===.
|
|
954
|
+
*/
|
|
955
|
+
IsNodeSelected(node) {
|
|
956
|
+
return UUIDsEqual(this.SelectedNode?.ID, node.ID);
|
|
957
|
+
}
|
|
958
|
+
TruncateLabel(text, maxLen) {
|
|
959
|
+
if (!text)
|
|
960
|
+
return '';
|
|
961
|
+
return text.length > maxLen ? text.substring(0, maxLen - 1) + '…' : text;
|
|
962
|
+
}
|
|
963
|
+
IsFocalNode(node) {
|
|
964
|
+
if (!node || !this.FocalNodeId)
|
|
965
|
+
return false;
|
|
966
|
+
const focalClean = this.stripCategoryPrefix(this.FocalNodeId).toLowerCase();
|
|
967
|
+
const nodeClean = this.stripCategoryPrefix(node.ID).toLowerCase();
|
|
968
|
+
const dataClean = node.Data?.['ID'] ? String(node.Data['ID']).toLowerCase() : '';
|
|
969
|
+
return UUIDsEqual(node.ID, this.FocalNodeId) || nodeClean === focalClean || dataClean === focalClean;
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Hosts prefix node IDs with `{category}:`. Categories are dynamic
|
|
973
|
+
* (`organization`, `asset`, `group`, plus caller keys), so strip any
|
|
974
|
+
* known-category prefix rather than a hardcoded subset.
|
|
975
|
+
*/
|
|
976
|
+
stripCategoryPrefix(id) {
|
|
977
|
+
const idx = id.indexOf(':');
|
|
978
|
+
if (idx <= 0) {
|
|
979
|
+
return id;
|
|
980
|
+
}
|
|
981
|
+
const prefix = id.slice(0, idx).toLowerCase();
|
|
982
|
+
const known = this.Categories?.some(c => c.Category.toLowerCase() === prefix) ||
|
|
983
|
+
DEFAULT_GRAPH_CATEGORIES.some(c => c.Category.toLowerCase() === prefix);
|
|
984
|
+
return known ? id.slice(idx + 1) : id;
|
|
985
|
+
}
|
|
986
|
+
static ɵfac = function GraphViewComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || GraphViewComponent)(); };
|
|
987
|
+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: GraphViewComponent, selectors: [["mj-graph-view"]], viewQuery: function GraphViewComponent_Query(rf, ctx) { if (rf & 1) {
|
|
988
|
+
i0.ɵɵviewQuery(_c0, 7)(_c1, 7);
|
|
989
|
+
} if (rf & 2) {
|
|
990
|
+
let _t;
|
|
991
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.WrapperRef = _t.first);
|
|
992
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.CanvasRef = _t.first);
|
|
993
|
+
} }, inputs: { Nodes: "Nodes", Edges: "Edges", Categories: "Categories", LayoutMode: "LayoutMode", MaxHopDistance: "MaxHopDistance", SelectedNodeId: "SelectedNodeId", FocalNodeId: "FocalNodeId", AutoOpenInspector: "AutoOpenInspector", ShowToolbar: "ShowToolbar", ShowLegend: "ShowLegend", ShowInspector: "ShowInspector", Searchable: "Searchable", AutoFitOnLoad: "AutoFitOnLoad", Physics: "Physics" }, outputs: { BeforeNodeSelect: "BeforeNodeSelect", NodeSelected: "NodeSelected", BeforeEdgeSelect: "BeforeEdgeSelect", EdgeSelected: "EdgeSelected", BeforeHopExpand: "BeforeHopExpand", HopExpanded: "HopExpanded", BeforeLayoutChange: "BeforeLayoutChange", LayoutChanged: "LayoutChanged", BeforeNodeNavigate: "BeforeNodeNavigate", NodeNavigated: "NodeNavigated", ViewportTransform: "ViewportTransform" }, features: [i0.ɵɵNgOnChangesFeature], decls: 12, vars: 6, consts: [["wrapper", ""], ["svgCanvas", ""], [1, "mj-graph-wrapper"], [1, "mj-graph-toolbar"], [1, "mj-graph-legend"], [1, "mj-graph-inspector"], [1, "mj-graph-svg-canvas", 3, "mousedown", "mousemove", "mouseup", "mouseleave", "wheel"], [1, "mj-node-group"], [2, "font-size", "12px", "font-weight", "700", "color", "#fff", "display", "flex", "align-items", "center", "gap", "6px"], [1, "fa-solid", "fa-circle-nodes", 2, "color", "var(--mj-brand-primary)"], [1, "mj-graph-separator"], ["type", "button", "title", "Force-Directed Physics", 1, "mj-graph-tool-btn", 3, "click"], [1, "fa-solid", "fa-atom"], ["type", "button", "title", "Circular Layout", 1, "mj-graph-tool-btn", 3, "click"], [1, "fa-solid", "fa-circle-notch"], ["type", "button", "title", "Zoom In", 1, "mj-graph-tool-btn", 3, "click"], [1, "fa-solid", "fa-magnifying-glass-plus"], ["type", "button", "title", "Zoom Out", 1, "mj-graph-tool-btn", 3, "click"], [1, "fa-solid", "fa-magnifying-glass-minus"], ["type", "button", "title", "Fit View", 1, "mj-graph-tool-btn", 3, "click"], [1, "fa-solid", "fa-expand"], ["type", "button", "title", "Re-simulate Physics", 1, "mj-graph-tool-btn", 3, "click"], [1, "fa-solid", "fa-arrows-rotate"], [2, "position", "relative"], [1, "fa-solid", "fa-magnifying-glass", 2, "position", "absolute", "left", "8px", "top", "50%", "transform", "translateY(-50%)", "font-size", "10px", "color", "var(--mj-text-muted)"], ["placeholder", "Search graph nodes...", 1, "mj-graph-search-input", 3, "ngModelChange", "ngModel"], [1, "mj-graph-legend-item"], [1, "mj-graph-legend-dot"], [1, "mj-graph-inspector-header"], [1, "mj-graph-inspector-title"], ["type", "button", 1, "mj-graph-inspector-close", 3, "click"], [1, "fa-solid", "fa-xmark"], [1, "mj-graph-inspector-card"], [1, "mj-graph-inspector-avatar"], [2, "overflow", "hidden", "flex", "1"], [2, "font-size", "13px", "font-weight", "700", "color", "#fff", "text-overflow", "ellipsis", "overflow", "hidden", "white-space", "nowrap"], [2, "display", "flex", "align-items", "center", "gap", "6px", "margin-top", "3px"], [2, "font-size", "11px", "color", "var(--mj-text-secondary)"], [2, "font-size", "10px", "padding", "1px 6px", "border-radius", "9999px", "background", "rgba(56, 189, 248, 0.15)", "color", "#38bdf8", "font-weight", "600", "border", "1px solid rgba(56, 189, 248, 0.3)"], [2, "font-size", "11.5px", "color", "var(--mj-text-secondary)", "line-height", "1.45"], [1, "mj-graph-inspector-actions"], ["type", "button", 1, "mj-graph-tool-btn", "active", 2, "justify-content", "center", "width", "100%"], ["type", "button", 1, "mj-graph-tool-btn", 2, "justify-content", "center", "width", "100%", 3, "click"], [1, "fa-solid", "fa-circle-nodes"], ["type", "button", 1, "mj-graph-tool-btn", "active", 2, "justify-content", "center", "width", "100%", 3, "click"], [1, "fa-solid", "fa-arrow-up-right-from-square"], [1, "mj-edge-path", 3, "click"], [1, "mj-edge-text"], [1, "mj-node-group", 3, "mousedown", "click", "dblclick"], ["r", "36", "fill", "rgba(56, 189, 248, 0.2)", "stroke", "#38bdf8", "stroke-width", "2"], ["r", "34", "fill", "none", "stroke", "rgba(56, 189, 248, 0.45)", "stroke-width", "2", "stroke-dasharray", "4 3"], ["fill", "#141f36", "stroke-width", "3", 1, "mj-node-circle"], [1, "mj-node-label"], [1, "mj-node-sublabel"]], template: function GraphViewComponent_Template(rf, ctx) { if (rf & 1) {
|
|
994
|
+
const _r1 = i0.ɵɵgetCurrentView();
|
|
995
|
+
i0.ɵɵelementStart(0, "div", 2, 0);
|
|
996
|
+
i0.ɵɵconditionalCreate(2, GraphViewComponent_Conditional_2_Template, 21, 5, "div", 3);
|
|
997
|
+
i0.ɵɵconditionalCreate(3, GraphViewComponent_Conditional_3_Template, 3, 0, "div", 4);
|
|
998
|
+
i0.ɵɵconditionalCreate(4, GraphViewComponent_Conditional_4_Template, 25, 9, "div", 5);
|
|
999
|
+
i0.ɵɵnamespaceSVG();
|
|
1000
|
+
i0.ɵɵelementStart(5, "svg", 6, 1);
|
|
1001
|
+
i0.ɵɵlistener("mousedown", function GraphViewComponent_Template_svg_mousedown_5_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.OnCanvasMouseDown($event)); })("mousemove", function GraphViewComponent_Template_svg_mousemove_5_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.OnCanvasMouseMove($event)); })("mouseup", function GraphViewComponent_Template_svg_mouseup_5_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.OnCanvasMouseUp($event)); })("mouseleave", function GraphViewComponent_Template_svg_mouseleave_5_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.OnCanvasMouseUp($event)); })("wheel", function GraphViewComponent_Template_svg_wheel_5_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.OnCanvasWheel($event)); });
|
|
1002
|
+
i0.ɵɵelementStart(7, "g");
|
|
1003
|
+
i0.ɵɵrepeaterCreate(8, GraphViewComponent_For_9_Template, 4, 9, ":svg:g", null, _forTrack0);
|
|
1004
|
+
i0.ɵɵrepeaterCreate(10, GraphViewComponent_For_11_Template, 8, 9, ":svg:g", 7, _forTrack0);
|
|
1005
|
+
i0.ɵɵelementEnd()()();
|
|
1006
|
+
} if (rf & 2) {
|
|
1007
|
+
i0.ɵɵadvance(2);
|
|
1008
|
+
i0.ɵɵconditional(ctx.ShowToolbar ? 2 : -1);
|
|
1009
|
+
i0.ɵɵadvance();
|
|
1010
|
+
i0.ɵɵconditional(ctx.ShowLegend && ctx.ActiveCategories.length > 0 ? 3 : -1);
|
|
1011
|
+
i0.ɵɵadvance();
|
|
1012
|
+
i0.ɵɵconditional(ctx.ShowInspector && ctx.SelectedNode ? 4 : -1);
|
|
1013
|
+
i0.ɵɵadvance();
|
|
1014
|
+
i0.ɵɵclassProp("is-panning", ctx.IsPanning);
|
|
1015
|
+
i0.ɵɵadvance(2);
|
|
1016
|
+
i0.ɵɵattribute("transform", "translate(" + ctx.PanX + "," + ctx.PanY + ") scale(" + ctx.Scale + ")");
|
|
1017
|
+
i0.ɵɵadvance();
|
|
1018
|
+
i0.ɵɵrepeater(ctx.Edges);
|
|
1019
|
+
i0.ɵɵadvance(2);
|
|
1020
|
+
i0.ɵɵrepeater(ctx.FilteredNodes);
|
|
1021
|
+
} }, dependencies: [CommonModule, FormsModule, i1.DefaultValueAccessor, i1.NgControlStatus, i1.NgModel], styles: ["[_nghost-%COMP%] {\n display: block;\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n background: var(--mj-bg-surface-sunken, #0b1220);\n border-radius: var(--mj-radius-lg, 12px);\n border: 1px solid var(--mj-border-default, #223254);\n user-select: none;\n}\n\n.mj-graph-wrapper[_ngcontent-%COMP%] {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n}\n\n.mj-graph-svg-canvas[_ngcontent-%COMP%] {\n width: 100%;\n height: 100%;\n cursor: grab;\n}\n\n.mj-graph-svg-canvas.is-panning[_ngcontent-%COMP%] {\n cursor: grabbing;\n}\n\n\n\n.mj-graph-toolbar[_ngcontent-%COMP%] {\n position: absolute;\n top: 14px;\n left: 14px;\n z-index: 20;\n display: flex;\n align-items: center;\n gap: 8px;\n background: rgba(20, 31, 54, 0.88);\n backdrop-filter: blur(10px);\n border: 1px solid var(--mj-border-default, #223254);\n padding: 6px 12px;\n border-radius: var(--mj-radius-lg, 10px);\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);\n}\n\n.mj-graph-tool-btn[_ngcontent-%COMP%] {\n background: var(--mj-bg-surface-elevated, #1a2744);\n border: 1px solid var(--mj-border-default, #223254);\n color: var(--mj-text-primary, #f8fafc);\n border-radius: 6px;\n padding: 5px 10px;\n font-size: 11.5px;\n font-weight: 600;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 6px;\n transition: all 0.15s ease;\n}\n\n.mj-graph-tool-btn[_ngcontent-%COMP%]:hover {\n border-color: var(--mj-brand-primary, #38bdf8);\n background: var(--mj-border-hover, #334876);\n}\n\n.mj-graph-tool-btn.active[_ngcontent-%COMP%] {\n background: var(--mj-brand-primary, #38bdf8);\n color: #090e1a;\n border-color: var(--mj-brand-primary, #38bdf8);\n}\n\n.mj-graph-separator[_ngcontent-%COMP%] {\n width: 1px;\n height: 18px;\n background: var(--mj-border-default, #223254);\n}\n\n.mj-graph-search-input[_ngcontent-%COMP%] {\n background: var(--mj-bg-surface-sunken, #090e1a);\n border: 1px solid var(--mj-border-default, #223254);\n border-radius: 6px;\n color: var(--mj-text-primary, #f8fafc);\n padding: 4px 8px 4px 24px;\n font-size: 11.5px;\n width: 150px;\n}\n\n.mj-graph-search-input[_ngcontent-%COMP%]:focus {\n outline: none;\n border-color: var(--mj-brand-primary, #38bdf8);\n width: 190px;\n}\n\n\n\n.mj-graph-legend[_ngcontent-%COMP%] {\n position: absolute;\n bottom: 14px;\n left: 14px;\n z-index: 20;\n display: flex;\n align-items: center;\n gap: 12px;\n background: rgba(20, 31, 54, 0.88);\n backdrop-filter: blur(10px);\n border: 1px solid var(--mj-border-default, #223254);\n padding: 6px 14px;\n border-radius: var(--mj-radius-lg, 10px);\n font-size: 11.5px;\n color: var(--mj-text-secondary, #94a3b8);\n}\n\n.mj-graph-legend-item[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.mj-graph-legend-dot[_ngcontent-%COMP%] {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n}\n\n\n\n.mj-graph-inspector[_ngcontent-%COMP%] {\n position: absolute;\n top: 14px;\n right: 14px;\n width: 300px;\n max-height: calc(100% - 28px);\n background: rgba(20, 31, 54, 0.95);\n backdrop-filter: blur(14px);\n border: 1px solid var(--mj-border-default, #223254);\n border-radius: var(--mj-radius-lg, 12px);\n padding: 16px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n z-index: 25;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);\n overflow-y: auto;\n scrollbar-width: thin;\n animation: _ngcontent-%COMP%_mjGraphSlide 0.2s ease;\n}\n\n.mj-graph-inspector-header[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n}\n\n.mj-graph-inspector-title[_ngcontent-%COMP%] {\n font-size: 11px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--mj-text-muted, #64748b);\n}\n\n.mj-graph-inspector-close[_ngcontent-%COMP%] {\n background: none;\n border: none;\n color: var(--mj-text-muted, #64748b);\n cursor: pointer;\n font-size: 14px;\n}\n\n.mj-graph-inspector-card[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 12px;\n background: var(--mj-bg-surface, #111a2e);\n border: 1px solid var(--mj-border-default, #223254);\n border-radius: 8px;\n padding: 10px 12px;\n}\n\n.mj-graph-inspector-avatar[_ngcontent-%COMP%] {\n width: 40px;\n height: 40px;\n border-radius: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 16px;\n color: #fff;\n flex-shrink: 0;\n}\n\n.mj-graph-inspector-actions[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 6px;\n margin-top: 4px;\n}\n\n\n\n.mj-edge-path[_ngcontent-%COMP%] {\n stroke: var(--mj-border-hover, #2d416b);\n stroke-width: 2;\n fill: none;\n transition: stroke 0.2s ease;\n}\n\n.mj-edge-path.is-highlighted[_ngcontent-%COMP%] {\n stroke: var(--mj-brand-primary, #38bdf8);\n stroke-width: 3;\n}\n\n.mj-edge-text[_ngcontent-%COMP%] {\n font-size: 10px;\n font-weight: 500;\n fill: var(--mj-text-muted, #64748b);\n text-anchor: middle;\n pointer-events: none;\n paint-order: stroke;\n stroke: var(--mj-bg-surface-sunken, #0b1220);\n stroke-width: 3px;\n stroke-linecap: butt;\n stroke-linejoin: miter;\n}\n\n.mj-node-group[_ngcontent-%COMP%] {\n cursor: pointer;\n transition: transform 0.1s ease;\n}\n\n.mj-node-group[_ngcontent-%COMP%]:hover .mj-node-circle[_ngcontent-%COMP%] {\n filter: drop-shadow(0 0 10px var(--mj-brand-primary, #38bdf8));\n}\n\n.mj-node-circle[_ngcontent-%COMP%] {\n transition: all 0.2s ease;\n}\n\n.mj-node-label[_ngcontent-%COMP%] {\n font-size: 11px;\n font-weight: 700;\n fill: var(--mj-text-primary, #f8fafc);\n text-anchor: middle;\n pointer-events: none;\n}\n\n.mj-node-sublabel[_ngcontent-%COMP%] {\n font-size: 9.5px;\n font-weight: 500;\n fill: var(--mj-text-secondary, #94a3b8);\n text-anchor: middle;\n pointer-events: none;\n}\n\n@keyframes _ngcontent-%COMP%_mjGraphSlide {\n from { opacity: 0; transform: translateX(20px); }\n to { opacity: 1; transform: translateX(0); }\n}"], changeDetection: 0 });
|
|
1022
|
+
}
|
|
1023
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(GraphViewComponent, [{
|
|
1024
|
+
type: Component,
|
|
1025
|
+
args: [{ selector: 'mj-graph-view', standalone: true, imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
1026
|
+
<div class="mj-graph-wrapper" #wrapper>
|
|
1027
|
+
<!-- 1. Floating Action Toolbar -->
|
|
1028
|
+
@if (ShowToolbar) {
|
|
1029
|
+
<div class="mj-graph-toolbar">
|
|
1030
|
+
<span style="font-size: 12px; font-weight: 700; color: #fff; display: flex; align-items: center; gap: 6px;">
|
|
1031
|
+
<i class="fa-solid fa-circle-nodes" style="color: var(--mj-brand-primary);"></i>
|
|
1032
|
+
Network Graph
|
|
1033
|
+
</span>
|
|
1034
|
+
|
|
1035
|
+
<div class="mj-graph-separator"></div>
|
|
1036
|
+
|
|
1037
|
+
<!-- Layout Mode Selector -->
|
|
1038
|
+
<button
|
|
1039
|
+
type="button"
|
|
1040
|
+
class="mj-graph-tool-btn"
|
|
1041
|
+
[class.active]="LayoutMode === 'force'"
|
|
1042
|
+
(click)="SetLayoutMode('force')"
|
|
1043
|
+
title="Force-Directed Physics">
|
|
1044
|
+
<i class="fa-solid fa-atom"></i> Force
|
|
1045
|
+
</button>
|
|
1046
|
+
<button
|
|
1047
|
+
type="button"
|
|
1048
|
+
class="mj-graph-tool-btn"
|
|
1049
|
+
[class.active]="LayoutMode === 'circular'"
|
|
1050
|
+
(click)="SetLayoutMode('circular')"
|
|
1051
|
+
title="Circular Layout">
|
|
1052
|
+
<i class="fa-solid fa-circle-notch"></i> Circular
|
|
1053
|
+
</button>
|
|
1054
|
+
|
|
1055
|
+
<div class="mj-graph-separator"></div>
|
|
1056
|
+
|
|
1057
|
+
<!-- Zoom Controls -->
|
|
1058
|
+
<button type="button" class="mj-graph-tool-btn" (click)="ZoomIn()" title="Zoom In">
|
|
1059
|
+
<i class="fa-solid fa-magnifying-glass-plus"></i>
|
|
1060
|
+
</button>
|
|
1061
|
+
<button type="button" class="mj-graph-tool-btn" (click)="ZoomOut()" title="Zoom Out">
|
|
1062
|
+
<i class="fa-solid fa-magnifying-glass-minus"></i>
|
|
1063
|
+
</button>
|
|
1064
|
+
<button type="button" class="mj-graph-tool-btn" (click)="FitToView()" title="Fit View">
|
|
1065
|
+
<i class="fa-solid fa-expand"></i>
|
|
1066
|
+
</button>
|
|
1067
|
+
<button type="button" class="mj-graph-tool-btn" (click)="Rearrange()" title="Re-simulate Physics">
|
|
1068
|
+
<i class="fa-solid fa-arrows-rotate"></i>
|
|
1069
|
+
</button>
|
|
1070
|
+
|
|
1071
|
+
@if (Searchable) {
|
|
1072
|
+
<div class="mj-graph-separator"></div>
|
|
1073
|
+
<div style="position: relative;">
|
|
1074
|
+
<i class="fa-solid fa-magnifying-glass" style="position: absolute; left: 8px; top: 50%; transform: translateY(-50%); font-size: 10px; color: var(--mj-text-muted);"></i>
|
|
1075
|
+
<input
|
|
1076
|
+
class="mj-graph-search-input"
|
|
1077
|
+
[(ngModel)]="SearchQuery"
|
|
1078
|
+
(ngModelChange)="SearchNodes($event)"
|
|
1079
|
+
placeholder="Search graph nodes..." />
|
|
1080
|
+
</div>
|
|
1081
|
+
}
|
|
1082
|
+
</div>
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
<!-- 2. Floating Dynamic Category Legend -->
|
|
1086
|
+
@if (ShowLegend && ActiveCategories.length > 0) {
|
|
1087
|
+
<div class="mj-graph-legend">
|
|
1088
|
+
@for (cat of ActiveCategories; track cat.Category) {
|
|
1089
|
+
<div class="mj-graph-legend-item">
|
|
1090
|
+
<div class="mj-graph-legend-dot" [style.background]="cat.Color"></div>
|
|
1091
|
+
{{ cat.Label }}
|
|
1092
|
+
</div>
|
|
1093
|
+
}
|
|
1094
|
+
</div>
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
<!-- 3. Selected Node Inspector Drawer -->
|
|
1098
|
+
@if (ShowInspector && SelectedNode) {
|
|
1099
|
+
<div class="mj-graph-inspector">
|
|
1100
|
+
<div class="mj-graph-inspector-header">
|
|
1101
|
+
<span class="mj-graph-inspector-title">Node Inspector</span>
|
|
1102
|
+
<button class="mj-graph-inspector-close" type="button" (click)="ClearSelection()">
|
|
1103
|
+
<i class="fa-solid fa-xmark"></i>
|
|
1104
|
+
</button>
|
|
1105
|
+
</div>
|
|
1106
|
+
|
|
1107
|
+
<div class="mj-graph-inspector-card">
|
|
1108
|
+
<div class="mj-graph-inspector-avatar" [style.background]="GetNodeColor(SelectedNode)">
|
|
1109
|
+
<i [class]="GetNodeIcon(SelectedNode)"></i>
|
|
1110
|
+
</div>
|
|
1111
|
+
<div style="overflow: hidden; flex: 1;">
|
|
1112
|
+
<div style="font-size: 13px; font-weight: 700; color: #fff; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;">
|
|
1113
|
+
{{ SelectedNode.Label }}
|
|
1114
|
+
</div>
|
|
1115
|
+
<div style="display: flex; align-items: center; gap: 6px; margin-top: 3px;">
|
|
1116
|
+
<span style="font-size: 11px; color: var(--mj-text-secondary);">
|
|
1117
|
+
{{ SelectedNode.Sublabel || SelectedNode.Category }}
|
|
1118
|
+
</span>
|
|
1119
|
+
@if (IsFocalNode(SelectedNode)) {
|
|
1120
|
+
<span style="font-size: 10px; padding: 1px 6px; border-radius: 9999px; background: rgba(56, 189, 248, 0.15); color: #38bdf8; font-weight: 600; border: 1px solid rgba(56, 189, 248, 0.3);">Current Record</span>
|
|
1121
|
+
}
|
|
1122
|
+
</div>
|
|
1123
|
+
</div>
|
|
1124
|
+
</div>
|
|
1125
|
+
|
|
1126
|
+
<div style="font-size: 11.5px; color: var(--mj-text-secondary); line-height: 1.45;">
|
|
1127
|
+
Direct Connected Edges: <strong>{{ GetNodeEdgeCount(SelectedNode.ID) }}</strong>
|
|
1128
|
+
</div>
|
|
1129
|
+
|
|
1130
|
+
<div class="mj-graph-inspector-actions">
|
|
1131
|
+
@if (!IsFocalNode(SelectedNode)) {
|
|
1132
|
+
<button
|
|
1133
|
+
type="button"
|
|
1134
|
+
class="mj-graph-tool-btn active"
|
|
1135
|
+
style="justify-content: center; width: 100%;"
|
|
1136
|
+
(click)="NavigateToEntity(SelectedNode)">
|
|
1137
|
+
<i class="fa-solid fa-arrow-up-right-from-square"></i> Open Entity Form
|
|
1138
|
+
</button>
|
|
1139
|
+
}
|
|
1140
|
+
<button
|
|
1141
|
+
type="button"
|
|
1142
|
+
class="mj-graph-tool-btn"
|
|
1143
|
+
style="justify-content: center; width: 100%;"
|
|
1144
|
+
(click)="ExpandHops(SelectedNode.ID, 1)">
|
|
1145
|
+
<i class="fa-solid fa-circle-nodes"></i> Expand 1-Hop Neighbors
|
|
1146
|
+
</button>
|
|
1147
|
+
</div>
|
|
1148
|
+
</div>
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
<!-- 4. Interactive SVG Graph Canvas -->
|
|
1152
|
+
<svg
|
|
1153
|
+
#svgCanvas
|
|
1154
|
+
class="mj-graph-svg-canvas"
|
|
1155
|
+
[class.is-panning]="IsPanning"
|
|
1156
|
+
(mousedown)="OnCanvasMouseDown($event)"
|
|
1157
|
+
(mousemove)="OnCanvasMouseMove($event)"
|
|
1158
|
+
(mouseup)="OnCanvasMouseUp($event)"
|
|
1159
|
+
(mouseleave)="OnCanvasMouseUp($event)"
|
|
1160
|
+
(wheel)="OnCanvasWheel($event)">
|
|
1161
|
+
|
|
1162
|
+
<g [attr.transform]="'translate(' + PanX + ',' + PanY + ') scale(' + Scale + ')'">
|
|
1163
|
+
<!-- Edges Layer -->
|
|
1164
|
+
@for (edge of Edges; track edge.ID) {
|
|
1165
|
+
<g>
|
|
1166
|
+
<line
|
|
1167
|
+
class="mj-edge-path"
|
|
1168
|
+
[class.is-highlighted]="IsEdgeHighlighted(edge)"
|
|
1169
|
+
[attr.x1]="GetNodeX(edge.SourceID)"
|
|
1170
|
+
[attr.y1]="GetNodeY(edge.SourceID)"
|
|
1171
|
+
[attr.x2]="GetNodeX(edge.TargetID)"
|
|
1172
|
+
[attr.y2]="GetNodeY(edge.TargetID)"
|
|
1173
|
+
(click)="OnEdgeClick(edge, $event)">
|
|
1174
|
+
</line>
|
|
1175
|
+
<text
|
|
1176
|
+
class="mj-edge-text"
|
|
1177
|
+
[attr.x]="(GetNodeX(edge.SourceID) + GetNodeX(edge.TargetID)) / 2"
|
|
1178
|
+
[attr.y]="(GetNodeY(edge.SourceID) + GetNodeY(edge.TargetID)) / 2 - 4">
|
|
1179
|
+
{{ edge.Label }}
|
|
1180
|
+
</text>
|
|
1181
|
+
</g>
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
<!-- Nodes Layer -->
|
|
1185
|
+
@for (node of FilteredNodes; track node.ID) {
|
|
1186
|
+
<g
|
|
1187
|
+
class="mj-node-group"
|
|
1188
|
+
[attr.transform]="'translate(' + (node.X || 0) + ',' + (node.Y || 0) + ')'"
|
|
1189
|
+
(mousedown)="OnNodeMouseDown(node, $event)"
|
|
1190
|
+
(click)="OnNodeClick(node, $event)"
|
|
1191
|
+
(dblclick)="NavigateToEntity(node)">
|
|
1192
|
+
|
|
1193
|
+
<!-- Outer Selection / Focal Halo -->
|
|
1194
|
+
@if (IsNodeSelected(node)) {
|
|
1195
|
+
<circle r="36" fill="rgba(56, 189, 248, 0.2)" stroke="#38bdf8" stroke-width="2"></circle>
|
|
1196
|
+
} @else if (IsFocalNode(node)) {
|
|
1197
|
+
<circle r="34" fill="none" stroke="rgba(56, 189, 248, 0.45)" stroke-width="2" stroke-dasharray="4 3"></circle>
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
<!-- Node Body Circle -->
|
|
1201
|
+
<circle
|
|
1202
|
+
class="mj-node-circle"
|
|
1203
|
+
[attr.r]="node.Radius || 26"
|
|
1204
|
+
fill="#141f36"
|
|
1205
|
+
[attr.stroke]="GetNodeColor(node)"
|
|
1206
|
+
stroke-width="3">
|
|
1207
|
+
</circle>
|
|
1208
|
+
|
|
1209
|
+
<!-- Node Icon Center -->
|
|
1210
|
+
<circle [attr.r]="(node.Radius || 26) - 8" [attr.fill]="GetNodeColor(node)"></circle>
|
|
1211
|
+
|
|
1212
|
+
<!-- Labels -->
|
|
1213
|
+
<text class="mj-node-label" [attr.y]="(node.Radius || 26) + 16">
|
|
1214
|
+
{{ TruncateLabel(node.Label, 18) }}
|
|
1215
|
+
</text>
|
|
1216
|
+
@if (node.Sublabel) {
|
|
1217
|
+
<text class="mj-node-sublabel" [attr.y]="(node.Radius || 26) + 28">
|
|
1218
|
+
{{ TruncateLabel(node.Sublabel, 20) }}
|
|
1219
|
+
</text>
|
|
1220
|
+
}
|
|
1221
|
+
</g>
|
|
1222
|
+
}
|
|
1223
|
+
</g>
|
|
1224
|
+
</svg>
|
|
1225
|
+
</div>
|
|
1226
|
+
`, styles: ["\n:host {\n display: block;\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n background: var(--mj-bg-surface-sunken, #0b1220);\n border-radius: var(--mj-radius-lg, 12px);\n border: 1px solid var(--mj-border-default, #223254);\n user-select: none;\n}\n\n.mj-graph-wrapper {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n}\n\n.mj-graph-svg-canvas {\n width: 100%;\n height: 100%;\n cursor: grab;\n}\n\n.mj-graph-svg-canvas.is-panning {\n cursor: grabbing;\n}\n\n/* Floating Toolbar */\n.mj-graph-toolbar {\n position: absolute;\n top: 14px;\n left: 14px;\n z-index: 20;\n display: flex;\n align-items: center;\n gap: 8px;\n background: rgba(20, 31, 54, 0.88);\n backdrop-filter: blur(10px);\n border: 1px solid var(--mj-border-default, #223254);\n padding: 6px 12px;\n border-radius: var(--mj-radius-lg, 10px);\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);\n}\n\n.mj-graph-tool-btn {\n background: var(--mj-bg-surface-elevated, #1a2744);\n border: 1px solid var(--mj-border-default, #223254);\n color: var(--mj-text-primary, #f8fafc);\n border-radius: 6px;\n padding: 5px 10px;\n font-size: 11.5px;\n font-weight: 600;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 6px;\n transition: all 0.15s ease;\n}\n\n.mj-graph-tool-btn:hover {\n border-color: var(--mj-brand-primary, #38bdf8);\n background: var(--mj-border-hover, #334876);\n}\n\n.mj-graph-tool-btn.active {\n background: var(--mj-brand-primary, #38bdf8);\n color: #090e1a;\n border-color: var(--mj-brand-primary, #38bdf8);\n}\n\n.mj-graph-separator {\n width: 1px;\n height: 18px;\n background: var(--mj-border-default, #223254);\n}\n\n.mj-graph-search-input {\n background: var(--mj-bg-surface-sunken, #090e1a);\n border: 1px solid var(--mj-border-default, #223254);\n border-radius: 6px;\n color: var(--mj-text-primary, #f8fafc);\n padding: 4px 8px 4px 24px;\n font-size: 11.5px;\n width: 150px;\n}\n\n.mj-graph-search-input:focus {\n outline: none;\n border-color: var(--mj-brand-primary, #38bdf8);\n width: 190px;\n}\n\n/* Floating Legend */\n.mj-graph-legend {\n position: absolute;\n bottom: 14px;\n left: 14px;\n z-index: 20;\n display: flex;\n align-items: center;\n gap: 12px;\n background: rgba(20, 31, 54, 0.88);\n backdrop-filter: blur(10px);\n border: 1px solid var(--mj-border-default, #223254);\n padding: 6px 14px;\n border-radius: var(--mj-radius-lg, 10px);\n font-size: 11.5px;\n color: var(--mj-text-secondary, #94a3b8);\n}\n\n.mj-graph-legend-item {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.mj-graph-legend-dot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n}\n\n/* Inspector Drawer */\n.mj-graph-inspector {\n position: absolute;\n top: 14px;\n right: 14px;\n width: 300px;\n max-height: calc(100% - 28px);\n background: rgba(20, 31, 54, 0.95);\n backdrop-filter: blur(14px);\n border: 1px solid var(--mj-border-default, #223254);\n border-radius: var(--mj-radius-lg, 12px);\n padding: 16px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n z-index: 25;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);\n overflow-y: auto;\n scrollbar-width: thin;\n animation: mjGraphSlide 0.2s ease;\n}\n\n.mj-graph-inspector-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n}\n\n.mj-graph-inspector-title {\n font-size: 11px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--mj-text-muted, #64748b);\n}\n\n.mj-graph-inspector-close {\n background: none;\n border: none;\n color: var(--mj-text-muted, #64748b);\n cursor: pointer;\n font-size: 14px;\n}\n\n.mj-graph-inspector-card {\n display: flex;\n align-items: center;\n gap: 12px;\n background: var(--mj-bg-surface, #111a2e);\n border: 1px solid var(--mj-border-default, #223254);\n border-radius: 8px;\n padding: 10px 12px;\n}\n\n.mj-graph-inspector-avatar {\n width: 40px;\n height: 40px;\n border-radius: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 16px;\n color: #fff;\n flex-shrink: 0;\n}\n\n.mj-graph-inspector-actions {\n display: flex;\n flex-direction: column;\n gap: 6px;\n margin-top: 4px;\n}\n\n/* SVG Rendering Elements */\n.mj-edge-path {\n stroke: var(--mj-border-hover, #2d416b);\n stroke-width: 2;\n fill: none;\n transition: stroke 0.2s ease;\n}\n\n.mj-edge-path.is-highlighted {\n stroke: var(--mj-brand-primary, #38bdf8);\n stroke-width: 3;\n}\n\n.mj-edge-text {\n font-size: 10px;\n font-weight: 500;\n fill: var(--mj-text-muted, #64748b);\n text-anchor: middle;\n pointer-events: none;\n paint-order: stroke;\n stroke: var(--mj-bg-surface-sunken, #0b1220);\n stroke-width: 3px;\n stroke-linecap: butt;\n stroke-linejoin: miter;\n}\n\n.mj-node-group {\n cursor: pointer;\n transition: transform 0.1s ease;\n}\n\n.mj-node-group:hover .mj-node-circle {\n filter: drop-shadow(0 0 10px var(--mj-brand-primary, #38bdf8));\n}\n\n.mj-node-circle {\n transition: all 0.2s ease;\n}\n\n.mj-node-label {\n font-size: 11px;\n font-weight: 700;\n fill: var(--mj-text-primary, #f8fafc);\n text-anchor: middle;\n pointer-events: none;\n}\n\n.mj-node-sublabel {\n font-size: 9.5px;\n font-weight: 500;\n fill: var(--mj-text-secondary, #94a3b8);\n text-anchor: middle;\n pointer-events: none;\n}\n\n@keyframes mjGraphSlide {\n from { opacity: 0; transform: translateX(20px); }\n to { opacity: 1; transform: translateX(0); }\n}\n"] }]
|
|
1227
|
+
}], null, { WrapperRef: [{
|
|
1228
|
+
type: ViewChild,
|
|
1229
|
+
args: ['wrapper', { static: true }]
|
|
1230
|
+
}], CanvasRef: [{
|
|
1231
|
+
type: ViewChild,
|
|
1232
|
+
args: ['svgCanvas', { static: true }]
|
|
1233
|
+
}], Nodes: [{
|
|
1234
|
+
type: Input
|
|
1235
|
+
}], Edges: [{
|
|
1236
|
+
type: Input
|
|
1237
|
+
}], Categories: [{
|
|
1238
|
+
type: Input
|
|
1239
|
+
}], LayoutMode: [{
|
|
1240
|
+
type: Input
|
|
1241
|
+
}], MaxHopDistance: [{
|
|
1242
|
+
type: Input
|
|
1243
|
+
}], SelectedNodeId: [{
|
|
1244
|
+
type: Input
|
|
1245
|
+
}], FocalNodeId: [{
|
|
1246
|
+
type: Input
|
|
1247
|
+
}], AutoOpenInspector: [{
|
|
1248
|
+
type: Input
|
|
1249
|
+
}], ShowToolbar: [{
|
|
1250
|
+
type: Input
|
|
1251
|
+
}], ShowLegend: [{
|
|
1252
|
+
type: Input
|
|
1253
|
+
}], ShowInspector: [{
|
|
1254
|
+
type: Input
|
|
1255
|
+
}], Searchable: [{
|
|
1256
|
+
type: Input
|
|
1257
|
+
}], AutoFitOnLoad: [{
|
|
1258
|
+
type: Input
|
|
1259
|
+
}], Physics: [{
|
|
1260
|
+
type: Input
|
|
1261
|
+
}], BeforeNodeSelect: [{
|
|
1262
|
+
type: Output
|
|
1263
|
+
}], NodeSelected: [{
|
|
1264
|
+
type: Output
|
|
1265
|
+
}], BeforeEdgeSelect: [{
|
|
1266
|
+
type: Output
|
|
1267
|
+
}], EdgeSelected: [{
|
|
1268
|
+
type: Output
|
|
1269
|
+
}], BeforeHopExpand: [{
|
|
1270
|
+
type: Output
|
|
1271
|
+
}], HopExpanded: [{
|
|
1272
|
+
type: Output
|
|
1273
|
+
}], BeforeLayoutChange: [{
|
|
1274
|
+
type: Output
|
|
1275
|
+
}], LayoutChanged: [{
|
|
1276
|
+
type: Output
|
|
1277
|
+
}], BeforeNodeNavigate: [{
|
|
1278
|
+
type: Output
|
|
1279
|
+
}], NodeNavigated: [{
|
|
1280
|
+
type: Output
|
|
1281
|
+
}], ViewportTransform: [{
|
|
1282
|
+
type: Output
|
|
1283
|
+
}] }); })();
|
|
1284
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(GraphViewComponent, { className: "GraphViewComponent", filePath: "src/lib/components/graph-view.component.ts", lineNumber: 534 }); })();
|
|
1285
|
+
//# sourceMappingURL=graph-view.component.js.map
|