Skip to content

@memberjunction/ng-clustering

Reusable Angular components for interactive cluster visualization. Provides an SVG scatter plot with a slide-in detail panel, a floating configuration panel, LLM-generated cluster labels, save/restore of visualizations, and a headless clustering service that wraps K-Means, DBSCAN, UMAP, and PCA into a single pipeline.

+---------------------------+ +-------------------------------+
| mj-cluster-config-panel | | mj-cluster-scatter |
| (floating config UI) | | (SVG scatter plot) |
| | | |
| Entity picker | | Points + Clusters -> dots |
| Algorithm (K-Means/DBSCAN)| | Zoom / Pan via viewBox |
| Distance metric | | Tooltip on hover |
| Run / Save buttons | | Legend overlay |
| Result metrics display | | Selection rings |
+--------+------------------+ +--------+----------------------+
| ^
| RunClustering (ClusterConfig) | Points, Clusters
v |
+--------+-----------------------------------+--+
| ClusteringService |
| |
| RunClustering(vectors, config) |
| 1. K-Means / DBSCAN via SimpleVectorService|
| 2. UMAP (or PCA fallback) to 2D |
| 3. Returns ClusterVisualizationResult |
+-----------------+-----------------------------+
|
v
@memberjunction/ai-vectors-memory
(SimpleVectorService)

The config panel emits a ClusterConfig; the parent fetches vectors (from a database, API, or any source), passes them to ClusteringService.RunClustering(), and feeds the result into the scatter component. Clicking a point opens a detail panel slide-in showing entity metadata, cluster members, and an “Open Record” button. Cluster labels can be generated via LLM using the “Cluster Naming” AI prompt. Visualizations can be saved and restored with full viewport state and cluster labels.

The package is part of the MemberJunction monorepo. Add it to your Angular module or standalone component imports:

import { ClusteringModule } from '@memberjunction/ng-clustering';
@NgModule({
imports: [ClusteringModule],
})
export class MyFeatureModule {}

Peer dependencies: @angular/core, @angular/common, @angular/forms (all 21.x).

my-dashboard.component.html
<mj-cluster-scatter
[Points]="result?.Points ?? []"
[Clusters]="result?.Clusters ?? []"
[IsLoading]="isRunning"
(PointClicked)="onPointClicked($event)">
</mj-cluster-scatter>
<mj-cluster-config-panel
[IsRunning]="isRunning"
[Metrics]="result?.Metrics ?? null"
[EntityOptions]="entityOptions"
(RunClustering)="onRun($event)">
</mj-cluster-config-panel>
my-dashboard.component.ts
import { ClusteringService, ClusterConfig, ClusterVisualizationResult } from '@memberjunction/ng-clustering';
export class MyDashboardComponent {
private clusteringService = inject(ClusteringService);
result: ClusterVisualizationResult | null = null;
isRunning = false;
async onRun(config: ClusterConfig): Promise<void> {
this.isRunning = true;
const vectors = await this.fetchMyVectors(config);
this.result = await this.clusteringService.RunClustering(vectors, config);
this.isRunning = false;
}
}

Selector: mj-cluster-scatter

InputTypeDefaultDescription
PointsClusterPoint[][]2D-projected points to render
ClustersClusterInfo[][]Cluster summaries for legend and color mapping
IsLoadingbooleanfalseShow loading overlay
DotRadiusnumber5Base radius (SVG units) for each data point
DotOpacitynumber0.75Base opacity for dots (0—1)
HighlightRadiusnumber8Radius for hovered/selected point rings
ShowLegendbooleantrueShow the color-coded cluster legend
ShowTooltipbooleantrueShow the tooltip popup on hover
TooltipFieldsstring[][]Metadata keys to show in tooltip (empty = all)
ColorPalettestring[]CLUSTER_COLORSOverride cluster colors (wraps around)
EnableZoombooleantrueEnable mouse-wheel zoom
EnablePanbooleantrueEnable click-and-drag pan
AnimateTransitionsbooleantrueAnimate dot/ring transitions
SelectedPointIdsSet<string>new Set()Externally controlled selection (by VectorKey)
MinZoomnumber0.5Minimum zoom level (viewBox multiplier)
MaxZoomnumber10Maximum zoom level (viewBox multiplier)
OutputPayloadDescription
PointClickedClusterPointA point was clicked
PointHoveredClusterPoint | nullMouse entered/left a point
AfterClusteringCompleteClusterVisualizationResultNew data received and centroids computed
ClusterSelectedClusterSelectedEventLegend item clicked
SelectionChangedSet<string>Selection set changed (user or programmatic)
ViewportChangedViewportRectVisible area changed after zoom/pan

These fire before the corresponding action. Set event.Cancel = true to suppress it.

OutputPayloadCancels
BeforePointClickCancelableEvent<ClusterPoint>Suppresses PointClicked
BeforePointHoverCancelableEvent<ClusterPoint>Suppresses tooltip show
BeforeZoomCancelableEvent<ViewportRect>Suppresses zoom/pan
MethodSignatureDescription
ZoomToCluster(clusterId: number): voidAnimate zoom to center on a cluster
ResetZoom(): voidReset to default viewport
GetVisiblePoints(): ClusterPoint[]Return points in current viewport
SelectPoints(ids: string[]): voidAdd points to selection
ClearSelection(): voidClear all selected points
ExportSVG(): stringExport scatter plot as SVG string
HighlightCluster(clusterId: number): voidSelect all points in a cluster

API Reference: ClusterConfigPanelComponent

Section titled “API Reference: ClusterConfigPanelComponent”

Selector: mj-cluster-config-panel

InputTypeDefaultDescription
IsRunningbooleanfalseDisable Run button and show spinner
MetricsClusterMetrics | nullnullDisplay result metrics when non-null
EntityOptionsClusterConfigPanelEntityOption[][]Entity choices for the dropdown
ShowSaveButtonbooleantrueShow the “Save this visualization” link
ShowAlgorithmPickerbooleantrueShow the algorithm dropdown
ShowMetricPickerbooleantrueShow the distance metric dropdown
DefaultAlgorithm'kmeans' | 'dbscan''kmeans'Initial algorithm selection
AvailableEntitiesstring[][]Filter entity picker (empty = show all)
CollapsedbooleanfalseStart panel in collapsed state
OutputPayloadDescription
RunClusteringClusterConfigRun button clicked (after validation)
SaveVisualizationvoidSave link clicked (after validation)
ConfigChangedClusterConfigAny config value changed
AlgorithmChangedClusterAlgorithmAlgorithm dropdown changed
OutputPayloadCancels
BeforeRunClusteringCancelableEvent<ClusterConfig>Suppresses RunClustering
BeforeSaveCancelableEvent<ClusterConfig>Suppresses SaveVisualization

All BeforeXXX events use the CancelableEvent<T> interface:

export interface CancelableEvent<T = unknown> {
Data: T; // The event payload
Cancel: boolean; // Set to true to prevent the default action
}

The component creates a CancelableEvent, emits it synchronously, then checks Cancel before proceeding. This works because Angular EventEmitter.emit() calls subscribers synchronously.

onBeforeRun(event: CancelableEvent<ClusterConfig>): void {
if (event.Data.MaxRecords > 2000) {
event.Cancel = true;
this.showWarning('Max records cannot exceed 2000 in demo mode');
}
}
onBeforeClick(event: CancelableEvent<ClusterPoint>): void {
if (event.Data.ClusterId === -1) {
event.Cancel = true; // ignore outlier clicks
}
}
onBeforeZoom(event: CancelableEvent<ViewportRect>): void {
if (event.Data.Width > 5000) {
event.Cancel = true; // don't allow zooming out too far
}
}

Clicking a data point in the scatter plot opens a slide-in detail panel on the right side. The panel displays:

  • Entity icon and record identifier at the top
  • Metadata entries parsed from the vector’s stored metadata (key-value pairs)
  • Cluster membership showing all other points in the same cluster, clickable to navigate between members
  • “Open Record” button that emits the OpenRecord event so the host application can navigate to the entity form

The detail panel state is managed internally by the scatter component via SelectedPoint, ShowDetailPanel, DetailMetadataEntries, and ClusterMembers properties.

OutputPayloadDescription
OpenRecordClusterPointFires when the user clicks “Open Record” in the detail panel
MethodSignatureDescription
CloseDetailPanel(): voidProgrammatically close the detail panel
OnClusterMemberClick(point: ClusterPoint): voidSelect a different cluster member
ToggleClusterMembers(): voidToggle cluster members list expansion

Cluster labels can be generated using the “Cluster Naming” AI prompt. When triggered, the service sends representative metadata from each cluster’s members to the LLM, which returns concise, descriptive labels. Labels are displayed:

  • On the scatter plot as text positioned above cluster centroids
  • In the legend alongside the color swatch
  • In the detail panel’s cluster section

Labels are stored as ClusterLabel[] (with ClusterId and Label fields) and included when saving a visualization.


Visualizations can be saved via the config panel’s “Save” action and later restored. A SavedClusterVisualization includes:

FieldDescription
ConfigThe ClusterConfig used for the run
PointsAll 2D-projected points
ClustersCluster summaries
MetricsClustering quality metrics
ViewportPan + zoom state (ViewportRect) at save time
ClusterLabelsLLM-generated (or user-edited) labels
SavedAtTimestamp

When restoring, the scatter component can be initialized with the saved Points, Clusters, and viewport state without re-running the clustering algorithm.


When a selected entity has two or more active entity documents, the config panel displays a document selector dropdown. This allows the user to choose which entity document (and its associated embedding model + vector index) to use for fetching vectors. The parent dashboard uses the FetchEntityVectors GraphQL query to retrieve vectors from the vector database filtered by the selected entity document.


All components use MemberJunction design tokens — no hardcoded colors. Key tokens in use:

TokenUsage
--mj-bg-pageScatter container background
--mj-bg-surfaceLegend background
--mj-bg-surface-elevatedConfig panel, tooltip
--mj-bg-surface-hoverLegend item hover
--mj-border-defaultPanel/legend borders
--mj-brand-primaryRun button, accents
--mj-text-primaryPrimary text
--mj-text-secondaryLabels
--mj-text-mutedCaptions, muted text
--mj-status-successGood silhouette score

Override cluster colors via the ColorPalette input:

<mj-cluster-scatter
[ColorPalette]="['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728']">
</mj-cluster-scatter>

When there are more clusters than colors, the palette wraps around. The default palette (CLUSTER_COLORS) provides 10 accessible, distinct colors.


The Knowledge Hub dashboard uses both components inside a BaseResourceComponent:

@RegisterClass(BaseResourceComponent, 'ClusterVisualizationResource')
@Component({ ... })
export class ClusterVisualizationResourceComponent extends BaseResourceComponent {
clusteringService = inject(ClusteringService);
result: ClusterVisualizationResult | null = null;
async OnRunClustering(config: ClusterConfig): Promise<void> {
const vectors = await this.fetchVectorsForEntity(config);
this.result = await this.clusteringService.RunClustering(vectors, config);
}
}

Use individual components with your own data source:

@Component({
template: `
<mj-cluster-scatter
[Points]="points"
[Clusters]="clusters"
[DotRadius]="4"
[ShowTooltip]="true"
[TooltipFields]="['Score', 'Category']"
[EnablePan]="true"
(BeforePointClick)="validateClick($event)"
(ClusterSelected)="onClusterPicked($event)">
</mj-cluster-scatter>
`
})
export class MyVisualizationComponent {
points: ClusterPoint[] = [];
clusters: ClusterInfo[] = [];
validateClick(event: CancelableEvent<ClusterPoint>): void {
if (event.Data.Metadata['locked']) {
event.Cancel = true;
}
}
onClusterPicked(event: ClusterSelectedEvent): void {
console.log(`Selected cluster ${event.Label} with ${event.MemberCount} members`);
}
}

Use ClusteringService without the Angular components:

const svc = new ClusteringService();
const result = await svc.RunClustering(myVectors, {
EntityName: 'Products',
Algorithm: 'kmeans',
K: 5,
Epsilon: 0,
MinPoints: 0,
DistanceMetric: 'cosine',
MaxRecords: 1000,
Filter: '',
});
console.log(`Found ${result.Clusters.length} clusters`);

ScenarioRecommendation
< 500 recordsExcellent performance; use UMAP for best quality
500—2000 recordsGood performance; UMAP may take 1—3 seconds
2000—5000 recordsAcceptable; consider PCA fallback for speed
> 5000 recordsSet MaxRecords to cap at 5000; SVG rendering may lag
  • UMAP preserves local structure (clusters stay tight) but is slower (O(n log n))
  • PCA is a fast linear fallback (O(n * d)) that works well for well-separated clusters
  • The service automatically falls back to PCA if UMAP is unavailable or fails
  • Each point is a <circle> element; above ~5000 elements the browser may struggle
  • Selection rings and highlight rings add extra elements per selected point
  • Use AnimateTransitions = false for better performance with large datasets

All types are exported from the package root:

import {
// Core data types
ClusterPoint,
ClusterInfo,
ClusterConfig,
ClusterMetrics,
ClusterVisualizationResult,
SavedClusterVisualization,
ClusterInputVector,
// Config panel helper
ClusterConfigPanelEntityOption,
// Event payloads
CancelableEvent,
ViewportRect,
ClusterSelectedEvent,
// Constants and factories
ClusterAlgorithm,
ClusterDistanceMetric,
CLUSTER_COLORS,
DefaultClusterConfig,
} from '@memberjunction/ng-clustering';