@memberjunction/ng-clustering 0.0.1 → 5.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +441 -27
- package/dist/__tests__/clustering-service.test.d.ts +2 -0
- package/dist/__tests__/clustering-service.test.d.ts.map +1 -0
- package/dist/__tests__/clustering-service.test.js +487 -0
- package/dist/__tests__/clustering-service.test.js.map +1 -0
- package/dist/__tests__/clustering-types.test.d.ts +2 -0
- package/dist/__tests__/clustering-types.test.d.ts.map +1 -0
- package/dist/__tests__/clustering-types.test.js +254 -0
- package/dist/__tests__/clustering-types.test.js.map +1 -0
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.js +7 -0
- package/dist/__tests__/index.test.js.map +1 -0
- package/dist/__tests__/scatter-component-logic.test.d.ts +2 -0
- package/dist/__tests__/scatter-component-logic.test.d.ts.map +1 -0
- package/dist/__tests__/scatter-component-logic.test.js +494 -0
- package/dist/__tests__/scatter-component-logic.test.js.map +1 -0
- package/dist/lib/cluster-config-panel.component.d.ts +274 -0
- package/dist/lib/cluster-config-panel.component.d.ts.map +1 -0
- package/dist/lib/cluster-config-panel.component.js +732 -0
- package/dist/lib/cluster-config-panel.component.js.map +1 -0
- package/dist/lib/cluster-scatter.component.d.ts +572 -0
- package/dist/lib/cluster-scatter.component.d.ts.map +1 -0
- package/dist/lib/cluster-scatter.component.js +1410 -0
- package/dist/lib/cluster-scatter.component.js.map +1 -0
- package/dist/lib/clustering.module.d.ts +12 -0
- package/dist/lib/clustering.module.d.ts.map +1 -0
- package/dist/lib/clustering.module.js +38 -0
- package/dist/lib/clustering.module.js.map +1 -0
- package/dist/lib/clustering.service.d.ts +43 -0
- package/dist/lib/clustering.service.d.ts.map +1 -0
- package/dist/lib/clustering.service.js +253 -0
- package/dist/lib/clustering.service.js.map +1 -0
- package/dist/lib/clustering.types.d.ts +271 -0
- package/dist/lib/clustering.types.d.ts.map +1 -0
- package/dist/lib/clustering.types.js +74 -0
- package/dist/lib/clustering.types.js.map +1 -0
- package/dist/public-api.d.ts +6 -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 +41 -6
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Floating glass-morphism configuration panel for clustering.
|
|
3
|
+
*
|
|
4
|
+
* `ClusterConfigPanelComponent` provides a draggable floating panel with
|
|
5
|
+
* entity picker, algorithm picker, parameter sliders, distance metric
|
|
6
|
+
* selector, max records input, run button, and result metrics display.
|
|
7
|
+
*
|
|
8
|
+
* It emits cancelable `BeforeXXX` events so consumers can intercept and
|
|
9
|
+
* validate before clustering starts or a visualization is saved.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```html
|
|
13
|
+
* <mj-cluster-config-panel
|
|
14
|
+
* [IsRunning]="isRunning"
|
|
15
|
+
* [Metrics]="result?.Metrics ?? null"
|
|
16
|
+
* [EntityOptions]="myEntityOptions"
|
|
17
|
+
* [ShowSaveButton]="true"
|
|
18
|
+
* [DefaultAlgorithm]="'dbscan'"
|
|
19
|
+
* [Collapsed]="false"
|
|
20
|
+
* (BeforeRunClustering)="onBeforeRun($event)"
|
|
21
|
+
* (RunClustering)="onRunClustering($event)"
|
|
22
|
+
* (ConfigChanged)="onConfigChanged($event)"
|
|
23
|
+
* (SaveVisualization)="onSave()">
|
|
24
|
+
* </mj-cluster-config-panel>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
import { Component, Input, Output, EventEmitter, ElementRef, ChangeDetectorRef, ViewChild, inject, } from '@angular/core';
|
|
28
|
+
import { DefaultClusterConfig, } from './clustering.types';
|
|
29
|
+
import * as i0 from "@angular/core";
|
|
30
|
+
import * as i1 from "@angular/forms";
|
|
31
|
+
const _c0 = ["panelElement"];
|
|
32
|
+
const _forTrack0 = ($index, $item) => $item.ID;
|
|
33
|
+
function ClusterConfigPanelComponent_Conditional_8_For_5_Template(rf, ctx) { if (rf & 1) {
|
|
34
|
+
i0.ɵɵelementStart(0, "option", 9);
|
|
35
|
+
i0.ɵɵtext(1);
|
|
36
|
+
i0.ɵɵelementEnd();
|
|
37
|
+
} if (rf & 2) {
|
|
38
|
+
const entity_r4 = ctx.$implicit;
|
|
39
|
+
i0.ɵɵproperty("value", entity_r4.Name);
|
|
40
|
+
i0.ɵɵadvance();
|
|
41
|
+
i0.ɵɵtextInterpolate(entity_r4.Name);
|
|
42
|
+
} }
|
|
43
|
+
function ClusterConfigPanelComponent_Conditional_8_Conditional_6_For_5_Template(rf, ctx) { if (rf & 1) {
|
|
44
|
+
i0.ɵɵelementStart(0, "option", 9);
|
|
45
|
+
i0.ɵɵtext(1);
|
|
46
|
+
i0.ɵɵelementEnd();
|
|
47
|
+
} if (rf & 2) {
|
|
48
|
+
const doc_r6 = ctx.$implicit;
|
|
49
|
+
i0.ɵɵproperty("value", doc_r6.ID);
|
|
50
|
+
i0.ɵɵadvance();
|
|
51
|
+
i0.ɵɵtextInterpolate(doc_r6.Name);
|
|
52
|
+
} }
|
|
53
|
+
function ClusterConfigPanelComponent_Conditional_8_Conditional_6_Template(rf, ctx) { if (rf & 1) {
|
|
54
|
+
const _r5 = i0.ɵɵgetCurrentView();
|
|
55
|
+
i0.ɵɵelementStart(0, "div", 7)(1, "label");
|
|
56
|
+
i0.ɵɵtext(2, "Document Template");
|
|
57
|
+
i0.ɵɵelementEnd();
|
|
58
|
+
i0.ɵɵelementStart(3, "select", 8);
|
|
59
|
+
i0.ɵɵlistener("ngModelChange", function ClusterConfigPanelComponent_Conditional_8_Conditional_6_Template_select_ngModelChange_3_listener($event) { i0.ɵɵrestoreView(_r5); const ctx_r2 = i0.ɵɵnextContext(2); ctx_r2.Config.EntityDocumentID = $event; return i0.ɵɵresetView(ctx_r2.OnFieldChanged()); });
|
|
60
|
+
i0.ɵɵrepeaterCreate(4, ClusterConfigPanelComponent_Conditional_8_Conditional_6_For_5_Template, 2, 2, "option", 9, _forTrack0);
|
|
61
|
+
i0.ɵɵelementEnd()();
|
|
62
|
+
} if (rf & 2) {
|
|
63
|
+
const ctx_r2 = i0.ɵɵnextContext(2);
|
|
64
|
+
i0.ɵɵadvance(3);
|
|
65
|
+
i0.ɵɵproperty("ngModel", ctx_r2.Config.EntityDocumentID);
|
|
66
|
+
i0.ɵɵadvance();
|
|
67
|
+
i0.ɵɵrepeater(ctx_r2.EntityDocOptions);
|
|
68
|
+
} }
|
|
69
|
+
function ClusterConfigPanelComponent_Conditional_8_Conditional_12_For_5_Template(rf, ctx) { if (rf & 1) {
|
|
70
|
+
i0.ɵɵelementStart(0, "option", 9);
|
|
71
|
+
i0.ɵɵtext(1);
|
|
72
|
+
i0.ɵɵelementEnd();
|
|
73
|
+
} if (rf & 2) {
|
|
74
|
+
const alg_r8 = ctx.$implicit;
|
|
75
|
+
i0.ɵɵproperty("value", alg_r8.Value);
|
|
76
|
+
i0.ɵɵadvance();
|
|
77
|
+
i0.ɵɵtextInterpolate(alg_r8.Label);
|
|
78
|
+
} }
|
|
79
|
+
function ClusterConfigPanelComponent_Conditional_8_Conditional_12_Template(rf, ctx) { if (rf & 1) {
|
|
80
|
+
const _r7 = i0.ɵɵgetCurrentView();
|
|
81
|
+
i0.ɵɵelementStart(0, "div", 7)(1, "label");
|
|
82
|
+
i0.ɵɵtext(2, "Algorithm");
|
|
83
|
+
i0.ɵɵelementEnd();
|
|
84
|
+
i0.ɵɵelementStart(3, "select", 8);
|
|
85
|
+
i0.ɵɵlistener("ngModelChange", function ClusterConfigPanelComponent_Conditional_8_Conditional_12_Template_select_ngModelChange_3_listener($event) { i0.ɵɵrestoreView(_r7); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.OnAlgorithmChanged($event)); });
|
|
86
|
+
i0.ɵɵrepeaterCreate(4, ClusterConfigPanelComponent_Conditional_8_Conditional_12_For_5_Template, 2, 2, "option", 9, i0.ɵɵcomponentInstance().TrackAlgorithmBy, true);
|
|
87
|
+
i0.ɵɵelementEnd()();
|
|
88
|
+
} if (rf & 2) {
|
|
89
|
+
const ctx_r2 = i0.ɵɵnextContext(2);
|
|
90
|
+
i0.ɵɵadvance(3);
|
|
91
|
+
i0.ɵɵproperty("ngModel", ctx_r2.Config.Algorithm);
|
|
92
|
+
i0.ɵɵadvance();
|
|
93
|
+
i0.ɵɵrepeater(ctx_r2.AlgorithmOptions);
|
|
94
|
+
} }
|
|
95
|
+
function ClusterConfigPanelComponent_Conditional_8_Conditional_13_Template(rf, ctx) { if (rf & 1) {
|
|
96
|
+
const _r9 = i0.ɵɵgetCurrentView();
|
|
97
|
+
i0.ɵɵelementStart(0, "div", 7)(1, "label");
|
|
98
|
+
i0.ɵɵtext(2);
|
|
99
|
+
i0.ɵɵelementEnd();
|
|
100
|
+
i0.ɵɵelementStart(3, "input", 16);
|
|
101
|
+
i0.ɵɵlistener("ngModelChange", function ClusterConfigPanelComponent_Conditional_8_Conditional_13_Template_input_ngModelChange_3_listener($event) { i0.ɵɵrestoreView(_r9); const ctx_r2 = i0.ɵɵnextContext(2); ctx_r2.Config.K = $event; return i0.ɵɵresetView(ctx_r2.OnFieldChanged()); });
|
|
102
|
+
i0.ɵɵelementEnd()();
|
|
103
|
+
} if (rf & 2) {
|
|
104
|
+
const ctx_r2 = i0.ɵɵnextContext(2);
|
|
105
|
+
i0.ɵɵadvance(2);
|
|
106
|
+
i0.ɵɵtextInterpolate1("Clusters (K): ", ctx_r2.Config.K);
|
|
107
|
+
i0.ɵɵadvance();
|
|
108
|
+
i0.ɵɵproperty("ngModel", ctx_r2.Config.K)("min", 2)("max", 20)("step", 1);
|
|
109
|
+
} }
|
|
110
|
+
function ClusterConfigPanelComponent_Conditional_8_Conditional_14_Template(rf, ctx) { if (rf & 1) {
|
|
111
|
+
const _r10 = i0.ɵɵgetCurrentView();
|
|
112
|
+
i0.ɵɵelementStart(0, "div", 7)(1, "label");
|
|
113
|
+
i0.ɵɵtext(2);
|
|
114
|
+
i0.ɵɵelementEnd();
|
|
115
|
+
i0.ɵɵelementStart(3, "input", 16);
|
|
116
|
+
i0.ɵɵlistener("ngModelChange", function ClusterConfigPanelComponent_Conditional_8_Conditional_14_Template_input_ngModelChange_3_listener($event) { i0.ɵɵrestoreView(_r10); const ctx_r2 = i0.ɵɵnextContext(2); ctx_r2.Config.Epsilon = $event; return i0.ɵɵresetView(ctx_r2.OnFieldChanged()); });
|
|
117
|
+
i0.ɵɵelementEnd()();
|
|
118
|
+
} if (rf & 2) {
|
|
119
|
+
const ctx_r2 = i0.ɵɵnextContext(2);
|
|
120
|
+
i0.ɵɵadvance(2);
|
|
121
|
+
i0.ɵɵtextInterpolate1("Epsilon: ", ctx_r2.Config.Epsilon);
|
|
122
|
+
i0.ɵɵadvance();
|
|
123
|
+
i0.ɵɵproperty("ngModel", ctx_r2.Config.Epsilon)("min", 0.05)("max", 0.95)("step", 0.05);
|
|
124
|
+
} }
|
|
125
|
+
function ClusterConfigPanelComponent_Conditional_8_Conditional_15_Template(rf, ctx) { if (rf & 1) {
|
|
126
|
+
const _r11 = i0.ɵɵgetCurrentView();
|
|
127
|
+
i0.ɵɵelementStart(0, "div", 7)(1, "label");
|
|
128
|
+
i0.ɵɵtext(2);
|
|
129
|
+
i0.ɵɵelementEnd();
|
|
130
|
+
i0.ɵɵelementStart(3, "input", 16);
|
|
131
|
+
i0.ɵɵlistener("ngModelChange", function ClusterConfigPanelComponent_Conditional_8_Conditional_15_Template_input_ngModelChange_3_listener($event) { i0.ɵɵrestoreView(_r11); const ctx_r2 = i0.ɵɵnextContext(2); ctx_r2.Config.MinPoints = $event; return i0.ɵɵresetView(ctx_r2.OnFieldChanged()); });
|
|
132
|
+
i0.ɵɵelementEnd()();
|
|
133
|
+
} if (rf & 2) {
|
|
134
|
+
const ctx_r2 = i0.ɵɵnextContext(2);
|
|
135
|
+
i0.ɵɵadvance(2);
|
|
136
|
+
i0.ɵɵtextInterpolate1("Min Points: ", ctx_r2.Config.MinPoints);
|
|
137
|
+
i0.ɵɵadvance();
|
|
138
|
+
i0.ɵɵproperty("ngModel", ctx_r2.Config.MinPoints)("min", 2)("max", 20)("step", 1);
|
|
139
|
+
} }
|
|
140
|
+
function ClusterConfigPanelComponent_Conditional_8_Conditional_17_For_5_Template(rf, ctx) { if (rf & 1) {
|
|
141
|
+
i0.ɵɵelementStart(0, "option", 9);
|
|
142
|
+
i0.ɵɵtext(1);
|
|
143
|
+
i0.ɵɵelementEnd();
|
|
144
|
+
} if (rf & 2) {
|
|
145
|
+
const metric_r13 = ctx.$implicit;
|
|
146
|
+
i0.ɵɵproperty("value", metric_r13.Value);
|
|
147
|
+
i0.ɵɵadvance();
|
|
148
|
+
i0.ɵɵtextInterpolate(metric_r13.Label);
|
|
149
|
+
} }
|
|
150
|
+
function ClusterConfigPanelComponent_Conditional_8_Conditional_17_Template(rf, ctx) { if (rf & 1) {
|
|
151
|
+
const _r12 = i0.ɵɵgetCurrentView();
|
|
152
|
+
i0.ɵɵelementStart(0, "div", 7)(1, "label");
|
|
153
|
+
i0.ɵɵtext(2, "Distance Metric");
|
|
154
|
+
i0.ɵɵelementEnd();
|
|
155
|
+
i0.ɵɵelementStart(3, "select", 8);
|
|
156
|
+
i0.ɵɵlistener("ngModelChange", function ClusterConfigPanelComponent_Conditional_8_Conditional_17_Template_select_ngModelChange_3_listener($event) { i0.ɵɵrestoreView(_r12); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.OnMetricChanged($event)); });
|
|
157
|
+
i0.ɵɵrepeaterCreate(4, ClusterConfigPanelComponent_Conditional_8_Conditional_17_For_5_Template, 2, 2, "option", 9, i0.ɵɵcomponentInstance().TrackMetricBy, true);
|
|
158
|
+
i0.ɵɵelementEnd()();
|
|
159
|
+
} if (rf & 2) {
|
|
160
|
+
const ctx_r2 = i0.ɵɵnextContext(2);
|
|
161
|
+
i0.ɵɵadvance(3);
|
|
162
|
+
i0.ɵɵproperty("ngModel", ctx_r2.Config.DistanceMetric);
|
|
163
|
+
i0.ɵɵadvance();
|
|
164
|
+
i0.ɵɵrepeater(ctx_r2.MetricOptions);
|
|
165
|
+
} }
|
|
166
|
+
function ClusterConfigPanelComponent_Conditional_8_Conditional_23_Template(rf, ctx) { if (rf & 1) {
|
|
167
|
+
i0.ɵɵelement(0, "i", 17);
|
|
168
|
+
i0.ɵɵtext(1, " Running... ");
|
|
169
|
+
} }
|
|
170
|
+
function ClusterConfigPanelComponent_Conditional_8_Conditional_24_Template(rf, ctx) { if (rf & 1) {
|
|
171
|
+
i0.ɵɵelement(0, "i", 18);
|
|
172
|
+
i0.ɵɵtext(1, " Run Clustering ");
|
|
173
|
+
} }
|
|
174
|
+
function ClusterConfigPanelComponent_Conditional_8_Conditional_25_Conditional_21_Template(rf, ctx) { if (rf & 1) {
|
|
175
|
+
i0.ɵɵelementStart(0, "div", 19)(1, "span", 20);
|
|
176
|
+
i0.ɵɵtext(2, "Outliers");
|
|
177
|
+
i0.ɵɵelementEnd();
|
|
178
|
+
i0.ɵɵelementStart(3, "span", 21);
|
|
179
|
+
i0.ɵɵtext(4);
|
|
180
|
+
i0.ɵɵelementEnd()();
|
|
181
|
+
} if (rf & 2) {
|
|
182
|
+
const ctx_r2 = i0.ɵɵnextContext(3);
|
|
183
|
+
i0.ɵɵadvance(4);
|
|
184
|
+
i0.ɵɵtextInterpolate(ctx_r2.Metrics.OutlierCount);
|
|
185
|
+
} }
|
|
186
|
+
function ClusterConfigPanelComponent_Conditional_8_Conditional_25_Template(rf, ctx) { if (rf & 1) {
|
|
187
|
+
i0.ɵɵelementStart(0, "div", 14)(1, "div", 19)(2, "span", 20);
|
|
188
|
+
i0.ɵɵtext(3, "Silhouette");
|
|
189
|
+
i0.ɵɵelementEnd();
|
|
190
|
+
i0.ɵɵelementStart(4, "span", 21);
|
|
191
|
+
i0.ɵɵtext(5);
|
|
192
|
+
i0.ɵɵelementEnd()();
|
|
193
|
+
i0.ɵɵelementStart(6, "div", 19)(7, "span", 20);
|
|
194
|
+
i0.ɵɵtext(8, "Clusters");
|
|
195
|
+
i0.ɵɵelementEnd();
|
|
196
|
+
i0.ɵɵelementStart(9, "span", 21);
|
|
197
|
+
i0.ɵɵtext(10);
|
|
198
|
+
i0.ɵɵelementEnd()();
|
|
199
|
+
i0.ɵɵelementStart(11, "div", 19)(12, "span", 20);
|
|
200
|
+
i0.ɵɵtext(13, "Records");
|
|
201
|
+
i0.ɵɵelementEnd();
|
|
202
|
+
i0.ɵɵelementStart(14, "span", 21);
|
|
203
|
+
i0.ɵɵtext(15);
|
|
204
|
+
i0.ɵɵelementEnd()();
|
|
205
|
+
i0.ɵɵelementStart(16, "div", 19)(17, "span", 20);
|
|
206
|
+
i0.ɵɵtext(18, "Time");
|
|
207
|
+
i0.ɵɵelementEnd();
|
|
208
|
+
i0.ɵɵelementStart(19, "span", 21);
|
|
209
|
+
i0.ɵɵtext(20);
|
|
210
|
+
i0.ɵɵelementEnd()();
|
|
211
|
+
i0.ɵɵconditionalCreate(21, ClusterConfigPanelComponent_Conditional_8_Conditional_25_Conditional_21_Template, 5, 1, "div", 19);
|
|
212
|
+
i0.ɵɵelementEnd();
|
|
213
|
+
} if (rf & 2) {
|
|
214
|
+
const ctx_r2 = i0.ɵɵnextContext(2);
|
|
215
|
+
i0.ɵɵadvance(4);
|
|
216
|
+
i0.ɵɵclassProp("good", ctx_r2.IsSilhouetteGood(ctx_r2.Metrics.SilhouetteScore));
|
|
217
|
+
i0.ɵɵadvance();
|
|
218
|
+
i0.ɵɵtextInterpolate1(" ", ctx_r2.FormatSilhouette(ctx_r2.Metrics.SilhouetteScore), " ");
|
|
219
|
+
i0.ɵɵadvance(5);
|
|
220
|
+
i0.ɵɵtextInterpolate(ctx_r2.Metrics.ClusterCount);
|
|
221
|
+
i0.ɵɵadvance(5);
|
|
222
|
+
i0.ɵɵtextInterpolate(ctx_r2.Metrics.RecordCount);
|
|
223
|
+
i0.ɵɵadvance(5);
|
|
224
|
+
i0.ɵɵtextInterpolate(ctx_r2.FormatTime(ctx_r2.Metrics.ComputationTimeMs));
|
|
225
|
+
i0.ɵɵadvance();
|
|
226
|
+
i0.ɵɵconditional(ctx_r2.Metrics.OutlierCount > 0 ? 21 : -1);
|
|
227
|
+
} }
|
|
228
|
+
function ClusterConfigPanelComponent_Conditional_8_Conditional_26_Template(rf, ctx) { if (rf & 1) {
|
|
229
|
+
const _r14 = i0.ɵɵgetCurrentView();
|
|
230
|
+
i0.ɵɵelementStart(0, "div", 22);
|
|
231
|
+
i0.ɵɵlistener("click", function ClusterConfigPanelComponent_Conditional_8_Conditional_26_Template_div_click_0_listener() { i0.ɵɵrestoreView(_r14); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.OnSave()); });
|
|
232
|
+
i0.ɵɵelement(1, "i", 23);
|
|
233
|
+
i0.ɵɵtext(2, " Save this visualization ");
|
|
234
|
+
i0.ɵɵelementEnd();
|
|
235
|
+
} }
|
|
236
|
+
function ClusterConfigPanelComponent_Conditional_8_Template(rf, ctx) { if (rf & 1) {
|
|
237
|
+
const _r2 = i0.ɵɵgetCurrentView();
|
|
238
|
+
i0.ɵɵelementStart(0, "div", 7)(1, "label");
|
|
239
|
+
i0.ɵɵtext(2, "Entity");
|
|
240
|
+
i0.ɵɵelementEnd();
|
|
241
|
+
i0.ɵɵelementStart(3, "select", 8);
|
|
242
|
+
i0.ɵɵlistener("ngModelChange", function ClusterConfigPanelComponent_Conditional_8_Template_select_ngModelChange_3_listener($event) { i0.ɵɵrestoreView(_r2); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.OnEntityChanged($event)); });
|
|
243
|
+
i0.ɵɵrepeaterCreate(4, ClusterConfigPanelComponent_Conditional_8_For_5_Template, 2, 2, "option", 9, i0.ɵɵcomponentInstance().TrackEntityBy, true);
|
|
244
|
+
i0.ɵɵelementEnd()();
|
|
245
|
+
i0.ɵɵconditionalCreate(6, ClusterConfigPanelComponent_Conditional_8_Conditional_6_Template, 6, 1, "div", 7);
|
|
246
|
+
i0.ɵɵelementStart(7, "div", 7)(8, "label");
|
|
247
|
+
i0.ɵɵtext(9, "Filter (optional)");
|
|
248
|
+
i0.ɵɵelementEnd();
|
|
249
|
+
i0.ɵɵelementStart(10, "input", 10);
|
|
250
|
+
i0.ɵɵtwoWayListener("ngModelChange", function ClusterConfigPanelComponent_Conditional_8_Template_input_ngModelChange_10_listener($event) { i0.ɵɵrestoreView(_r2); const ctx_r2 = i0.ɵɵnextContext(); i0.ɵɵtwoWayBindingSet(ctx_r2.Config.Filter, $event) || (ctx_r2.Config.Filter = $event); return i0.ɵɵresetView($event); });
|
|
251
|
+
i0.ɵɵlistener("ngModelChange", function ClusterConfigPanelComponent_Conditional_8_Template_input_ngModelChange_10_listener() { i0.ɵɵrestoreView(_r2); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.OnFieldChanged()); });
|
|
252
|
+
i0.ɵɵelementEnd()();
|
|
253
|
+
i0.ɵɵelementStart(11, "div", 11);
|
|
254
|
+
i0.ɵɵconditionalCreate(12, ClusterConfigPanelComponent_Conditional_8_Conditional_12_Template, 6, 1, "div", 7);
|
|
255
|
+
i0.ɵɵconditionalCreate(13, ClusterConfigPanelComponent_Conditional_8_Conditional_13_Template, 4, 5, "div", 7)(14, ClusterConfigPanelComponent_Conditional_8_Conditional_14_Template, 4, 5, "div", 7);
|
|
256
|
+
i0.ɵɵelementEnd();
|
|
257
|
+
i0.ɵɵconditionalCreate(15, ClusterConfigPanelComponent_Conditional_8_Conditional_15_Template, 4, 5, "div", 7);
|
|
258
|
+
i0.ɵɵelementStart(16, "div", 11);
|
|
259
|
+
i0.ɵɵconditionalCreate(17, ClusterConfigPanelComponent_Conditional_8_Conditional_17_Template, 6, 1, "div", 7);
|
|
260
|
+
i0.ɵɵelementStart(18, "div", 7)(19, "label");
|
|
261
|
+
i0.ɵɵtext(20, "Max Records");
|
|
262
|
+
i0.ɵɵelementEnd();
|
|
263
|
+
i0.ɵɵelementStart(21, "input", 12);
|
|
264
|
+
i0.ɵɵtwoWayListener("ngModelChange", function ClusterConfigPanelComponent_Conditional_8_Template_input_ngModelChange_21_listener($event) { i0.ɵɵrestoreView(_r2); const ctx_r2 = i0.ɵɵnextContext(); i0.ɵɵtwoWayBindingSet(ctx_r2.Config.MaxRecords, $event) || (ctx_r2.Config.MaxRecords = $event); return i0.ɵɵresetView($event); });
|
|
265
|
+
i0.ɵɵlistener("ngModelChange", function ClusterConfigPanelComponent_Conditional_8_Template_input_ngModelChange_21_listener() { i0.ɵɵrestoreView(_r2); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.OnFieldChanged()); });
|
|
266
|
+
i0.ɵɵelementEnd()()();
|
|
267
|
+
i0.ɵɵelementStart(22, "button", 13);
|
|
268
|
+
i0.ɵɵlistener("click", function ClusterConfigPanelComponent_Conditional_8_Template_button_click_22_listener() { i0.ɵɵrestoreView(_r2); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.OnRun()); });
|
|
269
|
+
i0.ɵɵconditionalCreate(23, ClusterConfigPanelComponent_Conditional_8_Conditional_23_Template, 2, 0)(24, ClusterConfigPanelComponent_Conditional_8_Conditional_24_Template, 2, 0);
|
|
270
|
+
i0.ɵɵelementEnd();
|
|
271
|
+
i0.ɵɵconditionalCreate(25, ClusterConfigPanelComponent_Conditional_8_Conditional_25_Template, 22, 7, "div", 14);
|
|
272
|
+
i0.ɵɵconditionalCreate(26, ClusterConfigPanelComponent_Conditional_8_Conditional_26_Template, 3, 0, "div", 15);
|
|
273
|
+
} if (rf & 2) {
|
|
274
|
+
const ctx_r2 = i0.ɵɵnextContext();
|
|
275
|
+
i0.ɵɵadvance(3);
|
|
276
|
+
i0.ɵɵproperty("ngModel", ctx_r2.Config.EntityName);
|
|
277
|
+
i0.ɵɵadvance();
|
|
278
|
+
i0.ɵɵrepeater(ctx_r2.FilteredEntityOptions);
|
|
279
|
+
i0.ɵɵadvance(2);
|
|
280
|
+
i0.ɵɵconditional(ctx_r2.EntityDocOptions.length > 1 ? 6 : -1);
|
|
281
|
+
i0.ɵɵadvance(4);
|
|
282
|
+
i0.ɵɵtwoWayProperty("ngModel", ctx_r2.Config.Filter);
|
|
283
|
+
i0.ɵɵadvance(2);
|
|
284
|
+
i0.ɵɵconditional(ctx_r2.ShowAlgorithmPicker ? 12 : -1);
|
|
285
|
+
i0.ɵɵadvance();
|
|
286
|
+
i0.ɵɵconditional(ctx_r2.IsKMeans ? 13 : 14);
|
|
287
|
+
i0.ɵɵadvance(2);
|
|
288
|
+
i0.ɵɵconditional(!ctx_r2.IsKMeans ? 15 : -1);
|
|
289
|
+
i0.ɵɵadvance(2);
|
|
290
|
+
i0.ɵɵconditional(ctx_r2.ShowMetricPicker ? 17 : -1);
|
|
291
|
+
i0.ɵɵadvance(4);
|
|
292
|
+
i0.ɵɵtwoWayProperty("ngModel", ctx_r2.Config.MaxRecords);
|
|
293
|
+
i0.ɵɵproperty("min", 10)("max", 5000);
|
|
294
|
+
i0.ɵɵadvance();
|
|
295
|
+
i0.ɵɵproperty("disabled", ctx_r2.IsRunning || !ctx_r2.Config.EntityName);
|
|
296
|
+
i0.ɵɵadvance();
|
|
297
|
+
i0.ɵɵconditional(ctx_r2.IsRunning ? 23 : 24);
|
|
298
|
+
i0.ɵɵadvance(2);
|
|
299
|
+
i0.ɵɵconditional(ctx_r2.Metrics && ctx_r2.Metrics.RecordCount > 0 ? 25 : -1);
|
|
300
|
+
i0.ɵɵadvance();
|
|
301
|
+
i0.ɵɵconditional(ctx_r2.ShowSaveButton ? 26 : -1);
|
|
302
|
+
} }
|
|
303
|
+
export class ClusterConfigPanelComponent {
|
|
304
|
+
constructor() {
|
|
305
|
+
this.cdr = inject(ChangeDetectorRef);
|
|
306
|
+
// ================================================================
|
|
307
|
+
// Data Inputs
|
|
308
|
+
// ================================================================
|
|
309
|
+
/**
|
|
310
|
+
* Whether a clustering run is currently in progress.
|
|
311
|
+
*
|
|
312
|
+
* When `true`, the Run button is disabled and shows a spinner.
|
|
313
|
+
*
|
|
314
|
+
* @default false
|
|
315
|
+
*/
|
|
316
|
+
this.IsRunning = false;
|
|
317
|
+
/**
|
|
318
|
+
* Result metrics from the most recent clustering run.
|
|
319
|
+
*
|
|
320
|
+
* When non-null, the metrics section is displayed at the bottom
|
|
321
|
+
* of the panel showing silhouette score, cluster count, etc.
|
|
322
|
+
*/
|
|
323
|
+
this.Metrics = null;
|
|
324
|
+
/**
|
|
325
|
+
* Entity options for the dropdown.
|
|
326
|
+
*
|
|
327
|
+
* The parent component is responsible for populating this list,
|
|
328
|
+
* typically from MJ Metadata or a subset filtered by
|
|
329
|
+
* `AvailableEntities`.
|
|
330
|
+
*/
|
|
331
|
+
this._entityOptions = [];
|
|
332
|
+
/** Entity Document options for the selected entity. Only shown when 2+ docs exist. */
|
|
333
|
+
this.EntityDocOptions = [];
|
|
334
|
+
// ================================================================
|
|
335
|
+
// Customization Inputs
|
|
336
|
+
// ================================================================
|
|
337
|
+
/**
|
|
338
|
+
* Whether to display the "Save this visualization" link.
|
|
339
|
+
*
|
|
340
|
+
* @default true
|
|
341
|
+
*/
|
|
342
|
+
this.ShowSaveButton = true;
|
|
343
|
+
/**
|
|
344
|
+
* Whether to display the algorithm picker dropdown.
|
|
345
|
+
*
|
|
346
|
+
* When `false`, the algorithm is fixed to `DefaultAlgorithm`.
|
|
347
|
+
*
|
|
348
|
+
* @default true
|
|
349
|
+
*/
|
|
350
|
+
this.ShowAlgorithmPicker = true;
|
|
351
|
+
/**
|
|
352
|
+
* Whether to display the distance metric picker dropdown.
|
|
353
|
+
*
|
|
354
|
+
* @default true
|
|
355
|
+
*/
|
|
356
|
+
this.ShowMetricPicker = true;
|
|
357
|
+
/**
|
|
358
|
+
* Default algorithm selection when the panel initializes.
|
|
359
|
+
*
|
|
360
|
+
* @default 'kmeans'
|
|
361
|
+
*/
|
|
362
|
+
this.DefaultAlgorithm = 'kmeans';
|
|
363
|
+
/**
|
|
364
|
+
* Restrict the entity picker to a specific list of entity names.
|
|
365
|
+
*
|
|
366
|
+
* When empty (the default), all entity options from the
|
|
367
|
+
* `EntityOptions` input are shown. When non-empty, only entities
|
|
368
|
+
* whose `Name` appears in this array are shown.
|
|
369
|
+
*
|
|
370
|
+
* @default []
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```html
|
|
374
|
+
* <mj-cluster-config-panel
|
|
375
|
+
* [AvailableEntities]="['Companies', 'Contacts', 'Deals']">
|
|
376
|
+
* </mj-cluster-config-panel>
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
this.AvailableEntities = [];
|
|
380
|
+
/**
|
|
381
|
+
* Whether the panel starts in a collapsed (minimized) state.
|
|
382
|
+
*
|
|
383
|
+
* The user can toggle between collapsed and expanded by clicking
|
|
384
|
+
* the collapse chevron in the panel header.
|
|
385
|
+
*
|
|
386
|
+
* @default false
|
|
387
|
+
*/
|
|
388
|
+
this.Collapsed = false;
|
|
389
|
+
// ================================================================
|
|
390
|
+
// Standard Outputs
|
|
391
|
+
// ================================================================
|
|
392
|
+
/**
|
|
393
|
+
* Emitted when the user clicks Run and `BeforeRunClustering` was not
|
|
394
|
+
* canceled. Payload is a snapshot of the current config.
|
|
395
|
+
*/
|
|
396
|
+
this.RunClustering = new EventEmitter();
|
|
397
|
+
/**
|
|
398
|
+
* Emitted when the user clicks the Save link and `BeforeSave` was
|
|
399
|
+
* not canceled.
|
|
400
|
+
*/
|
|
401
|
+
this.SaveVisualization = new EventEmitter();
|
|
402
|
+
// ================================================================
|
|
403
|
+
// Cancelable Outputs
|
|
404
|
+
// ================================================================
|
|
405
|
+
/**
|
|
406
|
+
* Fires **before** a clustering run starts. Set `event.Cancel = true`
|
|
407
|
+
* to prevent the run (e.g., for validation).
|
|
408
|
+
*
|
|
409
|
+
* @example
|
|
410
|
+
* ```typescript
|
|
411
|
+
* onBeforeRun(event: CancelableEvent<ClusterConfig>): void {
|
|
412
|
+
* if (!event.Data.EntityName) {
|
|
413
|
+
* event.Cancel = true;
|
|
414
|
+
* alert('Please select an entity first.');
|
|
415
|
+
* }
|
|
416
|
+
* }
|
|
417
|
+
* ```
|
|
418
|
+
*/
|
|
419
|
+
this.BeforeRunClustering = new EventEmitter();
|
|
420
|
+
/**
|
|
421
|
+
* Fires **before** a visualization is saved. Set `event.Cancel = true`
|
|
422
|
+
* to prevent the save (e.g., to prompt for a name first).
|
|
423
|
+
*/
|
|
424
|
+
this.BeforeSave = new EventEmitter();
|
|
425
|
+
// ================================================================
|
|
426
|
+
// Informational Outputs
|
|
427
|
+
// ================================================================
|
|
428
|
+
/**
|
|
429
|
+
* Fires whenever any configuration value changes (entity, algorithm,
|
|
430
|
+
* K, epsilon, distance metric, max records, filter).
|
|
431
|
+
*
|
|
432
|
+
* Payload is a snapshot of the entire config at the time of change.
|
|
433
|
+
*/
|
|
434
|
+
this.ConfigChanged = new EventEmitter();
|
|
435
|
+
/**
|
|
436
|
+
* Fires specifically when the algorithm selection changes.
|
|
437
|
+
*
|
|
438
|
+
* Useful for showing/hiding algorithm-specific UI in the parent.
|
|
439
|
+
*/
|
|
440
|
+
this.AlgorithmChanged = new EventEmitter();
|
|
441
|
+
// ================================================================
|
|
442
|
+
// Internal State
|
|
443
|
+
// ================================================================
|
|
444
|
+
/** The working configuration object. */
|
|
445
|
+
this.Config = DefaultClusterConfig();
|
|
446
|
+
/** Filtered entity list for display (filtered by AvailableEntities). */
|
|
447
|
+
this.FilteredEntityOptions = [];
|
|
448
|
+
/** Whether the panel body is currently visible. */
|
|
449
|
+
this.IsExpanded = true;
|
|
450
|
+
/** Algorithm options for the dropdown. */
|
|
451
|
+
this.AlgorithmOptions = [
|
|
452
|
+
{ Value: 'kmeans', Label: 'K-Means' },
|
|
453
|
+
{ Value: 'dbscan', Label: 'DBSCAN' },
|
|
454
|
+
];
|
|
455
|
+
/** Distance metric options for the dropdown. */
|
|
456
|
+
this.MetricOptions = [
|
|
457
|
+
{ Value: 'cosine', Label: 'Cosine' },
|
|
458
|
+
{ Value: 'euclidean', Label: 'Euclidean' },
|
|
459
|
+
{ Value: 'dotproduct', Label: 'Dot Product' },
|
|
460
|
+
];
|
|
461
|
+
// Dragging state
|
|
462
|
+
/** Current panel top offset in pixels. */
|
|
463
|
+
this.PanelTop = 12;
|
|
464
|
+
/** Current panel left offset in pixels. */
|
|
465
|
+
this.PanelLeft = 12;
|
|
466
|
+
this.isDragging = false;
|
|
467
|
+
this.dragStartX = 0;
|
|
468
|
+
this.dragStartY = 0;
|
|
469
|
+
this.dragStartTop = 0;
|
|
470
|
+
this.dragStartLeft = 0;
|
|
471
|
+
this.boundOnMouseMove = null;
|
|
472
|
+
this.boundOnMouseUp = null;
|
|
473
|
+
}
|
|
474
|
+
set EntityOptions(value) {
|
|
475
|
+
this._entityOptions = value || [];
|
|
476
|
+
this.rebuildFilteredEntities();
|
|
477
|
+
}
|
|
478
|
+
get EntityOptions() {
|
|
479
|
+
return this._entityOptions;
|
|
480
|
+
}
|
|
481
|
+
// ================================================================
|
|
482
|
+
// Lifecycle
|
|
483
|
+
// ================================================================
|
|
484
|
+
ngOnInit() {
|
|
485
|
+
// Apply defaults
|
|
486
|
+
this.Config.Algorithm = this.DefaultAlgorithm;
|
|
487
|
+
this.IsExpanded = !this.Collapsed;
|
|
488
|
+
this.rebuildFilteredEntities();
|
|
489
|
+
this.boundOnMouseMove = this.onMouseMove.bind(this);
|
|
490
|
+
this.boundOnMouseUp = this.onMouseUp.bind(this);
|
|
491
|
+
document.addEventListener('mousemove', this.boundOnMouseMove);
|
|
492
|
+
document.addEventListener('mouseup', this.boundOnMouseUp);
|
|
493
|
+
}
|
|
494
|
+
ngOnDestroy() {
|
|
495
|
+
if (this.boundOnMouseMove)
|
|
496
|
+
document.removeEventListener('mousemove', this.boundOnMouseMove);
|
|
497
|
+
if (this.boundOnMouseUp)
|
|
498
|
+
document.removeEventListener('mouseup', this.boundOnMouseUp);
|
|
499
|
+
}
|
|
500
|
+
// ================================================================
|
|
501
|
+
// Public Template Methods
|
|
502
|
+
// ================================================================
|
|
503
|
+
/**
|
|
504
|
+
* Whether the current algorithm is K-Means (controls which
|
|
505
|
+
* parameter sliders are shown).
|
|
506
|
+
*/
|
|
507
|
+
get IsKMeans() {
|
|
508
|
+
return this.Config.Algorithm === 'kmeans';
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Handle algorithm dropdown change.
|
|
512
|
+
*
|
|
513
|
+
* @param value New algorithm value string.
|
|
514
|
+
*/
|
|
515
|
+
OnAlgorithmChanged(value) {
|
|
516
|
+
const newAlg = value;
|
|
517
|
+
this.Config.Algorithm = newAlg;
|
|
518
|
+
this.AlgorithmChanged.emit(newAlg);
|
|
519
|
+
this.emitConfigChanged();
|
|
520
|
+
this.cdr.detectChanges();
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Handle distance metric dropdown change.
|
|
524
|
+
*
|
|
525
|
+
* @param value New metric value string.
|
|
526
|
+
*/
|
|
527
|
+
OnMetricChanged(value) {
|
|
528
|
+
this.Config.DistanceMetric = value;
|
|
529
|
+
this.emitConfigChanged();
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Handle entity dropdown change.
|
|
533
|
+
*
|
|
534
|
+
* @param value New entity name.
|
|
535
|
+
*/
|
|
536
|
+
OnEntityChanged(value) {
|
|
537
|
+
this.Config.EntityName = value;
|
|
538
|
+
this.emitConfigChanged();
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Handle changes to any numeric/text config field (K, Epsilon,
|
|
542
|
+
* MinPoints, MaxRecords, Filter).
|
|
543
|
+
*/
|
|
544
|
+
OnFieldChanged() {
|
|
545
|
+
this.emitConfigChanged();
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Handle Run button click. Fires `BeforeRunClustering`, and if not
|
|
549
|
+
* canceled, emits `RunClustering`.
|
|
550
|
+
*/
|
|
551
|
+
OnRun() {
|
|
552
|
+
if (!this.Config.EntityName || this.IsRunning)
|
|
553
|
+
return;
|
|
554
|
+
const snapshot = { ...this.Config };
|
|
555
|
+
const cancelable = { Data: snapshot, Cancel: false };
|
|
556
|
+
this.BeforeRunClustering.emit(cancelable);
|
|
557
|
+
if (cancelable.Cancel)
|
|
558
|
+
return;
|
|
559
|
+
this.RunClustering.emit(snapshot);
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Handle Save link click. Fires `BeforeSave`, and if not canceled,
|
|
563
|
+
* emits `SaveVisualization`.
|
|
564
|
+
*/
|
|
565
|
+
OnSave() {
|
|
566
|
+
const cancelable = { Data: { ...this.Config }, Cancel: false };
|
|
567
|
+
this.BeforeSave.emit(cancelable);
|
|
568
|
+
if (cancelable.Cancel)
|
|
569
|
+
return;
|
|
570
|
+
this.SaveVisualization.emit();
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Toggle the panel between expanded and collapsed states.
|
|
574
|
+
*/
|
|
575
|
+
ToggleCollapse() {
|
|
576
|
+
this.IsExpanded = !this.IsExpanded;
|
|
577
|
+
this.cdr.detectChanges();
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Format a silhouette score to 2 decimal places.
|
|
581
|
+
*
|
|
582
|
+
* @param score The raw silhouette score.
|
|
583
|
+
* @returns Formatted string (e.g., `"0.72"`).
|
|
584
|
+
*/
|
|
585
|
+
FormatSilhouette(score) {
|
|
586
|
+
return score.toFixed(2);
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Whether a silhouette score indicates good clustering quality.
|
|
590
|
+
*
|
|
591
|
+
* @param score The silhouette score to evaluate.
|
|
592
|
+
* @returns `true` if score >= 0.5.
|
|
593
|
+
*/
|
|
594
|
+
IsSilhouetteGood(score) {
|
|
595
|
+
return score >= 0.5;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Format a computation time in milliseconds to a human-readable string.
|
|
599
|
+
*
|
|
600
|
+
* @param ms Milliseconds.
|
|
601
|
+
* @returns e.g., `"1.2s"` or `"450ms"`.
|
|
602
|
+
*/
|
|
603
|
+
FormatTime(ms) {
|
|
604
|
+
return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Start dragging the panel via header mousedown.
|
|
608
|
+
*
|
|
609
|
+
* @param event The native mouse event.
|
|
610
|
+
*/
|
|
611
|
+
OnHeaderMouseDown(event) {
|
|
612
|
+
event.preventDefault();
|
|
613
|
+
this.isDragging = true;
|
|
614
|
+
this.dragStartX = event.clientX;
|
|
615
|
+
this.dragStartY = event.clientY;
|
|
616
|
+
this.dragStartTop = this.PanelTop;
|
|
617
|
+
this.dragStartLeft = this.PanelLeft;
|
|
618
|
+
}
|
|
619
|
+
/** @internal TrackBy for algorithm options. */
|
|
620
|
+
TrackAlgorithmBy(_index, item) {
|
|
621
|
+
return item.Value;
|
|
622
|
+
}
|
|
623
|
+
/** @internal TrackBy for metric options. */
|
|
624
|
+
TrackMetricBy(_index, item) {
|
|
625
|
+
return item.Value;
|
|
626
|
+
}
|
|
627
|
+
/** @internal TrackBy for entity options. */
|
|
628
|
+
TrackEntityBy(_index, item) {
|
|
629
|
+
return item.Name;
|
|
630
|
+
}
|
|
631
|
+
// ================================================================
|
|
632
|
+
// Private Methods
|
|
633
|
+
// ================================================================
|
|
634
|
+
/**
|
|
635
|
+
* Rebuild the filtered entity list based on `AvailableEntities`.
|
|
636
|
+
*/
|
|
637
|
+
rebuildFilteredEntities() {
|
|
638
|
+
if (this.AvailableEntities.length > 0) {
|
|
639
|
+
const allowedSet = new Set(this.AvailableEntities);
|
|
640
|
+
this.FilteredEntityOptions = this.EntityOptions.filter(e => allowedSet.has(e.Name));
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
this.FilteredEntityOptions = this.EntityOptions;
|
|
644
|
+
}
|
|
645
|
+
if (this.FilteredEntityOptions.length > 0 && !this.Config.EntityName) {
|
|
646
|
+
this.Config.EntityName = this.FilteredEntityOptions[0].Name;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
/** Emit `ConfigChanged` with a snapshot of the current config. */
|
|
650
|
+
emitConfigChanged() {
|
|
651
|
+
this.ConfigChanged.emit({ ...this.Config });
|
|
652
|
+
}
|
|
653
|
+
onMouseMove(event) {
|
|
654
|
+
if (!this.isDragging)
|
|
655
|
+
return;
|
|
656
|
+
const dx = event.clientX - this.dragStartX;
|
|
657
|
+
const dy = event.clientY - this.dragStartY;
|
|
658
|
+
this.PanelTop = Math.max(0, this.dragStartTop + dy);
|
|
659
|
+
this.PanelLeft = Math.max(0, this.dragStartLeft + dx);
|
|
660
|
+
this.cdr.detectChanges();
|
|
661
|
+
}
|
|
662
|
+
onMouseUp(_event) {
|
|
663
|
+
this.isDragging = false;
|
|
664
|
+
}
|
|
665
|
+
static { this.ɵfac = function ClusterConfigPanelComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || ClusterConfigPanelComponent)(); }; }
|
|
666
|
+
static { this.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: ClusterConfigPanelComponent, selectors: [["mj-cluster-config-panel"]], viewQuery: function ClusterConfigPanelComponent_Query(rf, ctx) { if (rf & 1) {
|
|
667
|
+
i0.ɵɵviewQuery(_c0, 5);
|
|
668
|
+
} if (rf & 2) {
|
|
669
|
+
let _t;
|
|
670
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.panelRef = _t.first);
|
|
671
|
+
} }, inputs: { IsRunning: "IsRunning", Metrics: "Metrics", EntityOptions: "EntityOptions", EntityDocOptions: "EntityDocOptions", ShowSaveButton: "ShowSaveButton", ShowAlgorithmPicker: "ShowAlgorithmPicker", ShowMetricPicker: "ShowMetricPicker", DefaultAlgorithm: "DefaultAlgorithm", AvailableEntities: "AvailableEntities", Collapsed: "Collapsed" }, outputs: { RunClustering: "RunClustering", SaveVisualization: "SaveVisualization", BeforeRunClustering: "BeforeRunClustering", BeforeSave: "BeforeSave", ConfigChanged: "ConfigChanged", AlgorithmChanged: "AlgorithmChanged" }, standalone: false, decls: 9, vars: 9, consts: [["panelElement", ""], [1, "config-panel"], [1, "panel-header", 3, "mousedown"], [1, "fa-solid", "fa-sliders"], [1, "header-actions"], ["title", "Toggle panel", 1, "fa-solid", 3, "click"], [1, "fa-solid", "fa-grip-dots-vertical", "drag-handle"], [1, "form-group"], [3, "ngModelChange", "ngModel"], [3, "value"], ["type", "text", "placeholder", "e.g. Status = 'Active'", 3, "ngModelChange", "ngModel"], [1, "form-row"], ["type", "number", 3, "ngModelChange", "ngModel", "min", "max"], [1, "run-btn", 3, "click", "disabled"], [1, "metrics-section"], [1, "save-link"], ["type", "range", 3, "ngModelChange", "ngModel", "min", "max", "step"], [1, "fa-solid", "fa-spinner", "fa-spin"], [1, "fa-solid", "fa-play"], [1, "metric-item"], [1, "metric-label"], [1, "metric-value"], [1, "save-link", 3, "click"], [1, "fa-solid", "fa-bookmark"]], template: function ClusterConfigPanelComponent_Template(rf, ctx) { if (rf & 1) {
|
|
672
|
+
const _r1 = i0.ɵɵgetCurrentView();
|
|
673
|
+
i0.ɵɵelementStart(0, "div", 1, 0)(2, "h3", 2);
|
|
674
|
+
i0.ɵɵlistener("mousedown", function ClusterConfigPanelComponent_Template_h3_mousedown_2_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.OnHeaderMouseDown($event)); });
|
|
675
|
+
i0.ɵɵelement(3, "i", 3);
|
|
676
|
+
i0.ɵɵtext(4, " Configuration ");
|
|
677
|
+
i0.ɵɵelementStart(5, "span", 4)(6, "i", 5);
|
|
678
|
+
i0.ɵɵlistener("click", function ClusterConfigPanelComponent_Template_i_click_6_listener($event) { i0.ɵɵrestoreView(_r1); ctx.ToggleCollapse(); return i0.ɵɵresetView($event.stopPropagation()); });
|
|
679
|
+
i0.ɵɵelementEnd();
|
|
680
|
+
i0.ɵɵelement(7, "i", 6);
|
|
681
|
+
i0.ɵɵelementEnd()();
|
|
682
|
+
i0.ɵɵconditionalCreate(8, ClusterConfigPanelComponent_Conditional_8_Template, 27, 14);
|
|
683
|
+
i0.ɵɵelementEnd();
|
|
684
|
+
} if (rf & 2) {
|
|
685
|
+
i0.ɵɵstyleProp("top", ctx.PanelTop, "px")("left", ctx.PanelLeft, "px");
|
|
686
|
+
i0.ɵɵadvance(6);
|
|
687
|
+
i0.ɵɵclassProp("fa-chevron-up", ctx.IsExpanded)("fa-chevron-down", !ctx.IsExpanded);
|
|
688
|
+
i0.ɵɵadvance(2);
|
|
689
|
+
i0.ɵɵconditional(ctx.IsExpanded ? 8 : -1);
|
|
690
|
+
} }, dependencies: [i1.NgSelectOption, i1.ɵNgSelectMultipleOption, i1.DefaultValueAccessor, i1.NumberValueAccessor, i1.RangeValueAccessor, i1.SelectControlValueAccessor, i1.NgControlStatus, i1.MinValidator, i1.MaxValidator, i1.NgModel], styles: [".config-panel[_ngcontent-%COMP%] {\n position: absolute;\n width: 300px;\n background: color-mix(in srgb, var(--mj-bg-surface-elevated) 95%, transparent);\n backdrop-filter: blur(20px);\n -webkit-backdrop-filter: blur(20px);\n border: 1px solid var(--mj-border-default);\n border-radius: 14px;\n padding: 20px;\n box-shadow: 0 20px 60px color-mix(in srgb, var(--mj-bg-page) 40%, transparent);\n z-index: 10;\n}\n\n.panel-header[_ngcontent-%COMP%] {\n font-size: 0.8rem;\n font-weight: 600;\n margin: 0 0 16px 0;\n display: flex;\n align-items: center;\n gap: 8px;\n color: var(--mj-text-primary);\n cursor: move;\n user-select: none;\n}\n\n.panel-header[_ngcontent-%COMP%] i[_ngcontent-%COMP%]:first-child {\n color: var(--mj-brand-primary);\n}\n\n.header-actions[_ngcontent-%COMP%] {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.header-actions[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n cursor: pointer;\n transition: color 0.15s;\n}\n\n.header-actions[_ngcontent-%COMP%] i[_ngcontent-%COMP%]:hover {\n color: var(--mj-text-primary);\n}\n\n.drag-handle[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n}\n\n\n\n.form-group[_ngcontent-%COMP%] {\n margin-bottom: 14px;\n}\n\n.form-group[_ngcontent-%COMP%] label[_ngcontent-%COMP%] {\n display: block;\n font-size: 0.7rem;\n font-weight: 500;\n color: var(--mj-text-secondary);\n margin-bottom: 5px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.form-group[_ngcontent-%COMP%] select[_ngcontent-%COMP%], \n.form-group[_ngcontent-%COMP%] input[type=\"text\"][_ngcontent-%COMP%], \n.form-group[_ngcontent-%COMP%] input[type=\"number\"][_ngcontent-%COMP%] {\n width: 100%;\n padding: 8px 12px;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: 7px;\n color: var(--mj-text-primary);\n font-size: 0.82rem;\n outline: none;\n transition: border 0.15s;\n}\n\n.form-group[_ngcontent-%COMP%] select[_ngcontent-%COMP%]:focus, \n.form-group[_ngcontent-%COMP%] input[type=\"text\"][_ngcontent-%COMP%]:focus, \n.form-group[_ngcontent-%COMP%] input[type=\"number\"][_ngcontent-%COMP%]:focus {\n border-color: var(--mj-brand-primary);\n}\n\n.form-group[_ngcontent-%COMP%] input[type=\"range\"][_ngcontent-%COMP%] {\n width: 100%;\n accent-color: var(--mj-brand-primary);\n}\n\n.form-row[_ngcontent-%COMP%] {\n display: flex;\n gap: 10px;\n}\n\n.form-row[_ngcontent-%COMP%] .form-group[_ngcontent-%COMP%] {\n flex: 1;\n}\n\n\n\n.run-btn[_ngcontent-%COMP%] {\n width: 100%;\n padding: 10px;\n background: var(--mj-brand-primary);\n border: none;\n border-radius: 8px;\n color: var(--mj-text-inverse);\n font-size: 0.82rem;\n font-weight: 600;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n transition: background 0.15s;\n margin-top: 4px;\n}\n\n.run-btn[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: var(--mj-brand-primary-hover);\n}\n\n.run-btn[_ngcontent-%COMP%]:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n\n\n.metrics-section[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 8px;\n margin-top: 14px;\n padding-top: 14px;\n border-top: 1px solid var(--mj-border-default);\n}\n\n.metric-item[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.metric-label[_ngcontent-%COMP%] {\n font-size: 0.65rem;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--mj-text-muted);\n}\n\n.metric-value[_ngcontent-%COMP%] {\n font-size: 0.85rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.metric-value.good[_ngcontent-%COMP%] {\n color: var(--mj-status-success);\n}\n\n\n\n.save-link[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n margin-top: 10px;\n font-size: 0.75rem;\n color: var(--mj-text-muted);\n cursor: pointer;\n transition: color 0.15s;\n}\n\n.save-link[_ngcontent-%COMP%]:hover {\n color: var(--mj-brand-primary);\n}"] }); }
|
|
691
|
+
}
|
|
692
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ClusterConfigPanelComponent, [{
|
|
693
|
+
type: Component,
|
|
694
|
+
args: [{ standalone: false, selector: 'mj-cluster-config-panel', template: "<div #panelElement\n class=\"config-panel\"\n [style.top.px]=\"PanelTop\"\n [style.left.px]=\"PanelLeft\">\n\n <!-- Draggable header -->\n <h3 class=\"panel-header\"\n (mousedown)=\"OnHeaderMouseDown($event)\">\n <i class=\"fa-solid fa-sliders\"></i>\n Configuration\n <span class=\"header-actions\">\n <i class=\"fa-solid\"\n [class.fa-chevron-up]=\"IsExpanded\"\n [class.fa-chevron-down]=\"!IsExpanded\"\n (click)=\"ToggleCollapse(); $event.stopPropagation()\"\n title=\"Toggle panel\"></i>\n <i class=\"fa-solid fa-grip-dots-vertical drag-handle\"></i>\n </span>\n </h3>\n\n @if (IsExpanded) {\n <!-- Entity picker -->\n <div class=\"form-group\">\n <label>Entity</label>\n <select [ngModel]=\"Config.EntityName\"\n (ngModelChange)=\"OnEntityChanged($event)\">\n @for (entity of FilteredEntityOptions; track TrackEntityBy($index, entity)) {\n <option [value]=\"entity.Name\">{{ entity.Name }}</option>\n }\n </select>\n </div>\n\n <!-- Entity Document selector (only shown when 2+ docs for selected entity) -->\n @if (EntityDocOptions.length > 1) {\n <div class=\"form-group\">\n <label>Document Template</label>\n <select [ngModel]=\"Config.EntityDocumentID\"\n (ngModelChange)=\"Config.EntityDocumentID = $event; OnFieldChanged()\">\n @for (doc of EntityDocOptions; track doc.ID) {\n <option [value]=\"doc.ID\">{{ doc.Name }}</option>\n }\n </select>\n </div>\n }\n\n <!-- Filter -->\n <div class=\"form-group\">\n <label>Filter (optional)</label>\n <input type=\"text\"\n [(ngModel)]=\"Config.Filter\"\n (ngModelChange)=\"OnFieldChanged()\"\n placeholder=\"e.g. Status = 'Active'\">\n </div>\n\n <!-- Algorithm + K/Epsilon row -->\n <div class=\"form-row\">\n @if (ShowAlgorithmPicker) {\n <div class=\"form-group\">\n <label>Algorithm</label>\n <select [ngModel]=\"Config.Algorithm\"\n (ngModelChange)=\"OnAlgorithmChanged($event)\">\n @for (alg of AlgorithmOptions; track TrackAlgorithmBy($index, alg)) {\n <option [value]=\"alg.Value\">{{ alg.Label }}</option>\n }\n </select>\n </div>\n }\n\n @if (IsKMeans) {\n <div class=\"form-group\">\n <label>Clusters (K): {{ Config.K }}</label>\n <input type=\"range\"\n [ngModel]=\"Config.K\"\n (ngModelChange)=\"Config.K = $event; OnFieldChanged()\"\n [min]=\"2\"\n [max]=\"20\"\n [step]=\"1\">\n </div>\n } @else {\n <div class=\"form-group\">\n <label>Epsilon: {{ Config.Epsilon }}</label>\n <input type=\"range\"\n [ngModel]=\"Config.Epsilon\"\n (ngModelChange)=\"Config.Epsilon = $event; OnFieldChanged()\"\n [min]=\"0.05\"\n [max]=\"0.95\"\n [step]=\"0.05\">\n </div>\n }\n </div>\n\n <!-- DBSCAN MinPoints row (only visible for DBSCAN) -->\n @if (!IsKMeans) {\n <div class=\"form-group\">\n <label>Min Points: {{ Config.MinPoints }}</label>\n <input type=\"range\"\n [ngModel]=\"Config.MinPoints\"\n (ngModelChange)=\"Config.MinPoints = $event; OnFieldChanged()\"\n [min]=\"2\"\n [max]=\"20\"\n [step]=\"1\">\n </div>\n }\n\n <!-- Distance metric + Max records row -->\n <div class=\"form-row\">\n @if (ShowMetricPicker) {\n <div class=\"form-group\">\n <label>Distance Metric</label>\n <select [ngModel]=\"Config.DistanceMetric\"\n (ngModelChange)=\"OnMetricChanged($event)\">\n @for (metric of MetricOptions; track TrackMetricBy($index, metric)) {\n <option [value]=\"metric.Value\">{{ metric.Label }}</option>\n }\n </select>\n </div>\n }\n <div class=\"form-group\">\n <label>Max Records</label>\n <input type=\"number\"\n [(ngModel)]=\"Config.MaxRecords\"\n (ngModelChange)=\"OnFieldChanged()\"\n [min]=\"10\"\n [max]=\"5000\">\n </div>\n </div>\n\n <!-- Run button -->\n <button class=\"run-btn\"\n [disabled]=\"IsRunning || !Config.EntityName\"\n (click)=\"OnRun()\">\n @if (IsRunning) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i> Running...\n } @else {\n <i class=\"fa-solid fa-play\"></i> Run Clustering\n }\n </button>\n\n <!-- Metrics display -->\n @if (Metrics && Metrics.RecordCount > 0) {\n <div class=\"metrics-section\">\n <div class=\"metric-item\">\n <span class=\"metric-label\">Silhouette</span>\n <span class=\"metric-value\"\n [class.good]=\"IsSilhouetteGood(Metrics.SilhouetteScore)\">\n {{ FormatSilhouette(Metrics.SilhouetteScore) }}\n </span>\n </div>\n <div class=\"metric-item\">\n <span class=\"metric-label\">Clusters</span>\n <span class=\"metric-value\">{{ Metrics.ClusterCount }}</span>\n </div>\n <div class=\"metric-item\">\n <span class=\"metric-label\">Records</span>\n <span class=\"metric-value\">{{ Metrics.RecordCount }}</span>\n </div>\n <div class=\"metric-item\">\n <span class=\"metric-label\">Time</span>\n <span class=\"metric-value\">{{ FormatTime(Metrics.ComputationTimeMs) }}</span>\n </div>\n @if (Metrics.OutlierCount > 0) {\n <div class=\"metric-item\">\n <span class=\"metric-label\">Outliers</span>\n <span class=\"metric-value\">{{ Metrics.OutlierCount }}</span>\n </div>\n }\n </div>\n }\n\n <!-- Save link -->\n @if (ShowSaveButton) {\n <div class=\"save-link\" (click)=\"OnSave()\">\n <i class=\"fa-solid fa-bookmark\"></i> Save this visualization\n </div>\n }\n }\n</div>\n", styles: [".config-panel {\n position: absolute;\n width: 300px;\n background: color-mix(in srgb, var(--mj-bg-surface-elevated) 95%, transparent);\n backdrop-filter: blur(20px);\n -webkit-backdrop-filter: blur(20px);\n border: 1px solid var(--mj-border-default);\n border-radius: 14px;\n padding: 20px;\n box-shadow: 0 20px 60px color-mix(in srgb, var(--mj-bg-page) 40%, transparent);\n z-index: 10;\n}\n\n.panel-header {\n font-size: 0.8rem;\n font-weight: 600;\n margin: 0 0 16px 0;\n display: flex;\n align-items: center;\n gap: 8px;\n color: var(--mj-text-primary);\n cursor: move;\n user-select: none;\n}\n\n.panel-header i:first-child {\n color: var(--mj-brand-primary);\n}\n\n.header-actions {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.header-actions i {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n cursor: pointer;\n transition: color 0.15s;\n}\n\n.header-actions i:hover {\n color: var(--mj-text-primary);\n}\n\n.drag-handle {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n}\n\n/* Form elements */\n.form-group {\n margin-bottom: 14px;\n}\n\n.form-group label {\n display: block;\n font-size: 0.7rem;\n font-weight: 500;\n color: var(--mj-text-secondary);\n margin-bottom: 5px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.form-group select,\n.form-group input[type=\"text\"],\n.form-group input[type=\"number\"] {\n width: 100%;\n padding: 8px 12px;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: 7px;\n color: var(--mj-text-primary);\n font-size: 0.82rem;\n outline: none;\n transition: border 0.15s;\n}\n\n.form-group select:focus,\n.form-group input[type=\"text\"]:focus,\n.form-group input[type=\"number\"]:focus {\n border-color: var(--mj-brand-primary);\n}\n\n.form-group input[type=\"range\"] {\n width: 100%;\n accent-color: var(--mj-brand-primary);\n}\n\n.form-row {\n display: flex;\n gap: 10px;\n}\n\n.form-row .form-group {\n flex: 1;\n}\n\n/* Run button */\n.run-btn {\n width: 100%;\n padding: 10px;\n background: var(--mj-brand-primary);\n border: none;\n border-radius: 8px;\n color: var(--mj-text-inverse);\n font-size: 0.82rem;\n font-weight: 600;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n transition: background 0.15s;\n margin-top: 4px;\n}\n\n.run-btn:hover:not(:disabled) {\n background: var(--mj-brand-primary-hover);\n}\n\n.run-btn:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n/* Metrics */\n.metrics-section {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 8px;\n margin-top: 14px;\n padding-top: 14px;\n border-top: 1px solid var(--mj-border-default);\n}\n\n.metric-item {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.metric-label {\n font-size: 0.65rem;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--mj-text-muted);\n}\n\n.metric-value {\n font-size: 0.85rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.metric-value.good {\n color: var(--mj-status-success);\n}\n\n/* Save link */\n.save-link {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n margin-top: 10px;\n font-size: 0.75rem;\n color: var(--mj-text-muted);\n cursor: pointer;\n transition: color 0.15s;\n}\n\n.save-link:hover {\n color: var(--mj-brand-primary);\n}\n\n"] }]
|
|
695
|
+
}], null, { IsRunning: [{
|
|
696
|
+
type: Input
|
|
697
|
+
}], Metrics: [{
|
|
698
|
+
type: Input
|
|
699
|
+
}], EntityOptions: [{
|
|
700
|
+
type: Input
|
|
701
|
+
}], EntityDocOptions: [{
|
|
702
|
+
type: Input
|
|
703
|
+
}], ShowSaveButton: [{
|
|
704
|
+
type: Input
|
|
705
|
+
}], ShowAlgorithmPicker: [{
|
|
706
|
+
type: Input
|
|
707
|
+
}], ShowMetricPicker: [{
|
|
708
|
+
type: Input
|
|
709
|
+
}], DefaultAlgorithm: [{
|
|
710
|
+
type: Input
|
|
711
|
+
}], AvailableEntities: [{
|
|
712
|
+
type: Input
|
|
713
|
+
}], Collapsed: [{
|
|
714
|
+
type: Input
|
|
715
|
+
}], RunClustering: [{
|
|
716
|
+
type: Output
|
|
717
|
+
}], SaveVisualization: [{
|
|
718
|
+
type: Output
|
|
719
|
+
}], BeforeRunClustering: [{
|
|
720
|
+
type: Output
|
|
721
|
+
}], BeforeSave: [{
|
|
722
|
+
type: Output
|
|
723
|
+
}], ConfigChanged: [{
|
|
724
|
+
type: Output
|
|
725
|
+
}], AlgorithmChanged: [{
|
|
726
|
+
type: Output
|
|
727
|
+
}], panelRef: [{
|
|
728
|
+
type: ViewChild,
|
|
729
|
+
args: ['panelElement', { static: false }]
|
|
730
|
+
}] }); })();
|
|
731
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(ClusterConfigPanelComponent, { className: "ClusterConfigPanelComponent", filePath: "src/lib/cluster-config-panel.component.ts", lineNumber: 57 }); })();
|
|
732
|
+
//# sourceMappingURL=cluster-config-panel.component.js.map
|