@memberjunction/ng-clustering 5.23.0 → 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/dist/lib/cluster-scatter.component.d.ts +60 -2
- package/dist/lib/cluster-scatter.component.d.ts.map +1 -1
- package/dist/lib/cluster-scatter.component.js +184 -131
- package/dist/lib/cluster-scatter.component.js.map +1 -1
- package/dist/lib/clustering.module.d.ts +2 -1
- package/dist/lib/clustering.module.d.ts.map +1 -1
- package/dist/lib/clustering.module.js +6 -2
- package/dist/lib/clustering.module.js.map +1 -1
- package/package.json +3 -2
|
@@ -30,8 +30,9 @@
|
|
|
30
30
|
import { Component, Input, Output, EventEmitter, ElementRef, ChangeDetectorRef, ViewChild, ViewEncapsulation, inject, } from '@angular/core';
|
|
31
31
|
import { CLUSTER_COLORS, } from './clustering.types';
|
|
32
32
|
import * as i0 from "@angular/core";
|
|
33
|
+
import * as i1 from "@angular/forms";
|
|
34
|
+
import * as i2 from "@memberjunction/ng-entity-card";
|
|
33
35
|
const _c0 = ["svgElement"];
|
|
34
|
-
const _forTrack0 = ($index, $item) => $item.Key;
|
|
35
36
|
function ClusterScatterComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
36
37
|
i0.ɵɵelementStart(0, "div", 2)(1, "div", 8);
|
|
37
38
|
i0.ɵɵelement(2, "i", 9);
|
|
@@ -170,7 +171,7 @@ function ClusterScatterComponent_Conditional_3_Template(rf, ctx) { if (rf & 1) {
|
|
|
170
171
|
} if (rf & 2) {
|
|
171
172
|
const ctx_r1 = i0.ɵɵnextContext();
|
|
172
173
|
i0.ɵɵclassProp("no-transitions", !ctx_r1.AnimateTransitions);
|
|
173
|
-
i0.ɵɵattribute("viewBox", ctx_r1.
|
|
174
|
+
i0.ɵɵattribute("viewBox", ctx_r1.ViewBoxString);
|
|
174
175
|
i0.ɵɵadvance(3);
|
|
175
176
|
i0.ɵɵrepeater(ctx_r1.ClusterCentroids);
|
|
176
177
|
i0.ɵɵadvance(3);
|
|
@@ -188,24 +189,47 @@ function ClusterScatterComponent_Conditional_3_Template(rf, ctx) { if (rf & 1) {
|
|
|
188
189
|
i0.ɵɵadvance(2);
|
|
189
190
|
i0.ɵɵconditional(ctx_r1.HighlightedKey ? 18 : -1);
|
|
190
191
|
} }
|
|
192
|
+
function ClusterScatterComponent_Conditional_4_For_4_Conditional_2_Template(rf, ctx) { if (rf & 1) {
|
|
193
|
+
const _r14 = i0.ɵɵgetCurrentView();
|
|
194
|
+
i0.ɵɵelementStart(0, "input", 28);
|
|
195
|
+
i0.ɵɵtwoWayListener("ngModelChange", function ClusterScatterComponent_Conditional_4_For_4_Conditional_2_Template_input_ngModelChange_0_listener($event) { i0.ɵɵrestoreView(_r14); const ctx_r1 = i0.ɵɵnextContext(3); i0.ɵɵtwoWayBindingSet(ctx_r1.EditingLabelDraft, $event) || (ctx_r1.EditingLabelDraft = $event); return i0.ɵɵresetView($event); });
|
|
196
|
+
i0.ɵɵlistener("keydown", function ClusterScatterComponent_Conditional_4_For_4_Conditional_2_Template_input_keydown_0_listener($event) { i0.ɵɵrestoreView(_r14); const cluster_r13 = i0.ɵɵnextContext().$implicit; const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.OnLabelEditKeydown($event, cluster_r13)); })("blur", function ClusterScatterComponent_Conditional_4_For_4_Conditional_2_Template_input_blur_0_listener() { i0.ɵɵrestoreView(_r14); const cluster_r13 = i0.ɵɵnextContext().$implicit; const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.CommitLabelEdit(cluster_r13)); })("click", function ClusterScatterComponent_Conditional_4_For_4_Conditional_2_Template_input_click_0_listener($event) { i0.ɵɵrestoreView(_r14); return i0.ɵɵresetView($event.stopPropagation()); });
|
|
197
|
+
i0.ɵɵelementEnd();
|
|
198
|
+
} if (rf & 2) {
|
|
199
|
+
const ctx_r1 = i0.ɵɵnextContext(3);
|
|
200
|
+
i0.ɵɵtwoWayProperty("ngModel", ctx_r1.EditingLabelDraft);
|
|
201
|
+
} }
|
|
202
|
+
function ClusterScatterComponent_Conditional_4_For_4_Conditional_3_Template(rf, ctx) { if (rf & 1) {
|
|
203
|
+
const _r15 = i0.ɵɵgetCurrentView();
|
|
204
|
+
i0.ɵɵelementStart(0, "span", 29);
|
|
205
|
+
i0.ɵɵtext(1);
|
|
206
|
+
i0.ɵɵelementEnd();
|
|
207
|
+
i0.ɵɵelementStart(2, "button", 30);
|
|
208
|
+
i0.ɵɵlistener("click", function ClusterScatterComponent_Conditional_4_For_4_Conditional_3_Template_button_click_2_listener($event) { i0.ɵɵrestoreView(_r15); const cluster_r13 = i0.ɵɵnextContext().$implicit; const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.StartLabelEdit(cluster_r13, $event)); });
|
|
209
|
+
i0.ɵɵelement(3, "i", 31);
|
|
210
|
+
i0.ɵɵelementEnd();
|
|
211
|
+
} if (rf & 2) {
|
|
212
|
+
const cluster_r13 = i0.ɵɵnextContext().$implicit;
|
|
213
|
+
i0.ɵɵadvance();
|
|
214
|
+
i0.ɵɵtextInterpolate(cluster_r13.Label);
|
|
215
|
+
} }
|
|
191
216
|
function ClusterScatterComponent_Conditional_4_For_4_Template(rf, ctx) { if (rf & 1) {
|
|
192
217
|
const _r12 = i0.ɵɵgetCurrentView();
|
|
193
218
|
i0.ɵɵelementStart(0, "div", 24);
|
|
194
219
|
i0.ɵɵlistener("click", function ClusterScatterComponent_Conditional_4_For_4_Template_div_click_0_listener() { const cluster_r13 = i0.ɵɵrestoreView(_r12).$implicit; const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.OnClusterLegendClick(cluster_r13)); });
|
|
195
220
|
i0.ɵɵelement(1, "div", 25);
|
|
196
|
-
i0.ɵɵ
|
|
197
|
-
i0.ɵɵtext(3);
|
|
198
|
-
i0.ɵɵelementEnd();
|
|
221
|
+
i0.ɵɵconditionalCreate(2, ClusterScatterComponent_Conditional_4_For_4_Conditional_2_Template, 1, 1, "input", 26)(3, ClusterScatterComponent_Conditional_4_For_4_Conditional_3_Template, 4, 1);
|
|
199
222
|
i0.ɵɵelementStart(4, "span", 27);
|
|
200
223
|
i0.ɵɵtext(5);
|
|
201
224
|
i0.ɵɵelementEnd()();
|
|
202
225
|
} if (rf & 2) {
|
|
203
226
|
const cluster_r13 = ctx.$implicit;
|
|
227
|
+
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
204
228
|
i0.ɵɵadvance();
|
|
205
229
|
i0.ɵɵstyleProp("background", cluster_r13.Color);
|
|
206
|
-
i0.ɵɵadvance(
|
|
207
|
-
i0.ɵɵ
|
|
208
|
-
i0.ɵɵadvance(
|
|
230
|
+
i0.ɵɵadvance();
|
|
231
|
+
i0.ɵɵconditional(ctx_r1.EditingClusterId === cluster_r13.Id ? 2 : 3);
|
|
232
|
+
i0.ɵɵadvance(3);
|
|
209
233
|
i0.ɵɵtextInterpolate(cluster_r13.MemberCount);
|
|
210
234
|
} }
|
|
211
235
|
function ClusterScatterComponent_Conditional_4_Template(rf, ctx) { if (rf & 1) {
|
|
@@ -219,81 +243,27 @@ function ClusterScatterComponent_Conditional_4_Template(rf, ctx) { if (rf & 1) {
|
|
|
219
243
|
i0.ɵɵadvance(3);
|
|
220
244
|
i0.ɵɵrepeater(ctx_r1.Clusters);
|
|
221
245
|
} }
|
|
222
|
-
function ClusterScatterComponent_Conditional_5_Conditional_2_Template(rf, ctx) { if (rf & 1) {
|
|
223
|
-
i0.ɵɵelementStart(0, "div", 30);
|
|
224
|
-
i0.ɵɵelement(1, "i");
|
|
225
|
-
i0.ɵɵelementEnd();
|
|
226
|
-
} if (rf & 2) {
|
|
227
|
-
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
228
|
-
i0.ɵɵadvance();
|
|
229
|
-
i0.ɵɵclassMap(ctx_r1.HoveredPoint.Metadata["EntityIcon"]);
|
|
230
|
-
} }
|
|
231
|
-
function ClusterScatterComponent_Conditional_5_Conditional_6_Template(rf, ctx) { if (rf & 1) {
|
|
232
|
-
i0.ɵɵelementStart(0, "div", 33);
|
|
233
|
-
i0.ɵɵtext(1);
|
|
234
|
-
i0.ɵɵelementEnd();
|
|
235
|
-
} if (rf & 2) {
|
|
236
|
-
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
237
|
-
i0.ɵɵadvance();
|
|
238
|
-
i0.ɵɵtextInterpolate(ctx_r1.HoveredPoint.Metadata["Entity"]);
|
|
239
|
-
} }
|
|
240
|
-
function ClusterScatterComponent_Conditional_5_Conditional_10_For_2_Conditional_0_Template(rf, ctx) { if (rf & 1) {
|
|
241
|
-
i0.ɵɵelementStart(0, "div", 39);
|
|
242
|
-
i0.ɵɵtext(1);
|
|
243
|
-
i0.ɵɵelementEnd();
|
|
244
|
-
} if (rf & 2) {
|
|
245
|
-
const entry_r14 = i0.ɵɵnextContext().$implicit;
|
|
246
|
-
i0.ɵɵadvance();
|
|
247
|
-
i0.ɵɵtextInterpolate(entry_r14.Value);
|
|
248
|
-
} }
|
|
249
|
-
function ClusterScatterComponent_Conditional_5_Conditional_10_For_2_Template(rf, ctx) { if (rf & 1) {
|
|
250
|
-
i0.ɵɵconditionalCreate(0, ClusterScatterComponent_Conditional_5_Conditional_10_For_2_Conditional_0_Template, 2, 1, "div", 39);
|
|
251
|
-
} if (rf & 2) {
|
|
252
|
-
const entry_r14 = ctx.$implicit;
|
|
253
|
-
i0.ɵɵconditional(entry_r14.Key !== "Entity" && entry_r14.Key !== "EntityIcon" && entry_r14.Key !== "RecordID" && entry_r14.Key !== "Name" && entry_r14.Key !== "TemplateID" && entry_r14.Key !== "__mj_UpdatedAt" ? 0 : -1);
|
|
254
|
-
} }
|
|
255
|
-
function ClusterScatterComponent_Conditional_5_Conditional_10_Template(rf, ctx) { if (rf & 1) {
|
|
256
|
-
i0.ɵɵelementStart(0, "div", 36);
|
|
257
|
-
i0.ɵɵrepeaterCreate(1, ClusterScatterComponent_Conditional_5_Conditional_10_For_2_Template, 1, 1, null, null, _forTrack0);
|
|
258
|
-
i0.ɵɵelementEnd();
|
|
259
|
-
} if (rf & 2) {
|
|
260
|
-
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
261
|
-
i0.ɵɵadvance();
|
|
262
|
-
i0.ɵɵrepeater(ctx_r1.TooltipEntries);
|
|
263
|
-
} }
|
|
264
246
|
function ClusterScatterComponent_Conditional_5_Template(rf, ctx) { if (rf & 1) {
|
|
265
|
-
i0.ɵɵelementStart(0, "div",
|
|
266
|
-
i0.ɵɵ
|
|
267
|
-
i0.ɵɵelementStart(
|
|
268
|
-
i0.ɵɵ
|
|
269
|
-
i0.ɵɵ
|
|
270
|
-
i0.ɵɵconditionalCreate(6, ClusterScatterComponent_Conditional_5_Conditional_6_Template, 2, 1, "div", 33);
|
|
271
|
-
i0.ɵɵelementEnd()();
|
|
272
|
-
i0.ɵɵelementStart(7, "div", 34);
|
|
273
|
-
i0.ɵɵelement(8, "i", 35);
|
|
274
|
-
i0.ɵɵtext(9);
|
|
247
|
+
i0.ɵɵelementStart(0, "div", 32);
|
|
248
|
+
i0.ɵɵelement(1, "mj-entity-card", 33);
|
|
249
|
+
i0.ɵɵelementStart(2, "div", 34);
|
|
250
|
+
i0.ɵɵelement(3, "i", 35);
|
|
251
|
+
i0.ɵɵtext(4);
|
|
275
252
|
i0.ɵɵelementEnd();
|
|
276
|
-
i0.ɵɵ
|
|
277
|
-
i0.ɵɵ
|
|
278
|
-
i0.ɵɵ
|
|
279
|
-
i0.ɵɵtext(13, " Click to view details ");
|
|
253
|
+
i0.ɵɵelementStart(5, "div", 36);
|
|
254
|
+
i0.ɵɵelement(6, "i", 37);
|
|
255
|
+
i0.ɵɵtext(7, " Click to view details ");
|
|
280
256
|
i0.ɵɵelementEnd()();
|
|
281
257
|
} if (rf & 2) {
|
|
282
258
|
const ctx_r1 = i0.ɵɵnextContext();
|
|
283
259
|
i0.ɵɵstyleProp("left", ctx_r1.TooltipX, "px")("top", ctx_r1.TooltipY, "px");
|
|
284
|
-
i0.ɵɵadvance(2);
|
|
285
|
-
i0.ɵɵconditional((ctx_r1.HoveredPoint.Metadata == null ? null : ctx_r1.HoveredPoint.Metadata["EntityIcon"]) ? 2 : -1);
|
|
286
|
-
i0.ɵɵadvance(3);
|
|
287
|
-
i0.ɵɵtextInterpolate(ctx_r1.HoveredPoint.Label);
|
|
288
260
|
i0.ɵɵadvance();
|
|
289
|
-
i0.ɵɵ
|
|
261
|
+
i0.ɵɵproperty("EntityName", ctx_r1.EntityName)("Record", ctx_r1.HoveredPoint.Metadata);
|
|
290
262
|
i0.ɵɵadvance(3);
|
|
291
263
|
i0.ɵɵtextInterpolate1(" ", ctx_r1.GetClusterLabel(ctx_r1.HoveredPoint), " ");
|
|
292
|
-
i0.ɵɵadvance();
|
|
293
|
-
i0.ɵɵconditional(ctx_r1.TooltipEntries.length > 0 ? 10 : -1);
|
|
294
264
|
} }
|
|
295
265
|
function ClusterScatterComponent_Conditional_7_Conditional_2_Template(rf, ctx) { if (rf & 1) {
|
|
296
|
-
i0.ɵɵelementStart(0, "div",
|
|
266
|
+
i0.ɵɵelementStart(0, "div", 40);
|
|
297
267
|
i0.ɵɵelement(1, "i");
|
|
298
268
|
i0.ɵɵelementEnd();
|
|
299
269
|
} if (rf & 2) {
|
|
@@ -302,7 +272,7 @@ function ClusterScatterComponent_Conditional_7_Conditional_2_Template(rf, ctx) {
|
|
|
302
272
|
i0.ɵɵclassMap(ctx_r1.SelectedPoint.Metadata["EntityIcon"]);
|
|
303
273
|
} }
|
|
304
274
|
function ClusterScatterComponent_Conditional_7_Conditional_6_Template(rf, ctx) { if (rf & 1) {
|
|
305
|
-
i0.ɵɵelementStart(0, "span",
|
|
275
|
+
i0.ɵɵelementStart(0, "span", 43);
|
|
306
276
|
i0.ɵɵtext(1);
|
|
307
277
|
i0.ɵɵelementEnd();
|
|
308
278
|
} if (rf & 2) {
|
|
@@ -310,41 +280,18 @@ function ClusterScatterComponent_Conditional_7_Conditional_6_Template(rf, ctx) {
|
|
|
310
280
|
i0.ɵɵadvance();
|
|
311
281
|
i0.ɵɵtextInterpolate(ctx_r1.SelectedPoint.Metadata["Entity"]);
|
|
312
282
|
} }
|
|
313
|
-
function ClusterScatterComponent_Conditional_7_Conditional_12_For_2_Template(rf, ctx) { if (rf & 1) {
|
|
314
|
-
i0.ɵɵelementStart(0, "div", 58)(1, "div", 59);
|
|
315
|
-
i0.ɵɵtext(2);
|
|
316
|
-
i0.ɵɵelementEnd();
|
|
317
|
-
i0.ɵɵelementStart(3, "div", 60);
|
|
318
|
-
i0.ɵɵtext(4);
|
|
319
|
-
i0.ɵɵelementEnd()();
|
|
320
|
-
} if (rf & 2) {
|
|
321
|
-
const entry_r16 = ctx.$implicit;
|
|
322
|
-
i0.ɵɵadvance(2);
|
|
323
|
-
i0.ɵɵtextInterpolate(entry_r16.Key);
|
|
324
|
-
i0.ɵɵadvance(2);
|
|
325
|
-
i0.ɵɵtextInterpolate(entry_r16.Value);
|
|
326
|
-
} }
|
|
327
|
-
function ClusterScatterComponent_Conditional_7_Conditional_12_Template(rf, ctx) { if (rf & 1) {
|
|
328
|
-
i0.ɵɵelementStart(0, "div", 50);
|
|
329
|
-
i0.ɵɵrepeaterCreate(1, ClusterScatterComponent_Conditional_7_Conditional_12_For_2_Template, 5, 2, "div", 58, _forTrack0);
|
|
330
|
-
i0.ɵɵelementEnd();
|
|
331
|
-
} if (rf & 2) {
|
|
332
|
-
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
333
|
-
i0.ɵɵadvance();
|
|
334
|
-
i0.ɵɵrepeater(ctx_r1.DetailEntries);
|
|
335
|
-
} }
|
|
336
283
|
function ClusterScatterComponent_Conditional_7_Conditional_20_For_2_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
337
|
-
i0.ɵɵelement(0, "i",
|
|
284
|
+
i0.ɵɵelement(0, "i", 60);
|
|
338
285
|
} if (rf & 2) {
|
|
339
286
|
const member_r18 = i0.ɵɵnextContext().$implicit;
|
|
340
287
|
i0.ɵɵclassMap(member_r18.Metadata["EntityIcon"]);
|
|
341
288
|
} }
|
|
342
289
|
function ClusterScatterComponent_Conditional_7_Conditional_20_For_2_Template(rf, ctx) { if (rf & 1) {
|
|
343
290
|
const _r17 = i0.ɵɵgetCurrentView();
|
|
344
|
-
i0.ɵɵelementStart(0, "div",
|
|
291
|
+
i0.ɵɵelementStart(0, "div", 57);
|
|
345
292
|
i0.ɵɵlistener("click", function ClusterScatterComponent_Conditional_7_Conditional_20_For_2_Template_div_click_0_listener() { const member_r18 = i0.ɵɵrestoreView(_r17).$implicit; const ctx_r1 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r1.OnClusterMemberClick(member_r18)); });
|
|
346
|
-
i0.ɵɵconditionalCreate(1, ClusterScatterComponent_Conditional_7_Conditional_20_For_2_Conditional_1_Template, 1, 2, "i",
|
|
347
|
-
i0.ɵɵelementStart(2, "span",
|
|
293
|
+
i0.ɵɵconditionalCreate(1, ClusterScatterComponent_Conditional_7_Conditional_20_For_2_Conditional_1_Template, 1, 2, "i", 58);
|
|
294
|
+
i0.ɵɵelementStart(2, "span", 59);
|
|
348
295
|
i0.ɵɵtext(3);
|
|
349
296
|
i0.ɵɵelementEnd()();
|
|
350
297
|
} if (rf & 2) {
|
|
@@ -357,8 +304,8 @@ function ClusterScatterComponent_Conditional_7_Conditional_20_For_2_Template(rf,
|
|
|
357
304
|
i0.ɵɵtextInterpolate(member_r18.Label);
|
|
358
305
|
} }
|
|
359
306
|
function ClusterScatterComponent_Conditional_7_Conditional_20_Template(rf, ctx) { if (rf & 1) {
|
|
360
|
-
i0.ɵɵelementStart(0, "div",
|
|
361
|
-
i0.ɵɵrepeaterCreate(1, ClusterScatterComponent_Conditional_7_Conditional_20_For_2_Template, 4, 4, "div",
|
|
307
|
+
i0.ɵɵelementStart(0, "div", 53);
|
|
308
|
+
i0.ɵɵrepeaterCreate(1, ClusterScatterComponent_Conditional_7_Conditional_20_For_2_Template, 4, 4, "div", 56, i0.ɵɵcomponentInstance().TrackMemberBy, true);
|
|
362
309
|
i0.ɵɵelementEnd();
|
|
363
310
|
} if (rf & 2) {
|
|
364
311
|
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
@@ -366,37 +313,37 @@ function ClusterScatterComponent_Conditional_7_Conditional_20_Template(rf, ctx)
|
|
|
366
313
|
i0.ɵɵrepeater(ctx_r1.ClusterMembers);
|
|
367
314
|
} }
|
|
368
315
|
function ClusterScatterComponent_Conditional_7_Template(rf, ctx) { if (rf & 1) {
|
|
369
|
-
const
|
|
370
|
-
i0.ɵɵelementStart(0, "div",
|
|
371
|
-
i0.ɵɵconditionalCreate(2, ClusterScatterComponent_Conditional_7_Conditional_2_Template, 2, 2, "div",
|
|
372
|
-
i0.ɵɵelementStart(3, "div",
|
|
316
|
+
const _r16 = i0.ɵɵgetCurrentView();
|
|
317
|
+
i0.ɵɵelementStart(0, "div", 38)(1, "div", 39);
|
|
318
|
+
i0.ɵɵconditionalCreate(2, ClusterScatterComponent_Conditional_7_Conditional_2_Template, 2, 2, "div", 40);
|
|
319
|
+
i0.ɵɵelementStart(3, "div", 41)(4, "div", 42);
|
|
373
320
|
i0.ɵɵtext(5);
|
|
374
321
|
i0.ɵɵelementEnd();
|
|
375
|
-
i0.ɵɵconditionalCreate(6, ClusterScatterComponent_Conditional_7_Conditional_6_Template, 2, 1, "span",
|
|
322
|
+
i0.ɵɵconditionalCreate(6, ClusterScatterComponent_Conditional_7_Conditional_6_Template, 2, 1, "span", 43);
|
|
376
323
|
i0.ɵɵelementEnd()();
|
|
377
|
-
i0.ɵɵelementStart(7, "button",
|
|
378
|
-
i0.ɵɵlistener("click", function ClusterScatterComponent_Conditional_7_Template_button_click_7_listener() { i0.ɵɵrestoreView(
|
|
379
|
-
i0.ɵɵelement(8, "i",
|
|
324
|
+
i0.ɵɵelementStart(7, "button", 44);
|
|
325
|
+
i0.ɵɵlistener("click", function ClusterScatterComponent_Conditional_7_Template_button_click_7_listener() { i0.ɵɵrestoreView(_r16); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.CloseDetailPanel()); });
|
|
326
|
+
i0.ɵɵelement(8, "i", 45);
|
|
380
327
|
i0.ɵɵelementEnd()();
|
|
381
|
-
i0.ɵɵelementStart(9, "div",
|
|
382
|
-
i0.ɵɵelement(10, "span",
|
|
328
|
+
i0.ɵɵelementStart(9, "div", 46);
|
|
329
|
+
i0.ɵɵelement(10, "span", 47);
|
|
383
330
|
i0.ɵɵtext(11);
|
|
384
331
|
i0.ɵɵelementEnd();
|
|
385
|
-
i0.ɵɵ
|
|
386
|
-
i0.ɵɵelementStart(13, "div",
|
|
387
|
-
i0.ɵɵlistener("click", function ClusterScatterComponent_Conditional_7_Template_button_click_14_listener() { i0.ɵɵrestoreView(
|
|
388
|
-
i0.ɵɵelement(15, "i",
|
|
332
|
+
i0.ɵɵelement(12, "mj-entity-card", 48);
|
|
333
|
+
i0.ɵɵelementStart(13, "div", 49)(14, "button", 50);
|
|
334
|
+
i0.ɵɵlistener("click", function ClusterScatterComponent_Conditional_7_Template_button_click_14_listener() { i0.ɵɵrestoreView(_r16); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.ToggleClusterMembers()); });
|
|
335
|
+
i0.ɵɵelement(15, "i", 51);
|
|
389
336
|
i0.ɵɵelementStart(16, "span");
|
|
390
337
|
i0.ɵɵtext(17, "Cluster Members");
|
|
391
338
|
i0.ɵɵelementEnd();
|
|
392
|
-
i0.ɵɵelementStart(18, "span",
|
|
339
|
+
i0.ɵɵelementStart(18, "span", 52);
|
|
393
340
|
i0.ɵɵtext(19);
|
|
394
341
|
i0.ɵɵelementEnd()();
|
|
395
|
-
i0.ɵɵconditionalCreate(20, ClusterScatterComponent_Conditional_7_Conditional_20_Template, 3, 0, "div",
|
|
342
|
+
i0.ɵɵconditionalCreate(20, ClusterScatterComponent_Conditional_7_Conditional_20_Template, 3, 0, "div", 53);
|
|
396
343
|
i0.ɵɵelementEnd();
|
|
397
|
-
i0.ɵɵelementStart(21, "div",
|
|
398
|
-
i0.ɵɵlistener("click", function ClusterScatterComponent_Conditional_7_Template_button_click_22_listener() { i0.ɵɵrestoreView(
|
|
399
|
-
i0.ɵɵelement(23, "i",
|
|
344
|
+
i0.ɵɵelementStart(21, "div", 54)(22, "button", 55);
|
|
345
|
+
i0.ɵɵlistener("click", function ClusterScatterComponent_Conditional_7_Template_button_click_22_listener() { i0.ɵɵrestoreView(_r16); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnOpenRecordClick()); });
|
|
346
|
+
i0.ɵɵelement(23, "i", 37);
|
|
400
347
|
i0.ɵɵtext(24, " Open Record ");
|
|
401
348
|
i0.ɵɵelementEnd()();
|
|
402
349
|
} if (rf & 2) {
|
|
@@ -412,7 +359,7 @@ function ClusterScatterComponent_Conditional_7_Template(rf, ctx) { if (rf & 1) {
|
|
|
412
359
|
i0.ɵɵadvance();
|
|
413
360
|
i0.ɵɵtextInterpolate1(" ", ctx_r1.GetClusterLabel(ctx_r1.SelectedPoint), " ");
|
|
414
361
|
i0.ɵɵadvance();
|
|
415
|
-
i0.ɵɵ
|
|
362
|
+
i0.ɵɵproperty("EntityName", ctx_r1.EntityName)("Record", ctx_r1.SelectedPoint.Metadata);
|
|
416
363
|
i0.ɵɵadvance(3);
|
|
417
364
|
i0.ɵɵclassProp("fa-chevron-down", ctx_r1.ClusterMembersExpanded)("fa-chevron-right", !ctx_r1.ClusterMembersExpanded);
|
|
418
365
|
i0.ɵɵadvance(4);
|
|
@@ -494,6 +441,32 @@ export class ClusterScatterComponent {
|
|
|
494
441
|
* ```
|
|
495
442
|
*/
|
|
496
443
|
this.TooltipFields = [];
|
|
444
|
+
/**
|
|
445
|
+
* Ordered list of metadata field keys for prioritized display in tooltips
|
|
446
|
+
* and the detail panel. When set, fields are displayed in this order instead
|
|
447
|
+
* of raw metadata key order. Fields not in this list appear after the
|
|
448
|
+
* prioritized fields.
|
|
449
|
+
*
|
|
450
|
+
* Typically computed from entity metadata (DefaultInView, Sequence) by the
|
|
451
|
+
* consuming component and passed in.
|
|
452
|
+
*
|
|
453
|
+
* @default [] (no prioritization — original key order)
|
|
454
|
+
*/
|
|
455
|
+
this.FieldPriority = [];
|
|
456
|
+
/**
|
|
457
|
+
* Map of field names to human-readable display names.
|
|
458
|
+
* Used to show "First Name" instead of "FirstName" in tooltips and detail panels.
|
|
459
|
+
* If a field is not in this map, the raw key is shown.
|
|
460
|
+
*
|
|
461
|
+
* @default {} (use raw key names)
|
|
462
|
+
*/
|
|
463
|
+
this.FieldDisplayNames = {};
|
|
464
|
+
/**
|
|
465
|
+
* The entity name for metadata-driven field display in tooltips and
|
|
466
|
+
* detail panels. When set, the mj-entity-card component uses EntityInfo
|
|
467
|
+
* to prioritize fields by IsNameField and DefaultInView.
|
|
468
|
+
*/
|
|
469
|
+
this.EntityName = null;
|
|
497
470
|
/**
|
|
498
471
|
* Override the default cluster color palette.
|
|
499
472
|
*
|
|
@@ -626,11 +599,25 @@ export class ClusterScatterComponent {
|
|
|
626
599
|
* Fires after every zoom or pan that changes the visible viewport.
|
|
627
600
|
*/
|
|
628
601
|
this.ViewportChanged = new EventEmitter();
|
|
602
|
+
/**
|
|
603
|
+
* Fires when the user edits a cluster label inline.
|
|
604
|
+
* Payload: `{ ClusterId, OldLabel, NewLabel }`.
|
|
605
|
+
*/
|
|
606
|
+
this.LabelEdited = new EventEmitter();
|
|
607
|
+
// ================================================================
|
|
608
|
+
// Label Editing State
|
|
609
|
+
// ================================================================
|
|
610
|
+
/** The cluster ID currently being edited, or null if none. */
|
|
611
|
+
this.EditingClusterId = null;
|
|
612
|
+
/** The draft label text during editing. */
|
|
613
|
+
this.EditingLabelDraft = '';
|
|
629
614
|
// ================================================================
|
|
630
615
|
// Internal State
|
|
631
616
|
// ================================================================
|
|
632
617
|
/** SVG viewBox parameters: [minX, minY, width, height]. */
|
|
633
|
-
this.
|
|
618
|
+
this._viewBox = [0, 0, 1000, 700];
|
|
619
|
+
/** Cached viewBox string for template binding (avoids NG0100 on recalc). */
|
|
620
|
+
this.ViewBoxString = '0 0 1000 700';
|
|
634
621
|
/** Currently hovered point (null when idle). */
|
|
635
622
|
this.HoveredPoint = null;
|
|
636
623
|
/** Screen-space tooltip X position. */
|
|
@@ -671,6 +658,45 @@ export class ClusterScatterComponent {
|
|
|
671
658
|
this.defaultWidth = 1000;
|
|
672
659
|
this.defaultHeight = 700;
|
|
673
660
|
}
|
|
661
|
+
/** Start inline editing for a cluster label. */
|
|
662
|
+
StartLabelEdit(cluster, event) {
|
|
663
|
+
event.stopPropagation();
|
|
664
|
+
this.EditingClusterId = cluster.Id;
|
|
665
|
+
this.EditingLabelDraft = cluster.Label;
|
|
666
|
+
this.cdr.detectChanges();
|
|
667
|
+
}
|
|
668
|
+
/** Commit the edited label. */
|
|
669
|
+
CommitLabelEdit(cluster) {
|
|
670
|
+
const oldLabel = cluster.Label;
|
|
671
|
+
const newLabel = this.EditingLabelDraft.trim();
|
|
672
|
+
if (newLabel.length > 0 && newLabel !== oldLabel) {
|
|
673
|
+
cluster.Label = newLabel;
|
|
674
|
+
this.LabelEdited.emit({ ClusterId: cluster.Id, OldLabel: oldLabel, NewLabel: newLabel });
|
|
675
|
+
}
|
|
676
|
+
this.EditingClusterId = null;
|
|
677
|
+
this.cdr.detectChanges();
|
|
678
|
+
}
|
|
679
|
+
/** Cancel editing. */
|
|
680
|
+
CancelLabelEdit() {
|
|
681
|
+
this.EditingClusterId = null;
|
|
682
|
+
this.cdr.detectChanges();
|
|
683
|
+
}
|
|
684
|
+
/** Handle keydown in the edit input. */
|
|
685
|
+
OnLabelEditKeydown(event, cluster) {
|
|
686
|
+
if (event.key === 'Enter') {
|
|
687
|
+
this.CommitLabelEdit(cluster);
|
|
688
|
+
}
|
|
689
|
+
else if (event.key === 'Escape') {
|
|
690
|
+
this.CancelLabelEdit();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
get ViewBox() {
|
|
694
|
+
return this._viewBox;
|
|
695
|
+
}
|
|
696
|
+
set ViewBox(value) {
|
|
697
|
+
this._viewBox = value;
|
|
698
|
+
this.ViewBoxString = value.join(' ');
|
|
699
|
+
}
|
|
674
700
|
/** Get the current viewport transform for saving/restoring. */
|
|
675
701
|
GetViewportTransform() {
|
|
676
702
|
return {
|
|
@@ -1157,13 +1183,15 @@ export class ClusterScatterComponent {
|
|
|
1157
1183
|
}
|
|
1158
1184
|
/**
|
|
1159
1185
|
* Compute the metadata entries for the detail panel, excluding internal fields.
|
|
1186
|
+
* Uses FieldPriority for ordering when available.
|
|
1160
1187
|
*/
|
|
1161
1188
|
computeDetailEntries(point) {
|
|
1162
1189
|
const hidden = this.internalMetadataKeys;
|
|
1163
|
-
|
|
1190
|
+
const raw = Object.entries(point.Metadata)
|
|
1164
1191
|
.filter(([key]) => !hidden.has(key))
|
|
1165
1192
|
.filter(([, value]) => value != null && String(value).trim() !== '')
|
|
1166
1193
|
.map(([Key, Value]) => ({ Key, Value }));
|
|
1194
|
+
this.DetailEntries = this.sortEntriesByPriority(raw);
|
|
1167
1195
|
}
|
|
1168
1196
|
/**
|
|
1169
1197
|
* Compute the list of cluster members for the selected point's cluster.
|
|
@@ -1249,17 +1277,34 @@ export class ClusterScatterComponent {
|
|
|
1249
1277
|
}
|
|
1250
1278
|
/**
|
|
1251
1279
|
* Compute the tooltip entries based on TooltipFields filter.
|
|
1280
|
+
* Uses FieldPriority for ordering when available.
|
|
1252
1281
|
*/
|
|
1253
1282
|
computeTooltipEntries(point) {
|
|
1254
1283
|
const entries = Object.entries(point.Metadata);
|
|
1284
|
+
let mapped;
|
|
1255
1285
|
if (this.TooltipFields.length > 0) {
|
|
1256
|
-
|
|
1286
|
+
mapped = entries
|
|
1257
1287
|
.filter(([key]) => this.TooltipFields.includes(key))
|
|
1258
1288
|
.map(([Key, Value]) => ({ Key, Value }));
|
|
1259
1289
|
}
|
|
1260
1290
|
else {
|
|
1261
|
-
|
|
1291
|
+
mapped = entries.map(([Key, Value]) => ({ Key, Value }));
|
|
1262
1292
|
}
|
|
1293
|
+
this.TooltipEntries = this.sortEntriesByPriority(mapped);
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Sort entries using the FieldPriority ordering.
|
|
1297
|
+
* Fields in FieldPriority come first (in priority order), then remaining fields.
|
|
1298
|
+
*/
|
|
1299
|
+
sortEntriesByPriority(entries) {
|
|
1300
|
+
if (this.FieldPriority.length === 0)
|
|
1301
|
+
return entries;
|
|
1302
|
+
const priorityIndex = new Map(this.FieldPriority.map((key, idx) => [key, idx]));
|
|
1303
|
+
return [...entries].sort((a, b) => {
|
|
1304
|
+
const aIdx = priorityIndex.get(a.Key) ?? 99999;
|
|
1305
|
+
const bIdx = priorityIndex.get(b.Key) ?? 99999;
|
|
1306
|
+
return aIdx - bIdx;
|
|
1307
|
+
});
|
|
1263
1308
|
}
|
|
1264
1309
|
static { this.ɵfac = function ClusterScatterComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || ClusterScatterComponent)(); }; }
|
|
1265
1310
|
static { this.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: ClusterScatterComponent, selectors: [["mj-cluster-scatter"]], viewQuery: function ClusterScatterComponent_Query(rf, ctx) { if (rf & 1) {
|
|
@@ -1267,15 +1312,15 @@ export class ClusterScatterComponent {
|
|
|
1267
1312
|
} if (rf & 2) {
|
|
1268
1313
|
let _t;
|
|
1269
1314
|
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.svgRef = _t.first);
|
|
1270
|
-
} }, inputs: { Points: "Points", Clusters: "Clusters", IsLoading: "IsLoading", DotRadius: "DotRadius", DotOpacity: "DotOpacity", HighlightRadius: "HighlightRadius", ShowLegend: "ShowLegend", ShowTooltip: "ShowTooltip", TooltipFields: "TooltipFields", ColorPalette: "ColorPalette", EnableZoom: "EnableZoom", EnablePan: "EnablePan", AnimateTransitions: "AnimateTransitions", SelectedPointIds: "SelectedPointIds", MinZoom: "MinZoom", MaxZoom: "MaxZoom" }, outputs: { PointClicked: "PointClicked", OpenRecordRequested: "OpenRecordRequested", PointHovered: "PointHovered", BeforePointClick: "BeforePointClick", BeforePointHover: "BeforePointHover", BeforeZoom: "BeforeZoom", AfterClusteringComplete: "AfterClusteringComplete", ClusterSelected: "ClusterSelected", SelectionChanged: "SelectionChanged", ViewportChanged: "ViewportChanged" }, standalone: false, features: [i0.ɵɵNgOnChangesFeature], decls: 8, vars: 8, consts: [["svgElement", ""], [1, "scatter-container"], [1, "loading-overlay"], [1, "empty-state"], ["preserveAspectRatio", "xMidYMid meet", 1, "scatter-svg", 3, "no-transitions"], [1, "legend"], [1, "tooltip", 3, "left", "top"], [1, "detail-panel"], [1, "loading-spinner"], [1, "fa-solid", "fa-circle-nodes", "fa-spin"], [1, "fa-solid", "fa-chart-scatter"], [1, "empty-hint"], ["preserveAspectRatio", "xMidYMid meet", 1, "scatter-svg", 3, "wheel", "mousedown"], ["cx", "50%", "cy", "50%", "r", "50%"], [1, "grid-lines"], [1, "data-point"], ["offset", "0%", "stop-opacity", "0.12"], ["offset", "100%", "stop-opacity", "0"], ["text-anchor", "middle", 1, "cluster-label"], [1, "data-point", 3, "mouseenter", "mouseleave", "click"], ["fill", "none", "stroke-width", "2", "opacity", "0.8", 1, "selection-ring"], ["fill", "none", "stroke-width", "2.5", "opacity", "0.9"], [1, "legend-title"], [1, "legend-item"], [1, "legend-item", 3, "click"], [1, "legend-dot"], [1, "legend-label"], [1, "legend-count"], [1, "
|
|
1315
|
+
} }, inputs: { Points: "Points", Clusters: "Clusters", IsLoading: "IsLoading", DotRadius: "DotRadius", DotOpacity: "DotOpacity", HighlightRadius: "HighlightRadius", ShowLegend: "ShowLegend", ShowTooltip: "ShowTooltip", TooltipFields: "TooltipFields", FieldPriority: "FieldPriority", FieldDisplayNames: "FieldDisplayNames", EntityName: "EntityName", ColorPalette: "ColorPalette", EnableZoom: "EnableZoom", EnablePan: "EnablePan", AnimateTransitions: "AnimateTransitions", SelectedPointIds: "SelectedPointIds", MinZoom: "MinZoom", MaxZoom: "MaxZoom" }, outputs: { PointClicked: "PointClicked", OpenRecordRequested: "OpenRecordRequested", PointHovered: "PointHovered", BeforePointClick: "BeforePointClick", BeforePointHover: "BeforePointHover", BeforeZoom: "BeforeZoom", AfterClusteringComplete: "AfterClusteringComplete", ClusterSelected: "ClusterSelected", SelectionChanged: "SelectionChanged", ViewportChanged: "ViewportChanged", LabelEdited: "LabelEdited" }, standalone: false, features: [i0.ɵɵNgOnChangesFeature], decls: 8, vars: 8, consts: [["svgElement", ""], [1, "scatter-container"], [1, "loading-overlay"], [1, "empty-state"], ["preserveAspectRatio", "xMidYMid meet", 1, "scatter-svg", 3, "no-transitions"], [1, "legend"], [1, "tooltip", 3, "left", "top"], [1, "detail-panel"], [1, "loading-spinner"], [1, "fa-solid", "fa-circle-nodes", "fa-spin"], [1, "fa-solid", "fa-chart-scatter"], [1, "empty-hint"], ["preserveAspectRatio", "xMidYMid meet", 1, "scatter-svg", 3, "wheel", "mousedown"], ["cx", "50%", "cy", "50%", "r", "50%"], [1, "grid-lines"], [1, "data-point"], ["offset", "0%", "stop-opacity", "0.12"], ["offset", "100%", "stop-opacity", "0"], ["text-anchor", "middle", 1, "cluster-label"], [1, "data-point", 3, "mouseenter", "mouseleave", "click"], ["fill", "none", "stroke-width", "2", "opacity", "0.8", 1, "selection-ring"], ["fill", "none", "stroke-width", "2.5", "opacity", "0.9"], [1, "legend-title"], [1, "legend-item"], [1, "legend-item", 3, "click"], [1, "legend-dot"], ["autofocus", "", 1, "legend-label-input", 3, "ngModel"], [1, "legend-count"], ["autofocus", "", 1, "legend-label-input", 3, "ngModelChange", "keydown", "blur", "click", "ngModel"], [1, "legend-label"], ["title", "Edit label", 1, "legend-edit-btn", 3, "click"], [1, "fa-solid", "fa-pencil"], [1, "tooltip"], ["Variant", "compact", 3, "EntityName", "Record"], [1, "tt-cluster"], [1, "fa-solid", "fa-circle", 2, "font-size", "7px", "vertical-align", "middle"], [1, "tt-action"], [1, "fa-solid", "fa-arrow-up-right-from-square"], [1, "detail-header"], [1, "detail-header-content"], [1, "detail-icon"], [1, "detail-header-text"], [1, "detail-name"], [1, "detail-entity-badge"], ["title", "Close", 1, "detail-close", 3, "click"], [1, "fa-solid", "fa-times"], [1, "detail-cluster"], [1, "detail-cluster-dot"], ["Variant", "detail", "CssClass", "detail-entity-card", 3, "EntityName", "Record"], [1, "detail-members-section"], [1, "detail-members-toggle", 3, "click"], [1, "fa-solid"], [1, "detail-members-count"], [1, "detail-members-list"], [1, "detail-footer"], [1, "detail-open-btn", 3, "click"], [1, "detail-member-row", 3, "active"], [1, "detail-member-row", 3, "click"], [1, "member-icon", 3, "class"], [1, "member-label"], [1, "member-icon"]], template: function ClusterScatterComponent_Template(rf, ctx) { if (rf & 1) {
|
|
1271
1316
|
i0.ɵɵelementStart(0, "div", 1);
|
|
1272
1317
|
i0.ɵɵconditionalCreate(1, ClusterScatterComponent_Conditional_1_Template, 5, 0, "div", 2);
|
|
1273
1318
|
i0.ɵɵconditionalCreate(2, ClusterScatterComponent_Conditional_2_Template, 6, 0, "div", 3);
|
|
1274
1319
|
i0.ɵɵconditionalCreate(3, ClusterScatterComponent_Conditional_3_Template, 19, 4, ":svg:svg", 4);
|
|
1275
1320
|
i0.ɵɵconditionalCreate(4, ClusterScatterComponent_Conditional_4_Template, 5, 0, "div", 5);
|
|
1276
|
-
i0.ɵɵconditionalCreate(5, ClusterScatterComponent_Conditional_5_Template,
|
|
1321
|
+
i0.ɵɵconditionalCreate(5, ClusterScatterComponent_Conditional_5_Template, 8, 7, "div", 6);
|
|
1277
1322
|
i0.ɵɵelementStart(6, "div", 7);
|
|
1278
|
-
i0.ɵɵconditionalCreate(7, ClusterScatterComponent_Conditional_7_Template, 25,
|
|
1323
|
+
i0.ɵɵconditionalCreate(7, ClusterScatterComponent_Conditional_7_Template, 25, 14);
|
|
1279
1324
|
i0.ɵɵelementEnd()();
|
|
1280
1325
|
} if (rf & 2) {
|
|
1281
1326
|
i0.ɵɵadvance();
|
|
@@ -1292,11 +1337,11 @@ export class ClusterScatterComponent {
|
|
|
1292
1337
|
i0.ɵɵclassProp("detail-panel-open", ctx.ShowDetailPanel && ctx.SelectedPoint);
|
|
1293
1338
|
i0.ɵɵadvance();
|
|
1294
1339
|
i0.ɵɵconditional(ctx.SelectedPoint ? 7 : -1);
|
|
1295
|
-
} }, styles: [":host {\n display: block;\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n.scatter-container {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n background: var(--mj-bg-page);\n}\n\n/* SVG */\n.scatter-svg {\n width: 100%;\n height: 100%;\n cursor: grab;\n user-select: none;\n}\n\n.scatter-svg:active {\n cursor: grabbing;\n}\n\n/* Grid lines */\n.grid-lines line {\n stroke: var(--mj-border-subtle);\n stroke-width: 0.5;\n}\n\n/* Data points */\n.data-point {\n cursor: pointer;\n transition: opacity 0.15s ease, r 0.15s ease;\n}\n\n.data-point:hover {\n filter: brightness(1.2);\n}\n\n/* Selection rings */\n.selection-ring {\n pointer-events: none;\n transition: r 0.15s ease;\n}\n\n/* Disable transitions when AnimateTransitions is false */\n.no-transitions .data-point,\n.no-transitions .selection-ring {\n transition: none;\n}\n\n/* Loading overlay */\n.loading-overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: color-mix(in srgb, var(--mj-bg-page) 80%, transparent);\n z-index: 5;\n}\n\n.loading-spinner {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n color: var(--mj-text-secondary);\n font-size: 0.9rem;\n}\n\n.loading-spinner i {\n font-size: 2rem;\n color: var(--mj-brand-primary);\n}\n\n/* Empty state */\n.empty-state {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n color: var(--mj-text-muted);\n gap: 8px;\n}\n\n.empty-state i {\n font-size: 3rem;\n margin-bottom: 8px;\n opacity: 0.4;\n}\n\n.empty-state p {\n font-size: 1rem;\n margin: 0;\n}\n\n.empty-hint {\n font-size: 0.8rem !important;\n opacity: 0.7;\n}\n\n/* Legend */\n.legend {\n position: absolute;\n bottom: 20px;\n left: 20px;\n background: color-mix(in srgb, var(--mj-bg-surface) 92%, transparent);\n backdrop-filter: blur(12px);\n border: 1px solid var(--mj-border-default);\n border-radius: 10px;\n padding: 14px 18px;\n z-index: 3;\n}\n\n.legend-title {\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n margin-bottom: 8px;\n}\n\n.legend-item {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 0.78rem;\n margin-bottom: 4px;\n color: var(--mj-text-primary);\n cursor: pointer;\n border-radius: 4px;\n padding: 2px 4px;\n margin-left: -4px;\n margin-right: -4px;\n transition: background 0.15s ease;\n}\n\n.legend-item:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.legend-dot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n flex-shrink: 0;\n}\n\n.legend-label {\n flex: 1;\n}\n\n.legend-count {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n margin-left: auto;\n padding-left: 16px;\n}\n\n/* Cluster labels on chart */\n.cluster-label {\n font-size: 14px;\n font-weight: 600;\n opacity: 0.85;\n pointer-events: none;\n text-shadow:\n 0 0 6px var(--mj-bg-page),\n 0 0 12px var(--mj-bg-page),\n 1px 1px 2px var(--mj-bg-page);\n}\n\n/* Tooltip */\n.tooltip {\n position: fixed;\n background: color-mix(in srgb, var(--mj-bg-surface-elevated) 96%, transparent);\n backdrop-filter: blur(16px);\n border: 1px solid var(--mj-border-strong);\n border-radius: 10px;\n padding: 14px 16px;\n width: 280px;\n box-shadow: 0 12px 40px color-mix(in srgb, var(--mj-bg-page) 40%, transparent);\n pointer-events: none;\n z-index: 50;\n}\n\n.tt-header {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n margin-bottom: 6px;\n}\n\n.tt-icon {\n width: 2rem;\n height: 2rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 6px;\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n font-size: 0.85rem;\n flex-shrink: 0;\n}\n\n.tt-header-text {\n min-width: 0;\n flex: 1;\n}\n\n.tt-name {\n font-size: 0.85rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.tt-entity {\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n margin-top: 1px;\n}\n\n.tt-cluster {\n font-size: 0.72rem;\n color: var(--mj-brand-primary);\n margin-bottom: 6px;\n}\n\n.tt-fields {\n font-size: 0.72rem;\n color: var(--mj-text-secondary);\n line-height: 1.6;\n}\n\n.tt-field-row {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.tt-action {\n margin-top: 8px;\n padding-top: 8px;\n border-top: 1px solid var(--mj-border-default);\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n}\n\n.tt-action i {\n margin-right: 4px;\n}\n\n/* ================================================================\n Detail Panel (right slide-in)\n ================================================================ */\n\n.detail-panel {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: 320px;\n background: color-mix(in srgb, var(--mj-bg-surface) 92%, transparent);\n backdrop-filter: blur(16px);\n border-left: 1px solid var(--mj-border-default);\n z-index: 10;\n display: flex;\n flex-direction: column;\n transform: translateX(100%);\n transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);\n box-shadow: -4px 0 24px color-mix(in srgb, var(--mj-bg-page) 30%, transparent);\n overflow: hidden;\n}\n\n.detail-panel-open {\n transform: translateX(0);\n}\n\n/* Header */\n.detail-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n padding: 16px 16px 12px;\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.detail-header-content {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n min-width: 0;\n flex: 1;\n}\n\n.detail-icon {\n width: 2.25rem;\n height: 2.25rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n font-size: 0.95rem;\n flex-shrink: 0;\n}\n\n.detail-header-text {\n min-width: 0;\n flex: 1;\n}\n\n.detail-name {\n font-size: 0.95rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n line-height: 1.3;\n word-wrap: break-word;\n}\n\n.detail-entity-badge {\n display: inline-block;\n margin-top: 4px;\n padding: 1px 8px;\n border-radius: 10px;\n font-size: 0.68rem;\n font-weight: 500;\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n}\n\n.detail-close {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--mj-text-muted);\n font-size: 0.85rem;\n padding: 4px 6px;\n border-radius: 4px;\n transition: color 0.15s ease, background 0.15s ease;\n flex-shrink: 0;\n}\n\n.detail-close:hover {\n color: var(--mj-text-primary);\n background: var(--mj-bg-surface-hover);\n}\n\n/* Cluster badge */\n.detail-cluster {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 16px;\n font-size: 0.78rem;\n font-weight: 500;\n color: var(--mj-text-secondary);\n}\n\n.detail-cluster-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n flex-shrink: 0;\n}\n\n/* Metadata fields */\n.detail-fields {\n padding: 4px 16px 8px;\n overflow-y: auto;\n flex-shrink: 0;\n}\n\n.detail-field {\n padding: 6px 0;\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.detail-field:last-child {\n border-bottom: none;\n}\n\n.detail-field-key {\n font-size: 0.68rem;\n font-weight: 500;\n color: var(--mj-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.3px;\n margin-bottom: 2px;\n}\n\n.detail-field-value {\n font-size: 0.8rem;\n color: var(--mj-text-primary);\n line-height: 1.4;\n word-wrap: break-word;\n}\n\n/* Cluster Members Section */\n.detail-members-section {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n border-top: 1px solid var(--mj-border-default);\n}\n\n.detail-members-toggle {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 10px 16px;\n background: none;\n border: none;\n cursor: pointer;\n font-size: 0.78rem;\n font-weight: 600;\n color: var(--mj-text-secondary);\n text-align: left;\n transition: background 0.15s ease;\n}\n\n.detail-members-toggle:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.detail-members-toggle i {\n font-size: 0.65rem;\n width: 12px;\n text-align: center;\n color: var(--mj-text-muted);\n}\n\n.detail-members-count {\n margin-left: auto;\n font-size: 0.7rem;\n font-weight: 400;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-sunken);\n padding: 1px 7px;\n border-radius: 8px;\n}\n\n.detail-members-list {\n flex: 1;\n overflow-y: auto;\n padding: 0 8px 8px;\n}\n\n.detail-member-row {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 8px;\n border-radius: 6px;\n cursor: pointer;\n font-size: 0.78rem;\n color: var(--mj-text-primary);\n transition: background 0.12s ease;\n}\n\n.detail-member-row:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.detail-member-row.active {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n font-weight: 500;\n}\n\n.detail-member-row .member-icon {\n font-size: 0.72rem;\n color: var(--mj-text-muted);\n width: 16px;\n text-align: center;\n flex-shrink: 0;\n}\n\n.detail-member-row.active .member-icon {\n color: var(--mj-brand-primary);\n}\n\n.member-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Footer */\n.detail-footer {\n padding: 12px 16px;\n border-top: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.detail-open-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n width: 100%;\n padding: 8px 16px;\n border: none;\n border-radius: 8px;\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n font-size: 0.82rem;\n font-weight: 500;\n cursor: pointer;\n transition: background 0.15s ease;\n}\n\n.detail-open-btn:hover {\n background: var(--mj-brand-primary-hover);\n}\n\n.detail-open-btn i {\n font-size: 0.75rem;\n}\n"], encapsulation: 2 }); }
|
|
1340
|
+
} }, dependencies: [i1.DefaultValueAccessor, i1.NgControlStatus, i1.NgModel, i2.MJEntityCardComponent], styles: [":host {\n display: block;\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n.scatter-container {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n background: var(--mj-bg-page);\n}\n\n/* SVG */\n.scatter-svg {\n width: 100%;\n height: 100%;\n cursor: grab;\n user-select: none;\n}\n\n.scatter-svg:active {\n cursor: grabbing;\n}\n\n/* Grid lines */\n.grid-lines line {\n stroke: var(--mj-border-subtle);\n stroke-width: 0.5;\n}\n\n/* Data points */\n.data-point {\n cursor: pointer;\n transition: opacity 0.15s ease, r 0.15s ease;\n}\n\n.data-point:hover {\n filter: brightness(1.2);\n}\n\n/* Selection rings */\n.selection-ring {\n pointer-events: none;\n transition: r 0.15s ease;\n}\n\n/* Disable transitions when AnimateTransitions is false */\n.no-transitions .data-point,\n.no-transitions .selection-ring {\n transition: none;\n}\n\n/* Loading overlay */\n.loading-overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: color-mix(in srgb, var(--mj-bg-page) 80%, transparent);\n z-index: 5;\n}\n\n.loading-spinner {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n color: var(--mj-text-secondary);\n font-size: 0.9rem;\n}\n\n.loading-spinner i {\n font-size: 2rem;\n color: var(--mj-brand-primary);\n}\n\n/* Empty state */\n.empty-state {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n color: var(--mj-text-muted);\n gap: 8px;\n}\n\n.empty-state i {\n font-size: 3rem;\n margin-bottom: 8px;\n opacity: 0.4;\n}\n\n.empty-state p {\n font-size: 1rem;\n margin: 0;\n}\n\n.empty-hint {\n font-size: 0.8rem !important;\n opacity: 0.7;\n}\n\n/* Legend */\n.legend {\n position: absolute;\n bottom: 20px;\n left: 20px;\n background: color-mix(in srgb, var(--mj-bg-surface) 92%, transparent);\n backdrop-filter: blur(12px);\n border: 1px solid var(--mj-border-default);\n border-radius: 10px;\n padding: 14px 18px;\n z-index: 3;\n}\n\n.legend-title {\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n margin-bottom: 8px;\n}\n\n.legend-item {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 0.78rem;\n margin-bottom: 4px;\n color: var(--mj-text-primary);\n cursor: pointer;\n border-radius: 4px;\n padding: 2px 4px;\n margin-left: -4px;\n margin-right: -4px;\n transition: background 0.15s ease;\n}\n\n.legend-item:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.legend-dot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n flex-shrink: 0;\n}\n\n.legend-label {\n flex: 1;\n}\n\n.legend-edit-btn {\n display: none;\n align-items: center;\n justify-content: center;\n width: 18px;\n height: 18px;\n padding: 0;\n border: none;\n background: none;\n color: var(--mj-text-muted);\n cursor: pointer;\n font-size: 0.6rem;\n border-radius: 4px;\n transition: color 0.15s ease;\n}\n\n.legend-item:hover .legend-edit-btn {\n display: inline-flex;\n}\n\n.legend-edit-btn:hover {\n color: var(--mj-brand-primary);\n}\n\n.legend-label-input {\n flex: 1;\n padding: 2px 4px;\n border: 1px solid var(--mj-border-focus);\n border-radius: 4px;\n background: var(--mj-bg-surface);\n color: var(--mj-text-primary);\n font-size: 0.75rem;\n outline: none;\n min-width: 60px;\n}\n\n.legend-count {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n margin-left: auto;\n padding-left: 16px;\n}\n\n/* Cluster labels on chart */\n.cluster-label {\n font-size: 14px;\n font-weight: 600;\n opacity: 0.85;\n pointer-events: none;\n text-shadow:\n 0 0 6px var(--mj-bg-page),\n 0 0 12px var(--mj-bg-page),\n 1px 1px 2px var(--mj-bg-page);\n}\n\n/* Tooltip */\n.tooltip {\n position: fixed;\n background: color-mix(in srgb, var(--mj-bg-surface-elevated) 96%, transparent);\n backdrop-filter: blur(16px);\n border: 1px solid var(--mj-border-strong);\n border-radius: 10px;\n padding: 14px 16px;\n width: 280px;\n box-shadow: 0 12px 40px color-mix(in srgb, var(--mj-bg-page) 40%, transparent);\n pointer-events: none;\n z-index: 50;\n}\n\n.tt-header {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n margin-bottom: 6px;\n}\n\n.tt-icon {\n width: 2rem;\n height: 2rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 6px;\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n font-size: 0.85rem;\n flex-shrink: 0;\n}\n\n.tt-header-text {\n min-width: 0;\n flex: 1;\n}\n\n.tt-name {\n font-size: 0.85rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.tt-entity {\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n margin-top: 1px;\n}\n\n.tt-cluster {\n font-size: 0.72rem;\n color: var(--mj-brand-primary);\n margin-bottom: 6px;\n}\n\n.tt-fields {\n font-size: 0.72rem;\n color: var(--mj-text-secondary);\n line-height: 1.6;\n}\n\n.tt-field-row {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.tt-action {\n margin-top: 8px;\n padding-top: 8px;\n border-top: 1px solid var(--mj-border-default);\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n}\n\n.tt-action i {\n margin-right: 4px;\n}\n\n/* ================================================================\n Detail Panel (right slide-in)\n ================================================================ */\n\n.detail-panel {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: 320px;\n background: color-mix(in srgb, var(--mj-bg-surface) 92%, transparent);\n backdrop-filter: blur(16px);\n border-left: 1px solid var(--mj-border-default);\n z-index: 10;\n display: flex;\n flex-direction: column;\n transform: translateX(100%);\n transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);\n box-shadow: -4px 0 24px color-mix(in srgb, var(--mj-bg-page) 30%, transparent);\n overflow: hidden;\n}\n\n.detail-panel-open {\n transform: translateX(0);\n}\n\n/* Header */\n.detail-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n padding: 16px 16px 12px;\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.detail-header-content {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n min-width: 0;\n flex: 1;\n}\n\n.detail-icon {\n width: 2.25rem;\n height: 2.25rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n font-size: 0.95rem;\n flex-shrink: 0;\n}\n\n.detail-header-text {\n min-width: 0;\n flex: 1;\n}\n\n.detail-name {\n font-size: 0.95rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n line-height: 1.3;\n word-wrap: break-word;\n}\n\n.detail-entity-badge {\n display: inline-block;\n margin-top: 4px;\n padding: 1px 8px;\n border-radius: 10px;\n font-size: 0.68rem;\n font-weight: 500;\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n}\n\n.detail-close {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--mj-text-muted);\n font-size: 0.85rem;\n padding: 4px 6px;\n border-radius: 4px;\n transition: color 0.15s ease, background 0.15s ease;\n flex-shrink: 0;\n}\n\n.detail-close:hover {\n color: var(--mj-text-primary);\n background: var(--mj-bg-surface-hover);\n}\n\n/* Cluster badge */\n.detail-cluster {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 16px;\n font-size: 0.78rem;\n font-weight: 500;\n color: var(--mj-text-secondary);\n}\n\n.detail-cluster-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n flex-shrink: 0;\n}\n\n/* Metadata fields */\n.detail-fields {\n padding: 4px 16px 8px;\n overflow-y: auto;\n flex-shrink: 0;\n}\n\n.detail-field {\n padding: 6px 0;\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.detail-field:last-child {\n border-bottom: none;\n}\n\n.detail-field-key {\n font-size: 0.68rem;\n font-weight: 500;\n color: var(--mj-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.3px;\n margin-bottom: 2px;\n}\n\n.detail-field-value {\n font-size: 0.8rem;\n color: var(--mj-text-primary);\n line-height: 1.4;\n word-wrap: break-word;\n}\n\n/* Cluster Members Section */\n.detail-members-section {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n border-top: 1px solid var(--mj-border-default);\n}\n\n.detail-members-toggle {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 10px 16px;\n background: none;\n border: none;\n cursor: pointer;\n font-size: 0.78rem;\n font-weight: 600;\n color: var(--mj-text-secondary);\n text-align: left;\n transition: background 0.15s ease;\n}\n\n.detail-members-toggle:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.detail-members-toggle i {\n font-size: 0.65rem;\n width: 12px;\n text-align: center;\n color: var(--mj-text-muted);\n}\n\n.detail-members-count {\n margin-left: auto;\n font-size: 0.7rem;\n font-weight: 400;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-sunken);\n padding: 1px 7px;\n border-radius: 8px;\n}\n\n.detail-members-list {\n flex: 1;\n overflow-y: auto;\n padding: 0 8px 8px;\n}\n\n.detail-member-row {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 8px;\n border-radius: 6px;\n cursor: pointer;\n font-size: 0.78rem;\n color: var(--mj-text-primary);\n transition: background 0.12s ease;\n}\n\n.detail-member-row:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.detail-member-row.active {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n font-weight: 500;\n}\n\n.detail-member-row .member-icon {\n font-size: 0.72rem;\n color: var(--mj-text-muted);\n width: 16px;\n text-align: center;\n flex-shrink: 0;\n}\n\n.detail-member-row.active .member-icon {\n color: var(--mj-brand-primary);\n}\n\n.member-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Footer */\n.detail-footer {\n padding: 12px 16px;\n border-top: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.detail-open-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n width: 100%;\n padding: 8px 16px;\n border: none;\n border-radius: 8px;\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n font-size: 0.82rem;\n font-weight: 500;\n cursor: pointer;\n transition: background 0.15s ease;\n}\n\n.detail-open-btn:hover {\n background: var(--mj-brand-primary-hover);\n}\n\n.detail-open-btn i {\n font-size: 0.75rem;\n}\n"], encapsulation: 2 }); }
|
|
1296
1341
|
}
|
|
1297
1342
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ClusterScatterComponent, [{
|
|
1298
1343
|
type: Component,
|
|
1299
|
-
args: [{ standalone: false, selector: 'mj-cluster-scatter', encapsulation: ViewEncapsulation.None, template: "<div class=\"scatter-container\">\n <!-- Loading overlay -->\n @if (IsLoading) {\n <div class=\"loading-overlay\">\n <div class=\"loading-spinner\">\n <i class=\"fa-solid fa-circle-nodes fa-spin\"></i>\n <span>Computing clusters...</span>\n </div>\n </div>\n }\n\n <!-- Empty state -->\n @if (!IsLoading && Points.length === 0) {\n <div class=\"empty-state\">\n <i class=\"fa-solid fa-chart-scatter\"></i>\n <p>No data to display</p>\n <p class=\"empty-hint\">Configure and run clustering to see results</p>\n </div>\n }\n\n <!-- SVG scatter plot -->\n @if (Points.length > 0) {\n <svg #svgElement\n class=\"scatter-svg\"\n [class.no-transitions]=\"!AnimateTransitions\"\n [attr.viewBox]=\"GetViewBoxString()\"\n preserveAspectRatio=\"xMidYMid meet\"\n (wheel)=\"OnWheel($event)\"\n (mousedown)=\"OnMouseDown($event)\">\n\n <!-- Gradient definitions for cluster glow -->\n <defs>\n @for (centroid of ClusterCentroids; track TrackCentroidBy($index, centroid)) {\n <radialGradient [attr.id]=\"'cluster-glow-' + centroid.ClusterId\"\n cx=\"50%\" cy=\"50%\" r=\"50%\">\n <stop offset=\"0%\" [attr.stop-color]=\"centroid.Color\" stop-opacity=\"0.12\"/>\n <stop offset=\"100%\" [attr.stop-color]=\"centroid.Color\" stop-opacity=\"0\"/>\n </radialGradient>\n }\n </defs>\n\n <!-- Background grid lines -->\n <g class=\"grid-lines\">\n @for (y of HorizontalGridLines; track y) {\n <line [attr.x1]=\"0\" [attr.y1]=\"y\" [attr.x2]=\"1000\" [attr.y2]=\"y\"/>\n }\n @for (x of VerticalGridLines; track x) {\n <line [attr.x1]=\"x\" [attr.y1]=\"0\" [attr.x2]=\"x\" [attr.y2]=\"700\"/>\n }\n </g>\n\n <!-- Cluster glow circles -->\n @for (centroid of ClusterCentroids; track TrackCentroidBy($index, centroid)) {\n <circle [attr.cx]=\"centroid.Cx\"\n [attr.cy]=\"centroid.Cy\"\n [attr.r]=\"centroid.R\"\n [attr.fill]=\"'url(#cluster-glow-' + centroid.ClusterId + ')'\"/>\n }\n\n <!-- Cluster labels (positioned above centroids) -->\n @for (centroid of ClusterCentroids; track TrackCentroidBy($index, centroid)) {\n @if (GetClusterLabelById(centroid.ClusterId); as label) {\n <text class=\"cluster-label\"\n [attr.x]=\"centroid.Cx\"\n [attr.y]=\"centroid.Cy - centroid.R - 8\"\n text-anchor=\"middle\"\n [attr.fill]=\"centroid.Color\">{{ label }}</text>\n }\n }\n\n <!-- Data points -->\n @for (point of Points; track TrackPointBy($index, point)) {\n <circle class=\"data-point\"\n [attr.cx]=\"point.X\"\n [attr.cy]=\"point.Y\"\n [attr.r]=\"GetPointRadius(point)\"\n [attr.fill]=\"GetPointColor(point)\"\n [attr.opacity]=\"GetPointOpacity(point)\"\n (mouseenter)=\"OnPointMouseEnter(point, $event)\"\n (mouseleave)=\"OnPointMouseLeave()\"\n (click)=\"OnPointClick(point); $event.stopPropagation()\"/>\n }\n\n <!-- Selection rings for selected points -->\n @for (point of Points; track TrackPointBy($index, point)) {\n @if (IsSelected(point)) {\n <circle class=\"selection-ring\"\n [attr.cx]=\"point.X\"\n [attr.cy]=\"point.Y\"\n [attr.r]=\"HighlightRadius + 2\"\n fill=\"none\"\n [attr.stroke]=\"GetPointColor(point)\"\n stroke-width=\"2\"\n opacity=\"0.8\"/>\n }\n }\n\n <!-- Highlight ring for last-clicked point -->\n @if (HighlightedKey) {\n @for (point of Points; track TrackPointBy($index, point)) {\n @if (IsHighlighted(point)) {\n <circle [attr.cx]=\"point.X\"\n [attr.cy]=\"point.Y\"\n [attr.r]=\"HighlightRadius + 4\"\n fill=\"none\"\n [attr.stroke]=\"GetPointColor(point)\"\n stroke-width=\"2.5\"\n opacity=\"0.9\"/>\n }\n }\n }\n </svg>\n }\n\n <!-- Legend overlay -->\n @if (ShowLegend && Clusters.length > 0) {\n <div class=\"legend\">\n <div class=\"legend-title\">Clusters</div>\n @for (cluster of Clusters; track TrackClusterBy($index, cluster)) {\n <div class=\"legend-item\" (click)=\"OnClusterLegendClick(cluster)\">\n <div class=\"legend-dot\" [style.background]=\"cluster.Color\"></div>\n <span class=\"legend-label\">{{ cluster.Label }}</span>\n <span class=\"legend-count\">{{ cluster.MemberCount }}</span>\n </div>\n }\n </div>\n }\n\n <!-- Tooltip -->\n @if (ShowTooltip && HoveredPoint) {\n <div class=\"tooltip\"\n [style.left.px]=\"TooltipX\"\n [style.top.px]=\"TooltipY\">\n <div class=\"tt-header\">\n @if (HoveredPoint.Metadata?.['EntityIcon']) {\n <div class=\"tt-icon\">\n <i [class]=\"HoveredPoint.Metadata['EntityIcon']\"></i>\n </div>\n }\n <div class=\"tt-header-text\">\n <div class=\"tt-name\">{{ HoveredPoint.Label }}</div>\n @if (HoveredPoint.Metadata?.['Entity']) {\n <div class=\"tt-entity\">{{ HoveredPoint.Metadata['Entity'] }}</div>\n }\n </div>\n </div>\n <div class=\"tt-cluster\">\n <i class=\"fa-solid fa-circle\" style=\"font-size: 7px; vertical-align: middle\"></i>\n {{ GetClusterLabel(HoveredPoint) }}\n </div>\n @if (TooltipEntries.length > 0) {\n <div class=\"tt-fields\">\n @for (entry of TooltipEntries; track entry.Key) {\n @if (entry.Key !== 'Entity' && entry.Key !== 'EntityIcon' && entry.Key !== 'RecordID' && entry.Key !== 'Name' && entry.Key !== 'TemplateID' && entry.Key !== '__mj_UpdatedAt') {\n <div class=\"tt-field-row\">{{ entry.Value }}</div>\n }\n }\n </div>\n }\n <div class=\"tt-action\">\n <i class=\"fa-solid fa-arrow-up-right-from-square\"></i> Click to view details\n </div>\n </div>\n }\n\n <!-- Detail Panel (slide-in from right) -->\n <div class=\"detail-panel\" [class.detail-panel-open]=\"ShowDetailPanel && SelectedPoint\">\n @if (SelectedPoint) {\n <!-- Panel Header -->\n <div class=\"detail-header\">\n <div class=\"detail-header-content\">\n @if (SelectedPoint.Metadata?.['EntityIcon']) {\n <div class=\"detail-icon\">\n <i [class]=\"SelectedPoint.Metadata['EntityIcon']\"></i>\n </div>\n }\n <div class=\"detail-header-text\">\n <div class=\"detail-name\">{{ SelectedPoint.Label }}</div>\n @if (SelectedPoint.Metadata?.['Entity']) {\n <span class=\"detail-entity-badge\">{{ SelectedPoint.Metadata['Entity'] }}</span>\n }\n </div>\n </div>\n <button class=\"detail-close\" (click)=\"CloseDetailPanel()\" title=\"Close\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n\n <!-- Cluster badge -->\n <div class=\"detail-cluster\">\n <span class=\"detail-cluster-dot\"\n [style.background]=\"GetClusterColor(SelectedPoint.ClusterId)\"></span>\n {{ GetClusterLabel(SelectedPoint) }}\n </div>\n\n <!-- Metadata fields -->\n @if (DetailEntries.length > 0) {\n <div class=\"detail-fields\">\n @for (entry of DetailEntries; track entry.Key) {\n <div class=\"detail-field\">\n <div class=\"detail-field-key\">{{ entry.Key }}</div>\n <div class=\"detail-field-value\">{{ entry.Value }}</div>\n </div>\n }\n </div>\n }\n\n <!-- Cluster Members -->\n <div class=\"detail-members-section\">\n <button class=\"detail-members-toggle\" (click)=\"ToggleClusterMembers()\">\n <i class=\"fa-solid\"\n [class.fa-chevron-down]=\"ClusterMembersExpanded\"\n [class.fa-chevron-right]=\"!ClusterMembersExpanded\"></i>\n <span>Cluster Members</span>\n <span class=\"detail-members-count\">{{ ClusterMembers.length }}</span>\n </button>\n\n @if (ClusterMembersExpanded) {\n <div class=\"detail-members-list\">\n @for (member of ClusterMembers; track TrackMemberBy($index, member)) {\n <div class=\"detail-member-row\"\n [class.active]=\"member.VectorKey === SelectedPoint.VectorKey\"\n (click)=\"OnClusterMemberClick(member)\">\n @if (member.Metadata?.['EntityIcon']) {\n <i [class]=\"member.Metadata['EntityIcon']\" class=\"member-icon\"></i>\n }\n <span class=\"member-label\">{{ member.Label }}</span>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Open Record button -->\n <div class=\"detail-footer\">\n <button class=\"detail-open-btn\" (click)=\"OnOpenRecordClick()\">\n <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\n Open Record\n </button>\n </div>\n }\n </div>\n</div>\n", styles: [":host {\n display: block;\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n.scatter-container {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n background: var(--mj-bg-page);\n}\n\n/* SVG */\n.scatter-svg {\n width: 100%;\n height: 100%;\n cursor: grab;\n user-select: none;\n}\n\n.scatter-svg:active {\n cursor: grabbing;\n}\n\n/* Grid lines */\n.grid-lines line {\n stroke: var(--mj-border-subtle);\n stroke-width: 0.5;\n}\n\n/* Data points */\n.data-point {\n cursor: pointer;\n transition: opacity 0.15s ease, r 0.15s ease;\n}\n\n.data-point:hover {\n filter: brightness(1.2);\n}\n\n/* Selection rings */\n.selection-ring {\n pointer-events: none;\n transition: r 0.15s ease;\n}\n\n/* Disable transitions when AnimateTransitions is false */\n.no-transitions .data-point,\n.no-transitions .selection-ring {\n transition: none;\n}\n\n/* Loading overlay */\n.loading-overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: color-mix(in srgb, var(--mj-bg-page) 80%, transparent);\n z-index: 5;\n}\n\n.loading-spinner {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n color: var(--mj-text-secondary);\n font-size: 0.9rem;\n}\n\n.loading-spinner i {\n font-size: 2rem;\n color: var(--mj-brand-primary);\n}\n\n/* Empty state */\n.empty-state {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n color: var(--mj-text-muted);\n gap: 8px;\n}\n\n.empty-state i {\n font-size: 3rem;\n margin-bottom: 8px;\n opacity: 0.4;\n}\n\n.empty-state p {\n font-size: 1rem;\n margin: 0;\n}\n\n.empty-hint {\n font-size: 0.8rem !important;\n opacity: 0.7;\n}\n\n/* Legend */\n.legend {\n position: absolute;\n bottom: 20px;\n left: 20px;\n background: color-mix(in srgb, var(--mj-bg-surface) 92%, transparent);\n backdrop-filter: blur(12px);\n border: 1px solid var(--mj-border-default);\n border-radius: 10px;\n padding: 14px 18px;\n z-index: 3;\n}\n\n.legend-title {\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n margin-bottom: 8px;\n}\n\n.legend-item {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 0.78rem;\n margin-bottom: 4px;\n color: var(--mj-text-primary);\n cursor: pointer;\n border-radius: 4px;\n padding: 2px 4px;\n margin-left: -4px;\n margin-right: -4px;\n transition: background 0.15s ease;\n}\n\n.legend-item:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.legend-dot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n flex-shrink: 0;\n}\n\n.legend-label {\n flex: 1;\n}\n\n.legend-count {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n margin-left: auto;\n padding-left: 16px;\n}\n\n/* Cluster labels on chart */\n.cluster-label {\n font-size: 14px;\n font-weight: 600;\n opacity: 0.85;\n pointer-events: none;\n text-shadow:\n 0 0 6px var(--mj-bg-page),\n 0 0 12px var(--mj-bg-page),\n 1px 1px 2px var(--mj-bg-page);\n}\n\n/* Tooltip */\n.tooltip {\n position: fixed;\n background: color-mix(in srgb, var(--mj-bg-surface-elevated) 96%, transparent);\n backdrop-filter: blur(16px);\n border: 1px solid var(--mj-border-strong);\n border-radius: 10px;\n padding: 14px 16px;\n width: 280px;\n box-shadow: 0 12px 40px color-mix(in srgb, var(--mj-bg-page) 40%, transparent);\n pointer-events: none;\n z-index: 50;\n}\n\n.tt-header {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n margin-bottom: 6px;\n}\n\n.tt-icon {\n width: 2rem;\n height: 2rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 6px;\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n font-size: 0.85rem;\n flex-shrink: 0;\n}\n\n.tt-header-text {\n min-width: 0;\n flex: 1;\n}\n\n.tt-name {\n font-size: 0.85rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.tt-entity {\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n margin-top: 1px;\n}\n\n.tt-cluster {\n font-size: 0.72rem;\n color: var(--mj-brand-primary);\n margin-bottom: 6px;\n}\n\n.tt-fields {\n font-size: 0.72rem;\n color: var(--mj-text-secondary);\n line-height: 1.6;\n}\n\n.tt-field-row {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.tt-action {\n margin-top: 8px;\n padding-top: 8px;\n border-top: 1px solid var(--mj-border-default);\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n}\n\n.tt-action i {\n margin-right: 4px;\n}\n\n/* ================================================================\n Detail Panel (right slide-in)\n ================================================================ */\n\n.detail-panel {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: 320px;\n background: color-mix(in srgb, var(--mj-bg-surface) 92%, transparent);\n backdrop-filter: blur(16px);\n border-left: 1px solid var(--mj-border-default);\n z-index: 10;\n display: flex;\n flex-direction: column;\n transform: translateX(100%);\n transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);\n box-shadow: -4px 0 24px color-mix(in srgb, var(--mj-bg-page) 30%, transparent);\n overflow: hidden;\n}\n\n.detail-panel-open {\n transform: translateX(0);\n}\n\n/* Header */\n.detail-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n padding: 16px 16px 12px;\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.detail-header-content {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n min-width: 0;\n flex: 1;\n}\n\n.detail-icon {\n width: 2.25rem;\n height: 2.25rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n font-size: 0.95rem;\n flex-shrink: 0;\n}\n\n.detail-header-text {\n min-width: 0;\n flex: 1;\n}\n\n.detail-name {\n font-size: 0.95rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n line-height: 1.3;\n word-wrap: break-word;\n}\n\n.detail-entity-badge {\n display: inline-block;\n margin-top: 4px;\n padding: 1px 8px;\n border-radius: 10px;\n font-size: 0.68rem;\n font-weight: 500;\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n}\n\n.detail-close {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--mj-text-muted);\n font-size: 0.85rem;\n padding: 4px 6px;\n border-radius: 4px;\n transition: color 0.15s ease, background 0.15s ease;\n flex-shrink: 0;\n}\n\n.detail-close:hover {\n color: var(--mj-text-primary);\n background: var(--mj-bg-surface-hover);\n}\n\n/* Cluster badge */\n.detail-cluster {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 16px;\n font-size: 0.78rem;\n font-weight: 500;\n color: var(--mj-text-secondary);\n}\n\n.detail-cluster-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n flex-shrink: 0;\n}\n\n/* Metadata fields */\n.detail-fields {\n padding: 4px 16px 8px;\n overflow-y: auto;\n flex-shrink: 0;\n}\n\n.detail-field {\n padding: 6px 0;\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.detail-field:last-child {\n border-bottom: none;\n}\n\n.detail-field-key {\n font-size: 0.68rem;\n font-weight: 500;\n color: var(--mj-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.3px;\n margin-bottom: 2px;\n}\n\n.detail-field-value {\n font-size: 0.8rem;\n color: var(--mj-text-primary);\n line-height: 1.4;\n word-wrap: break-word;\n}\n\n/* Cluster Members Section */\n.detail-members-section {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n border-top: 1px solid var(--mj-border-default);\n}\n\n.detail-members-toggle {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 10px 16px;\n background: none;\n border: none;\n cursor: pointer;\n font-size: 0.78rem;\n font-weight: 600;\n color: var(--mj-text-secondary);\n text-align: left;\n transition: background 0.15s ease;\n}\n\n.detail-members-toggle:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.detail-members-toggle i {\n font-size: 0.65rem;\n width: 12px;\n text-align: center;\n color: var(--mj-text-muted);\n}\n\n.detail-members-count {\n margin-left: auto;\n font-size: 0.7rem;\n font-weight: 400;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-sunken);\n padding: 1px 7px;\n border-radius: 8px;\n}\n\n.detail-members-list {\n flex: 1;\n overflow-y: auto;\n padding: 0 8px 8px;\n}\n\n.detail-member-row {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 8px;\n border-radius: 6px;\n cursor: pointer;\n font-size: 0.78rem;\n color: var(--mj-text-primary);\n transition: background 0.12s ease;\n}\n\n.detail-member-row:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.detail-member-row.active {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n font-weight: 500;\n}\n\n.detail-member-row .member-icon {\n font-size: 0.72rem;\n color: var(--mj-text-muted);\n width: 16px;\n text-align: center;\n flex-shrink: 0;\n}\n\n.detail-member-row.active .member-icon {\n color: var(--mj-brand-primary);\n}\n\n.member-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Footer */\n.detail-footer {\n padding: 12px 16px;\n border-top: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.detail-open-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n width: 100%;\n padding: 8px 16px;\n border: none;\n border-radius: 8px;\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n font-size: 0.82rem;\n font-weight: 500;\n cursor: pointer;\n transition: background 0.15s ease;\n}\n\n.detail-open-btn:hover {\n background: var(--mj-brand-primary-hover);\n}\n\n.detail-open-btn i {\n font-size: 0.75rem;\n}\n"] }]
|
|
1344
|
+
args: [{ standalone: false, selector: 'mj-cluster-scatter', encapsulation: ViewEncapsulation.None, template: "<div class=\"scatter-container\">\n <!-- Loading overlay -->\n @if (IsLoading) {\n <div class=\"loading-overlay\">\n <div class=\"loading-spinner\">\n <i class=\"fa-solid fa-circle-nodes fa-spin\"></i>\n <span>Computing clusters...</span>\n </div>\n </div>\n }\n\n <!-- Empty state -->\n @if (!IsLoading && Points.length === 0) {\n <div class=\"empty-state\">\n <i class=\"fa-solid fa-chart-scatter\"></i>\n <p>No data to display</p>\n <p class=\"empty-hint\">Configure and run clustering to see results</p>\n </div>\n }\n\n <!-- SVG scatter plot -->\n @if (Points.length > 0) {\n <svg #svgElement\n class=\"scatter-svg\"\n [class.no-transitions]=\"!AnimateTransitions\"\n [attr.viewBox]=\"ViewBoxString\"\n preserveAspectRatio=\"xMidYMid meet\"\n (wheel)=\"OnWheel($event)\"\n (mousedown)=\"OnMouseDown($event)\">\n\n <!-- Gradient definitions for cluster glow -->\n <defs>\n @for (centroid of ClusterCentroids; track TrackCentroidBy($index, centroid)) {\n <radialGradient [attr.id]=\"'cluster-glow-' + centroid.ClusterId\"\n cx=\"50%\" cy=\"50%\" r=\"50%\">\n <stop offset=\"0%\" [attr.stop-color]=\"centroid.Color\" stop-opacity=\"0.12\"/>\n <stop offset=\"100%\" [attr.stop-color]=\"centroid.Color\" stop-opacity=\"0\"/>\n </radialGradient>\n }\n </defs>\n\n <!-- Background grid lines -->\n <g class=\"grid-lines\">\n @for (y of HorizontalGridLines; track y) {\n <line [attr.x1]=\"0\" [attr.y1]=\"y\" [attr.x2]=\"1000\" [attr.y2]=\"y\"/>\n }\n @for (x of VerticalGridLines; track x) {\n <line [attr.x1]=\"x\" [attr.y1]=\"0\" [attr.x2]=\"x\" [attr.y2]=\"700\"/>\n }\n </g>\n\n <!-- Cluster glow circles -->\n @for (centroid of ClusterCentroids; track TrackCentroidBy($index, centroid)) {\n <circle [attr.cx]=\"centroid.Cx\"\n [attr.cy]=\"centroid.Cy\"\n [attr.r]=\"centroid.R\"\n [attr.fill]=\"'url(#cluster-glow-' + centroid.ClusterId + ')'\"/>\n }\n\n <!-- Cluster labels (positioned above centroids) -->\n @for (centroid of ClusterCentroids; track TrackCentroidBy($index, centroid)) {\n @if (GetClusterLabelById(centroid.ClusterId); as label) {\n <text class=\"cluster-label\"\n [attr.x]=\"centroid.Cx\"\n [attr.y]=\"centroid.Cy - centroid.R - 8\"\n text-anchor=\"middle\"\n [attr.fill]=\"centroid.Color\">{{ label }}</text>\n }\n }\n\n <!-- Data points -->\n @for (point of Points; track TrackPointBy($index, point)) {\n <circle class=\"data-point\"\n [attr.cx]=\"point.X\"\n [attr.cy]=\"point.Y\"\n [attr.r]=\"GetPointRadius(point)\"\n [attr.fill]=\"GetPointColor(point)\"\n [attr.opacity]=\"GetPointOpacity(point)\"\n (mouseenter)=\"OnPointMouseEnter(point, $event)\"\n (mouseleave)=\"OnPointMouseLeave()\"\n (click)=\"OnPointClick(point); $event.stopPropagation()\"/>\n }\n\n <!-- Selection rings for selected points -->\n @for (point of Points; track TrackPointBy($index, point)) {\n @if (IsSelected(point)) {\n <circle class=\"selection-ring\"\n [attr.cx]=\"point.X\"\n [attr.cy]=\"point.Y\"\n [attr.r]=\"HighlightRadius + 2\"\n fill=\"none\"\n [attr.stroke]=\"GetPointColor(point)\"\n stroke-width=\"2\"\n opacity=\"0.8\"/>\n }\n }\n\n <!-- Highlight ring for last-clicked point -->\n @if (HighlightedKey) {\n @for (point of Points; track TrackPointBy($index, point)) {\n @if (IsHighlighted(point)) {\n <circle [attr.cx]=\"point.X\"\n [attr.cy]=\"point.Y\"\n [attr.r]=\"HighlightRadius + 4\"\n fill=\"none\"\n [attr.stroke]=\"GetPointColor(point)\"\n stroke-width=\"2.5\"\n opacity=\"0.9\"/>\n }\n }\n }\n </svg>\n }\n\n <!-- Legend overlay -->\n @if (ShowLegend && Clusters.length > 0) {\n <div class=\"legend\">\n <div class=\"legend-title\">Clusters</div>\n @for (cluster of Clusters; track TrackClusterBy($index, cluster)) {\n <div class=\"legend-item\" (click)=\"OnClusterLegendClick(cluster)\">\n <div class=\"legend-dot\" [style.background]=\"cluster.Color\"></div>\n @if (EditingClusterId === cluster.Id) {\n <input class=\"legend-label-input\"\n [(ngModel)]=\"EditingLabelDraft\"\n (keydown)=\"OnLabelEditKeydown($event, cluster)\"\n (blur)=\"CommitLabelEdit(cluster)\"\n (click)=\"$event.stopPropagation()\"\n autofocus />\n } @else {\n <span class=\"legend-label\">{{ cluster.Label }}</span>\n <button class=\"legend-edit-btn\"\n title=\"Edit label\"\n (click)=\"StartLabelEdit(cluster, $event)\">\n <i class=\"fa-solid fa-pencil\"></i>\n </button>\n }\n <span class=\"legend-count\">{{ cluster.MemberCount }}</span>\n </div>\n }\n </div>\n }\n\n <!-- Tooltip -->\n @if (ShowTooltip && HoveredPoint) {\n <div class=\"tooltip\"\n [style.left.px]=\"TooltipX\"\n [style.top.px]=\"TooltipY\">\n <mj-entity-card\n [EntityName]=\"EntityName\"\n [Record]=\"HoveredPoint.Metadata\"\n Variant=\"compact\">\n </mj-entity-card>\n <div class=\"tt-cluster\">\n <i class=\"fa-solid fa-circle\" style=\"font-size: 7px; vertical-align: middle\"></i>\n {{ GetClusterLabel(HoveredPoint) }}\n </div>\n <div class=\"tt-action\">\n <i class=\"fa-solid fa-arrow-up-right-from-square\"></i> Click to view details\n </div>\n </div>\n }\n\n <!-- Detail Panel (slide-in from right) -->\n <div class=\"detail-panel\" [class.detail-panel-open]=\"ShowDetailPanel && SelectedPoint\">\n @if (SelectedPoint) {\n <!-- Panel Header -->\n <div class=\"detail-header\">\n <div class=\"detail-header-content\">\n @if (SelectedPoint.Metadata?.['EntityIcon']) {\n <div class=\"detail-icon\">\n <i [class]=\"SelectedPoint.Metadata['EntityIcon']\"></i>\n </div>\n }\n <div class=\"detail-header-text\">\n <div class=\"detail-name\">{{ SelectedPoint.Label }}</div>\n @if (SelectedPoint.Metadata?.['Entity']) {\n <span class=\"detail-entity-badge\">{{ SelectedPoint.Metadata['Entity'] }}</span>\n }\n </div>\n </div>\n <button class=\"detail-close\" (click)=\"CloseDetailPanel()\" title=\"Close\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n\n <!-- Cluster badge -->\n <div class=\"detail-cluster\">\n <span class=\"detail-cluster-dot\"\n [style.background]=\"GetClusterColor(SelectedPoint.ClusterId)\"></span>\n {{ GetClusterLabel(SelectedPoint) }}\n </div>\n\n <!-- Metadata fields via entity card -->\n <mj-entity-card\n [EntityName]=\"EntityName\"\n [Record]=\"SelectedPoint.Metadata\"\n Variant=\"detail\"\n CssClass=\"detail-entity-card\">\n </mj-entity-card>\n\n <!-- Cluster Members -->\n <div class=\"detail-members-section\">\n <button class=\"detail-members-toggle\" (click)=\"ToggleClusterMembers()\">\n <i class=\"fa-solid\"\n [class.fa-chevron-down]=\"ClusterMembersExpanded\"\n [class.fa-chevron-right]=\"!ClusterMembersExpanded\"></i>\n <span>Cluster Members</span>\n <span class=\"detail-members-count\">{{ ClusterMembers.length }}</span>\n </button>\n\n @if (ClusterMembersExpanded) {\n <div class=\"detail-members-list\">\n @for (member of ClusterMembers; track TrackMemberBy($index, member)) {\n <div class=\"detail-member-row\"\n [class.active]=\"member.VectorKey === SelectedPoint.VectorKey\"\n (click)=\"OnClusterMemberClick(member)\">\n @if (member.Metadata?.['EntityIcon']) {\n <i [class]=\"member.Metadata['EntityIcon']\" class=\"member-icon\"></i>\n }\n <span class=\"member-label\">{{ member.Label }}</span>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Open Record button -->\n <div class=\"detail-footer\">\n <button class=\"detail-open-btn\" (click)=\"OnOpenRecordClick()\">\n <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\n Open Record\n </button>\n </div>\n }\n </div>\n</div>\n", styles: [":host {\n display: block;\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n.scatter-container {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n background: var(--mj-bg-page);\n}\n\n/* SVG */\n.scatter-svg {\n width: 100%;\n height: 100%;\n cursor: grab;\n user-select: none;\n}\n\n.scatter-svg:active {\n cursor: grabbing;\n}\n\n/* Grid lines */\n.grid-lines line {\n stroke: var(--mj-border-subtle);\n stroke-width: 0.5;\n}\n\n/* Data points */\n.data-point {\n cursor: pointer;\n transition: opacity 0.15s ease, r 0.15s ease;\n}\n\n.data-point:hover {\n filter: brightness(1.2);\n}\n\n/* Selection rings */\n.selection-ring {\n pointer-events: none;\n transition: r 0.15s ease;\n}\n\n/* Disable transitions when AnimateTransitions is false */\n.no-transitions .data-point,\n.no-transitions .selection-ring {\n transition: none;\n}\n\n/* Loading overlay */\n.loading-overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: color-mix(in srgb, var(--mj-bg-page) 80%, transparent);\n z-index: 5;\n}\n\n.loading-spinner {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n color: var(--mj-text-secondary);\n font-size: 0.9rem;\n}\n\n.loading-spinner i {\n font-size: 2rem;\n color: var(--mj-brand-primary);\n}\n\n/* Empty state */\n.empty-state {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n color: var(--mj-text-muted);\n gap: 8px;\n}\n\n.empty-state i {\n font-size: 3rem;\n margin-bottom: 8px;\n opacity: 0.4;\n}\n\n.empty-state p {\n font-size: 1rem;\n margin: 0;\n}\n\n.empty-hint {\n font-size: 0.8rem !important;\n opacity: 0.7;\n}\n\n/* Legend */\n.legend {\n position: absolute;\n bottom: 20px;\n left: 20px;\n background: color-mix(in srgb, var(--mj-bg-surface) 92%, transparent);\n backdrop-filter: blur(12px);\n border: 1px solid var(--mj-border-default);\n border-radius: 10px;\n padding: 14px 18px;\n z-index: 3;\n}\n\n.legend-title {\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n margin-bottom: 8px;\n}\n\n.legend-item {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 0.78rem;\n margin-bottom: 4px;\n color: var(--mj-text-primary);\n cursor: pointer;\n border-radius: 4px;\n padding: 2px 4px;\n margin-left: -4px;\n margin-right: -4px;\n transition: background 0.15s ease;\n}\n\n.legend-item:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.legend-dot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n flex-shrink: 0;\n}\n\n.legend-label {\n flex: 1;\n}\n\n.legend-edit-btn {\n display: none;\n align-items: center;\n justify-content: center;\n width: 18px;\n height: 18px;\n padding: 0;\n border: none;\n background: none;\n color: var(--mj-text-muted);\n cursor: pointer;\n font-size: 0.6rem;\n border-radius: 4px;\n transition: color 0.15s ease;\n}\n\n.legend-item:hover .legend-edit-btn {\n display: inline-flex;\n}\n\n.legend-edit-btn:hover {\n color: var(--mj-brand-primary);\n}\n\n.legend-label-input {\n flex: 1;\n padding: 2px 4px;\n border: 1px solid var(--mj-border-focus);\n border-radius: 4px;\n background: var(--mj-bg-surface);\n color: var(--mj-text-primary);\n font-size: 0.75rem;\n outline: none;\n min-width: 60px;\n}\n\n.legend-count {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n margin-left: auto;\n padding-left: 16px;\n}\n\n/* Cluster labels on chart */\n.cluster-label {\n font-size: 14px;\n font-weight: 600;\n opacity: 0.85;\n pointer-events: none;\n text-shadow:\n 0 0 6px var(--mj-bg-page),\n 0 0 12px var(--mj-bg-page),\n 1px 1px 2px var(--mj-bg-page);\n}\n\n/* Tooltip */\n.tooltip {\n position: fixed;\n background: color-mix(in srgb, var(--mj-bg-surface-elevated) 96%, transparent);\n backdrop-filter: blur(16px);\n border: 1px solid var(--mj-border-strong);\n border-radius: 10px;\n padding: 14px 16px;\n width: 280px;\n box-shadow: 0 12px 40px color-mix(in srgb, var(--mj-bg-page) 40%, transparent);\n pointer-events: none;\n z-index: 50;\n}\n\n.tt-header {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n margin-bottom: 6px;\n}\n\n.tt-icon {\n width: 2rem;\n height: 2rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 6px;\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n font-size: 0.85rem;\n flex-shrink: 0;\n}\n\n.tt-header-text {\n min-width: 0;\n flex: 1;\n}\n\n.tt-name {\n font-size: 0.85rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.tt-entity {\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n margin-top: 1px;\n}\n\n.tt-cluster {\n font-size: 0.72rem;\n color: var(--mj-brand-primary);\n margin-bottom: 6px;\n}\n\n.tt-fields {\n font-size: 0.72rem;\n color: var(--mj-text-secondary);\n line-height: 1.6;\n}\n\n.tt-field-row {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.tt-action {\n margin-top: 8px;\n padding-top: 8px;\n border-top: 1px solid var(--mj-border-default);\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n}\n\n.tt-action i {\n margin-right: 4px;\n}\n\n/* ================================================================\n Detail Panel (right slide-in)\n ================================================================ */\n\n.detail-panel {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: 320px;\n background: color-mix(in srgb, var(--mj-bg-surface) 92%, transparent);\n backdrop-filter: blur(16px);\n border-left: 1px solid var(--mj-border-default);\n z-index: 10;\n display: flex;\n flex-direction: column;\n transform: translateX(100%);\n transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);\n box-shadow: -4px 0 24px color-mix(in srgb, var(--mj-bg-page) 30%, transparent);\n overflow: hidden;\n}\n\n.detail-panel-open {\n transform: translateX(0);\n}\n\n/* Header */\n.detail-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n padding: 16px 16px 12px;\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.detail-header-content {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n min-width: 0;\n flex: 1;\n}\n\n.detail-icon {\n width: 2.25rem;\n height: 2.25rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n font-size: 0.95rem;\n flex-shrink: 0;\n}\n\n.detail-header-text {\n min-width: 0;\n flex: 1;\n}\n\n.detail-name {\n font-size: 0.95rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n line-height: 1.3;\n word-wrap: break-word;\n}\n\n.detail-entity-badge {\n display: inline-block;\n margin-top: 4px;\n padding: 1px 8px;\n border-radius: 10px;\n font-size: 0.68rem;\n font-weight: 500;\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n}\n\n.detail-close {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--mj-text-muted);\n font-size: 0.85rem;\n padding: 4px 6px;\n border-radius: 4px;\n transition: color 0.15s ease, background 0.15s ease;\n flex-shrink: 0;\n}\n\n.detail-close:hover {\n color: var(--mj-text-primary);\n background: var(--mj-bg-surface-hover);\n}\n\n/* Cluster badge */\n.detail-cluster {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 16px;\n font-size: 0.78rem;\n font-weight: 500;\n color: var(--mj-text-secondary);\n}\n\n.detail-cluster-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n flex-shrink: 0;\n}\n\n/* Metadata fields */\n.detail-fields {\n padding: 4px 16px 8px;\n overflow-y: auto;\n flex-shrink: 0;\n}\n\n.detail-field {\n padding: 6px 0;\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.detail-field:last-child {\n border-bottom: none;\n}\n\n.detail-field-key {\n font-size: 0.68rem;\n font-weight: 500;\n color: var(--mj-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.3px;\n margin-bottom: 2px;\n}\n\n.detail-field-value {\n font-size: 0.8rem;\n color: var(--mj-text-primary);\n line-height: 1.4;\n word-wrap: break-word;\n}\n\n/* Cluster Members Section */\n.detail-members-section {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n border-top: 1px solid var(--mj-border-default);\n}\n\n.detail-members-toggle {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 10px 16px;\n background: none;\n border: none;\n cursor: pointer;\n font-size: 0.78rem;\n font-weight: 600;\n color: var(--mj-text-secondary);\n text-align: left;\n transition: background 0.15s ease;\n}\n\n.detail-members-toggle:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.detail-members-toggle i {\n font-size: 0.65rem;\n width: 12px;\n text-align: center;\n color: var(--mj-text-muted);\n}\n\n.detail-members-count {\n margin-left: auto;\n font-size: 0.7rem;\n font-weight: 400;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-sunken);\n padding: 1px 7px;\n border-radius: 8px;\n}\n\n.detail-members-list {\n flex: 1;\n overflow-y: auto;\n padding: 0 8px 8px;\n}\n\n.detail-member-row {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 8px;\n border-radius: 6px;\n cursor: pointer;\n font-size: 0.78rem;\n color: var(--mj-text-primary);\n transition: background 0.12s ease;\n}\n\n.detail-member-row:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.detail-member-row.active {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n font-weight: 500;\n}\n\n.detail-member-row .member-icon {\n font-size: 0.72rem;\n color: var(--mj-text-muted);\n width: 16px;\n text-align: center;\n flex-shrink: 0;\n}\n\n.detail-member-row.active .member-icon {\n color: var(--mj-brand-primary);\n}\n\n.member-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Footer */\n.detail-footer {\n padding: 12px 16px;\n border-top: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.detail-open-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n width: 100%;\n padding: 8px 16px;\n border: none;\n border-radius: 8px;\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n font-size: 0.82rem;\n font-weight: 500;\n cursor: pointer;\n transition: background 0.15s ease;\n}\n\n.detail-open-btn:hover {\n background: var(--mj-brand-primary-hover);\n}\n\n.detail-open-btn i {\n font-size: 0.75rem;\n}\n"] }]
|
|
1300
1345
|
}], null, { Points: [{
|
|
1301
1346
|
type: Input
|
|
1302
1347
|
}], Clusters: [{
|
|
@@ -1315,6 +1360,12 @@ export class ClusterScatterComponent {
|
|
|
1315
1360
|
type: Input
|
|
1316
1361
|
}], TooltipFields: [{
|
|
1317
1362
|
type: Input
|
|
1363
|
+
}], FieldPriority: [{
|
|
1364
|
+
type: Input
|
|
1365
|
+
}], FieldDisplayNames: [{
|
|
1366
|
+
type: Input
|
|
1367
|
+
}], EntityName: [{
|
|
1368
|
+
type: Input
|
|
1318
1369
|
}], ColorPalette: [{
|
|
1319
1370
|
type: Input
|
|
1320
1371
|
}], EnableZoom: [{
|
|
@@ -1349,6 +1400,8 @@ export class ClusterScatterComponent {
|
|
|
1349
1400
|
type: Output
|
|
1350
1401
|
}], ViewportChanged: [{
|
|
1351
1402
|
type: Output
|
|
1403
|
+
}], LabelEdited: [{
|
|
1404
|
+
type: Output
|
|
1352
1405
|
}], svgRef: [{
|
|
1353
1406
|
type: ViewChild,
|
|
1354
1407
|
args: ['svgElement', { static: false }]
|