@memberjunction/ng-clustering 5.23.0 → 5.25.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.
@@ -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.GetViewBoxString());
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.ɵɵelementStart(2, "span", 26);
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(2);
207
- i0.ɵɵtextInterpolate(cluster_r13.Label);
208
- i0.ɵɵadvance(2);
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", 28)(1, "div", 29);
266
- i0.ɵɵconditionalCreate(2, ClusterScatterComponent_Conditional_5_Conditional_2_Template, 2, 2, "div", 30);
267
- i0.ɵɵelementStart(3, "div", 31)(4, "div", 32);
268
- i0.ɵɵtext(5);
269
- i0.ɵɵelementEnd();
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.ɵɵconditionalCreate(10, ClusterScatterComponent_Conditional_5_Conditional_10_Template, 3, 0, "div", 36);
277
- i0.ɵɵelementStart(11, "div", 37);
278
- i0.ɵɵelement(12, "i", 38);
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.ɵɵconditional((ctx_r1.HoveredPoint.Metadata == null ? null : ctx_r1.HoveredPoint.Metadata["Entity"]) ? 6 : -1);
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", 42);
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", 45);
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", 65);
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", 62);
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", 63);
347
- i0.ɵɵelementStart(2, "span", 64);
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", 55);
361
- i0.ɵɵrepeaterCreate(1, ClusterScatterComponent_Conditional_7_Conditional_20_For_2_Template, 4, 4, "div", 61, i0.ɵɵcomponentInstance().TrackMemberBy, true);
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 _r15 = i0.ɵɵgetCurrentView();
370
- i0.ɵɵelementStart(0, "div", 40)(1, "div", 41);
371
- i0.ɵɵconditionalCreate(2, ClusterScatterComponent_Conditional_7_Conditional_2_Template, 2, 2, "div", 42);
372
- i0.ɵɵelementStart(3, "div", 43)(4, "div", 44);
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", 45);
322
+ i0.ɵɵconditionalCreate(6, ClusterScatterComponent_Conditional_7_Conditional_6_Template, 2, 1, "span", 43);
376
323
  i0.ɵɵelementEnd()();
377
- i0.ɵɵelementStart(7, "button", 46);
378
- i0.ɵɵlistener("click", function ClusterScatterComponent_Conditional_7_Template_button_click_7_listener() { i0.ɵɵrestoreView(_r15); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.CloseDetailPanel()); });
379
- i0.ɵɵelement(8, "i", 47);
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", 48);
382
- i0.ɵɵelement(10, "span", 49);
328
+ i0.ɵɵelementStart(9, "div", 46);
329
+ i0.ɵɵelement(10, "span", 47);
383
330
  i0.ɵɵtext(11);
384
331
  i0.ɵɵelementEnd();
385
- i0.ɵɵconditionalCreate(12, ClusterScatterComponent_Conditional_7_Conditional_12_Template, 3, 0, "div", 50);
386
- i0.ɵɵelementStart(13, "div", 51)(14, "button", 52);
387
- i0.ɵɵlistener("click", function ClusterScatterComponent_Conditional_7_Template_button_click_14_listener() { i0.ɵɵrestoreView(_r15); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.ToggleClusterMembers()); });
388
- i0.ɵɵelement(15, "i", 53);
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", 54);
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", 55);
342
+ i0.ɵɵconditionalCreate(20, ClusterScatterComponent_Conditional_7_Conditional_20_Template, 3, 0, "div", 53);
396
343
  i0.ɵɵelementEnd();
397
- i0.ɵɵelementStart(21, "div", 56)(22, "button", 57);
398
- i0.ɵɵlistener("click", function ClusterScatterComponent_Conditional_7_Template_button_click_22_listener() { i0.ɵɵrestoreView(_r15); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnOpenRecordClick()); });
399
- i0.ɵɵelement(23, "i", 38);
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.ɵɵconditional(ctx_r1.DetailEntries.length > 0 ? 12 : -1);
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.ViewBox = [0, 0, 1000, 700];
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
- this.DetailEntries = Object.entries(point.Metadata)
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
- this.TooltipEntries = entries
1286
+ mapped = entries
1257
1287
  .filter(([key]) => this.TooltipFields.includes(key))
1258
1288
  .map(([Key, Value]) => ({ Key, Value }));
1259
1289
  }
1260
1290
  else {
1261
- this.TooltipEntries = entries.map(([Key, Value]) => ({ Key, Value }));
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, "tooltip"], [1, "tt-header"], [1, "tt-icon"], [1, "tt-header-text"], [1, "tt-name"], [1, "tt-entity"], [1, "tt-cluster"], [1, "fa-solid", "fa-circle", 2, "font-size", "7px", "vertical-align", "middle"], [1, "tt-fields"], [1, "tt-action"], [1, "fa-solid", "fa-arrow-up-right-from-square"], [1, "tt-field-row"], [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"], [1, "detail-fields"], [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-field"], [1, "detail-field-key"], [1, "detail-field-value"], [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) {
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, 14, 9, "div", 6);
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, 13);
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 }]