@memberjunction/ng-clustering 5.39.0 → 5.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/cluster-view-type.test.d.ts +2 -0
- package/dist/__tests__/cluster-view-type.test.d.ts.map +1 -0
- package/dist/__tests__/cluster-view-type.test.js +63 -0
- package/dist/__tests__/cluster-view-type.test.js.map +1 -0
- package/dist/lib/cluster-config-panel.component.d.ts +39 -1
- package/dist/lib/cluster-config-panel.component.d.ts.map +1 -1
- package/dist/lib/cluster-config-panel.component.js +287 -88
- package/dist/lib/cluster-config-panel.component.js.map +1 -1
- package/dist/lib/cluster-scatter.component.d.ts +53 -1
- package/dist/lib/cluster-scatter.component.d.ts.map +1 -1
- package/dist/lib/cluster-scatter.component.js +385 -135
- package/dist/lib/cluster-scatter.component.js.map +1 -1
- package/dist/lib/clustering.module.d.ts +7 -4
- package/dist/lib/clustering.module.d.ts.map +1 -1
- package/dist/lib/clustering.module.js +20 -3
- package/dist/lib/clustering.module.js.map +1 -1
- package/dist/lib/clustering.service.d.ts +63 -3
- package/dist/lib/clustering.service.d.ts.map +1 -1
- package/dist/lib/clustering.service.js +169 -48
- package/dist/lib/clustering.service.js.map +1 -1
- package/dist/lib/clustering.types.d.ts +17 -2
- package/dist/lib/clustering.types.d.ts.map +1 -1
- package/dist/lib/clustering.types.js +3 -0
- package/dist/lib/clustering.types.js.map +1 -1
- package/dist/lib/view-type/cluster-view-prop-sheet.component.d.ts +31 -0
- package/dist/lib/view-type/cluster-view-prop-sheet.component.d.ts.map +1 -0
- package/dist/lib/view-type/cluster-view-prop-sheet.component.js +185 -0
- package/dist/lib/view-type/cluster-view-prop-sheet.component.js.map +1 -0
- package/dist/lib/view-type/cluster-view-renderer.component.d.ts +143 -0
- package/dist/lib/view-type/cluster-view-renderer.component.d.ts.map +1 -0
- package/dist/lib/view-type/cluster-view-renderer.component.js +508 -0
- package/dist/lib/view-type/cluster-view-renderer.component.js.map +1 -0
- package/dist/lib/view-type/cluster-view-type.d.ts +35 -0
- package/dist/lib/view-type/cluster-view-type.d.ts.map +1 -0
- package/dist/lib/view-type/cluster-view-type.js +51 -0
- package/dist/lib/view-type/cluster-view-type.js.map +1 -0
- package/dist/lib/view-type/cluster-view.types.d.ts +31 -0
- package/dist/lib/view-type/cluster-view.types.d.ts.map +1 -0
- package/dist/lib/view-type/cluster-view.types.js +29 -0
- package/dist/lib/view-type/cluster-view.types.js.map +1 -0
- package/dist/lib/view-type/entity-document-availability.engine.d.ts +80 -0
- package/dist/lib/view-type/entity-document-availability.engine.d.ts.map +1 -0
- package/dist/lib/view-type/entity-document-availability.engine.js +149 -0
- package/dist/lib/view-type/entity-document-availability.engine.js.map +1 -0
- package/dist/public-api.d.ts +5 -0
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +6 -0
- package/dist/public-api.js.map +1 -1
- package/package.json +11 -3
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { __decorate } from "tslib";
|
|
2
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
3
|
+
import { BaseViewTypeDescriptor } from '@memberjunction/ng-entity-viewer';
|
|
4
|
+
import { ClusterViewRendererComponent } from './cluster-view-renderer.component';
|
|
5
|
+
import { ClusterViewPropSheetComponent } from './cluster-view-prop-sheet.component';
|
|
6
|
+
import { EntityDocumentAvailabilityEngine } from './entity-document-availability.engine';
|
|
7
|
+
/**
|
|
8
|
+
* ClusterViewType
|
|
9
|
+
* ---------------
|
|
10
|
+
* The Cluster **view-type plug-in** descriptor. This is the proof that a feature package
|
|
11
|
+
* (ng-clustering) can contribute a brand-new entity-viewer view type by depending only on the
|
|
12
|
+
* host's contracts (`@memberjunction/ng-entity-viewer`) — the host needs zero changes.
|
|
13
|
+
*
|
|
14
|
+
* - **RendererComponent** → {@link ClusterViewRendererComponent} (wraps `mj-cluster-scatter`).
|
|
15
|
+
* - **PropSheetComponent** → {@link ClusterViewPropSheetComponent} (algorithm/K/2D-3D/etc.).
|
|
16
|
+
* - **IsAvailableFor** → true only for entities that have an active Entity Document (vectors),
|
|
17
|
+
* read synchronously from {@link EntityDocumentAvailabilityEngine}.
|
|
18
|
+
* - **EnsureAvailabilityData** → preloads that engine so the sync predicate has data.
|
|
19
|
+
*
|
|
20
|
+
* Registered with the ClassFactory under `'ClusterViewType'`, matching the `DriverClass` of the
|
|
21
|
+
* seeded `MJ: View Types` "Cluster" row.
|
|
22
|
+
*/
|
|
23
|
+
let ClusterViewType = class ClusterViewType extends BaseViewTypeDescriptor {
|
|
24
|
+
constructor() {
|
|
25
|
+
super(...arguments);
|
|
26
|
+
this.Name = 'ClusterViewType';
|
|
27
|
+
this.DisplayName = 'Cluster';
|
|
28
|
+
this.Icon = 'fa-solid fa-diagram-project';
|
|
29
|
+
this.RendererComponent = ClusterViewRendererComponent;
|
|
30
|
+
this.PropSheetComponent = ClusterViewPropSheetComponent;
|
|
31
|
+
}
|
|
32
|
+
IsAvailableFor(entity, _provider) {
|
|
33
|
+
return EntityDocumentAvailabilityEngine.Instance.HasVectorsForEntity(entity);
|
|
34
|
+
}
|
|
35
|
+
async EnsureAvailabilityData(provider) {
|
|
36
|
+
await EntityDocumentAvailabilityEngine.Instance.Config(false, provider?.CurrentUser, provider ?? undefined);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
ClusterViewType = __decorate([
|
|
40
|
+
RegisterClass(BaseViewTypeDescriptor, 'ClusterViewType')
|
|
41
|
+
], ClusterViewType);
|
|
42
|
+
export { ClusterViewType };
|
|
43
|
+
/**
|
|
44
|
+
* Tree-shaking guard. ng-clustering sets `"sideEffects": false`, so without an explicit
|
|
45
|
+
* reference the bundler can drop this module's `@RegisterClass` side effect. Call this once
|
|
46
|
+
* from app bootstrap (or import the ClusteringModule) to guarantee the descriptor registers.
|
|
47
|
+
*/
|
|
48
|
+
export function LoadClusterViewType() {
|
|
49
|
+
// no-op; presence prevents tree-shaking of the @RegisterClass side effect above.
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=cluster-view-type.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cluster-view-type.js","sourceRoot":"","sources":["../../../src/lib/view-type/cluster-view-type.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,EAAE,6BAA6B,EAAE,MAAM,qCAAqC,CAAC;AACpF,OAAO,EAAE,gCAAgC,EAAE,MAAM,uCAAuC,CAAC;AAEzF;;;;;;;;;;;;;;;GAeG;AAEI,IAAM,eAAe,GAArB,MAAM,eAAgB,SAAQ,sBAAsB;IAApD;;QACI,SAAI,GAAG,iBAAiB,CAAC;QACzB,gBAAW,GAAG,SAAS,CAAC;QACxB,SAAI,GAAG,6BAA6B,CAAC;QACrC,sBAAiB,GAAkB,4BAA4B,CAAC;QACvD,uBAAkB,GAAkB,6BAA6B,CAAC;IAStF,CAAC;IAPU,cAAc,CAAC,MAAkB,EAAE,SAA6B;QACvE,OAAO,gCAAgC,CAAC,QAAQ,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC/E,CAAC;IAEQ,KAAK,CAAC,sBAAsB,CAAC,QAA4B;QAChE,MAAM,gCAAgC,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,IAAI,SAAS,CAAC,CAAC;IAC9G,CAAC;CACF,CAAA;AAdY,eAAe;IAD3B,aAAa,CAAC,sBAAsB,EAAE,iBAAiB,CAAC;GAC5C,eAAe,CAc3B;;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB;IACjC,iFAAiF;AACnF,CAAC","sourcesContent":["import { Type } from '@angular/core';\nimport { EntityInfo, IMetadataProvider } from '@memberjunction/core';\nimport { RegisterClass } from '@memberjunction/global';\nimport { BaseViewTypeDescriptor } from '@memberjunction/ng-entity-viewer';\nimport { ClusterViewRendererComponent } from './cluster-view-renderer.component';\nimport { ClusterViewPropSheetComponent } from './cluster-view-prop-sheet.component';\nimport { EntityDocumentAvailabilityEngine } from './entity-document-availability.engine';\n\n/**\n * ClusterViewType\n * ---------------\n * The Cluster **view-type plug-in** descriptor. This is the proof that a feature package\n * (ng-clustering) can contribute a brand-new entity-viewer view type by depending only on the\n * host's contracts (`@memberjunction/ng-entity-viewer`) — the host needs zero changes.\n *\n * - **RendererComponent** → {@link ClusterViewRendererComponent} (wraps `mj-cluster-scatter`).\n * - **PropSheetComponent** → {@link ClusterViewPropSheetComponent} (algorithm/K/2D-3D/etc.).\n * - **IsAvailableFor** → true only for entities that have an active Entity Document (vectors),\n * read synchronously from {@link EntityDocumentAvailabilityEngine}.\n * - **EnsureAvailabilityData** → preloads that engine so the sync predicate has data.\n *\n * Registered with the ClassFactory under `'ClusterViewType'`, matching the `DriverClass` of the\n * seeded `MJ: View Types` \"Cluster\" row.\n */\n@RegisterClass(BaseViewTypeDescriptor, 'ClusterViewType')\nexport class ClusterViewType extends BaseViewTypeDescriptor {\n readonly Name = 'ClusterViewType';\n readonly DisplayName = 'Cluster';\n readonly Icon = 'fa-solid fa-diagram-project';\n readonly RendererComponent: Type<unknown> = ClusterViewRendererComponent;\n override readonly PropSheetComponent: Type<unknown> = ClusterViewPropSheetComponent;\n\n override IsAvailableFor(entity: EntityInfo, _provider?: IMetadataProvider): boolean {\n return EntityDocumentAvailabilityEngine.Instance.HasVectorsForEntity(entity);\n }\n\n override async EnsureAvailabilityData(provider?: IMetadataProvider): Promise<void> {\n await EntityDocumentAvailabilityEngine.Instance.Config(false, provider?.CurrentUser, provider ?? undefined);\n }\n}\n\n/**\n * Tree-shaking guard. ng-clustering sets `\"sideEffects\": false`, so without an explicit\n * reference the bundler can drop this module's `@RegisterClass` side effect. Call this once\n * from app bootstrap (or import the ClusteringModule) to guarantee the descriptor registers.\n */\nexport function LoadClusterViewType(): void {\n // no-op; presence prevents tree-shaking of the @RegisterClass side effect above.\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ClusterAlgorithm } from '../clustering.types';
|
|
2
|
+
/**
|
|
3
|
+
* Configuration for the Cluster **view type** (the entity-viewer plug-in), persisted per
|
|
4
|
+
* `UserView` in `DisplayState.viewTypeConfigs` keyed by the Cluster `MJ: View Types` row ID.
|
|
5
|
+
*
|
|
6
|
+
* This is the plug-in-owned config shape referenced opaquely by the host as
|
|
7
|
+
* `Record<string, unknown>`; the Cluster renderer + prop-sheet interpret it with this type.
|
|
8
|
+
*/
|
|
9
|
+
export interface ClusterViewConfig {
|
|
10
|
+
/** Clustering algorithm. */
|
|
11
|
+
algorithm: ClusterAlgorithm;
|
|
12
|
+
/** Number of clusters for K-Means (ignored for DBSCAN). */
|
|
13
|
+
k: number;
|
|
14
|
+
/** Projected layout dimensionality: 2 (SVG) or 3 (rotatable). */
|
|
15
|
+
dimensions: 2 | 3;
|
|
16
|
+
/** Legend mode: color by assigned cluster (default) or by source entity. */
|
|
17
|
+
colorBy: 'cluster' | 'entity';
|
|
18
|
+
/** Maximum records to fetch for clustering. */
|
|
19
|
+
maxRecords: number;
|
|
20
|
+
/** Whether to name clusters with an LLM (server-side). */
|
|
21
|
+
nameClusters: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Sensible defaults for a fresh Cluster view. */
|
|
24
|
+
export declare const DEFAULT_CLUSTER_VIEW_CONFIG: ClusterViewConfig;
|
|
25
|
+
/**
|
|
26
|
+
* Normalizes an opaque host config map into a fully-populated {@link ClusterViewConfig},
|
|
27
|
+
* filling any missing keys from {@link DEFAULT_CLUSTER_VIEW_CONFIG}. Tolerant of partial or
|
|
28
|
+
* empty input (e.g. a brand-new view with no saved config).
|
|
29
|
+
*/
|
|
30
|
+
export declare function toClusterViewConfig(raw: Record<string, unknown> | null | undefined): ClusterViewConfig;
|
|
31
|
+
//# sourceMappingURL=cluster-view.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cluster-view.types.d.ts","sourceRoot":"","sources":["../../../src/lib/view-type/cluster-view.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,4BAA4B;IAC5B,SAAS,EAAE,gBAAgB,CAAC;IAC5B,2DAA2D;IAC3D,CAAC,EAAE,MAAM,CAAC;IACV,iEAAiE;IACjE,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC;IAClB,4EAA4E;IAC5E,OAAO,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC9B,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,kDAAkD;AAClD,eAAO,MAAM,2BAA2B,EAAE,iBAOzC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,iBAAiB,CAatG"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Sensible defaults for a fresh Cluster view. */
|
|
2
|
+
export const DEFAULT_CLUSTER_VIEW_CONFIG = {
|
|
3
|
+
algorithm: 'kmeans',
|
|
4
|
+
k: 5,
|
|
5
|
+
dimensions: 2,
|
|
6
|
+
colorBy: 'cluster',
|
|
7
|
+
maxRecords: 500,
|
|
8
|
+
nameClusters: true,
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Normalizes an opaque host config map into a fully-populated {@link ClusterViewConfig},
|
|
12
|
+
* filling any missing keys from {@link DEFAULT_CLUSTER_VIEW_CONFIG}. Tolerant of partial or
|
|
13
|
+
* empty input (e.g. a brand-new view with no saved config).
|
|
14
|
+
*/
|
|
15
|
+
export function toClusterViewConfig(raw) {
|
|
16
|
+
const r = raw ?? {};
|
|
17
|
+
const algorithm = r['algorithm'] === 'dbscan' ? 'dbscan' : 'kmeans';
|
|
18
|
+
const dimensions = r['dimensions'] === 3 ? 3 : 2;
|
|
19
|
+
const colorBy = r['colorBy'] === 'entity' ? 'entity' : 'cluster';
|
|
20
|
+
return {
|
|
21
|
+
algorithm,
|
|
22
|
+
k: typeof r['k'] === 'number' ? r['k'] : DEFAULT_CLUSTER_VIEW_CONFIG.k,
|
|
23
|
+
dimensions,
|
|
24
|
+
colorBy,
|
|
25
|
+
maxRecords: typeof r['maxRecords'] === 'number' ? r['maxRecords'] : DEFAULT_CLUSTER_VIEW_CONFIG.maxRecords,
|
|
26
|
+
nameClusters: typeof r['nameClusters'] === 'boolean' ? r['nameClusters'] : DEFAULT_CLUSTER_VIEW_CONFIG.nameClusters,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=cluster-view.types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cluster-view.types.js","sourceRoot":"","sources":["../../../src/lib/view-type/cluster-view.types.ts"],"names":[],"mappings":"AAwBA,kDAAkD;AAClD,MAAM,CAAC,MAAM,2BAA2B,GAAsB;IAC5D,SAAS,EAAE,QAAQ;IACnB,CAAC,EAAE,CAAC;IACJ,UAAU,EAAE,CAAC;IACb,OAAO,EAAE,SAAS;IAClB,UAAU,EAAE,GAAG;IACf,YAAY,EAAE,IAAI;CACnB,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAA+C;IACjF,MAAM,CAAC,GAAG,GAAG,IAAI,EAAE,CAAC;IACpB,MAAM,SAAS,GAAG,CAAC,CAAC,WAAW,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;IACpE,MAAM,UAAU,GAAG,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,MAAM,OAAO,GAAG,CAAC,CAAC,SAAS,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;IACjE,OAAO;QACL,SAAS;QACT,CAAC,EAAE,OAAO,CAAC,CAAC,GAAG,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,GAAG,CAAY,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC;QAClF,UAAU;QACV,OAAO;QACP,UAAU,EAAE,OAAO,CAAC,CAAC,YAAY,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,YAAY,CAAY,CAAC,CAAC,CAAC,2BAA2B,CAAC,UAAU;QACtH,YAAY,EAAE,OAAO,CAAC,CAAC,cAAc,CAAC,KAAK,SAAS,CAAC,CAAC,CAAE,CAAC,CAAC,cAAc,CAAa,CAAC,CAAC,CAAC,2BAA2B,CAAC,YAAY;KACjI,CAAC;AACJ,CAAC","sourcesContent":["import { ClusterAlgorithm } from '../clustering.types';\n\n/**\n * Configuration for the Cluster **view type** (the entity-viewer plug-in), persisted per\n * `UserView` in `DisplayState.viewTypeConfigs` keyed by the Cluster `MJ: View Types` row ID.\n *\n * This is the plug-in-owned config shape referenced opaquely by the host as\n * `Record<string, unknown>`; the Cluster renderer + prop-sheet interpret it with this type.\n */\nexport interface ClusterViewConfig {\n /** Clustering algorithm. */\n algorithm: ClusterAlgorithm;\n /** Number of clusters for K-Means (ignored for DBSCAN). */\n k: number;\n /** Projected layout dimensionality: 2 (SVG) or 3 (rotatable). */\n dimensions: 2 | 3;\n /** Legend mode: color by assigned cluster (default) or by source entity. */\n colorBy: 'cluster' | 'entity';\n /** Maximum records to fetch for clustering. */\n maxRecords: number;\n /** Whether to name clusters with an LLM (server-side). */\n nameClusters: boolean;\n}\n\n/** Sensible defaults for a fresh Cluster view. */\nexport const DEFAULT_CLUSTER_VIEW_CONFIG: ClusterViewConfig = {\n algorithm: 'kmeans',\n k: 5,\n dimensions: 2,\n colorBy: 'cluster',\n maxRecords: 500,\n nameClusters: true,\n};\n\n/**\n * Normalizes an opaque host config map into a fully-populated {@link ClusterViewConfig},\n * filling any missing keys from {@link DEFAULT_CLUSTER_VIEW_CONFIG}. Tolerant of partial or\n * empty input (e.g. a brand-new view with no saved config).\n */\nexport function toClusterViewConfig(raw: Record<string, unknown> | null | undefined): ClusterViewConfig {\n const r = raw ?? {};\n const algorithm = r['algorithm'] === 'dbscan' ? 'dbscan' : 'kmeans';\n const dimensions = r['dimensions'] === 3 ? 3 : 2;\n const colorBy = r['colorBy'] === 'entity' ? 'entity' : 'cluster';\n return {\n algorithm,\n k: typeof r['k'] === 'number' ? (r['k'] as number) : DEFAULT_CLUSTER_VIEW_CONFIG.k,\n dimensions,\n colorBy,\n maxRecords: typeof r['maxRecords'] === 'number' ? (r['maxRecords'] as number) : DEFAULT_CLUSTER_VIEW_CONFIG.maxRecords,\n nameClusters: typeof r['nameClusters'] === 'boolean' ? (r['nameClusters'] as boolean) : DEFAULT_CLUSTER_VIEW_CONFIG.nameClusters,\n };\n}\n"]}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { BaseEngine, EntityInfo, IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
2
|
+
import { MJEntityDocumentEntity } from '@memberjunction/core-entities';
|
|
3
|
+
/**
|
|
4
|
+
* EntityDocumentAvailabilityEngine
|
|
5
|
+
* --------------------------------
|
|
6
|
+
* A tiny, browser-safe cache that answers one synchronous question for the
|
|
7
|
+
* {@link ClusterViewType} availability predicate: *does this entity actually have vectors we
|
|
8
|
+
* can cluster?* — meaning it has an active `MJ: Entity Documents` definition **and** at least
|
|
9
|
+
* one populated `MJ: Entity Record Documents` row (vectorized record). An entity that has an
|
|
10
|
+
* Entity Document configured but no records vectorized yet is NOT clusterable, so the Cluster
|
|
11
|
+
* view type must not be offered for it.
|
|
12
|
+
*
|
|
13
|
+
* The descriptor's `IsAvailableFor` must be synchronous, but the answer depends on data — so the
|
|
14
|
+
* descriptor's async `EnsureAvailabilityData` hook calls {@link Config} (awaited once before the
|
|
15
|
+
* predicates run), and `IsAvailableFor` then reads the populated cache via
|
|
16
|
+
* {@link HasVectorsForEntity}.
|
|
17
|
+
*
|
|
18
|
+
* Extends {@link BaseEngine} for caching + entity-event auto-refresh, and honors the per-request
|
|
19
|
+
* provider (multi-provider safe).
|
|
20
|
+
*/
|
|
21
|
+
export declare class EntityDocumentAvailabilityEngine extends BaseEngine<EntityDocumentAvailabilityEngine> {
|
|
22
|
+
/** The global singleton instance. Do not construct directly. */
|
|
23
|
+
static get Instance(): EntityDocumentAvailabilityEngine;
|
|
24
|
+
private _entityDocuments;
|
|
25
|
+
/** Normalized entity IDs that have ≥1 populated vector record. */
|
|
26
|
+
private _entityIDsWithVectors;
|
|
27
|
+
private _vectorPresenceLoaded;
|
|
28
|
+
/**
|
|
29
|
+
* In-flight vector-presence probe, shared by all concurrent {@link Config} callers so they
|
|
30
|
+
* await the SAME load instead of each kicking off (or short-circuiting past) a partial one.
|
|
31
|
+
* Cleared when the probe settles. This is the linchpin of the fix for the Cluster view-type
|
|
32
|
+
* race: the switcher's availability hook and the renderer's `resolveEntityDocumentID` both
|
|
33
|
+
* call `Config` near-simultaneously on first open.
|
|
34
|
+
*/
|
|
35
|
+
private _vectorPresenceInFlight;
|
|
36
|
+
/**
|
|
37
|
+
* Loads the active Entity Documents and then probes which of their entities actually have
|
|
38
|
+
* populated vectors. Cheap to call repeatedly — the Entity Documents load is cached by
|
|
39
|
+
* BaseEngine, and the vector-presence probe runs once (until `forceRefresh`).
|
|
40
|
+
*/
|
|
41
|
+
Config(forceRefresh?: boolean, contextUser?: UserInfo, provider?: IMetadataProvider): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Ensure the vector-presence cache is populated exactly once (or re-populated on
|
|
44
|
+
* `forceRefresh`), de-duplicating concurrent callers via {@link _vectorPresenceInFlight}.
|
|
45
|
+
*
|
|
46
|
+
* CRITICAL: `_vectorPresenceLoaded` is flipped to `true` only AFTER the probe resolves and
|
|
47
|
+
* the result set is swapped in — never before the `await`. The previous implementation set
|
|
48
|
+
* the flag at the top of `loadVectorPresence`, so a second concurrent `Config` saw "loaded"
|
|
49
|
+
* while the set was still empty and returned an availability answer of "no vectors" — exactly
|
|
50
|
+
* the flip-flop / "nothing shown then no-vectors" symptom on first Cluster open.
|
|
51
|
+
*/
|
|
52
|
+
private ensureVectorPresence;
|
|
53
|
+
/**
|
|
54
|
+
* Determine, per active Entity Document, whether it has at least one populated vector record,
|
|
55
|
+
* and return the set of owning entity IDs. Uses a single batched {@link RunView.RunViews} with
|
|
56
|
+
* `MaxRows: 1` per document so it's a lightweight existence probe, not a full fetch. Returns a
|
|
57
|
+
* fresh set that the caller swaps in atomically once fully populated (see {@link ensureVectorPresence}).
|
|
58
|
+
*/
|
|
59
|
+
private loadVectorPresence;
|
|
60
|
+
/** All cached active Entity Document rows. */
|
|
61
|
+
get EntityDocuments(): MJEntityDocumentEntity[];
|
|
62
|
+
/**
|
|
63
|
+
* Return the active Entity Document rows for a given entity, matched by EntityID
|
|
64
|
+
* (case-insensitive UUID comparison). Used by the Cluster view-type renderer to
|
|
65
|
+
* resolve a **deterministic** source document for the entity's vectors — picking
|
|
66
|
+
* the first active doc explicitly rather than letting the server choose one
|
|
67
|
+
* non-deterministically.
|
|
68
|
+
*
|
|
69
|
+
* @param entity The entity to find active documents for.
|
|
70
|
+
* @returns The active Entity Document rows for that entity (possibly empty).
|
|
71
|
+
*/
|
|
72
|
+
GetActiveEntityDocumentsForEntity(entity: EntityInfo | null): MJEntityDocumentEntity[];
|
|
73
|
+
/**
|
|
74
|
+
* True only when the entity has an active Entity Document **and** at least one populated vector
|
|
75
|
+
* record — i.e. it can actually be clustered. Returns false for entities that have an Entity
|
|
76
|
+
* Document configured but no vectorized records yet. UUID matching is case-insensitive.
|
|
77
|
+
*/
|
|
78
|
+
HasVectorsForEntity(entity: EntityInfo | null): boolean;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=entity-document-availability.engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-document-availability.engine.d.ts","sourceRoot":"","sources":["../../../src/lib/view-type/entity-document-availability.engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAA4B,UAAU,EAAE,iBAAiB,EAAY,QAAQ,EAAW,MAAM,sBAAsB,CAAC;AACxI,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAGvE;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,gCAAiC,SAAQ,UAAU,CAAC,gCAAgC,CAAC;IAChG,gEAAgE;IAChE,WAAkB,QAAQ,IAAI,gCAAgC,CAE7D;IAED,OAAO,CAAC,gBAAgB,CAAgC;IACxD,kEAAkE;IAClE,OAAO,CAAC,qBAAqB,CAAqB;IAClD,OAAO,CAAC,qBAAqB,CAAS;IACtC;;;;;;OAMG;IACH,OAAO,CAAC,uBAAuB,CAA8B;IAE7D;;;;OAIG;IACU,MAAM,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAehH;;;;;;;;;OASG;YACW,oBAAoB;IAyBlC;;;;;OAKG;YACW,kBAAkB;IA6BhC,8CAA8C;IAC9C,IAAW,eAAe,IAAI,sBAAsB,EAAE,CAErD;IAED;;;;;;;;;OASG;IACI,iCAAiC,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,sBAAsB,EAAE;IAQ7F;;;;OAIG;IACI,mBAAmB,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,OAAO;CAM/D"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { BaseEngine, Metadata, RunView } from '@memberjunction/core';
|
|
2
|
+
import { NormalizeUUID } from '@memberjunction/global';
|
|
3
|
+
/**
|
|
4
|
+
* EntityDocumentAvailabilityEngine
|
|
5
|
+
* --------------------------------
|
|
6
|
+
* A tiny, browser-safe cache that answers one synchronous question for the
|
|
7
|
+
* {@link ClusterViewType} availability predicate: *does this entity actually have vectors we
|
|
8
|
+
* can cluster?* — meaning it has an active `MJ: Entity Documents` definition **and** at least
|
|
9
|
+
* one populated `MJ: Entity Record Documents` row (vectorized record). An entity that has an
|
|
10
|
+
* Entity Document configured but no records vectorized yet is NOT clusterable, so the Cluster
|
|
11
|
+
* view type must not be offered for it.
|
|
12
|
+
*
|
|
13
|
+
* The descriptor's `IsAvailableFor` must be synchronous, but the answer depends on data — so the
|
|
14
|
+
* descriptor's async `EnsureAvailabilityData` hook calls {@link Config} (awaited once before the
|
|
15
|
+
* predicates run), and `IsAvailableFor` then reads the populated cache via
|
|
16
|
+
* {@link HasVectorsForEntity}.
|
|
17
|
+
*
|
|
18
|
+
* Extends {@link BaseEngine} for caching + entity-event auto-refresh, and honors the per-request
|
|
19
|
+
* provider (multi-provider safe).
|
|
20
|
+
*/
|
|
21
|
+
export class EntityDocumentAvailabilityEngine extends BaseEngine {
|
|
22
|
+
constructor() {
|
|
23
|
+
super(...arguments);
|
|
24
|
+
this._entityDocuments = [];
|
|
25
|
+
/** Normalized entity IDs that have ≥1 populated vector record. */
|
|
26
|
+
this._entityIDsWithVectors = new Set();
|
|
27
|
+
this._vectorPresenceLoaded = false;
|
|
28
|
+
/**
|
|
29
|
+
* In-flight vector-presence probe, shared by all concurrent {@link Config} callers so they
|
|
30
|
+
* await the SAME load instead of each kicking off (or short-circuiting past) a partial one.
|
|
31
|
+
* Cleared when the probe settles. This is the linchpin of the fix for the Cluster view-type
|
|
32
|
+
* race: the switcher's availability hook and the renderer's `resolveEntityDocumentID` both
|
|
33
|
+
* call `Config` near-simultaneously on first open.
|
|
34
|
+
*/
|
|
35
|
+
this._vectorPresenceInFlight = null;
|
|
36
|
+
}
|
|
37
|
+
/** The global singleton instance. Do not construct directly. */
|
|
38
|
+
static get Instance() {
|
|
39
|
+
return super.getInstance();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Loads the active Entity Documents and then probes which of their entities actually have
|
|
43
|
+
* populated vectors. Cheap to call repeatedly — the Entity Documents load is cached by
|
|
44
|
+
* BaseEngine, and the vector-presence probe runs once (until `forceRefresh`).
|
|
45
|
+
*/
|
|
46
|
+
async Config(forceRefresh, contextUser, provider) {
|
|
47
|
+
const c = [
|
|
48
|
+
{
|
|
49
|
+
Type: 'entity',
|
|
50
|
+
EntityName: 'MJ: Entity Documents',
|
|
51
|
+
PropertyName: '_entityDocuments',
|
|
52
|
+
Filter: "Status = 'Active'",
|
|
53
|
+
CacheLocal: true,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
await this.Load(c, provider ?? Metadata.Provider, forceRefresh, contextUser);
|
|
57
|
+
await this.ensureVectorPresence(forceRefresh ?? false, provider, contextUser);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Ensure the vector-presence cache is populated exactly once (or re-populated on
|
|
61
|
+
* `forceRefresh`), de-duplicating concurrent callers via {@link _vectorPresenceInFlight}.
|
|
62
|
+
*
|
|
63
|
+
* CRITICAL: `_vectorPresenceLoaded` is flipped to `true` only AFTER the probe resolves and
|
|
64
|
+
* the result set is swapped in — never before the `await`. The previous implementation set
|
|
65
|
+
* the flag at the top of `loadVectorPresence`, so a second concurrent `Config` saw "loaded"
|
|
66
|
+
* while the set was still empty and returned an availability answer of "no vectors" — exactly
|
|
67
|
+
* the flip-flop / "nothing shown then no-vectors" symptom on first Cluster open.
|
|
68
|
+
*/
|
|
69
|
+
async ensureVectorPresence(forceRefresh, provider, contextUser) {
|
|
70
|
+
if (forceRefresh) {
|
|
71
|
+
this._vectorPresenceLoaded = false;
|
|
72
|
+
this._vectorPresenceInFlight = null;
|
|
73
|
+
}
|
|
74
|
+
if (this._vectorPresenceLoaded) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!this._vectorPresenceInFlight) {
|
|
78
|
+
this._vectorPresenceInFlight = this.loadVectorPresence(provider, contextUser)
|
|
79
|
+
.then((populated) => {
|
|
80
|
+
this._entityIDsWithVectors = populated;
|
|
81
|
+
this._vectorPresenceLoaded = true;
|
|
82
|
+
})
|
|
83
|
+
.finally(() => {
|
|
84
|
+
this._vectorPresenceInFlight = null;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
await this._vectorPresenceInFlight;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Determine, per active Entity Document, whether it has at least one populated vector record,
|
|
91
|
+
* and return the set of owning entity IDs. Uses a single batched {@link RunView.RunViews} with
|
|
92
|
+
* `MaxRows: 1` per document so it's a lightweight existence probe, not a full fetch. Returns a
|
|
93
|
+
* fresh set that the caller swaps in atomically once fully populated (see {@link ensureVectorPresence}).
|
|
94
|
+
*/
|
|
95
|
+
async loadVectorPresence(provider, contextUser) {
|
|
96
|
+
const entityIDsWithVectors = new Set();
|
|
97
|
+
const docs = this._entityDocuments;
|
|
98
|
+
if (docs.length === 0) {
|
|
99
|
+
return entityIDsWithVectors;
|
|
100
|
+
}
|
|
101
|
+
const rv = provider ? RunView.FromMetadataProvider(provider) : new RunView();
|
|
102
|
+
const results = await rv.RunViews(docs.map((d) => ({
|
|
103
|
+
EntityName: 'MJ: Entity Record Documents',
|
|
104
|
+
ExtraFilter: `EntityDocumentID = '${d.ID}' AND VectorJSON IS NOT NULL`,
|
|
105
|
+
Fields: ['ID'],
|
|
106
|
+
MaxRows: 1,
|
|
107
|
+
ResultType: 'simple',
|
|
108
|
+
})), contextUser);
|
|
109
|
+
results.forEach((res, i) => {
|
|
110
|
+
if (res?.Success && (res.Results?.length ?? 0) > 0) {
|
|
111
|
+
entityIDsWithVectors.add(NormalizeUUID(docs[i].EntityID));
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return entityIDsWithVectors;
|
|
115
|
+
}
|
|
116
|
+
/** All cached active Entity Document rows. */
|
|
117
|
+
get EntityDocuments() {
|
|
118
|
+
return this._entityDocuments;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Return the active Entity Document rows for a given entity, matched by EntityID
|
|
122
|
+
* (case-insensitive UUID comparison). Used by the Cluster view-type renderer to
|
|
123
|
+
* resolve a **deterministic** source document for the entity's vectors — picking
|
|
124
|
+
* the first active doc explicitly rather than letting the server choose one
|
|
125
|
+
* non-deterministically.
|
|
126
|
+
*
|
|
127
|
+
* @param entity The entity to find active documents for.
|
|
128
|
+
* @returns The active Entity Document rows for that entity (possibly empty).
|
|
129
|
+
*/
|
|
130
|
+
GetActiveEntityDocumentsForEntity(entity) {
|
|
131
|
+
if (!entity) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
const target = NormalizeUUID(entity.ID);
|
|
135
|
+
return this._entityDocuments.filter((d) => NormalizeUUID(d.EntityID) === target);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* True only when the entity has an active Entity Document **and** at least one populated vector
|
|
139
|
+
* record — i.e. it can actually be clustered. Returns false for entities that have an Entity
|
|
140
|
+
* Document configured but no vectorized records yet. UUID matching is case-insensitive.
|
|
141
|
+
*/
|
|
142
|
+
HasVectorsForEntity(entity) {
|
|
143
|
+
if (!entity) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
return this._entityIDsWithVectors.has(NormalizeUUID(entity.ID));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=entity-document-availability.engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-document-availability.engine.js","sourceRoot":"","sources":["../../../src/lib/view-type/entity-document-availability.engine.ts"],"names":[],"mappings":"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","sourcesContent":["import { BaseEngine, BaseEnginePropertyConfig, EntityInfo, IMetadataProvider, Metadata, UserInfo, RunView } from '@memberjunction/core';\nimport { MJEntityDocumentEntity } from '@memberjunction/core-entities';\nimport { NormalizeUUID } from '@memberjunction/global';\n\n/**\n * EntityDocumentAvailabilityEngine\n * --------------------------------\n * A tiny, browser-safe cache that answers one synchronous question for the\n * {@link ClusterViewType} availability predicate: *does this entity actually have vectors we\n * can cluster?* — meaning it has an active `MJ: Entity Documents` definition **and** at least\n * one populated `MJ: Entity Record Documents` row (vectorized record). An entity that has an\n * Entity Document configured but no records vectorized yet is NOT clusterable, so the Cluster\n * view type must not be offered for it.\n *\n * The descriptor's `IsAvailableFor` must be synchronous, but the answer depends on data — so the\n * descriptor's async `EnsureAvailabilityData` hook calls {@link Config} (awaited once before the\n * predicates run), and `IsAvailableFor` then reads the populated cache via\n * {@link HasVectorsForEntity}.\n *\n * Extends {@link BaseEngine} for caching + entity-event auto-refresh, and honors the per-request\n * provider (multi-provider safe).\n */\nexport class EntityDocumentAvailabilityEngine extends BaseEngine<EntityDocumentAvailabilityEngine> {\n /** The global singleton instance. Do not construct directly. */\n public static get Instance(): EntityDocumentAvailabilityEngine {\n return super.getInstance<EntityDocumentAvailabilityEngine>();\n }\n\n private _entityDocuments: MJEntityDocumentEntity[] = [];\n /** Normalized entity IDs that have ≥1 populated vector record. */\n private _entityIDsWithVectors = new Set<string>();\n private _vectorPresenceLoaded = false;\n /**\n * In-flight vector-presence probe, shared by all concurrent {@link Config} callers so they\n * await the SAME load instead of each kicking off (or short-circuiting past) a partial one.\n * Cleared when the probe settles. This is the linchpin of the fix for the Cluster view-type\n * race: the switcher's availability hook and the renderer's `resolveEntityDocumentID` both\n * call `Config` near-simultaneously on first open.\n */\n private _vectorPresenceInFlight: Promise<void> | null = null;\n\n /**\n * Loads the active Entity Documents and then probes which of their entities actually have\n * populated vectors. Cheap to call repeatedly — the Entity Documents load is cached by\n * BaseEngine, and the vector-presence probe runs once (until `forceRefresh`).\n */\n public async Config(forceRefresh?: boolean, contextUser?: UserInfo, provider?: IMetadataProvider): Promise<void> {\n const c: Partial<BaseEnginePropertyConfig>[] = [\n {\n Type: 'entity',\n EntityName: 'MJ: Entity Documents',\n PropertyName: '_entityDocuments',\n Filter: \"Status = 'Active'\",\n CacheLocal: true,\n },\n ];\n await this.Load(c, provider ?? Metadata.Provider, forceRefresh, contextUser);\n\n await this.ensureVectorPresence(forceRefresh ?? false, provider, contextUser);\n }\n\n /**\n * Ensure the vector-presence cache is populated exactly once (or re-populated on\n * `forceRefresh`), de-duplicating concurrent callers via {@link _vectorPresenceInFlight}.\n *\n * CRITICAL: `_vectorPresenceLoaded` is flipped to `true` only AFTER the probe resolves and\n * the result set is swapped in — never before the `await`. The previous implementation set\n * the flag at the top of `loadVectorPresence`, so a second concurrent `Config` saw \"loaded\"\n * while the set was still empty and returned an availability answer of \"no vectors\" — exactly\n * the flip-flop / \"nothing shown then no-vectors\" symptom on first Cluster open.\n */\n private async ensureVectorPresence(\n forceRefresh: boolean,\n provider?: IMetadataProvider,\n contextUser?: UserInfo,\n ): Promise<void> {\n if (forceRefresh) {\n this._vectorPresenceLoaded = false;\n this._vectorPresenceInFlight = null;\n }\n if (this._vectorPresenceLoaded) {\n return;\n }\n if (!this._vectorPresenceInFlight) {\n this._vectorPresenceInFlight = this.loadVectorPresence(provider, contextUser)\n .then((populated) => {\n this._entityIDsWithVectors = populated;\n this._vectorPresenceLoaded = true;\n })\n .finally(() => {\n this._vectorPresenceInFlight = null;\n });\n }\n await this._vectorPresenceInFlight;\n }\n\n /**\n * Determine, per active Entity Document, whether it has at least one populated vector record,\n * and return the set of owning entity IDs. Uses a single batched {@link RunView.RunViews} with\n * `MaxRows: 1` per document so it's a lightweight existence probe, not a full fetch. Returns a\n * fresh set that the caller swaps in atomically once fully populated (see {@link ensureVectorPresence}).\n */\n private async loadVectorPresence(provider?: IMetadataProvider, contextUser?: UserInfo): Promise<Set<string>> {\n const entityIDsWithVectors = new Set<string>();\n\n const docs = this._entityDocuments;\n if (docs.length === 0) {\n return entityIDsWithVectors;\n }\n\n const rv = provider ? RunView.FromMetadataProvider(provider) : new RunView();\n const results = await rv.RunViews(\n docs.map((d) => ({\n EntityName: 'MJ: Entity Record Documents',\n ExtraFilter: `EntityDocumentID = '${d.ID}' AND VectorJSON IS NOT NULL`,\n Fields: ['ID'],\n MaxRows: 1,\n ResultType: 'simple' as const,\n })),\n contextUser,\n );\n\n results.forEach((res, i) => {\n if (res?.Success && (res.Results?.length ?? 0) > 0) {\n entityIDsWithVectors.add(NormalizeUUID(docs[i].EntityID));\n }\n });\n\n return entityIDsWithVectors;\n }\n\n /** All cached active Entity Document rows. */\n public get EntityDocuments(): MJEntityDocumentEntity[] {\n return this._entityDocuments;\n }\n\n /**\n * Return the active Entity Document rows for a given entity, matched by EntityID\n * (case-insensitive UUID comparison). Used by the Cluster view-type renderer to\n * resolve a **deterministic** source document for the entity's vectors — picking\n * the first active doc explicitly rather than letting the server choose one\n * non-deterministically.\n *\n * @param entity The entity to find active documents for.\n * @returns The active Entity Document rows for that entity (possibly empty).\n */\n public GetActiveEntityDocumentsForEntity(entity: EntityInfo | null): MJEntityDocumentEntity[] {\n if (!entity) {\n return [];\n }\n const target = NormalizeUUID(entity.ID);\n return this._entityDocuments.filter((d) => NormalizeUUID(d.EntityID) === target);\n }\n\n /**\n * True only when the entity has an active Entity Document **and** at least one populated vector\n * record — i.e. it can actually be clustered. Returns false for entities that have an Entity\n * Document configured but no vectorized records yet. UUID matching is case-insensitive.\n */\n public HasVectorsForEntity(entity: EntityInfo | null): boolean {\n if (!entity) {\n return false;\n }\n return this._entityIDsWithVectors.has(NormalizeUUID(entity.ID));\n }\n}\n"]}
|
package/dist/public-api.d.ts
CHANGED
|
@@ -3,4 +3,9 @@ export * from './lib/cluster-scatter.component';
|
|
|
3
3
|
export * from './lib/cluster-config-panel.component';
|
|
4
4
|
export * from './lib/clustering.service';
|
|
5
5
|
export * from './lib/clustering.types';
|
|
6
|
+
export * from './lib/view-type/cluster-view.types';
|
|
7
|
+
export * from './lib/view-type/entity-document-availability.engine';
|
|
8
|
+
export * from './lib/view-type/cluster-view-renderer.component';
|
|
9
|
+
export * from './lib/view-type/cluster-view-prop-sheet.component';
|
|
10
|
+
export * from './lib/view-type/cluster-view-type';
|
|
6
11
|
//# sourceMappingURL=public-api.d.ts.map
|
package/dist/public-api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public-api.d.ts","sourceRoot":"","sources":["../src/public-api.ts"],"names":[],"mappings":"AAGA,cAAc,yBAAyB,CAAC;AAGxC,cAAc,iCAAiC,CAAC;AAChD,cAAc,sCAAsC,CAAC;AAGrD,cAAc,0BAA0B,CAAC;AAGzC,cAAc,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"public-api.d.ts","sourceRoot":"","sources":["../src/public-api.ts"],"names":[],"mappings":"AAGA,cAAc,yBAAyB,CAAC;AAGxC,cAAc,iCAAiC,CAAC;AAChD,cAAc,sCAAsC,CAAC;AAGrD,cAAc,0BAA0B,CAAC;AAGzC,cAAc,wBAAwB,CAAC;AAGvC,cAAc,oCAAoC,CAAC;AACnD,cAAc,qDAAqD,CAAC;AACpE,cAAc,iDAAiD,CAAC;AAChE,cAAc,mDAAmD,CAAC;AAClE,cAAc,mCAAmC,CAAC"}
|
package/dist/public-api.js
CHANGED
|
@@ -8,4 +8,10 @@ export * from './lib/cluster-config-panel.component';
|
|
|
8
8
|
export * from './lib/clustering.service';
|
|
9
9
|
// Types
|
|
10
10
|
export * from './lib/clustering.types';
|
|
11
|
+
// Cluster view-type plug-in (entity-viewer integration)
|
|
12
|
+
export * from './lib/view-type/cluster-view.types';
|
|
13
|
+
export * from './lib/view-type/entity-document-availability.engine';
|
|
14
|
+
export * from './lib/view-type/cluster-view-renderer.component';
|
|
15
|
+
export * from './lib/view-type/cluster-view-prop-sheet.component';
|
|
16
|
+
export * from './lib/view-type/cluster-view-type';
|
|
11
17
|
//# sourceMappingURL=public-api.js.map
|
package/dist/public-api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public-api.js","sourceRoot":"","sources":["../src/public-api.ts"],"names":[],"mappings":"AAAA,sDAAsD;AAEtD,SAAS;AACT,cAAc,yBAAyB,CAAC;AAExC,aAAa;AACb,cAAc,iCAAiC,CAAC;AAChD,cAAc,sCAAsC,CAAC;AAErD,UAAU;AACV,cAAc,0BAA0B,CAAC;AAEzC,QAAQ;AACR,cAAc,wBAAwB,CAAC","sourcesContent":["// Public API Surface of @memberjunction/ng-clustering\n\n// Module\nexport * from './lib/clustering.module';\n\n// Components\nexport * from './lib/cluster-scatter.component';\nexport * from './lib/cluster-config-panel.component';\n\n// Service\nexport * from './lib/clustering.service';\n\n// Types\nexport * from './lib/clustering.types';\n"]}
|
|
1
|
+
{"version":3,"file":"public-api.js","sourceRoot":"","sources":["../src/public-api.ts"],"names":[],"mappings":"AAAA,sDAAsD;AAEtD,SAAS;AACT,cAAc,yBAAyB,CAAC;AAExC,aAAa;AACb,cAAc,iCAAiC,CAAC;AAChD,cAAc,sCAAsC,CAAC;AAErD,UAAU;AACV,cAAc,0BAA0B,CAAC;AAEzC,QAAQ;AACR,cAAc,wBAAwB,CAAC;AAEvC,wDAAwD;AACxD,cAAc,oCAAoC,CAAC;AACnD,cAAc,qDAAqD,CAAC;AACpE,cAAc,iDAAiD,CAAC;AAChE,cAAc,mDAAmD,CAAC;AAClE,cAAc,mCAAmC,CAAC","sourcesContent":["// Public API Surface of @memberjunction/ng-clustering\n\n// Module\nexport * from './lib/clustering.module';\n\n// Components\nexport * from './lib/cluster-scatter.component';\nexport * from './lib/cluster-config-panel.component';\n\n// Service\nexport * from './lib/clustering.service';\n\n// Types\nexport * from './lib/clustering.types';\n\n// Cluster view-type plug-in (entity-viewer integration)\nexport * from './lib/view-type/cluster-view.types';\nexport * from './lib/view-type/entity-document-availability.engine';\nexport * from './lib/view-type/cluster-view-renderer.component';\nexport * from './lib/view-type/cluster-view-prop-sheet.component';\nexport * from './lib/view-type/cluster-view-type';\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/ng-clustering",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.40.0",
|
|
4
4
|
"description": "MemberJunction: Reusable Angular clustering visualization components with scatter plot, config panel, and clustering service",
|
|
5
5
|
"main": "./dist/public-api.js",
|
|
6
6
|
"typings": "./dist/public-api.d.ts",
|
|
@@ -32,8 +32,16 @@
|
|
|
32
32
|
"@angular/forms": "21.1.3"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@memberjunction/ai-
|
|
36
|
-
"@memberjunction/
|
|
35
|
+
"@memberjunction/ai-engine-base": "5.40.0",
|
|
36
|
+
"@memberjunction/ai-vectors-memory": "5.40.0",
|
|
37
|
+
"@memberjunction/core": "5.40.0",
|
|
38
|
+
"@memberjunction/core-entities": "5.40.0",
|
|
39
|
+
"@memberjunction/global": "5.40.0",
|
|
40
|
+
"@memberjunction/graphql-dataprovider": "5.40.0",
|
|
41
|
+
"@memberjunction/ng-base-types": "5.40.0",
|
|
42
|
+
"@memberjunction/ng-entity-card": "5.40.0",
|
|
43
|
+
"@memberjunction/ng-entity-viewer": "5.40.0",
|
|
44
|
+
"rxjs": "^7.8.2",
|
|
37
45
|
"tslib": "^2.8.1",
|
|
38
46
|
"umap-js": "^1.4.0"
|
|
39
47
|
},
|