@memberjunction/ng-clustering 5.43.0 → 5.45.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.
@@ -1 +1 @@
1
- {"version":3,"file":"cluster-config-panel.component.js","sourceRoot":"","sources":["../../src/lib/cluster-config-panel.component.ts","../../src/lib/cluster-config-panel.component.html"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Floating glass-morphism configuration panel for clustering.\n *\n * `ClusterConfigPanelComponent` provides a draggable floating panel with\n * entity picker, algorithm picker, parameter sliders, distance metric\n * selector, max records input, run button, and result metrics display.\n *\n * It emits cancelable `BeforeXXX` events so consumers can intercept and\n * validate before clustering starts or a visualization is saved.\n *\n * @example\n * ```html\n * <mj-cluster-config-panel\n * [IsRunning]=\"isRunning\"\n * [Metrics]=\"result?.Metrics ?? null\"\n * [EntityOptions]=\"myEntityOptions\"\n * [ShowSaveButton]=\"true\"\n * [DefaultAlgorithm]=\"'dbscan'\"\n * [Collapsed]=\"false\"\n * (BeforeRunClustering)=\"onBeforeRun($event)\"\n * (RunClustering)=\"onRunClustering($event)\"\n * (ConfigChanged)=\"onConfigChanged($event)\"\n * (SaveVisualization)=\"onSave()\">\n * </mj-cluster-config-panel>\n * ```\n */\n\nimport {\n Component,\n Input,\n Output,\n EventEmitter,\n ElementRef,\n ChangeDetectorRef,\n OnInit,\n OnDestroy,\n ViewChild,\n inject,\n} from '@angular/core';\nimport {\n ClusterConfig,\n ClusterConfigPanelEntityOption,\n ClusterConfigPanelEntityDocOption,\n ClusterMetrics,\n ClusterAlgorithm,\n ClusterDistanceMetric,\n CancelableEvent,\n DefaultClusterConfig,\n} from './clustering.types';\n\n@Component({\n standalone: false,\n selector: 'mj-cluster-config-panel',\n templateUrl: './cluster-config-panel.component.html',\n styleUrls: ['./cluster-config-panel.component.css'],\n})\nexport class ClusterConfigPanelComponent implements OnInit, OnDestroy {\n private cdr = inject(ChangeDetectorRef);\n\n // ================================================================\n // Data Inputs\n // ================================================================\n\n /**\n * Whether a clustering run is currently in progress.\n *\n * When `true`, the Run button is disabled and shows a spinner.\n *\n * @default false\n */\n @Input() IsRunning = false;\n\n /**\n * Result metrics from the most recent clustering run.\n *\n * When non-null, the metrics section is displayed at the bottom\n * of the panel showing silhouette score, cluster count, etc.\n */\n @Input() Metrics: ClusterMetrics | null = null;\n\n /**\n * Entity options for the dropdown.\n *\n * The parent component is responsible for populating this list,\n * typically from MJ Metadata or a subset filtered by\n * `AvailableEntities`.\n */\n private _entityOptions: ClusterConfigPanelEntityOption[] = [];\n\n @Input()\n set EntityOptions(value: ClusterConfigPanelEntityOption[]) {\n this._entityOptions = value || [];\n this.rebuildFilteredEntities();\n }\n get EntityOptions(): ClusterConfigPanelEntityOption[] {\n return this._entityOptions;\n }\n\n /** Entity Document options for the selected entity. Only shown when 2+ docs exist. */\n @Input() EntityDocOptions: ClusterConfigPanelEntityDocOption[] = [];\n\n /**\n * All Entity Documents across entities (each with `EntityName`), used to power\n * the optional multi-entity source selector. When two or more are selected the\n * run becomes a multi-entity analysis (their vectors are merged server-side).\n */\n @Input() AllEntityDocOptions: ClusterConfigPanelEntityDocOption[] = [];\n\n /** Whether to show the 2D/3D projection toggle. @default true */\n @Input() ShowDimensionToggle = true;\n\n /** Whether to show the multi-entity source selector. @default true */\n @Input() ShowMultiEntity = true;\n\n // ================================================================\n // Customization Inputs\n // ================================================================\n\n /**\n * Whether to display the \"Save this visualization\" link.\n *\n * @default true\n */\n @Input() ShowSaveButton = true;\n\n /**\n * Whether to display the algorithm picker dropdown.\n *\n * When `false`, the algorithm is fixed to `DefaultAlgorithm`.\n *\n * @default true\n */\n @Input() ShowAlgorithmPicker = true;\n\n /**\n * Whether to display the distance metric picker dropdown.\n *\n * @default true\n */\n @Input() ShowMetricPicker = true;\n\n /**\n * Default algorithm selection when the panel initializes.\n *\n * @default 'kmeans'\n */\n @Input() DefaultAlgorithm: ClusterAlgorithm = 'kmeans';\n\n /**\n * Restrict the entity picker to a specific list of entity names.\n *\n * When empty (the default), all entity options from the\n * `EntityOptions` input are shown. When non-empty, only entities\n * whose `Name` appears in this array are shown.\n *\n * @default []\n *\n * @example\n * ```html\n * <mj-cluster-config-panel\n * [AvailableEntities]=\"['Companies', 'Contacts', 'Deals']\">\n * </mj-cluster-config-panel>\n * ```\n */\n @Input() AvailableEntities: string[] = [];\n\n /**\n * Whether the panel starts in a collapsed (minimized) state.\n *\n * The user can toggle between collapsed and expanded by clicking\n * the collapse chevron in the panel header.\n *\n * @default false\n */\n @Input() Collapsed = false;\n\n // ================================================================\n // Standard Outputs\n // ================================================================\n\n /**\n * Emitted when the user clicks Run and `BeforeRunClustering` was not\n * canceled. Payload is a snapshot of the current config.\n */\n @Output() RunClustering = new EventEmitter<ClusterConfig>();\n\n /**\n * Emitted when the user clicks the Save link and `BeforeSave` was\n * not canceled.\n */\n @Output() SaveVisualization = new EventEmitter<void>();\n\n // ================================================================\n // Cancelable Outputs\n // ================================================================\n\n /**\n * Fires **before** a clustering run starts. Set `event.Cancel = true`\n * to prevent the run (e.g., for validation).\n *\n * @example\n * ```typescript\n * onBeforeRun(event: CancelableEvent<ClusterConfig>): void {\n * if (!event.Data.EntityName) {\n * event.Cancel = true;\n * alert('Please select an entity first.');\n * }\n * }\n * ```\n */\n @Output() BeforeRunClustering = new EventEmitter<CancelableEvent<ClusterConfig>>();\n\n /**\n * Fires **before** a visualization is saved. Set `event.Cancel = true`\n * to prevent the save (e.g., to prompt for a name first).\n */\n @Output() BeforeSave = new EventEmitter<CancelableEvent<ClusterConfig>>();\n\n // ================================================================\n // Informational Outputs\n // ================================================================\n\n /**\n * Fires whenever any configuration value changes (entity, algorithm,\n * K, epsilon, distance metric, max records, filter).\n *\n * Payload is a snapshot of the entire config at the time of change.\n */\n @Output() ConfigChanged = new EventEmitter<ClusterConfig>();\n\n /**\n * Fires specifically when the algorithm selection changes.\n *\n * Useful for showing/hiding algorithm-specific UI in the parent.\n */\n @Output() AlgorithmChanged = new EventEmitter<ClusterAlgorithm>();\n\n /**\n * Fires when the 2D/3D projection toggle changes. Hosts should re-run (or\n * re-project) so the change is reflected immediately — a 3D projection needs\n * a Z coordinate that only a fresh run produces.\n */\n @Output() DimensionsChanged = new EventEmitter<2 | 3>();\n\n // ================================================================\n // Internal State\n // ================================================================\n\n /** The working configuration object. */\n public Config: ClusterConfig = DefaultClusterConfig();\n\n /** Filtered entity list for display (filtered by AvailableEntities). */\n public FilteredEntityOptions: ClusterConfigPanelEntityOption[] = [];\n\n /** Whether the panel body is currently visible. */\n public IsExpanded = true;\n\n /** Whether the multi-entity source selector is expanded. */\n public ShowMultiSource = false;\n\n /** Algorithm options for the dropdown. */\n public AlgorithmOptions: Array<{ Value: ClusterAlgorithm; Label: string }> = [\n { Value: 'kmeans', Label: 'K-Means' },\n { Value: 'dbscan', Label: 'DBSCAN' },\n ];\n\n /** Distance metric options for the dropdown. */\n public MetricOptions: Array<{ Value: ClusterDistanceMetric; Label: string }> = [\n { Value: 'cosine', Label: 'Cosine' },\n { Value: 'euclidean', Label: 'Euclidean' },\n { Value: 'dotproduct', Label: 'Dot Product' },\n ];\n\n // Dragging state\n /** Current panel top offset in pixels. */\n public PanelTop = 12;\n /** Current panel left offset in pixels. */\n public PanelLeft = 12;\n private isDragging = false;\n private dragStartX = 0;\n private dragStartY = 0;\n private dragStartTop = 0;\n private dragStartLeft = 0;\n private boundOnMouseMove: ((e: MouseEvent) => void) | null = null;\n private boundOnMouseUp: ((e: MouseEvent) => void) | null = null;\n\n @ViewChild('panelElement', { static: false }) private panelRef!: ElementRef<HTMLDivElement>;\n\n // ================================================================\n // Lifecycle\n // ================================================================\n\n ngOnInit(): void {\n // Apply defaults\n this.Config.Algorithm = this.DefaultAlgorithm;\n this.IsExpanded = !this.Collapsed;\n\n this.rebuildFilteredEntities();\n\n this.boundOnMouseMove = this.onMouseMove.bind(this);\n this.boundOnMouseUp = this.onMouseUp.bind(this);\n document.addEventListener('mousemove', this.boundOnMouseMove);\n document.addEventListener('mouseup', this.boundOnMouseUp);\n }\n\n ngOnDestroy(): void {\n if (this.boundOnMouseMove) document.removeEventListener('mousemove', this.boundOnMouseMove);\n if (this.boundOnMouseUp) document.removeEventListener('mouseup', this.boundOnMouseUp);\n }\n\n // ================================================================\n // Public Template Methods\n // ================================================================\n\n /**\n * Whether the current algorithm is K-Means (controls which\n * parameter sliders are shown).\n */\n public get IsKMeans(): boolean {\n return this.Config.Algorithm === 'kmeans';\n }\n\n /**\n * Handle algorithm dropdown change.\n *\n * @param value New algorithm value string.\n */\n public OnAlgorithmChanged(value: string): void {\n const newAlg = value as ClusterAlgorithm;\n this.Config.Algorithm = newAlg;\n this.AlgorithmChanged.emit(newAlg);\n this.emitConfigChanged();\n this.cdr.detectChanges();\n }\n\n /**\n * Handle distance metric dropdown change.\n *\n * @param value New metric value string.\n */\n public OnMetricChanged(value: string): void {\n this.Config.DistanceMetric = value as ClusterDistanceMetric;\n this.emitConfigChanged();\n }\n\n /**\n * Handle entity dropdown change.\n *\n * @param value New entity name.\n */\n public OnEntityChanged(value: string): void {\n this.Config.EntityName = value;\n this.emitConfigChanged();\n }\n\n /**\n * Handle changes to any numeric/text config field (K, Epsilon,\n * MinPoints, MaxRecords, Filter).\n */\n public OnFieldChanged(): void {\n this.emitConfigChanged();\n }\n\n // ----- Multi-entity / dimensions / color-by -----\n\n /** The currently selected entity-document IDs for multi-entity clustering. */\n public get SelectedDocIDs(): string[] {\n return this.Config.EntityDocumentIDs ?? [];\n }\n\n /** True when two or more source documents are selected (multi-entity mode). */\n public get IsMultiEntity(): boolean {\n return this.SelectedDocIDs.length >= 2;\n }\n\n /** Whether there are enough documents to offer multi-entity selection. */\n public get HasMultiSourceOptions(): boolean {\n return this.ShowMultiEntity && this.AllEntityDocOptions.length > 1;\n }\n\n /** Whether a given document is currently selected. */\n public IsDocSelected(id: string): boolean {\n return this.SelectedDocIDs.includes(id);\n }\n\n /** Toggle a document in/out of the multi-entity selection. */\n public ToggleDoc(id: string): void {\n const set = new Set(this.Config.EntityDocumentIDs ?? []);\n if (set.has(id)) {\n set.delete(id);\n } else {\n set.add(id);\n }\n this.Config.EntityDocumentIDs = [...set];\n // Once a multi-entity selection exists, color-by-entity is the useful default.\n if (this.IsMultiEntity && this.Config.ColorBy !== 'entity') {\n this.Config.ColorBy = 'entity';\n }\n this.emitConfigChanged();\n this.cdr.detectChanges();\n }\n\n /** Toggle the multi-source expander. */\n public ToggleMultiSource(): void {\n this.ShowMultiSource = !this.ShowMultiSource;\n this.cdr.detectChanges();\n }\n\n /** Set the projection dimensionality (2 or 3). */\n public SetDimensions(d: 2 | 3): void {\n if (this.Config.Dimensions === d) return;\n this.Config.Dimensions = d;\n this.emitConfigChanged();\n this.DimensionsChanged.emit(d);\n this.cdr.detectChanges();\n }\n\n /** Set the legend/color mode. */\n public SetColorBy(mode: 'cluster' | 'entity'): void {\n this.Config.ColorBy = mode;\n this.emitConfigChanged();\n this.cdr.detectChanges();\n }\n\n /** Whether the Run button can be clicked. */\n public get CanRun(): boolean {\n return !this.IsRunning && (!!this.Config.EntityName || this.SelectedDocIDs.length > 0);\n }\n\n /** Whether the Save button should be offered (there are results to save). */\n public get CanSave(): boolean {\n return this.ShowSaveButton && !!this.Metrics && this.Metrics.RecordCount > 0;\n }\n\n /**\n * Handle Run button click. Fires `BeforeRunClustering`, and if not\n * canceled, emits `RunClustering`.\n */\n public OnRun(): void {\n if (!this.CanRun) return;\n\n const snapshot = { ...this.Config };\n const cancelable: CancelableEvent<ClusterConfig> = { Data: snapshot, Cancel: false };\n this.BeforeRunClustering.emit(cancelable);\n if (cancelable.Cancel) return;\n\n this.RunClustering.emit(snapshot);\n }\n\n /**\n * Handle Save link click. Fires `BeforeSave`, and if not canceled,\n * emits `SaveVisualization`.\n */\n public OnSave(): void {\n const cancelable: CancelableEvent<ClusterConfig> = { Data: { ...this.Config }, Cancel: false };\n this.BeforeSave.emit(cancelable);\n if (cancelable.Cancel) return;\n\n this.SaveVisualization.emit();\n }\n\n /**\n * Toggle the panel between expanded and collapsed states.\n */\n public ToggleCollapse(): void {\n this.IsExpanded = !this.IsExpanded;\n this.cdr.detectChanges();\n }\n\n /**\n * Format a silhouette score to 2 decimal places.\n *\n * @param score The raw silhouette score.\n * @returns Formatted string (e.g., `\"0.72\"`).\n */\n public FormatSilhouette(score: number): string {\n return score.toFixed(2);\n }\n\n /**\n * Whether a silhouette score indicates good clustering quality.\n *\n * @param score The silhouette score to evaluate.\n * @returns `true` if score >= 0.5.\n */\n public IsSilhouetteGood(score: number): boolean {\n return score >= 0.5;\n }\n\n /**\n * Format a computation time in milliseconds to a human-readable string.\n *\n * @param ms Milliseconds.\n * @returns e.g., `\"1.2s\"` or `\"450ms\"`.\n */\n public FormatTime(ms: number): string {\n return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;\n }\n\n /**\n * Start dragging the panel via header mousedown.\n *\n * @param event The native mouse event.\n */\n public OnHeaderMouseDown(event: MouseEvent): void {\n event.preventDefault();\n this.isDragging = true;\n this.dragStartX = event.clientX;\n this.dragStartY = event.clientY;\n this.dragStartTop = this.PanelTop;\n this.dragStartLeft = this.PanelLeft;\n }\n\n /** @internal TrackBy for algorithm options. */\n public TrackAlgorithmBy(_index: number, item: { Value: string }): string {\n return item.Value;\n }\n\n /** @internal TrackBy for metric options. */\n public TrackMetricBy(_index: number, item: { Value: string }): string {\n return item.Value;\n }\n\n /** @internal TrackBy for entity options. */\n public TrackEntityBy(_index: number, item: ClusterConfigPanelEntityOption): string {\n return item.Name;\n }\n\n // ================================================================\n // Private Methods\n // ================================================================\n\n /**\n * Rebuild the filtered entity list based on `AvailableEntities`.\n */\n private rebuildFilteredEntities(): void {\n if (this.AvailableEntities.length > 0) {\n const allowedSet = new Set(this.AvailableEntities);\n this.FilteredEntityOptions = this.EntityOptions.filter(e => allowedSet.has(e.Name));\n } else {\n this.FilteredEntityOptions = this.EntityOptions;\n }\n\n if (this.FilteredEntityOptions.length > 0 && !this.Config.EntityName) {\n this.Config.EntityName = this.FilteredEntityOptions[0].Name;\n }\n }\n\n /** Emit `ConfigChanged` with a snapshot of the current config. */\n private emitConfigChanged(): void {\n this.ConfigChanged.emit({ ...this.Config });\n }\n\n private onMouseMove(event: MouseEvent): void {\n if (!this.isDragging) return;\n const dx = event.clientX - this.dragStartX;\n const dy = event.clientY - this.dragStartY;\n this.PanelTop = Math.max(0, this.dragStartTop + dy);\n this.PanelLeft = Math.max(0, this.dragStartLeft + dx);\n this.cdr.detectChanges();\n }\n\n private onMouseUp(_event: MouseEvent): void {\n this.isDragging = false;\n }\n}\n","<div #panelElement\n class=\"config-panel\"\n [style.top.px]=\"PanelTop\"\n [style.left.px]=\"PanelLeft\">\n\n <!-- Draggable header -->\n <h3 class=\"panel-header\"\n (mousedown)=\"OnHeaderMouseDown($event)\">\n <i class=\"fa-solid fa-sliders\"></i>\n Configuration\n <span class=\"header-actions\">\n <i class=\"fa-solid\"\n [class.fa-chevron-up]=\"IsExpanded\"\n [class.fa-chevron-down]=\"!IsExpanded\"\n (click)=\"ToggleCollapse(); $event.stopPropagation()\"\n title=\"Toggle panel\"></i>\n <i class=\"fa-solid fa-grip-dots-vertical drag-handle\"></i>\n </span>\n </h3>\n\n @if (IsExpanded) {\n <!-- Entity picker -->\n <div class=\"form-group\">\n <label>Entity</label>\n <select [ngModel]=\"Config.EntityName\"\n (ngModelChange)=\"OnEntityChanged($event)\">\n @for (entity of FilteredEntityOptions; track TrackEntityBy($index, entity)) {\n <option [value]=\"entity.Name\">{{ entity.Name }}</option>\n }\n </select>\n </div>\n\n <!-- Entity Document selector (only shown when 2+ docs for selected entity) -->\n @if (EntityDocOptions.length > 1) {\n <div class=\"form-group\">\n <label>Document Template</label>\n <select [ngModel]=\"Config.EntityDocumentID\"\n (ngModelChange)=\"Config.EntityDocumentID = $event; OnFieldChanged()\">\n @for (doc of EntityDocOptions; track doc.ID) {\n <option [value]=\"doc.ID\">{{ doc.Name }}</option>\n }\n </select>\n </div>\n }\n\n <!-- Multi-entity source selector -->\n @if (HasMultiSourceOptions) {\n <div class=\"form-group multi-source\">\n <button type=\"button\" class=\"multi-source-toggle\" (click)=\"ToggleMultiSource()\">\n <i class=\"fa-solid\" [class.fa-chevron-down]=\"!ShowMultiSource\" [class.fa-chevron-up]=\"ShowMultiSource\"></i>\n Multiple sources\n @if (IsMultiEntity) {\n <span class=\"multi-source-badge\">{{ SelectedDocIDs.length }}</span>\n }\n </button>\n @if (ShowMultiSource) {\n <div class=\"multi-source-list\">\n <div class=\"multi-source-hint\">Select 2+ documents to cluster them together. All must use the same embedding model.</div>\n @for (doc of AllEntityDocOptions; track doc.ID) {\n <label class=\"multi-source-item\">\n <input type=\"checkbox\" [checked]=\"IsDocSelected(doc.ID)\" (change)=\"ToggleDoc(doc.ID)\">\n <span class=\"multi-source-name\">{{ doc.Name }}</span>\n @if (doc.EntityName) { <span class=\"multi-source-entity\">{{ doc.EntityName }}</span> }\n </label>\n }\n </div>\n }\n </div>\n }\n\n <!-- Projection (2D / 3D) + Color-by toggles -->\n <div class=\"form-row toggle-row\">\n @if (ShowDimensionToggle) {\n <div class=\"form-group\">\n <label>Projection</label>\n <div class=\"seg-toggle\" role=\"group\">\n <button type=\"button\" class=\"seg-btn\" [class.active]=\"(Config.Dimensions ?? 2) === 2\" (click)=\"SetDimensions(2)\">2D</button>\n <button type=\"button\" class=\"seg-btn\" [class.active]=\"Config.Dimensions === 3\" (click)=\"SetDimensions(3)\">3D</button>\n </div>\n </div>\n }\n <div class=\"form-group\">\n <label>Color by</label>\n <div class=\"seg-toggle\" role=\"group\">\n <button type=\"button\" class=\"seg-btn\" [class.active]=\"(Config.ColorBy ?? 'cluster') === 'cluster'\" (click)=\"SetColorBy('cluster')\">Cluster</button>\n <button type=\"button\" class=\"seg-btn\" [class.active]=\"Config.ColorBy === 'entity'\" (click)=\"SetColorBy('entity')\">Entity</button>\n </div>\n </div>\n </div>\n\n <!-- Filter -->\n <div class=\"form-group\">\n <label>Filter (optional)</label>\n <input type=\"text\"\n [(ngModel)]=\"Config.Filter\"\n (ngModelChange)=\"OnFieldChanged()\"\n placeholder=\"e.g. Status = 'Active'\">\n </div>\n\n <!-- Algorithm + K/Epsilon row -->\n <div class=\"form-row\">\n @if (ShowAlgorithmPicker) {\n <div class=\"form-group\">\n <label>Algorithm</label>\n <select [ngModel]=\"Config.Algorithm\"\n (ngModelChange)=\"OnAlgorithmChanged($event)\">\n @for (alg of AlgorithmOptions; track TrackAlgorithmBy($index, alg)) {\n <option [value]=\"alg.Value\">{{ alg.Label }}</option>\n }\n </select>\n </div>\n }\n\n @if (IsKMeans) {\n <div class=\"form-group\">\n <label>Clusters (K): {{ Config.K }}</label>\n <input type=\"range\"\n [ngModel]=\"Config.K\"\n (ngModelChange)=\"Config.K = $event; OnFieldChanged()\"\n [min]=\"2\"\n [max]=\"20\"\n [step]=\"1\">\n </div>\n } @else {\n <div class=\"form-group\">\n <label>Epsilon: {{ Config.Epsilon }}</label>\n <input type=\"range\"\n [ngModel]=\"Config.Epsilon\"\n (ngModelChange)=\"Config.Epsilon = $event; OnFieldChanged()\"\n [min]=\"0.05\"\n [max]=\"0.95\"\n [step]=\"0.05\">\n </div>\n }\n </div>\n\n <!-- DBSCAN MinPoints row (only visible for DBSCAN) -->\n @if (!IsKMeans) {\n <div class=\"form-group\">\n <label>Min Points: {{ Config.MinPoints }}</label>\n <input type=\"range\"\n [ngModel]=\"Config.MinPoints\"\n (ngModelChange)=\"Config.MinPoints = $event; OnFieldChanged()\"\n [min]=\"2\"\n [max]=\"20\"\n [step]=\"1\">\n </div>\n }\n\n <!-- Distance metric + Max records row -->\n <div class=\"form-row\">\n @if (ShowMetricPicker) {\n <div class=\"form-group\">\n <label>Distance Metric</label>\n <select [ngModel]=\"Config.DistanceMetric\"\n (ngModelChange)=\"OnMetricChanged($event)\">\n @for (metric of MetricOptions; track TrackMetricBy($index, metric)) {\n <option [value]=\"metric.Value\">{{ metric.Label }}</option>\n }\n </select>\n </div>\n }\n <div class=\"form-group\">\n <label>Max Records</label>\n <input type=\"number\"\n [(ngModel)]=\"Config.MaxRecords\"\n (ngModelChange)=\"OnFieldChanged()\"\n [min]=\"10\"\n [max]=\"5000\">\n </div>\n </div>\n\n <!-- Run + Save action row -->\n <div class=\"action-row\">\n <button class=\"run-btn\"\n [disabled]=\"!CanRun\"\n (click)=\"OnRun()\">\n @if (IsRunning) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i> Running...\n } @else {\n <i class=\"fa-solid fa-play\"></i> Run Clustering\n }\n </button>\n @if (CanSave) {\n <button class=\"save-btn\" (click)=\"OnSave()\" title=\"Save this visualization\">\n <i class=\"fa-solid fa-bookmark\"></i> Save\n </button>\n }\n </div>\n\n <!-- Metrics display -->\n @if (Metrics && Metrics.RecordCount > 0) {\n <div class=\"metrics-section\">\n <div class=\"metric-item\">\n <span class=\"metric-label\">Silhouette</span>\n <span class=\"metric-value\"\n [class.good]=\"IsSilhouetteGood(Metrics.SilhouetteScore)\">\n {{ FormatSilhouette(Metrics.SilhouetteScore) }}\n </span>\n </div>\n <div class=\"metric-item\">\n <span class=\"metric-label\">Clusters</span>\n <span class=\"metric-value\">{{ Metrics.ClusterCount }}</span>\n </div>\n <div class=\"metric-item\">\n <span class=\"metric-label\">Records</span>\n <span class=\"metric-value\">{{ Metrics.RecordCount }}</span>\n </div>\n <div class=\"metric-item\">\n <span class=\"metric-label\">Time</span>\n <span class=\"metric-value\">{{ FormatTime(Metrics.ComputationTimeMs) }}</span>\n </div>\n @if (Metrics.OutlierCount > 0) {\n <div class=\"metric-item\">\n <span class=\"metric-label\">Outliers</span>\n <span class=\"metric-value\">{{ Metrics.OutlierCount }}</span>\n </div>\n }\n </div>\n }\n\n }\n</div>\n"]}
1
+ {"version":3,"file":"cluster-config-panel.component.js","sourceRoot":"","sources":["../../src/lib/cluster-config-panel.component.ts","../../src/lib/cluster-config-panel.component.html"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Floating glass-morphism configuration panel for clustering.\n *\n * `ClusterConfigPanelComponent` provides a draggable floating panel with\n * entity picker, algorithm picker, parameter sliders, distance metric\n * selector, max records input, run button, and result metrics display.\n *\n * It emits cancelable `BeforeXXX` events so consumers can intercept and\n * validate before clustering starts or a visualization is saved.\n *\n * @example\n * ```html\n * <mj-cluster-config-panel\n * [IsRunning]=\"isRunning\"\n * [Metrics]=\"result?.Metrics ?? null\"\n * [EntityOptions]=\"myEntityOptions\"\n * [ShowSaveButton]=\"true\"\n * [DefaultAlgorithm]=\"'dbscan'\"\n * [Collapsed]=\"false\"\n * (BeforeRunClustering)=\"onBeforeRun($event)\"\n * (RunClustering)=\"onRunClustering($event)\"\n * (ConfigChanged)=\"onConfigChanged($event)\"\n * (SaveVisualization)=\"onSave()\">\n * </mj-cluster-config-panel>\n * ```\n */\n\nimport {\n Component,\n Input,\n Output,\n EventEmitter,\n ElementRef,\n ChangeDetectorRef,\n OnInit,\n OnDestroy,\n ViewChild,\n inject,\n} from '@angular/core';\nimport {\n ClusterConfig,\n ClusterConfigPanelEntityOption,\n ClusterConfigPanelEntityDocOption,\n ClusterMetrics,\n ClusterAlgorithm,\n ClusterDistanceMetric,\n CancelableEvent,\n DefaultClusterConfig,\n} from './clustering.types';\n\n@Component({\n standalone: false,\n selector: 'mj-cluster-config-panel',\n templateUrl: './cluster-config-panel.component.html',\n styleUrls: ['./cluster-config-panel.component.css'],\n})\nexport class ClusterConfigPanelComponent implements OnInit, OnDestroy {\n private cdr = inject(ChangeDetectorRef);\n\n // ================================================================\n // Data Inputs\n // ================================================================\n\n /**\n * Whether a clustering run is currently in progress.\n *\n * When `true`, the Run button is disabled and shows a spinner.\n *\n * @default false\n */\n @Input() IsRunning = false;\n\n /**\n * Result metrics from the most recent clustering run.\n *\n * When non-null, the metrics section is displayed at the bottom\n * of the panel showing silhouette score, cluster count, etc.\n */\n @Input() Metrics: ClusterMetrics | null = null;\n\n /**\n * Entity options for the dropdown.\n *\n * The parent component is responsible for populating this list,\n * typically from MJ Metadata or a subset filtered by\n * `AvailableEntities`.\n */\n private _entityOptions: ClusterConfigPanelEntityOption[] = [];\n\n @Input()\n set EntityOptions(value: ClusterConfigPanelEntityOption[]) {\n this._entityOptions = value || [];\n this.rebuildFilteredEntities();\n }\n get EntityOptions(): ClusterConfigPanelEntityOption[] {\n return this._entityOptions;\n }\n\n /** Entity Document options for the selected entity. Only shown when 2+ docs exist. */\n @Input() EntityDocOptions: ClusterConfigPanelEntityDocOption[] = [];\n\n /**\n * All Entity Documents across entities (each with `EntityName`), used to power\n * the optional multi-entity source selector. When two or more are selected the\n * run becomes a multi-entity analysis (their vectors are merged server-side).\n */\n @Input() AllEntityDocOptions: ClusterConfigPanelEntityDocOption[] = [];\n\n /** Whether to show the 2D/3D projection toggle. @default true */\n @Input() ShowDimensionToggle = true;\n\n /** Whether to show the multi-entity source selector. @default true */\n @Input() ShowMultiEntity = true;\n\n // ================================================================\n // Customization Inputs\n // ================================================================\n\n /**\n * Whether to display the \"Save this visualization\" link.\n *\n * @default true\n */\n @Input() ShowSaveButton = true;\n\n /**\n * Whether to display the algorithm picker dropdown.\n *\n * When `false`, the algorithm is fixed to `DefaultAlgorithm`.\n *\n * @default true\n */\n @Input() ShowAlgorithmPicker = true;\n\n /**\n * Whether to display the distance metric picker dropdown.\n *\n * @default true\n */\n @Input() ShowMetricPicker = true;\n\n /**\n * Default algorithm selection when the panel initializes.\n *\n * @default 'kmeans'\n */\n @Input() DefaultAlgorithm: ClusterAlgorithm = 'kmeans';\n\n /**\n * Restrict the entity picker to a specific list of entity names.\n *\n * When empty (the default), all entity options from the\n * `EntityOptions` input are shown. When non-empty, only entities\n * whose `Name` appears in this array are shown.\n *\n * @default []\n *\n * @example\n * ```html\n * <mj-cluster-config-panel\n * [AvailableEntities]=\"['Companies', 'Contacts', 'Deals']\">\n * </mj-cluster-config-panel>\n * ```\n */\n @Input() AvailableEntities: string[] = [];\n\n /**\n * Whether the panel starts in a collapsed (minimized) state.\n *\n * The user can toggle between collapsed and expanded by clicking\n * the collapse chevron in the panel header.\n *\n * @default false\n */\n @Input() Collapsed = false;\n\n // ================================================================\n // Standard Outputs\n // ================================================================\n\n /**\n * Emitted when the user clicks Run and `BeforeRunClustering` was not\n * canceled. Payload is a snapshot of the current config.\n */\n @Output() RunClustering = new EventEmitter<ClusterConfig>();\n\n /**\n * Emitted when the user clicks the Save link and `BeforeSave` was\n * not canceled.\n */\n @Output() SaveVisualization = new EventEmitter<void>();\n\n // ================================================================\n // Cancelable Outputs\n // ================================================================\n\n /**\n * Fires **before** a clustering run starts. Set `event.Cancel = true`\n * to prevent the run (e.g., for validation).\n *\n * @example\n * ```typescript\n * onBeforeRun(event: CancelableEvent<ClusterConfig>): void {\n * if (!event.Data.EntityName) {\n * event.Cancel = true;\n * alert('Please select an entity first.');\n * }\n * }\n * ```\n */\n @Output() BeforeRunClustering = new EventEmitter<CancelableEvent<ClusterConfig>>();\n\n /**\n * Fires **before** a visualization is saved. Set `event.Cancel = true`\n * to prevent the save (e.g., to prompt for a name first).\n */\n @Output() BeforeSave = new EventEmitter<CancelableEvent<ClusterConfig>>();\n\n // ================================================================\n // Informational Outputs\n // ================================================================\n\n /**\n * Fires whenever any configuration value changes (entity, algorithm,\n * K, epsilon, distance metric, max records, filter).\n *\n * Payload is a snapshot of the entire config at the time of change.\n */\n @Output() ConfigChanged = new EventEmitter<ClusterConfig>();\n\n /**\n * Fires specifically when the algorithm selection changes.\n *\n * Useful for showing/hiding algorithm-specific UI in the parent.\n */\n @Output() AlgorithmChanged = new EventEmitter<ClusterAlgorithm>();\n\n /**\n * Fires when the 2D/3D projection toggle changes. Hosts should re-run (or\n * re-project) so the change is reflected immediately — a 3D projection needs\n * a Z coordinate that only a fresh run produces.\n */\n @Output() DimensionsChanged = new EventEmitter<2 | 3>();\n\n // ================================================================\n // Internal State\n // ================================================================\n\n /** The working configuration object. */\n public Config: ClusterConfig = DefaultClusterConfig();\n\n /** Filtered entity list for display (filtered by AvailableEntities). */\n public FilteredEntityOptions: ClusterConfigPanelEntityOption[] = [];\n\n /** Whether the panel body is currently visible. */\n public IsExpanded = true;\n\n /** Whether the multi-entity source selector is expanded. */\n public ShowMultiSource = false;\n\n /** Algorithm options for the dropdown. */\n public AlgorithmOptions: Array<{ Value: ClusterAlgorithm; Label: string }> = [\n { Value: 'kmeans', Label: 'K-Means' },\n { Value: 'dbscan', Label: 'DBSCAN' },\n ];\n\n /** Distance metric options for the dropdown. */\n public MetricOptions: Array<{ Value: ClusterDistanceMetric; Label: string }> = [\n { Value: 'cosine', Label: 'Cosine' },\n { Value: 'euclidean', Label: 'Euclidean' },\n { Value: 'dotproduct', Label: 'Dot Product' },\n ];\n\n // Dragging state\n /** Current panel top offset in pixels. */\n public PanelTop = 12;\n /** Current panel left offset in pixels. */\n public PanelLeft = 12;\n private isDragging = false;\n private dragStartX = 0;\n private dragStartY = 0;\n private dragStartTop = 0;\n private dragStartLeft = 0;\n private boundOnMouseMove: ((e: MouseEvent) => void) | null = null;\n private boundOnMouseUp: ((e: MouseEvent) => void) | null = null;\n\n @ViewChild('panelElement', { static: false }) private panelRef!: ElementRef<HTMLDivElement>;\n\n // ================================================================\n // Lifecycle\n // ================================================================\n\n ngOnInit(): void {\n // Apply defaults\n this.Config.Algorithm = this.DefaultAlgorithm;\n this.IsExpanded = !this.Collapsed;\n\n this.rebuildFilteredEntities();\n\n this.boundOnMouseMove = this.onMouseMove.bind(this);\n this.boundOnMouseUp = this.onMouseUp.bind(this);\n document.addEventListener('mousemove', this.boundOnMouseMove);\n document.addEventListener('mouseup', this.boundOnMouseUp);\n }\n\n ngOnDestroy(): void {\n if (this.boundOnMouseMove) document.removeEventListener('mousemove', this.boundOnMouseMove);\n if (this.boundOnMouseUp) document.removeEventListener('mouseup', this.boundOnMouseUp);\n }\n\n // ================================================================\n // Public Template Methods\n // ================================================================\n\n /**\n * Whether the current algorithm is K-Means (controls which\n * parameter sliders are shown).\n */\n public get IsKMeans(): boolean {\n return this.Config.Algorithm === 'kmeans';\n }\n\n /**\n * Handle algorithm dropdown change.\n *\n * @param value New algorithm value string.\n */\n public OnAlgorithmChanged(value: string): void {\n const newAlg = value as ClusterAlgorithm;\n this.Config.Algorithm = newAlg;\n this.AlgorithmChanged.emit(newAlg);\n this.emitConfigChanged();\n this.cdr.detectChanges();\n }\n\n /**\n * Handle distance metric dropdown change.\n *\n * @param value New metric value string.\n */\n public OnMetricChanged(value: string): void {\n this.Config.DistanceMetric = value as ClusterDistanceMetric;\n this.emitConfigChanged();\n }\n\n /**\n * Handle entity dropdown change.\n *\n * @param value New entity name.\n */\n public OnEntityChanged(value: string): void {\n this.Config.EntityName = value;\n this.emitConfigChanged();\n }\n\n /**\n * Handle changes to any numeric/text config field (K, Epsilon,\n * MinPoints, MaxRecords, Filter).\n */\n public OnFieldChanged(): void {\n this.emitConfigChanged();\n }\n\n // ----- Multi-entity / dimensions / color-by -----\n\n /** The currently selected entity-document IDs for multi-entity clustering. */\n public get SelectedDocIDs(): string[] {\n return this.Config.EntityDocumentIDs ?? [];\n }\n\n /** True when two or more source documents are selected (multi-entity mode). */\n public get IsMultiEntity(): boolean {\n return this.SelectedDocIDs.length >= 2;\n }\n\n /** Whether there are enough documents to offer multi-entity selection. */\n public get HasMultiSourceOptions(): boolean {\n return this.ShowMultiEntity && this.AllEntityDocOptions.length > 1;\n }\n\n /** Whether a given document is currently selected. */\n public IsDocSelected(id: string): boolean {\n return this.SelectedDocIDs.includes(id);\n }\n\n /** Toggle a document in/out of the multi-entity selection. */\n public ToggleDoc(id: string): void {\n const set = new Set(this.Config.EntityDocumentIDs ?? []);\n if (set.has(id)) {\n set.delete(id);\n } else {\n set.add(id);\n }\n this.Config.EntityDocumentIDs = [...set];\n // Once a multi-entity selection exists, color-by-entity is the useful default.\n if (this.IsMultiEntity && this.Config.ColorBy !== 'entity') {\n this.Config.ColorBy = 'entity';\n }\n this.emitConfigChanged();\n this.cdr.detectChanges();\n }\n\n /** Handle the multi-source accordion's expand/collapse. */\n public OnMultiSourceExpandedChange(expanded: boolean): void {\n this.ShowMultiSource = expanded;\n this.cdr.detectChanges();\n }\n\n /** Set the projection dimensionality (2 or 3). */\n public SetDimensions(d: 2 | 3): void {\n if (this.Config.Dimensions === d) return;\n this.Config.Dimensions = d;\n this.emitConfigChanged();\n this.DimensionsChanged.emit(d);\n this.cdr.detectChanges();\n }\n\n /** Set the legend/color mode. */\n public SetColorBy(mode: 'cluster' | 'entity'): void {\n this.Config.ColorBy = mode;\n this.emitConfigChanged();\n this.cdr.detectChanges();\n }\n\n /** Whether the Run button can be clicked. */\n public get CanRun(): boolean {\n return !this.IsRunning && (!!this.Config.EntityName || this.SelectedDocIDs.length > 0);\n }\n\n /** Whether the Save button should be offered (there are results to save). */\n public get CanSave(): boolean {\n return this.ShowSaveButton && !!this.Metrics && this.Metrics.RecordCount > 0;\n }\n\n /**\n * Handle Run button click. Fires `BeforeRunClustering`, and if not\n * canceled, emits `RunClustering`.\n */\n public OnRun(): void {\n if (!this.CanRun) return;\n\n const snapshot = { ...this.Config };\n const cancelable: CancelableEvent<ClusterConfig> = { Data: snapshot, Cancel: false };\n this.BeforeRunClustering.emit(cancelable);\n if (cancelable.Cancel) return;\n\n this.RunClustering.emit(snapshot);\n }\n\n /**\n * Handle Save link click. Fires `BeforeSave`, and if not canceled,\n * emits `SaveVisualization`.\n */\n public OnSave(): void {\n const cancelable: CancelableEvent<ClusterConfig> = { Data: { ...this.Config }, Cancel: false };\n this.BeforeSave.emit(cancelable);\n if (cancelable.Cancel) return;\n\n this.SaveVisualization.emit();\n }\n\n /**\n * Toggle the panel between expanded and collapsed states.\n */\n public ToggleCollapse(): void {\n this.IsExpanded = !this.IsExpanded;\n this.cdr.detectChanges();\n }\n\n /**\n * Format a silhouette score to 2 decimal places.\n *\n * @param score The raw silhouette score.\n * @returns Formatted string (e.g., `\"0.72\"`).\n */\n public FormatSilhouette(score: number): string {\n return score.toFixed(2);\n }\n\n /**\n * Whether a silhouette score indicates good clustering quality.\n *\n * @param score The silhouette score to evaluate.\n * @returns `true` if score >= 0.5.\n */\n public IsSilhouetteGood(score: number): boolean {\n return score >= 0.5;\n }\n\n /**\n * Format a computation time in milliseconds to a human-readable string.\n *\n * @param ms Milliseconds.\n * @returns e.g., `\"1.2s\"` or `\"450ms\"`.\n */\n public FormatTime(ms: number): string {\n return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;\n }\n\n /**\n * Start dragging the panel via header mousedown.\n *\n * @param event The native mouse event.\n */\n public OnHeaderMouseDown(event: MouseEvent): void {\n event.preventDefault();\n this.isDragging = true;\n this.dragStartX = event.clientX;\n this.dragStartY = event.clientY;\n this.dragStartTop = this.PanelTop;\n this.dragStartLeft = this.PanelLeft;\n }\n\n /** @internal TrackBy for algorithm options. */\n public TrackAlgorithmBy(_index: number, item: { Value: string }): string {\n return item.Value;\n }\n\n /** @internal TrackBy for metric options. */\n public TrackMetricBy(_index: number, item: { Value: string }): string {\n return item.Value;\n }\n\n /** @internal TrackBy for entity options. */\n public TrackEntityBy(_index: number, item: ClusterConfigPanelEntityOption): string {\n return item.Name;\n }\n\n // ================================================================\n // Private Methods\n // ================================================================\n\n /**\n * Rebuild the filtered entity list based on `AvailableEntities`.\n */\n private rebuildFilteredEntities(): void {\n if (this.AvailableEntities.length > 0) {\n const allowedSet = new Set(this.AvailableEntities);\n this.FilteredEntityOptions = this.EntityOptions.filter(e => allowedSet.has(e.Name));\n } else {\n this.FilteredEntityOptions = this.EntityOptions;\n }\n\n if (this.FilteredEntityOptions.length > 0 && !this.Config.EntityName) {\n this.Config.EntityName = this.FilteredEntityOptions[0].Name;\n }\n }\n\n /** Emit `ConfigChanged` with a snapshot of the current config. */\n private emitConfigChanged(): void {\n this.ConfigChanged.emit({ ...this.Config });\n }\n\n private onMouseMove(event: MouseEvent): void {\n if (!this.isDragging) return;\n const dx = event.clientX - this.dragStartX;\n const dy = event.clientY - this.dragStartY;\n this.PanelTop = Math.max(0, this.dragStartTop + dy);\n this.PanelLeft = Math.max(0, this.dragStartLeft + dx);\n this.cdr.detectChanges();\n }\n\n private onMouseUp(_event: MouseEvent): void {\n this.isDragging = false;\n }\n}\n","<div #panelElement\n class=\"config-panel\"\n [style.top.px]=\"PanelTop\"\n [style.left.px]=\"PanelLeft\">\n\n <!-- Draggable header -->\n <h3 class=\"panel-header\"\n (mousedown)=\"OnHeaderMouseDown($event)\">\n <i class=\"fa-solid fa-sliders\"></i>\n Configuration\n <span class=\"header-actions\">\n <i class=\"fa-solid\"\n [class.fa-chevron-up]=\"IsExpanded\"\n [class.fa-chevron-down]=\"!IsExpanded\"\n (click)=\"ToggleCollapse(); $event.stopPropagation()\"\n title=\"Toggle panel\"></i>\n <i class=\"fa-solid fa-grip-dots-vertical drag-handle\"></i>\n </span>\n </h3>\n\n @if (IsExpanded) {\n <!-- Entity picker -->\n <div class=\"form-group\">\n <label>Entity</label>\n <select [ngModel]=\"Config.EntityName\"\n (ngModelChange)=\"OnEntityChanged($event)\">\n @for (entity of FilteredEntityOptions; track TrackEntityBy($index, entity)) {\n <option [value]=\"entity.Name\">{{ entity.Name }}</option>\n }\n </select>\n </div>\n\n <!-- Entity Document selector (only shown when 2+ docs for selected entity) -->\n @if (EntityDocOptions.length > 1) {\n <div class=\"form-group\">\n <label>Document Template</label>\n <select [ngModel]=\"Config.EntityDocumentID\"\n (ngModelChange)=\"Config.EntityDocumentID = $event; OnFieldChanged()\">\n @for (doc of EntityDocOptions; track doc.ID) {\n <option [value]=\"doc.ID\">{{ doc.Name }}</option>\n }\n </select>\n </div>\n }\n\n <!-- Multi-entity source selector -->\n @if (HasMultiSourceOptions) {\n <div class=\"form-group multi-source\">\n <mj-accordion-panel\n Size=\"sm\"\n [FlushBody]=\"true\"\n [Expanded]=\"ShowMultiSource\"\n (ExpandedChange)=\"OnMultiSourceExpandedChange($event)\">\n <ng-template mjAccordionTitle>\n Multiple sources\n @if (IsMultiEntity) {\n <span class=\"mj-accordion-badge mj-accordion-badge--right\">{{ SelectedDocIDs.length }}</span>\n }\n </ng-template>\n <ng-template mjAccordionBody>\n <div class=\"multi-source-list\">\n <div class=\"multi-source-hint\">Select 2+ documents to cluster them together. All must use the same embedding model.</div>\n @for (doc of AllEntityDocOptions; track doc.ID) {\n <label class=\"multi-source-item\">\n <input type=\"checkbox\" [checked]=\"IsDocSelected(doc.ID)\" (change)=\"ToggleDoc(doc.ID)\">\n <span class=\"multi-source-name\">{{ doc.Name }}</span>\n @if (doc.EntityName) { <span class=\"multi-source-entity\">{{ doc.EntityName }}</span> }\n </label>\n }\n </div>\n </ng-template>\n </mj-accordion-panel>\n </div>\n }\n\n <!-- Projection (2D / 3D) + Color-by toggles -->\n <div class=\"form-row toggle-row\">\n @if (ShowDimensionToggle) {\n <div class=\"form-group\">\n <label>Projection</label>\n <div class=\"seg-toggle\" role=\"group\">\n <button type=\"button\" class=\"seg-btn\" [class.active]=\"(Config.Dimensions ?? 2) === 2\" (click)=\"SetDimensions(2)\">2D</button>\n <button type=\"button\" class=\"seg-btn\" [class.active]=\"Config.Dimensions === 3\" (click)=\"SetDimensions(3)\">3D</button>\n </div>\n </div>\n }\n <div class=\"form-group\">\n <label>Color by</label>\n <div class=\"seg-toggle\" role=\"group\">\n <button type=\"button\" class=\"seg-btn\" [class.active]=\"(Config.ColorBy ?? 'cluster') === 'cluster'\" (click)=\"SetColorBy('cluster')\">Cluster</button>\n <button type=\"button\" class=\"seg-btn\" [class.active]=\"Config.ColorBy === 'entity'\" (click)=\"SetColorBy('entity')\">Entity</button>\n </div>\n </div>\n </div>\n\n <!-- Filter -->\n <div class=\"form-group\">\n <label>Filter (optional)</label>\n <input type=\"text\"\n [(ngModel)]=\"Config.Filter\"\n (ngModelChange)=\"OnFieldChanged()\"\n placeholder=\"e.g. Status = 'Active'\">\n </div>\n\n <!-- Algorithm + K/Epsilon row -->\n <div class=\"form-row\">\n @if (ShowAlgorithmPicker) {\n <div class=\"form-group\">\n <label>Algorithm</label>\n <select [ngModel]=\"Config.Algorithm\"\n (ngModelChange)=\"OnAlgorithmChanged($event)\">\n @for (alg of AlgorithmOptions; track TrackAlgorithmBy($index, alg)) {\n <option [value]=\"alg.Value\">{{ alg.Label }}</option>\n }\n </select>\n </div>\n }\n\n @if (IsKMeans) {\n <div class=\"form-group\">\n <label>Clusters (K): {{ Config.K }}</label>\n <input type=\"range\"\n [ngModel]=\"Config.K\"\n (ngModelChange)=\"Config.K = $event; OnFieldChanged()\"\n [min]=\"2\"\n [max]=\"20\"\n [step]=\"1\">\n </div>\n } @else {\n <div class=\"form-group\">\n <label>Epsilon: {{ Config.Epsilon }}</label>\n <input type=\"range\"\n [ngModel]=\"Config.Epsilon\"\n (ngModelChange)=\"Config.Epsilon = $event; OnFieldChanged()\"\n [min]=\"0.05\"\n [max]=\"0.95\"\n [step]=\"0.05\">\n </div>\n }\n </div>\n\n <!-- DBSCAN MinPoints row (only visible for DBSCAN) -->\n @if (!IsKMeans) {\n <div class=\"form-group\">\n <label>Min Points: {{ Config.MinPoints }}</label>\n <input type=\"range\"\n [ngModel]=\"Config.MinPoints\"\n (ngModelChange)=\"Config.MinPoints = $event; OnFieldChanged()\"\n [min]=\"2\"\n [max]=\"20\"\n [step]=\"1\">\n </div>\n }\n\n <!-- Distance metric + Max records row -->\n <div class=\"form-row\">\n @if (ShowMetricPicker) {\n <div class=\"form-group\">\n <label>Distance Metric</label>\n <select [ngModel]=\"Config.DistanceMetric\"\n (ngModelChange)=\"OnMetricChanged($event)\">\n @for (metric of MetricOptions; track TrackMetricBy($index, metric)) {\n <option [value]=\"metric.Value\">{{ metric.Label }}</option>\n }\n </select>\n </div>\n }\n <div class=\"form-group\">\n <label>Max Records</label>\n <input type=\"number\"\n [(ngModel)]=\"Config.MaxRecords\"\n (ngModelChange)=\"OnFieldChanged()\"\n [min]=\"10\"\n [max]=\"5000\">\n </div>\n </div>\n\n <!-- Run + Save action row -->\n <div class=\"action-row\">\n <button class=\"run-btn\"\n [disabled]=\"!CanRun\"\n (click)=\"OnRun()\">\n @if (IsRunning) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i> Running...\n } @else {\n <i class=\"fa-solid fa-play\"></i> Run Clustering\n }\n </button>\n @if (CanSave) {\n <button class=\"save-btn\" (click)=\"OnSave()\" title=\"Save this visualization\">\n <i class=\"fa-solid fa-bookmark\"></i> Save\n </button>\n }\n </div>\n\n <!-- Metrics display -->\n @if (Metrics && Metrics.RecordCount > 0) {\n <div class=\"metrics-section\">\n <div class=\"metric-item\">\n <span class=\"metric-label\">Silhouette</span>\n <span class=\"metric-value\"\n [class.good]=\"IsSilhouetteGood(Metrics.SilhouetteScore)\">\n {{ FormatSilhouette(Metrics.SilhouetteScore) }}\n </span>\n </div>\n <div class=\"metric-item\">\n <span class=\"metric-label\">Clusters</span>\n <span class=\"metric-value\">{{ Metrics.ClusterCount }}</span>\n </div>\n <div class=\"metric-item\">\n <span class=\"metric-label\">Records</span>\n <span class=\"metric-value\">{{ Metrics.RecordCount }}</span>\n </div>\n <div class=\"metric-item\">\n <span class=\"metric-label\">Time</span>\n <span class=\"metric-value\">{{ FormatTime(Metrics.ComputationTimeMs) }}</span>\n </div>\n @if (Metrics.OutlierCount > 0) {\n <div class=\"metric-item\">\n <span class=\"metric-label\">Outliers</span>\n <span class=\"metric-value\">{{ Metrics.OutlierCount }}</span>\n </div>\n }\n </div>\n }\n\n }\n</div>\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cluster-scatter.component.dom.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cluster-scatter.component.dom.test.d.ts","sourceRoot":"","sources":["../../src/lib/cluster-scatter.component.dom.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,102 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { MJEntityCardComponent } from '@memberjunction/ng-entity-card';
5
+ import { MJEmptyStateComponent, MJAccordionModule } from '@memberjunction/ng-ui-components';
6
+ import { renderComponentFixture, query, queryAll, text, capture } from '@memberjunction/ng-test-utils';
7
+ import { ClusterScatterComponent } from './cluster-scatter.component';
8
+ /**
9
+ * DOM-level spec for <mj-cluster-scatter>. The component renders a pure SVG scatter
10
+ * plot — no media APIs (no getUserMedia/AudioContext/WebRTC), so the visual surface is
11
+ * honestly unit-testable in jsdom. The tooltip/detail panel render a child
12
+ * <mj-entity-card> (standalone) plus the "Cluster Members" <mj-accordion-panel>, so both
13
+ * MJEntityCardComponent and MJAccordionModule are registered — clicking a point opens that
14
+ * panel, and without the accordion registered its render throws NG0304 as an unhandled error.
15
+ *
16
+ * autoDetect is used because ngOnChanges recomputes centroids during init, which would
17
+ * otherwise trip the dev-mode NG0100 check on a single detectChanges().
18
+ */
19
+ function renderScatter(inputs = {}) {
20
+ return renderComponentFixture(ClusterScatterComponent, {
21
+ imports: [CommonModule, FormsModule, MJEntityCardComponent, MJEmptyStateComponent, MJAccordionModule],
22
+ declarations: [ClusterScatterComponent],
23
+ inputs,
24
+ autoDetect: true,
25
+ });
26
+ }
27
+ const CLUSTERS = [
28
+ { Id: 0, Label: 'Alpha', Color: '#5b8def', MemberCount: 2 },
29
+ { Id: 1, Label: 'Beta', Color: '#34d399', MemberCount: 1 },
30
+ ];
31
+ const POINTS = [
32
+ { X: 100, Y: 100, ClusterId: 0, Label: 'P1', VectorKey: 'k1', Metadata: { Name: 'P1' } },
33
+ { X: 200, Y: 200, ClusterId: 0, Label: 'P2', VectorKey: 'k2', Metadata: { Name: 'P2' } },
34
+ { X: 600, Y: 400, ClusterId: 1, Label: 'P3', VectorKey: 'k3', Metadata: { Name: 'P3' } },
35
+ ];
36
+ describe('ClusterScatterComponent (DOM)', () => {
37
+ it('shows the empty state when there are no points and not loading', () => {
38
+ const fixture = renderScatter({ Points: [], IsLoading: false });
39
+ expect(query(fixture, 'mj-empty-state')).not.toBeNull();
40
+ expect(text(fixture, 'mj-empty-state .mj-empty-state__title')).toBe('No data to display');
41
+ expect(query(fixture, 'svg.scatter-svg')).toBeNull();
42
+ });
43
+ it('shows the loading overlay when IsLoading is true', () => {
44
+ const fixture = renderScatter({ Points: [], IsLoading: true });
45
+ expect(query(fixture, '.loading-overlay')).not.toBeNull();
46
+ // Empty state is gated by !IsLoading, so it must NOT render while loading.
47
+ expect(query(fixture, 'mj-empty-state')).toBeNull();
48
+ });
49
+ it('renders the SVG with one data-point circle per point', () => {
50
+ const fixture = renderScatter({ Points: POINTS, Clusters: CLUSTERS });
51
+ expect(query(fixture, 'svg.scatter-svg')).not.toBeNull();
52
+ expect(queryAll(fixture, 'circle.data-point').length).toBe(3);
53
+ });
54
+ it('renders the cluster legend with a row per cluster (cluster color mode)', () => {
55
+ const fixture = renderScatter({ Points: POINTS, Clusters: CLUSTERS, ShowLegend: true });
56
+ expect(text(fixture, '.legend .legend-title')).toBe('Clusters');
57
+ const labels = queryAll(fixture, '.legend .legend-label').map((l) => l.textContent?.trim());
58
+ expect(labels).toEqual(['Alpha', 'Beta']);
59
+ });
60
+ it('hides the legend when ShowLegend is false', () => {
61
+ const fixture = renderScatter({ Points: POINTS, Clusters: CLUSTERS, ShowLegend: false });
62
+ expect(query(fixture, '.legend')).toBeNull();
63
+ });
64
+ it('emits ClusterSelected when a legend item is clicked', () => {
65
+ const fixture = renderScatter({ Points: POINTS, Clusters: CLUSTERS, ShowLegend: true });
66
+ const selected = capture(fixture.componentInstance.ClusterSelected);
67
+ queryAll(fixture, '.legend .legend-item')[1].click();
68
+ expect(selected.length).toBe(1);
69
+ expect(selected[0].ClusterId).toBe(1);
70
+ expect(selected[0].Label).toBe('Beta');
71
+ });
72
+ it('emits PointClicked and SelectionChanged when a point is clicked', () => {
73
+ const fixture = renderScatter({ Points: POINTS, Clusters: CLUSTERS });
74
+ const clicked = capture(fixture.componentInstance.PointClicked);
75
+ const selectionChanged = capture(fixture.componentInstance.SelectionChanged);
76
+ queryAll(fixture, 'circle.data-point')[0].dispatchEvent(new MouseEvent('click', { bubbles: true }));
77
+ expect(clicked.length).toBe(1);
78
+ expect(clicked[0].VectorKey).toBe('k1');
79
+ expect(selectionChanged.length).toBe(1);
80
+ expect(selectionChanged[0].has('k1')).toBe(true);
81
+ });
82
+ it('renders the entity legend (not cluster legend) when ColorBy is entity', () => {
83
+ const entityPoints = [
84
+ { X: 1, Y: 1, ClusterId: 0, Label: 'A', VectorKey: 'a', Metadata: { EntityName: 'Companies' } },
85
+ { X: 2, Y: 2, ClusterId: 0, Label: 'B', VectorKey: 'b', Metadata: { EntityName: 'Contacts' } },
86
+ ];
87
+ const fixture = renderScatter({ Points: entityPoints, Clusters: CLUSTERS, ColorBy: 'entity', ShowLegend: true });
88
+ expect(text(fixture, '.legend .legend-title')).toBe('Entities');
89
+ const names = queryAll(fixture, '.legend .legend-label').map((l) => l.textContent?.trim());
90
+ expect(names).toContain('Companies');
91
+ expect(names).toContain('Contacts');
92
+ });
93
+ it('renders selection rings for externally-selected points', () => {
94
+ const fixture = renderScatter({
95
+ Points: POINTS,
96
+ Clusters: CLUSTERS,
97
+ SelectedPointIds: new Set(['k1']),
98
+ });
99
+ expect(queryAll(fixture, 'circle.selection-ring').length).toBe(1);
100
+ });
101
+ });
102
+ //# sourceMappingURL=cluster-scatter.component.dom.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cluster-scatter.component.dom.test.js","sourceRoot":"","sources":["../../src/lib/cluster-scatter.component.dom.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect } from 'vitest';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { ComponentFixture } from '@angular/core/testing';\nimport { MJEntityCardComponent } from '@memberjunction/ng-entity-card';\nimport { MJEmptyStateComponent, MJAccordionModule } from '@memberjunction/ng-ui-components';\nimport { renderComponentFixture, query, queryAll, text, capture } from '@memberjunction/ng-test-utils';\nimport { ClusterScatterComponent } from './cluster-scatter.component';\nimport { ClusterPoint, ClusterInfo, ClusterSelectedEvent } from './clustering.types';\n\n/**\n * DOM-level spec for <mj-cluster-scatter>. The component renders a pure SVG scatter\n * plot — no media APIs (no getUserMedia/AudioContext/WebRTC), so the visual surface is\n * honestly unit-testable in jsdom. The tooltip/detail panel render a child\n * <mj-entity-card> (standalone) plus the \"Cluster Members\" <mj-accordion-panel>, so both\n * MJEntityCardComponent and MJAccordionModule are registered — clicking a point opens that\n * panel, and without the accordion registered its render throws NG0304 as an unhandled error.\n *\n * autoDetect is used because ngOnChanges recomputes centroids during init, which would\n * otherwise trip the dev-mode NG0100 check on a single detectChanges().\n */\nfunction renderScatter(inputs: Record<string, unknown> = {}): ComponentFixture<ClusterScatterComponent> {\n return renderComponentFixture(ClusterScatterComponent, {\n imports: [CommonModule, FormsModule, MJEntityCardComponent, MJEmptyStateComponent, MJAccordionModule],\n declarations: [ClusterScatterComponent],\n inputs,\n autoDetect: true,\n });\n}\n\nconst CLUSTERS: ClusterInfo[] = [\n { Id: 0, Label: 'Alpha', Color: '#5b8def', MemberCount: 2 },\n { Id: 1, Label: 'Beta', Color: '#34d399', MemberCount: 1 },\n];\n\nconst POINTS: ClusterPoint[] = [\n { X: 100, Y: 100, ClusterId: 0, Label: 'P1', VectorKey: 'k1', Metadata: { Name: 'P1' } },\n { X: 200, Y: 200, ClusterId: 0, Label: 'P2', VectorKey: 'k2', Metadata: { Name: 'P2' } },\n { X: 600, Y: 400, ClusterId: 1, Label: 'P3', VectorKey: 'k3', Metadata: { Name: 'P3' } },\n];\n\ndescribe('ClusterScatterComponent (DOM)', () => {\n it('shows the empty state when there are no points and not loading', () => {\n const fixture = renderScatter({ Points: [], IsLoading: false });\n expect(query(fixture, 'mj-empty-state')).not.toBeNull();\n expect(text(fixture, 'mj-empty-state .mj-empty-state__title')).toBe('No data to display');\n expect(query(fixture, 'svg.scatter-svg')).toBeNull();\n });\n\n it('shows the loading overlay when IsLoading is true', () => {\n const fixture = renderScatter({ Points: [], IsLoading: true });\n expect(query(fixture, '.loading-overlay')).not.toBeNull();\n // Empty state is gated by !IsLoading, so it must NOT render while loading.\n expect(query(fixture, 'mj-empty-state')).toBeNull();\n });\n\n it('renders the SVG with one data-point circle per point', () => {\n const fixture = renderScatter({ Points: POINTS, Clusters: CLUSTERS });\n expect(query(fixture, 'svg.scatter-svg')).not.toBeNull();\n expect(queryAll(fixture, 'circle.data-point').length).toBe(3);\n });\n\n it('renders the cluster legend with a row per cluster (cluster color mode)', () => {\n const fixture = renderScatter({ Points: POINTS, Clusters: CLUSTERS, ShowLegend: true });\n expect(text(fixture, '.legend .legend-title')).toBe('Clusters');\n const labels = queryAll(fixture, '.legend .legend-label').map((l) => l.textContent?.trim());\n expect(labels).toEqual(['Alpha', 'Beta']);\n });\n\n it('hides the legend when ShowLegend is false', () => {\n const fixture = renderScatter({ Points: POINTS, Clusters: CLUSTERS, ShowLegend: false });\n expect(query(fixture, '.legend')).toBeNull();\n });\n\n it('emits ClusterSelected when a legend item is clicked', () => {\n const fixture = renderScatter({ Points: POINTS, Clusters: CLUSTERS, ShowLegend: true });\n const selected = capture<ClusterSelectedEvent>(fixture.componentInstance.ClusterSelected);\n (queryAll(fixture, '.legend .legend-item')[1] as HTMLElement).click();\n expect(selected.length).toBe(1);\n expect(selected[0].ClusterId).toBe(1);\n expect(selected[0].Label).toBe('Beta');\n });\n\n it('emits PointClicked and SelectionChanged when a point is clicked', () => {\n const fixture = renderScatter({ Points: POINTS, Clusters: CLUSTERS });\n const clicked = capture<ClusterPoint>(fixture.componentInstance.PointClicked);\n const selectionChanged = capture<Set<string>>(fixture.componentInstance.SelectionChanged);\n (queryAll(fixture, 'circle.data-point')[0] as unknown as SVGElement).dispatchEvent(new MouseEvent('click', { bubbles: true }));\n expect(clicked.length).toBe(1);\n expect(clicked[0].VectorKey).toBe('k1');\n expect(selectionChanged.length).toBe(1);\n expect(selectionChanged[0].has('k1')).toBe(true);\n });\n\n it('renders the entity legend (not cluster legend) when ColorBy is entity', () => {\n const entityPoints: ClusterPoint[] = [\n { X: 1, Y: 1, ClusterId: 0, Label: 'A', VectorKey: 'a', Metadata: { EntityName: 'Companies' } },\n { X: 2, Y: 2, ClusterId: 0, Label: 'B', VectorKey: 'b', Metadata: { EntityName: 'Contacts' } },\n ];\n const fixture = renderScatter({ Points: entityPoints, Clusters: CLUSTERS, ColorBy: 'entity', ShowLegend: true });\n expect(text(fixture, '.legend .legend-title')).toBe('Entities');\n const names = queryAll(fixture, '.legend .legend-label').map((l) => l.textContent?.trim());\n expect(names).toContain('Companies');\n expect(names).toContain('Contacts');\n });\n\n it('renders selection rings for externally-selected points', () => {\n const fixture = renderScatter({\n Points: POINTS,\n Clusters: CLUSTERS,\n SelectedPointIds: new Set<string>(['k1']),\n });\n expect(queryAll(fixture, 'circle.selection-ring').length).toBe(1);\n });\n});\n"]}